Playwright 进阶:等待、拦截与截图
Playwright 把等待做成了默认行为:大多数操作会自动等到元素可交互,固定 sleep 从必写项变成例外。本章讲清安装与浏览器下载、sync_api 骨架、等待策略、请求拦截与截图用法,以及它和 Selenium 之间怎么选。
安装与浏览器下载
pip install playwright
playwright install chromium # 只装 Chromium,体积最小
playwright install --with-deps chromium # 无头服务器上补齐系统依赖库
pip install playwright 只装 Python 包,内核要单独下载,这是第一个坑:报「Executable doesn't exist」不是代码问题,是漏了 playwright install。精简镜像与 CI 容器还缺 libnss3、字体等系统库,用 --with-deps 让 Playwright 调系统包管理器补齐。
Docker 里更省事的做法是用官方镜像 mcr.microsoft.com/playwright/python,浏览器与依赖都已装好,把 pip install playwright 的版本与镜像标签对齐即可,省去自己补字体和依赖的时间。
sync_api 基本骨架
同步与异步 API 的调用方式几乎一样,只是少了 await;采集脚本用 sync_api 更好读,要在一个进程里管几百个页面时再换 async_api。
| 调用 | 作用 | 注意点 |
|---|---|---|
sync_playwright() | 启动 Playwright 运行时 | 用 with 管理,退出时自动清理 |
chromium.launch(headless=True) | 启动浏览器进程 | 生产默认无头,排查时临时开有界面 |
browser.new_context(...) | 创建隔离上下文 | Cookie、localStorage、缓存互不影响 |
context.new_page() | 新建标签页 | 一个上下文可开多个页面 |
page.goto(url, wait_until="networkidle") | 打开页面并等待 | 等网络静默,慢但对 SPA 稳 |
等待策略:让自动等待干活
| 写法 | 等到什么 | 适用场景 |
|---|---|---|
page.wait_for_selector(sel, state="visible") | 选择器匹配的元素可见 | 等列表项或按钮出现 |
page.wait_for_load_state("domcontentloaded") | DOM 构建完成 | 只需要文档结构 |
page.wait_for_load_state("load") | 资源加载完成 | 需要图片、样式就位 |
page.wait_for_load_state("networkidle") | 500 毫秒内无新请求 | SPA 首屏数据拉取完成 |
expect(locator).to_be_visible() | 断言可见,失败带重试与截图 | 关键环节的显式校验 |
locator.click()、locator.inner_text() | 元素可交互后自动执行与取值 | 默认行为,无需额外等待 |
自动等待比固定 sleep 可靠的原因很实在:它按条件是否满足判断,页面快就快走、慢就多等,不会本地跑 2 秒而到服务器上因网络抖动全部失败,失败时异常还会说明卡在哪个选择器。networkidle 别滥用:有长轮询或心跳请求的页面永远等不到网络静默,这种情况改用 domcontentloaded 加具体的元素等待。
路由拦截:观察、裁剪与提速
page.route 在请求发出前介入,用来做三件正当的事:只读观察请求参数、在自建测试环境里改写响应体、屏蔽图片与字体等静态资源以提速。
import json
def on_api(route) -> None:
if "/mock/" in route.request.url: # 只在自建测试环境里改写响应体
route.fulfill(status=200, content_type="application/json", body=json.dumps({"items": []}))
else:
route.continue_() # 其余请求原样放行,只做观察
page.route("**/api/**", on_api)
page.route("**/*.{png,jpg,webp,woff2}", lambda route: route.abort()) # 屏蔽静态资源
拦截的边界要说清楚:它可以少下载无用的图片、可以在自己的测试环境里构造边界数据,但不得用来绕过登录、付费墙、验证码等访问控制,也不要用它伪造请求头冒充别的客户端;只采集公开可访问的内容,涉及账号时只自动化自己的账号。
截图与上下文隔离
截图是排错性价比最高的手段:超时或断言失败时先看一眼图,往往就能判断是被弹窗挡住还是数据区域为空。
page.screenshot(path="shot.png", full_page=True) # 整页长图,适合留证
page.locator("#list .item").first.screenshot(path="card.png") # 只截某个元素
context 之间 Cookie、localStorage、缓存、代理全部独立,这是 Playwright 比 Selenium 更好用的地方之一。关闭顺序是先 page.close()、再 context.close()、最后 browser.close(),用 with 语句能让顺序自动正确。要把登录态带到下一个任务,导出 storage_state 后加载进新上下文,比在一个上下文里反复开页面更干净,也避免并发任务之间互相污染。
完整可运行脚本
from __future__ import annotations
import time
from playwright.sync_api import sync_playwright
TARGET_URL = "https://example.com/list" # 只采集条款允许抓取的目标
PAGE_LIMIT, PAGE_INTERVAL = 2, 3.0 # 页数上限与翻页间隔,给目标站点留余量
BLOCKED = ("image", "font", "media") # 丢掉与数据无关的静态资源
def main() -> None:
with sync_playwright() as pw: # with 负责清理运行时
browser = pw.chromium.launch(headless=True)
context = browser.new_context(viewport={"width": 1440, "height": 900})
context.route(
"**/*",
lambda route: route.abort()
if route.request.resource_type in BLOCKED # 只裁剪静态资源,不改动业务请求
else route.continue_(),
)
page = context.new_page()
rows: list[dict[str, str]] = []
for idx in range(1, PAGE_LIMIT + 1):
page.goto(f"{TARGET_URL}?p={idx}", wait_until="domcontentloaded")
page.wait_for_selector("#list .item", state="visible") # 自动等待,不用 sleep 猜
cards = page.locator("#list .item")
for i in range(cards.count()):
rows.append({"title": cards.nth(i).locator(".title").inner_text()})
page.screenshot(path=f"shot-page{idx}.png", full_page=True) # 留一份可核对的现场
time.sleep(PAGE_INTERVAL) # 主动降速,尊重目标站点的负载
print(f"共采集 {len(rows)} 条", rows[:3])
page.close() # 关闭顺序:page → context → browser
context.close()
browser.close()
if __name__ == "__main__":
main()
常见坑与调试
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
报 Executable doesn't exist | 只装了 Python 包,没下载内核 | 执行 playwright install chromium |
| 容器内启动即退出 | 缺系统依赖或沙箱受限 | 用 --with-deps 或官方 Python 镜像 |
停在 networkidle 直到超时 | 页面有长轮询、心跳请求 | 改 domcontentloaded 加元素等待 |
| 取到空字符串 | 用 inner_text() 读了隐藏元素 | 先 expect(locator).to_be_visible() |
| 拦截后样式错乱 | 屏蔽了页面渲染必需的资源 | 只拦图片、字体、媒体 |
调试三件套:PWDEBUG=1 python script.py 打开 Inspector 逐步执行、headless=False 直接看过程、截图与 page.content() 落盘留证。要在 CI 里复盘偶发失败,用官方 Trace 更省事:browser.start_tracing() 开始,context.tracing.stop(path="trace.zip") 结束,再用 playwright show-trace trace.zip 打开,每一步操作、每一个请求、每一帧都能回看。
与 Selenium 的取舍
| 维度 | Playwright | Selenium |
|---|---|---|
| 速度 | 快,协议少一次往返,启动与执行都更利落 | 较慢,浏览器启动与命令往返开销大 |
| API 现代程度 | locator 自动等待、断言内置、同步异步同构 | 显式等待需手写,API 偏底层 |
| 调试与录制 | Trace Viewer、Inspector、代码生成 | 依赖日志与截图,能力较弱 |
| 生态 | 自带内核、版本自洽,社区相对年轻 | 驱动由厂商提供,历史项目与云测平台支持更广 |
结论:新写的采集与端到端脚本优先用 Playwright,维护存量 Selenium 资产或必须对接只支持 Selenium 的云测平台时继续用 Selenium。两者都要遵守 robots.txt 与站点服务条款,控制请求频率与并发,不采集个人隐私与受版权保护的付费内容,数据使用遵守《个人信息保护法》《数据安全法》《著作权法》。
小结:先用 playwright install 把内核与依赖装齐,靠自动等待和 expect 断言替代固定 sleep,用 page.route 只做观察、裁剪与自建环境下的测试,用截图和 Trace 留证据,并把上下文当作隔离与登录态复用的单元;浏览器很重,能用接口就不要开浏览器。