当前位置:首页 > 编程开发 > 正文

Playwright 实战:登录、翻页、截图对比,三件最常做的事

约 6429 字 · 预计阅读 17 分钟
一句话总结:Playwright 解决的是「这个网页操作我每周要做一遍」的问题。这篇给三个可直接改用的模板:登录态复用(不用每次重新登录)、翻页采集(等对了才不丢数据)、截图对比(改版了能第一时间发现)。

先说它和 Selenium 的差别,因为这决定你要不要换。Playwright 自带浏览器、自带等待机制、原生支持文件下载和网络拦截,装一次 playwright install 就完事。代价是包大(几百 MB),但省下的是调驱动的痛苦。

前置

pip install playwright
playwright install chromium

只要 Chromium 就够跑大部分场景,需要测 Safari 内核时再加 webkit。

最小脚本

from playwright.sync_API import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto("https://example.com", wait_until="domcontentloaded")
    print(page.title())
    page.screenshot(path="shot.png", full_page=True)
    browser.close()

headless=False 建议先用来看清它在干什么,跑通了再改 True。

模板一:登录态复用

最烦的场景是「每天跑一次,每次都要登录」。登录本身不难,难的是验证码、二次验证、以及频繁登录触发的风控。

解法是把登录态存下来复用:

from playwright.sync_api import sync_playwright

def login_and_save():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=False)
        ctx = browser.new_context()
        page = ctx.new_page()
        page.goto("https://example.com/login")

        page.get_by_label("用户名").fill("your_user")
        page.get_by_label("密码").fill("your_pass")
        page.get_by_role("button", name="登录").click()

        # 等到登录后的特征元素出现,再保存状态
        page.get_by_role("navigation").wait_for(timeout=30_000)
        ctx.stoRAGe_state(path="auth.json")
        browser.close()

def reuse():
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        ctx = browser.new_context(storage_state="auth.json")
        page = ctx.new_page()
        page.goto("https://example.com/dashboard")
        print(page.title())
        browser.close()

storage_state 存的是 cookie 和 localStorage,一个 JSON 文件。这个文件等于你的登录凭证,别提交到 git,最好加进 .gitignore 并限制文件权限。

登录态会过期。工程上的处理是:先试复用,检测到被踢回登录页就自动走一次登录流程。这比「每n天手动更新一次」靠谱。

模板二:翻页采集

翻页的核心不是点击,是等待。点完下一页立刻取数据,拿到的还是上一页的内容,这是最常见的错误。

def crawl_list(uRL, pages=5):
    rows = []
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        page = browser.new_page()
        page.goto(url, wait_until="domcontentloaded")

        for i in range(pages):
            # 等表格出现并稳定
            page.locator("table tbody tr").first.wait_for(timeout=15_000)
            for tr in page.locator("table tbody tr").all():
                cells = tr.locator("td").all_inner_texts()
                if cells:
                    rows.append(cells)

            nxt = page.get_by_role("button", name="下一页")
            if not nxt.is_enabled():
                break
            # 关键:等 URL 或首行内容变化,再进入下一轮
            first_before = page.locator("table tbody tr").first.inner_text()
            nxt.click()
            page.wait_for_function(
                "prev => document.querySelector('table tbody tr')"
                " && document.querySelector('table tbody tr').innerText !== prev",
                arg=first_before, timeout=15_000,
            )
        browser.close()
    return rows

那个 wait_for_function 是这段的核心。它等的不是「点击完成」,而是「内容真的变了」。用 sleep(1) 也能跑,但网慢就丢数据,网快就白等。

模板三:截图对比

页面改版、样式错乱、按钮被遮挡,肉眼很难每次发现。截图对比能自动盯住:

from PIL import Image, ImageChops

def diff_ratio(a_path, b_path):
    a = Image.open(a_path).convert("RGB")
    b = Image.open(b_path).convert("RGB")
    if a.size != b.size:
        return 1.0                       # 尺寸变了,视为完全不同
    diff = ImageChops.difference(a, b).convert("L")
    changed = sum(1 for v in diff.getdata() if v > 12)
    return changed / (a.size[0] * a.size[1])

用法是:第一次跑存基线,之后每次跑对比,变化超过阈值(比如 2%)就告警。

两个前提必须满足:同一浏览器版本、同一视口尺寸、同一无头模式。这三点任意一个变了,截图就会整体偏移,误报一片。

