做Web自动化的同学,几乎没有人能绕开Selenium的入门路线。我最早也是从Selenium WebDriver起步,写了两年多的UI自动化,直到有一次被一个隐蔽的等待问题折腾了整整两天,才下定决心把目光投向Playwright。当时的第一感受是:这东西确实是冲着解决Selenium痛点来的。如今Playwright已经成了我日常Web自动化、页面巡检、甚至AI测试探索的首选框架,这篇就围绕Playwright框架的使用,把从安装到落地的完整链路拆开讲透,适合已经从Selenium转过来、或者刚接触Web自动化的朋友参考。
我写这篇文章不会去复述官方文档,而是以我实际踩坑之后的理解来聊:为什么选它、安装哪几关容易卡、Codegen生成脚本后怎么改、定位元素有哪些工程化方案、怎么接进Spring Boot或若依这类后台系统做页面巡检,以及遇到反自动化页面时合规的处理思路。最后会聊一下Playwright与AI Agent、MCP的组合玩法和个人判断。
1. 为什么是 Playwright:一次框架切换的完整复盘
1.1 Selenium 用得好好的,为什么要换
先说清楚一个事实:Selenium 本身并不差,它能活这么多年,社区生态和资料厚度是Playwright短期内追不上的。我切换的理由更多是“效率账”算不过来了。
最折磨人的是动态页面的等待问题。Selenium 的WebDriverWait虽然能用,但遇到复杂单页应用,要等元素出现、可点击、样式加载完成、接口返回后渲染,经常得叠加好几个expected_conditions,代码写得很啰嗦,还总在换机器、换网络环境后出现“本地能跑、服务器跑不了”的玄学问题。
另一个痛点是浏览器上下文管理。要模拟多标签页、多用户登录态,Selenium 得手动维护多个WebDriver实例,内存和稳定性都很头疼。还有无头模式下的兼容性、iframe 切换的繁琐、移动端模拟能力的缺失,这些都是我实际项目中反复碰到的问题。
Playwright 给的方案则比较彻底:自动等待是内置机制,而不是一个需要手动调用的工具——定位到元素之后,框架会自己等你需要的动作可执行;**多上下文(BrowserContext)**天然隔离 Cookie 和会话状态,多标签、多用户场景开箱即用。光是这两点,就帮我砍掉了大量样板代码。
1.2 Playwright 真正拉开差距的设计
除了自动等待,Playwright 还有几个设计层面的优势,长期用下来才会感受到:
第一个是浏览器内核级调试协议。很多人忽略这点,以为Playwright只是封装得更好,实际上它直接通过CDP协议与Chromium通信,能做的事远超“模拟用户点击”。比如监听网络请求、拦截并修改接口返回、模拟弱网、直接读取浏览器性能指标,这些在Selenium里要么要借助第三方代理,要么很难稳定实现。
第二个是Trace Viewer。这是我觉得回本最快的功能:跑完一条用例,会生成一份完整的“录像+DOM快照+网络请求时间线+控制台日志”,排查问题的时候再也不用靠猜。我现在的习惯是每次CI失败后优先取trace文件,定位速度比看截图快好几倍。
第三个是多语言与多浏览器的一致性。Python、Java、JavaScript、.NET 都有官方实现,API设计还高度统一。Chromium、Firefox、WebKit 三套内核用同一条代码跑测试,不需要像之前那样为浏览器差异写一堆兼容逻辑。
1.3 它到底适合谁,不适合谁
如果你面对的是强交互的单页应用、需要做UI回归或页面巡检、想在测试里做接口mock和网络模拟,Playwright 是当前性价比最高的选择之一。
但我也要说它不适合的场景:如果你只是做轻量的数据采集,不需要复杂交互,用Playwright反而偏重,因为要管浏览器进程、资源占用也更高。还有纯接口测试,没必要引入浏览器自动化,直接用Requests或httpx就行。另外,如果你的目标站点有极强的反自动化防护,Playwright本身不是银弹,后续第6章会专门展开讲。
2. 环境搭建:安装那几关,过了就顺了
2.1 pip 安装与浏览器内核下载
安装本身很简单,但大部分新手第一次都会在下载浏览器内核这一步卡住,所以我把完整过程拆开说明。
python -m venv venv source venv/bin/activate # Windows 下执行 venv\Scripts\activate pip install playwright playwright install chromium第一条pip install playwright装的是Python包,第二条playwright install chromium下载的是Chromium浏览器内核。这里的坑在于:由于网络原因,直接执行playwright install经常下载到一半就中断,或者速度非常慢。
我实测比较稳的做法是设置镜像环境变量再执行:
export PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright playwright install chromiumWindows PowerShell 下则是:
$env:PLAYWRIGHT_DOWNLOAD_HOST="https://npmmirror.com/mirrors/playwright" playwright install chromium这里要提醒一点:更换下载源之前,最好先删除本地已经下载了一半的缓存。Playwright的浏览器缓存目录默认在~/Library/Caches/ms-playwright(macOS)、%USERPROFILE%\AppData\Local\ms-playwright(Windows),把残留文件清掉再重试,能避免很多“明明设置了镜像还是失败”的怪问题。
2.2 install chromium 慢与失败的处理
如果你是在公司内网、或者服务器上无外网环境,就会遇到“下载慢/下载失败”的问题。另一个高频场景是:playwright install成功了,但启动浏览器时报缺少系统依赖。
在Linux服务器上,缺少依赖非常常见。官方提供了系统依赖安装命令,我一般直接跑:
playwright install --with-deps chromium这个命令会顺带把运行Chromium所需的操作系统库一起装上,比手动一个个apt install省心得多。如果是CentOS/RHEL系列,Playwright官方对Debian系支持最好,CentOS上偶尔会有依赖不全的情况,建议优先考虑用Docker容器跑。
2.3 离线安装与服务器无外网场景
某些生产环境完全无法访问外网,这时候有两个思路:
思路一:在有网的机器上下载好浏览器,整体打包传上去。常见路径是找到一个能正常playwright install的机器,把ms-playwright缓存目录打包,传到目标服务器对应的用户目录下解压。然后设置环境变量PLAYWRIGHT_BROWSERS_PATH=/your/custom/path,指向你存放浏览器内核的位置。这样做的好处是不污染系统目录,多个项目还能共享同一份浏览器。
思路二:只下载Python包,不走在线安装。在能联网的机器上执行pip download playwright,把依赖包全部下载到本地目录,再到目标机器上pip install --no-index --find-links=./packages playwright。配合思路一里的浏览器包,可以实现完全离线的安装,我在这类场景里验证过很多次,稳定可用。
2.4 集成 pytest 的两个姿势
纯脚本方式用顺手了以后,最好还是接上 pytest,因为用例组织、断言失败重跑、报告输出这些能力,脚本自己写会很啰嗦。
官方提供了pytest-playwright插件,安装后可以直接用page作为测试函数参数,框架自动帮你创建和关闭浏览器上下文。一个最小用例长这样:
def test_example(page): page.goto("https://example.com") assert page.title() == "Example Domain"如果不依赖插件,也可以自己在conftest.py里写fixture控制浏览器生命周期。我的建议是:优先用官方插件,因为它的fixture体系是经过大量项目验证的,比如browser_context、page、request这些内置fixture,基本覆盖了日常需求。只有当你需要多进程并行、或者自定义设备描述文件时,才考虑手写fixture层。
关于并行,pytest-playwright 默认是单进程顺序跑,想要加速可以配合pytest-xdist,加上--numprocesses=4就能多进程跑用例。但注意:并行场景下每个 worker 会启动独立的浏览器实例,内存开销会成倍增加,我在8G内存的机器上最多开4个worker,再多就容易崩。
3. Codegen 不是录制回放:用它生成骨架,再手动打磨
3.1 十分钟生成第一条可用脚本
Playwright 自带一个特别好用的工具:codegen。执行下面的命令,它会打开一个带高亮定位信息的浏览器窗口,你手动点几下页面,右侧就自动生成对应操作代码:
playwright codegen https://example.com初次使用的人通常会很兴奋,因为几乎不用写代码就能生成脚本。但我要强调一个观点:codegen 生成的代码适合当骨架和选定位符的辅助工具,不建议直接把生成结果当作最终用例使用。
原因有几个:录制过程会产生冗余操作,比如鼠标移动轨迹、无意义的点击;生成的定位符有时候会回退到很脆弱的写法(后面会细说);而且它不会自动生成合理的断言。所以正确的做法是:用codegen快速摸清页面的操作路径,再把生成的代码复制到测试文件里,在这个基础上做删减、补断言、参数化。
3.2 读懂 codegen 生成代码的套路
我拿一个最常见的登录场景来说。假设你录制了账号输入、密码输入、点击登录三个操作,codegen生成出来的代码大致是这样:
from playwright.sync_api import Playwright, sync_playwright def run(playwright: Playwright) -> None: browser = playwright.chromium.launch(headless=False) context = browser.new_context() page = context.new_page() page.goto("https://example.com/login") page.locator("#username").click() page.locator("#username").fill("admin") page.locator("#password").click() page.locator("#password").fill("123456") page.locator("button[type='submit']").click() browser.close()看到这段代码,你应该意识到几个问题:#username是否在当前页面上唯一?点击事件是否有必要(fill本身就带聚焦)?登录成功后页面跳转是否需要等待?这些都需要靠人去判断。codegen 解决的是“怎么操作”,不负责“怎么验证”,后者才是用例的价值所在。
3.3 从生成脚本到稳定用例的改造点
我是按下面几个步骤来做改造的:
第一步:精简等待逻辑。把所有sleep和手动等待全部删掉,让Playwright的自动等待机制接管。只在真正必须的时机用expect做显式等待。
第二步:用语义化定位替换脆弱定位。比如page.locator("#username")可以改成page.get_by_role("textbox", name="用户名"),后者在页面改版后更不容易挂。
第三步:补断言。登录成功的典型断言是等待某个只出现在登录状态下的元素出现,比如expect(page.get_by_text("欢迎回来,admin")).to_be_visible()。没有断言,脚本跑完完全不知道结果对不对。
第四步:参数化。把用户名、密码、站点URL提取为参数,方便不同环境复用。示例:
def test_login(page, base_url, username, password): page.goto(f"{base_url}/login") page.get_by_label("用户名").fill(username) page.get_by_label("密码").fill(password) page.get_by_role("button", name="登录").click() expect(page.get_by_text("欢迎回来")).to_be_visible()这一步做完,这条脚本才能算“可以进CI的用例”,而不是一段演示片段。
4. 定位元素的工程化手册:选择器、Frame、Shadow DOM 与等待
4.1 选择器优先级与推荐写法
定位元素是UI自动化里最见功力的环节,也是新手遇到报错最多的位置。我的建议是按下面这个优先级选择定位策略:
| 优先级 | 方式 | 示例 | 适用场景 |
|---|---|---|---|
| 1 | 文本定位 | page.get_by_text("提交") | 按钮、链接、标签 |
| 2 | 角色定位 | page.get_by_role("button", name="提交") | 表单控件 |
| 3 | 标签定位 | page.get_by_label("用户名") | 表单输入框 |
| 4 | 占位符 | page.get_by_placeholder("请输入用户名") | 输入框 |
| 5 | CSS | page.locator(".submit-btn") | 有稳定class的节点 |
| 6 | XPath | page.locator("//button[contains(text(),'提交')]") | 复杂结构兜底 |
为什么优先用文本和角色?因为它抗页面结构调整的能力最强。CSS选择器和XPath容易因为一个class改名就挂掉。真正稳妥的定位是“用户怎么看这个元素,自动化就怎么找它”,语义化定位符合这个原则。
还有个细节:locator对象建议只创建一次并复用,不要每次操作都重新定位。比如:
submit_btn = page.get_by_role("button", name="提交") submit_btn.click()这样既提高了可读性,后续要调整定位时只需改一处。
4.2 iframe 与 Shadow DOM 的越狱思路
iframe 是历史遗留却无法绕开的老大难。Selenium时代切iframe要用driver.switch_to.frame(),切完还要切回来,嵌套frame简直就是噩梦。Playwright的做法是把iframe当作普通元素树来访问,不需要什么“切换”概念。
frame = page.frame_locator("#main-iframe") frame.get_by_role("button", name="提交").click()page.frame_locator()拿到的是iframe内的定位器,可以直接继续链式调用,嵌套iframe就层层往下写,代码逻辑比Selenium清晰很多。需要访问iframe里的顶层页面时,回到page对象照常操作即可。
Shadow DOM 的情况类似。默认情况下,Playwright 的定位器可以直接穿透开放的 Shadow DOM 进行定位。但如果遇到闭合的closed模式,就获取不到内部元素。此时可以考虑用page.locator("css=selector")配合>>穿透语法,或者退回JavaScript执行的方式,把 Shadow Root 手动取出来。多数场景下,开放模式已经够用了。
4.3 动态页面的等待策略与超时
我见过太多自动化脚本挂在“元素还没渲染完就去找它”的问题上,所以这一段想单独说清楚。
Playwright的自动等待会针对动作类型做不同判断:点击之前会等元素稳定、可接收事件;填表之前会等元素可见、可编辑。这种机制已经覆盖了大多数动态渲染场景。
但对于那种“先出现骨架屏、再渲染真实内容”的页面,自动等待可能被骨架屏元素“骗过”。这时候需要用显式断言来兜底:
expect(page.get_by_text("加载完成")).to_be_visible(timeout=10000)你还可以在页面级设置超时时间:
page.set_default_timeout(10000)超时不是越大越好。比如登录跳转后如果10秒还没出现目标元素,与其一直等下去,不如快速失败并钉住问题。我通常将set_default_timeout设置在5到10秒之间,配合失败时的截图和trace,定位问题比傻等高效得多。
4.4 定位失败时的排查路径
locator找不到元素时,最常见的报错是TimeoutError。我的排查顺序是:
- 看截图:失败时自动截图,确认页面是不是跳到了意外的地方。
- 看当前URL:很多时候是登录态过期被踢回登录页,不是定位写错。
- 在trace viewer里看DOM快照:这个信息量最大,能直接看到元素的完整DOM结构,对比自己的定位写法。
- 检查是否在iframe里:如果元素在当前页面DOM里找不到,优先怀疑它在iframe或Shadow DOM中。
- 用codegen重新点一次:看实际页面在用户视角里怎么描述这个元素,对照生成的选择器,往往能迅速发现问题。
这套流程我一直在用,实战中90%的定位问题都能在这五步内解决。
5. 从单条用例到页面自动巡检:接入 Spring Boot / 若依的实际做法
5.1 巡检和测试用例的区别
热搜词里出现了“若依框架”“SpringBoot”“web页面自动化巡检”,这恰好是我近半年做得比较多的事。先说一个容易混淆的点:页面巡检和功能测试用例虽然用的都是Playwright,但目标完全不同。
测试用例是“验证某个功能是否符合预期”,输入输出都明确;巡检则是“持续确认线上页面是否可用、是否有明显异常”,覆盖范围更广,但每个点的断言可以做得更轻。比如登录后的首页巡检,可能只检查页面标题、关键模块是否渲染、核心接口是否返回200,而不用把每个按钮的交互都点一遍。
正因为目标不同,巡检脚本的稳定性和可观测性要求反而更高。毕竟测试用例可以接受“环境问题导致失败”,巡检要是频繁误报,团队很快就不看这个系统了。
5.2 任务拆分与调度设计
我推荐的架构是:若依后端负责配置和管理,Python侧独立部署Playwright巡检服务,两边通过接口或消息队列通信。
为什么不让若依直接调用Python?因为Spring Boot进程里嵌一个Chromium太重了,而且Python的Playwright生态和巡检脚本维护成本更低。如果你确实想在一个Java进程里全搞定,也可以尝试用JUnit或者TestNG调度Playwright的Java API,只是我不太推荐这样做——一旦页面复杂起来,Java侧写选择器和断言并不比Python省事多少。
具体落地方案可以这样设计:
- 在若依后台维护巡检任务表,记录巡检的URL、巡检项、执行频率、告警联系人。
- 巡检服务定时从后台拉取任务列表,执行完以后把结构化结果写回数据库。
- 若依后台展示巡检趋势图、失败详情、截图与trace文件。
Python侧的调度我用过两种方式:轻量场景直接用APScheduler加 cron 表达式;任务多了以后改成Celery。APScheduler胜在简单,Celery胜在分布式和重试机制完善。目前我的经验是:页面巡检任务如果少于50个,APScheduler完全够用,没必要上Celery。
5.3 结果落库与告警
巡检跑完,结果不能只打印在控制台。我通常会做成一个CheckResult模型,包含这样几个字段:
{ "task_id": "homepage_check", "url": "https://xxx.com/home", "status": "failed", "duration_ms": 3580, "error_message": "element not visible: .dashboard-title", "screenshot_path": "/data/screenshots/20250101_home.png", "trace_path": "/data/traces/20250101_home.zip" }写入数据库后,告警规则可以这样设计:单次失败仅记录,连续三次失败才推送钉钉/企业微信通知,避免偶发抖动打扰人。失败时自动附带截图和trace链接,运维收到告警就能直接点开看,省去“先登录服务器看日志”这一步,效率提升非常明显。
5.4 稳定运行的经验
这里分享几条让我少踩很多坑的经验:
第一条:定时重启浏览器上下文。长驻进程跑久了,Chromium的内存占用会缓慢上升,有的页面还会积累异常状态。我在每个巡检任务结束后强制关闭context,必要时重启浏览器实例,内存曲线稳定很多。
第二条:固定浏览器启动参数。巡检场景建议headless=True,同时加上--disable-dev-shm-usage(容器环境必备)、--no-sandbox(root用户运行必备)等参数。这些不写,在Docker里大概率会遇到莫名其妙的白屏或崩溃。
第三条:单独处理登录态。巡检需要登录的页面时,不要每次都走登录流程——又慢又容易触发风控。我在本地用context.storage_state()保存已登录的Cookie和Storage,巡检启动时直接恢复:
context = browser.new_context(storage_state="login_state.json")这样既快又稳。唯一要留心的是登录态过期时间,我一般每天自动重新登录一次并刷新存储文件。
6. 反自动化页面与滑块验证码:合规前提下的工程应对
6.1 常见防护机制做了什么
做巡检或者自动化的人,基本都会碰到“为什么我模拟用户操作却被识别了”的问题。这类防护通常从几个维度做检测:浏览器指纹是否真实、行为轨迹是否像真人、请求头是否完整、是否有自动化特征关键字(比如navigator.webdriver),以及环境是否在常见的数据中心IP段。
热词里有“playwright过瑞数”“滑块验证码”这类搜索,我能理解大家的需求,但这里必须先划清楚一条线:绕过安全防护去抓取数据,既不合规也有法律风险,本文不提供任何绕过方案。我下面分享的,全部是在合规前提下的工程应对思路,适用于你有授权、或者本身就是自己负责的系统。
6.2 在合规边界内的处理思路
第一类是滑块和验证码出现的场景。如果你是做自己的测试环境巡检,最合理的做法是在测试环境关掉验证码开关,或者配置一个万能测试码,而不是在脚本里模拟滑块。去模拟滑块本质上是在对抗安全机制,一旦上线被识别,代价远大于收益。
第二类是必须跑到生产环境但有防护的页面。我的方案是引入人工兜底:自动巡检发现验证码出现时,截图并推送给值班人员人工处理,而不是强行过码。你可以把验证码页面的出现当作一个“待人工介入”的告警事件,这本身就是一条完整的自动化链路。
第三类是降低触发风控的概率。给Playwright加上一致的UA、设置稳定的视口尺寸、浏览器启动时注入一些修正参数,让环境更接近普通用户。同时控制访问频率,从源头上减少触发怀疑的可能。
browser = playwright.chromium.launch( headless=True, args=["--disable-blink-features=AutomationControlled"] ) context = browser.new_context( user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", viewport={"width": 1920, "height": 1080} )这里要特别强调:这样做的目的是让自动化环境更接近正常用户,减少对目标系统的打扰和误判,而不是为了绕过合法防护去获取未授权数据。不要误解用途。
6.3 给巡检项目的一点提醒
如果你的巡检目标是第三方站点,我强烈建议先确认是否有授权或者协议允许。哪怕只是“定时打开看一眼”,对别人服务器来说也是持续的访问压力,最好在非高峰时段低频率执行。
还有个容易被忽略的细节:日志里不要记录绕过类操作步骤。团队协作时如果有人看到脚本里有类似“识别滑块并拖拽”的代码,很容易误用。保持代码库的合规性,对项目长期发展也是一种保护。
7. 下一步:Playwright 与 AI 的组合玩法与我的判断
7.1 Playwright MCP 是怎么回事
MCP(Model Context Protocol)是最近AI工具链里非常热的概念。官方也推出了Playwright的MCP实现,简单说就是:让AI模型通过标准协议直接控制浏览器,完成打开页面、点击、输入、断言等操作。
我实测过几个版本,效果有些超出预期。比如你可以直接用自然语言告诉模型“打开某个页面,点击登录,如果出现报错就截图”,Playwright MCP会把这些指令一步步翻译成浏览器操作。这对探索性测试和临时页面验证很有价值,因为事先不用写脚本,跟着思路走就行。
但目前的限制也很明显:面对复杂的业务流,AI模型可能会走错分支,还不太稳定。所以我的定位是——MCP适合做辅助探索和快速验证,不适合直接替换正式的回归用例。
7.2 与 browser-use 这类 Agent 方案的异同
热词里经常同时出现playwright mcp和browser use mcp,这里也聊聊区别。
browser-use是另一套让AI驱动浏览器的方案,更偏向“Agent模式”——它把浏览器当作一个可交互环境,用LLM来决策下一步动作,比如自动填写表单、自动提取信息。Playwright MCP则更偏向“协议层集成”,是给任何支持MCP的AI客户端提供浏览器操作能力的通用接口。
用一句话概括:browser-use 是一个“开着车的AI司机”,Playwright MCP 是“让AI能借用你的车”的接口标准。前者开箱即用但灵活性弱一些,后者更底层、更通用,适合接入你自己选的LLM或Agent框架。
7.3 我对这件事的应用判断
我认为理解MCP和Agent确实是自动化行业的下一个方向,但要分清主次。对多数团队来说,核心资产仍是你自己的测试用例和巡检逻辑,AI只是辅助工具,不能成为唯一依赖。
我目前比较认可的工作方式是:用自然语言描述测试目标,让Playwright MCP在本地先把脚本骨架生成出来,再由经验丰富的工程师去调整断言、补边界条件。这样既保留了AI的提效价值,又守住了自动化用例的稳定性底线。
如果你手头有Playwright基础,“玩转一下Playwright MCP”确实是不错的下一站——但请记住,自动化的根基永远是“能稳定重复执行”,这个要求不会因为接入了AI而改变。
最后再分享一个我一直在用的实战技巧:每次做稳定性排查,别急着改脚本,先打开trace viewer看一遍完整执行过程,你会发现大部分坑的答案都藏在时间线里。这个习惯帮我省下的时间和精力,远超过学框架本身花掉的那些。