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 的取舍

维度PlaywrightSelenium
速度快,协议少一次往返,启动与执行都更利落较慢,浏览器启动与命令往返开销大
API 现代程度locator 自动等待、断言内置、同步异步同构显式等待需手写,API 偏底层
调试与录制Trace Viewer、Inspector、代码生成依赖日志与截图,能力较弱
生态自带内核、版本自洽,社区相对年轻驱动由厂商提供,历史项目与云测平台支持更广

结论:新写的采集与端到端脚本优先用 Playwright,维护存量 Selenium 资产或必须对接只支持 Selenium 的云测平台时继续用 Selenium。两者都要遵守 robots.txt 与站点服务条款,控制请求频率与并发,不采集个人隐私与受版权保护的付费内容,数据使用遵守《个人信息保护法》《数据安全法》《著作权法》。

小结:先用 playwright install 把内核与依赖装齐,靠自动等待和 expect 断言替代固定 sleep,用 page.route 只做观察、裁剪与自建环境下的测试,用截图和 Trace 留证据,并把上下文当作隔离与登录态复用的单元;浏览器很重,能用接口就不要开浏览器。

笔记加载中…