我自己的经验是,截图对比比断言元素存在更早发现「页面视觉崩了」这类问题,因为它不关心你的选择器,只看像素。

图 1:左为 Browser → Context → Page 的层级关系,右为三个模板的关键动作

定位器:优先用角色和文字

选择器的稳定性直接决定脚本的寿命。

优先级从高到低:

  1. get_by_role("button", name="提交"):语义定位,最稳
  2. get_by_label("邮箱"):表单首选
  3. get_by_text("订单已创建"):适合断言
  4. get_by_test_id("subMIT-btn"):前端配合加 data-testid,最可控
  5. CSS / XPath:兜底方案,div > div:nth-child(3) 这种活不过一次改版

nth-child 这类索引选择器是最脆的。前端加了一个隐藏元素,你的脚本就全错位。

坑清单

坑一:用 time.sleep 等加载。 网快时白等,网慢时丢数据。全部换成 wait_for_selector、wait_for_function 或 expect(...)。

坑二:无头和有头模式截图不一致。 字体渲染、滚动条宽度都可能不同。做截图对比时,两种模式不能混着建基线。

坑三:下载文件没接住。 点下载按钮后要包在 with page.expect_download() as d: 里,否则文件落在临时目录被清掉。

with page.expect_download() as dl:
    page.get_by_role("button", name="导出").click()
dl.value.save_as("report.xlsx")

坑四:元素在 iframe 里。 直接 page.locator 找不到。要用 page.frame_locator("#iframe-id").get_by_role(...)。

坑五:把登录态提交进了代码库。 auth.json 里有完整 cookie,泄露等于账号泄露。加 .gitignore,CI 里用环境变量或密钥管理。

坑六:遇到 Cloudflare 硬刚。 这类反爬不是「加个 UA」能过的。要么用官方 API,要么放慢频率,要么放弃。硬绕既不划算也容易踩合规线。

边界

自动化只对你有权操作的站点做。给你自己的后台、你公司的系统、明确允许自动化的公开数据源,这些没问题。

需要注意的三条:目标站点的服务条款是否允许自动化;请求频率会不会影响对方服务;抓到的数据怎么存、存多久。这三点比技术实现更值得先想清楚。顺带一提:如果目标是拿接口数据而不是操作页面,抓包工具比浏览器自动化直接得多,见抓包三件套那篇。

FAQ

和 Selenium 比该选哪个?

新项目选 Playwright。它的自动等待机制省掉大量 WebDriverWait 样板代码,调试工具(playwright codegen 录制)也好用得多。

能跑在服务器上吗?

能,Linux 上装依赖后 headless=True 直接跑。Docker 里推荐用官方镜像 mcr.microsoft.com/playwright,不用自己装库。

会被识别成爬虫吗?

默认 UA 里带 HeadlessChrome 字样,容易被识别。可以自定义 UA 和视口,但别指望能绕过成熟的反爬系统。

怎么调试最有效率?

两个工具:playwright codegen <url> 录制操作生成代码;PWDEBUG=1 启动时逐步执行并暂停。比打日志快。

能处理验证码吗?

图形验证码需要额外识别服务,滑块类要模拟轨迹。我的建议是尽量避开:用登录态复用、用官方 API、或者让人手动过那一次。

结语

Playwright 的价值是把「人的操作」变成「可重复的脚本」。

三个模板覆盖了大多数需求:登录态解决「每次都要登录」,等待策略解决「数据不对」,截图对比解决「变化没人发现」。

先把这三件跑通,剩下的都是查文档。

参考资料

目录导航
  • 前置
  • 最小脚本
  • 模板一:登录态复用
  • 模板二:翻页采集
  • 模板三:截图对比
  • 定位器:优先用角色和文字
  • 坑清单
  • 边界
  • FAQ
  • 结语
    • 参考资料
  • 相关文章:

  • 写一个最小的 Agent 循环:80 行代码,和三个必须处理的边界2026-10-28 08:40:18
  • 从零写一个 MCP 服务器:让你的脚本变成 AI 能直接调的工具2026-10-28 08:40:18
  • RAG 实战:几百行代码搭一个自己的知识库问答(Python + SQLite,不碰框架)2026-10-28 08:40:18
  • MCP Server 开发实战:从零用 Python 给自己写一个 AI 工具,附完整代码与避坑清单2026-10-28 08:40:18
  • 发表评论

    ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。