Selenium 和 Playwright 是 Python 圈子里面做 Web UI 自动化绕不开的两个名字。我自己的项目从最早的 Selenium WebDriver 脚本,到后来全面切到 Playwright,中间踩了不少坑,也积累了一些真实的使用感受。很多刚开始接触自动化的朋友会问:这两个到底选哪个?其实这个问题没有绝对答案,但如果你能搞清楚它们背后的设计逻辑和各自擅长的场景,选型就会变得非常轻松。这篇文章我会从实际使用的角度,把两个框架的核心差异、代码写法的不同、常见坑位和排查思路梳理一遍,希望能帮你少走弯路。
1. 为什么大家总在对比这两个框架
1.1 它们其实是两代自动化方案的代表
Selenium 诞生得非常早,在很长一段时间里它就是 Web 自动化的代名词。它的核心思路是:通过 WebDriver 协议跟浏览器进行通信,模拟用户在页面上的操作,比如点击、输入、滚动、切换页面等等。因为历史久、生态成熟,Selenium 支持的语言非常多,Java、Python、C#、JavaScript 都有官方绑定,而且兼容的浏览器也很全,所以很多传统企业项目里跑了几年的回归测试脚本,底层基本都是 Selenium。
Playwright 是后面才出现的自动化库,最初是某浏览器团队为自身 Web 测试开发的一套工具,后来逐步开放成为跨浏览器自动化框架。它最核心的特点是采用 CDP 协议直接跟浏览器深度交互,同时还支持一些 Selenium 很难优雅实现的能力,比如网络请求拦截、模拟移动设备、多页面上下文的并发操作。换句话说,Selenium 是站在浏览器外面敲门的方案,Playwright 更像是直接坐在浏览器内部干活。
1.2 对比的核心维度是什么
我在实际评估一个自动化框架好不好用,通常只看四个维度:
- 环境安装是否折腾,驱动有没有版本同步问题
- 元素定位和等待机制是否足够智能,脚本容不容易写挂
- 对复杂场景的支持能力,比如 iframe、多标签页、文件下载、网络响应
- 长期维护成本,包括脚本稳定性、调试体验和团队上手速度
这四个维度也基本对应了我下面要展开的几个部分。我用一个真实做过的小项目来串起这些测试点,项目内容很简单,就是模拟登录一个后台管理系统,然后在列表中筛选数据、导出文件、最后验证导出结果。这个流程非常典型,覆盖了自动化测试里最常见的交互场景。
提示:如果你还没有实际接触过这两个框架,建议先跟着文章把环境装好,每一个代码片段都自己跑一遍,再回来看结论。工具这个东西,别人的经验只是参考,手上的手感才是自己的。
2. 安装与环境配置的真实体验
2.1 Selenium 的依赖与驱动陷阱
Selenium 的安装本身倒是不复杂,一条命令就搞定了:
pip install selenium但真正折腾人的是浏览器驱动。Selenium 本身不管理浏览器的版本,它需要通过 WebDriver 这个中间层去控制浏览器,而 WebDriver 的版本又必须跟浏览器主版本完全对应。比如浏览器升级到了某个大版本,你原来下载的驱动就失效了,脚本会直接报 session 创建失败之类的错误。
我自己早期最头疼的就是驱动维护这件事。每次浏览器自动升级,测试环境就跑不了,得重新下载驱动、放到系统 PATH 里,或者用 webdriver-manager 这类工具去自动配套版本。如果你所在的公司对版本管理不严格,开发同学经常升级浏览器,那么每天都可能收到“自动化脚本崩了”的消息。
后来我用 webdriver-manager 缓解了一些问题,它会在启动的时候自动检查并下载对应驱动,但前提是网络要能访问到下载地址,而且首次下载还是会卡一下。整体体验属于能跑,但是有点操心。
2.2 Playwright 一键搞定浏览器环境
Playwright 在安装体验上确实是降维打击:
pip install playwright playwright install后面这条命令会把 Chromium、Firefox、WebKit 三个浏览器内核直接下载到本地缓存里,完全绕开了系统浏览器版本的影响。也就是说,你测试脚本用的浏览器版本跟系统日常用的浏览器是隔离的,版本是 Playwright 自己控制的,不存在“浏览器升级脚本就挂”的问题。
这一点在团队协作里特别有用。不同同事机器上的测试环境可以完全一致,因为我用的 Chromium 版本跟你们用的 Chromium 版本是同一个。Selenium 那边想做到这个统一,就得要求所有人都装同一个浏览器版本、同一个驱动,协调成本明显更高。
2.3 补充说明:无头模式的支持差异
两个框架都支持无头模式,Selenium 需要给浏览器选项传一堆参数,比如:
from selenium.webdriver.chrome.options import Options opts = Options() opts.add_argument("--headless=new") opts.add_argument("--disable-gpu") opts.add_argument("--window-size=1920,1080")Playwright 直接在启动浏览器时传一个参数:
from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch(headless=True)从代码量来看,Playwright 明显更清爽。无头模式在 CI 环境里几乎是必用的,因为服务器上通常没有显示设备,所以这个 API 设计得是否简洁,直接影响到你写 CI 流水线时的体验。
如果你是在老项目里维护大量 Selenium 脚本,升级浏览器版本前记得先确认驱动版本,最好把驱动下载链接固定好。如果是新项目,个人建议直接选 Playwright。
3. 元素定位与等待机制:脚本稳定性的大头
3.1 定位符的写法差异
页面元素的定位方式是两个框架差异很直观的地方。Selenium 有很多种自带的定位方式,比如:
from selenium.webdriver.common.by import By driver.find_element(By.ID, "username") driver.find_element(By.XPATH, "//div[@data-role='list']//button")而 Playwright 采用了统一的 locator 概念,什么定位方式都放在一个 locator 里面:
page.locator("#username") page.locator("[data-role='list'] button")从表面看只是 API 形态不同,但 Playwright 的 locator 还有一个很大的优势:定位符可以像对象一样复用和组合。比如你可以先拿到一个列表容器,再在容器下面继续查子元素,这个在复杂的业务页面上写起来非常顺手,代码逻辑也清晰。
3.2 隐式等待 vs 自动等待
Selenium 里最经典的坑就是元素还没加载出来脚本就已经去点击了。早期的解决方案是强制睡觉,比如 time.sleep(2),后面大家学会了用显式等待:
from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, "submit")) ).click()这种方式没有问题,但问题是:你得手动判断“这个元素应该满足什么状态”,每个关键操作都要去写等待逻辑,代码量上去了,而且写不好就容易出现偶发失败。
Playwright 的默认处理方式是完全不同的。它的定位操作自带 actionability 判断,比如点击一个按钮的时候,Playwright 会自动等待元素出现、可见、稳定、可接收事件,全部满足才会真正点击。也就是说,你不需要为每一个元素去写等待逻辑,默认行为就是“等它能用再操作”。
这个设计让我在写脚本的时候轻松很多。原来 Selenium 脚本里密密麻麻的 WebDriverWait 和 expected_conditions,在 Playwright 里大多数都可以直接删掉。
3.3 超时时间设置的经验值
虽然 Playwright 会自动等待,但超时时间还是要设置的,防止页面真的卡死的时候脚本一直等下去。我通常会给 actions 设置一个 15 秒左右的超时:
page.set_default_timeout(15000)Selenium 的显式等待超时我喜欢设置成 10 到 20 秒。太长会让失败反馈变慢,太短又会造成偶发误报。这里没有绝对标准,建议根据页面实际情况调整,总之不要让等待时间成了脚本不稳定的来源。
注意:等待机制是自动化脚本稳定性的最大变量之一。如果你发现脚本时而成功时而失败,先不要怀疑定位写错,去检查等待逻辑是否可靠,绝大多数“偶发失败”都跟等待有关。
4. 核心操作在两种框架下的写法差异
4.1 iframe 的处理方式
后台管理系统里面 iframe 出现的频率特别高。Selenium 处理 iframe 需要切换上下文:
driver.switch_to.frame("main_iframe") # 操作 iframe 里面的元素 driver.switch_to.default_content()这个切来切去的过程很容易出错,尤其是多层 iframe 嵌套的时候,切换错一层就找不到元素。
Playwright 处理 iframe 用的是 Frame Locator,不需要手动切换上下文,直接在 iframe 内部定位元素:
frame = page.frame_locator("#main_iframe") frame.locator("input[name='keyword']").fill("测试数据")这种写法的好处是代码结构跟页面结构是对应的,不用维护“当前在哪个 frame 里”这种状态。我在实际项目里发现,iframe 越多的老页面,Playwright 的这种设计就越能减少低级错误。
4.2 多标签页和窗口切换
自动化里经常要处理打开新标签页再操作新页面的场景。Selenium 的做法是获取所有窗口句柄,然后切换:
handles = driver.window_handles driver.switch_to.window(handles[-1])Playwright 的思路更现代。它可以直接监听新页面事件并拿到新页面对象:
with page.expect_popup() as popup_info: page.click("a[target='_blank']") new_page = popup_info.value new_page.wait_for_load_state()这种写法的好处是脚本逻辑跟用户行为直接对应:触发一个会打开新窗口的动作,然后拿住新窗口,所有后续操作都在新页面对象上进行。跟 iframe 的处理一样,它不需要维护全局状态,代码更接近人的思维方式。
4.3 文件下载与上传
文件下载在测试里也是常客。Selenium 往往需要通过浏览器设置来修改下载目录,或者用一些第三方工具来做文件验证,步骤比较繁琐。Playwright 提供事件来处理下载事件:
with page.expect_download() as download_info: page.click("button#export") download = download_info.value download.save_as("report.xlsx")文件上传方面,Selenium 对非 input 标签的上传控件处理起来比较痛苦,经常要去操作系统弹窗。Playwright 提供了 set_input_files 方法,即使是自定义的上传控件,很多情况下也能通过指定文件路径的方式解决:
page.locator(".upload-dropzone").set_input_files("test.pdf")4.4 网络请求拦截
这块是 Playwright 的明星功能之一。我经常需要在测试里模拟接口返回的数据,或者是屏蔽掉一些外部统计脚本,避免它们影响页面加载。Selenium 想拦截网络请求是比较困难的,一般需要借助代理工具来配合,配置链路很重。
Playwright 只需要一个 route 方法就可以完成拦截和转发:
def mock_response(route): route.fulfill(json={"data": []}) page.route("**/api/list", mock_response) page.locator("button#refresh").click()依赖 Mock 数据做前端自动化测试的时候,这个功能简直不要太方便。以前我用 Selenium 加代理工具做 mock,配置了半天的上游代理和证书,换成 Playwright 之后一段代码就搞定了。
5. 代码风格、性能与并发能力的体验
5.1 新手友好程度和代码可读性
Selenium 的 API 非常经典,网上资料多,随便搜一个问题都能找到解答。从学习曲线的角度看,Selenium 更像是“经验丰富的老前辈”,你在社区里能找到各种冷门问题的答案。但它的 API 风格相对老化,有些操作写出来的代码比较啰嗦,传参方式也比较杂。
Playwright 的 API 设计更现代,同步和异步两套接口都做得非常清晰。官方文档的示例代码质量很高,照着文档基本能把核心功能跑通。对于刚接触自动化的朋友来说,Playwright 的上手体验更友好,因为它的很多操作都自带等待机制,新手不太容易写出那种“明明元素还没加载就去点击”的脆弱脚本。
5.2 运行效率与并发
Selenium 脚本通常是串行跑的,一个浏览器实例跑完一条用例再跑下一条。要提速就得自己写多线程,每个线程维护独立的 driver 实例,这种方案跑起来很费资源,而且驱动实例一多,稳定性也会下降。
Playwright 对并发的支持是内置设计的。它的每个 browser context 都是一个独立会话,你可以很轻松地开多个 context 去模拟多用户同时操作的场景,浏览器实例可以复用,资源消耗比 Selenium 的多线程方案要低。我在一次压测模拟里,用 Playwright 开了五个 context 同时操作同一个后台系统,总的运行时间差不多是串行脚本的三分之一到四分之一,而且几乎没出现会话互相干扰的问题。
5.3 调试体验对比
调试是自动化脚本开发里最花时间的环节,所以调试体验的差异很重要。
Selenium 脚本报错了,经常会给你一大段异常信息,但具体是哪一步操作出了问题,有时候要靠自己加日志慢慢定位。早期调试 Selenium 脚本时,我经常在整个流程里手动加截图和打印当前的页面 URL 来辅助排查。
Playwright 有一个特点,它对每个操作都会自动生成带步骤的 trace 记录,运行完脚本之后可以在打开的 trace viewer 里面看每一个操作的截图、网络请求和控制台信息。我在调试复杂流程的时候,直接打开 trace 看是哪一步出现异常,比盲猜快得多。
6. 常见问题、排查思路与选型建议
6.1 两个框架各自的高频问题
Selenium 这边的高频问题大多数集中在驱动和等待上,这里整理成一个速查表:
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 启动浏览器之后马上报错 | WebDriver 版本和浏览器不匹配 | 检查驱动版本并重新下载,或使用管理器自动匹配 |
| 元素明明存在但点击失败 | 元素被遮挡或处于不可交互状态 | 加显式等待并检查元素是否可点击 |
| 偶发性找不到元素 | 页面加载不稳定或等待时间不够 | 换用显式等待并延长超时时间 |
| 浏览器窗口一多就串会话 | 多线程共用了一个 driver 实例 | 确保每个线程都创建独立的 driver 实例 |
Playwright 这边常见问题更多的跟运行环境有关:
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 启动就报缺少依赖库 | 系统缺少浏览器运行所需的系统库 | 查看文档列出的依赖项并逐一安装 |
| 首次启动浏览器很慢 | 正在下载浏览器内核或初始化缓存 | 确保本地缓存完整,预下载浏览器 |
| 自动等待超时 | 页面元素确实没有出现或状态不对 | 检查定位符是否准确,看 trace 确认实际状态 |
6.2 排查技巧实录:一次导出按钮点击失败的定位过程
说一个实际的排查案例。某次我用 Selenium 写导出功能测试,点击导出按钮以后脚本报元素不可点击,但是肉眼打开页面按钮明明就在那里。
我当时没有急着改代码,而是先截图加打印元素状态,发现按钮其实是被一个透明的 loading 遮罩层盖住了。页面接口请求没结束的时候遮罩层一直存在,按钮表面上可见但实际上点不到。
这个问题的解决思路是等待遮罩层消失,再用显式等待确认按钮可点击,代码就稳定了。换成 Playwright 之后同样的问题处理起来更自动,因为 Playwright 默认就会检测元素是否被遮挡,元素不可交互的情况下它会一直等到可交互状态才去点击,不需要我手动写遮罩层的等待逻辑。
6.3 选型建议:什么时候用 Selenium,什么时候用 Playwright
Selenium 适合的场景:
- 团队已经有大量存量脚本,短期不可能重写
- 项目有特殊浏览器兼容要求,必须使用系统安装的特定浏览器版本
- 团队成员已经非常熟悉 Selenium,重学新技术成本较高
- 项目里需要用到多种编程语言编写测试代码
Playwright 适合的场景:
- 新项目刚起步,没有历史包袱
- 对脚本稳定性要求较高,不想花大量时间维护等待逻辑
- 需要模拟多用户并发操作,或者做网络请求拦截
- 需要做跨浏览器测试,且希望环境版本统一可控
- CI 环境里跑自动化,希望安装配置简单
从我个人的体会来说,凡是新起的自动化项目,我会优先选择 Playwright。它解决的很多问题都是 Selenium 时代的老大难问题,而 Selenium 的优势更多在于生态积累和团队熟悉度,不是技术上全面领先。
最后分享一个小经验:不管选哪个框架,自动化脚本维护的长期成本才是最大的成本。框架选型不是一锤子买卖,最好先在项目里挑一个核心流程做个原型验证,跑通之后再决定全面铺开。工具是手段,稳定、高效的测试反馈才是目的。