2038 字
约 6 分钟
1
Playwright 入门

Playwright 入门

#module-devops #pattern-context-manager

[!important] 这里不是泛泛的 Playwright 教程,而是围绕本项目怎么写来教——每个概念都从 app/browser.pyapp/douyin.pyapp/sender.py 的真实调用讲起。看完你就能读懂本项目的浏览器层,也能自己写新的自动化脚本。

1. Playwright 是什么

微软开源的跨浏览器自动化框架,用一套 API 驱动真实浏览器(Chromium / Firefox / WebKit)去模拟人的操作:点按钮、填表单、输键盘、上传文件、读页面内容。

关键词 一个比喻
无头(headless) 浏览器"隐身"跑在后台,不弹窗口(本项目 GHA 用 headless=true
有头(headed) 弹出真实窗口,看得见、也方便调试扫码(本项目 login.py 用它)
录制回放(trace) Playwright 录下每步操作+页面快照,事后逐帧回放查 bug(本项目失败产物 traces/*.zip

本项目用到它的三样东西:Chromium 浏览器 + async(异步)API + trace

另有 同步 APIsync_api)。本项目统一用异步 from playwright.async_api import ...。异步的好处:浏览器等待 IO 时不阻塞 Python 事件循环。本笔记默认异步写法

2. 安装

pip install playwright          # 装 Python 库
python -m playwright install chromium   # 装浏览器二进制(必须)
python -m playwright install --with-deps chromium   # GHA/Linux 额外装系统依赖

本机已装好 playwright。若 import playwright 报错,通常是缺浏览器二进制:python -m playwright install chromium

3. 最小骨架:启动 → 操作 → 关闭

本项目的生命线就是 app/browser.py 里的 open_douyin,它把整个生命周期包成一个异步上下文管理器。拆开看核心三层:

from playwright.async_api import async_playwright

async def demo():
    # ① 启动 Playwright 进程 + Chromium 浏览器
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=True)      # 无头
        # ② 上下文(context) = 一个隔离的"浏览器实例/标签隔离舱",装登录态
        context = await browser.new_context(
            viewport={"width": 1440, "height": 1000},          # 视口分辨率
            locale="zh-CN",                                    # 语言
        )
        # ③ 页面(page) = 一个标签页,所有"打点/输入"都发生在它上面
        page = await context.new_page()
        await page.goto("https://example.com", wait_until="domcontentloaded")
        print(await page.title())
        # ④ 自动逐层 close:context → browser → playwright(async with 帮你做)

对照本项目 open_douyin

playwright = await async_playwright().start()          # 不用 with,手动 start
browser   = await playwright.chromium.launch(**launch_args)   # headless / browser_path
context   = await browser.new_context(**context_args)          # viewport + locale + storage_state
page      = await context.new_page()
yield BrowserSession(page, context)                     # 交给上层用
# finally 里逆序: context.close() → browser.close() → playwright.stop()

[!warning] 关闭顺序必须逆序contextbrowserplaywright。本项目干脆用 async with/finally 兜底,保证泄漏。你是新手就无脑用 async with playwright 最省事。

登录态的注入(本项目两种玩法)

  • storage_state(Playwright 原生会话文件):把整个"已登录会话"(cookie + localStorage + origin 数据)一次性塞进 context:

    state = json.load(open("storage-state.json"))
    context = await browser.new_context(storage_state=state)
    

    本项目 scripts/login.py 扫码登录后正是 context.storage_state(path="storage-state.json") 存下来的。

  • cookie:手动 add_cookies 一组 Cookie(本项目 GHA 场景):

    await context.add_cookies([{"name":"sessionid", "value":"...", "domain":".douyin.com", "path":"/"}])
    

4. 找元素:locator(最重要的一块)

Playwright 定位页面的核心是 locator(定位器)——它不急着拿元素,而是"声明我要找什么",到真正 click/fill 时才去等。

CSS / 文本 selector

page.locator('input[placeholder*="搜索"]')     # CSS 属性子串匹配(本项目 SEARCH_INPUTS)
page.locator('[data-e2e="msg-item-content"]')  # 带稳定属性 data-e2e 的元素
page.locator("body").inner_text()              # 读整页可见文本

语义化 API(首选,比 CSS 更稳)

方法 找什么 本项目例子
get_by_text("好友", exact=True) 可见文本 好友定位
get_by_role("button", name="发送", exact=True) 按钮by语义角色 图片发送按钮
get_by_role("img", name="比心", exact=True) 图片by可访问名 表情定位
send_btn = page.get_by_role("button", name="发送", exact=True)
if await send_btn.count() and await send_btn.first.is_visible():
    await send_btn.first.click()

过滤 / 精确取子集

panel.locator('.emojiEmojiItememojiItem').filter(has_text="比心")  # 按含文本过滤
page.locator(selector).first        # 取第一个匹配
page.locator(selector).nth(index)   # 取第 N 个(从0)

等待 —— wait_for(替代瞎 sleep)

Playwright 会自动等待"匹配到元素/可操作",但显式等状态更稳:

await page.locator(selector).first.wait_for(state="visible", timeout=15_000)  # 可见
await page.wait_for_timeout(1_500)   # 本项目常用:给动画/渲染留时间
await page.wait_for_function("... JS 表达式 ...", arg=[...])  # 条件是任一段页面 JS

[!tip] 新手常见误区:用固定 sleep 硬等。正确思路是 wait_for(等元素出现)或 wait_for_function(等某条件成立)。本项目文字发送就是等"消息数变多 + 含内容":

await page.wait_for_function("([s,c,t]) => {...}", arg=[selector, before, content], timeout=10_000)

5. 操作元素:click / fill / 键盘

await locator.click()                                   # 普通点击(会等可点击)
await locator.click(force=True)                         # 强制点(绕过遮罩层检查)
await locator.evaluate("el => el.click()")              # 原生 JS 点击(本项目绕透明遮罩)
await locator.fill("")                                   # 清空
await locator.fill("好友名")                            # 填文字(输入框)
await page.keyboard.insert_text(content)                 # 一次敲入整段文本
await page.keyboard.press("Enter")                       # 按回车(发消息)

[!warning] 真实页面上 Playwright 的"可点击性"检查常被透明遮罩/滚动容器干扰。本项目专门用 element.evaluate("el => el.click()") 绕开误判(见 app/douyin.py)。如果你是新手,先会 click(),踩到"明明能点却报 timeout"再上这招。

6. 文件上传 set_input_files

不用模拟拖拽,直接把本地文件喂给文件 input(本项目图片消息):

await page.locator('input[type="file"][accept*="image"]').first.set_input_files("path/to/img.png")

7. Trace:录下每一步(排查神器)

await context.tracing.start(screenshots=True, snapshots=True, sources=False)
# ... 跑你的操作 ...
await context.tracing.stop(path="traces/demo.zip")
# 回放:
#   python -m playwright show-trace traces/demo.zip

本项目 save_trace 同款;失败时把 .zip 带回来逐帧看页面到底发生了什么。

8. 本项目「有序兜底」哲学(读懂的钥匙)

抖音前端经常改版,所以本项目从不 等一个 selector 找不到就抛错,而是一串候选挨个试

async def first_visible(page, selectors, timeout_ms=15_000):
    per = max(500, timeout_ms // max(1, len(selectors)))   # 每个候选分得一部分超时
    for selector in selectors:
        try:
            locator = page.locator(selector).first
            await locator.wait_for(state="visible", timeout=per)
            return locator                                   # 碰到第一个能用的,短路返回
        except Exception:
            continue
    raise PageOperationError(f"找不到页面元素,已尝试: {', '.join(selectors)}")

配合同模块的「从专用按钮 → 整行 → 文本 → 父级 → 属性 → 兜底放弃」退化链(DouyinChat._search_result),页面怎么改都能兜住。

[!tip] 写自己的自动化脚本时,复制这套 first_visible 思路:把可用的选择器都放进元组,按优先级排。 页面改版后,往元组头部加新选择器即可,无需改业务逻辑。

9. 结合本项目的「最小可用脚本」模板

把下面这段读懂,你就能开始写自己的自动化(仿 scripts/login.py):

from playwright.async_api import async_playwright

async def run_once():
    async with async_playwright() as p:
        browser = await p.chromium.launch(headless=False)      # 有头,便于看
        page = await (await browser.new_context(locale="zh-CN")).new_page()
        await page.goto("https://www.douyin.com/chat", wait_until="domcontentloaded")
        # 等搜索框 → 输入 → 点开 → 发消息 ... (用上面学的 locator + fill + Enter)
        await browser.close()

# import asyncio; asyncio.run(run_once())

常见坑速查

症状 原因 解法
browserType.launch 报找不到浏览器 没装浏览器二进制 python -m playwright install chromium
click() 一直 timeout 但人眼能点 透明遮罩挡住了可点击性检查 click(force=True)evaluate("el=>el.click()")
元素存在但不 visible 在折叠/滚动区外 scroll_into_view_if_needed() 再等
sleep 硬等总不稳 应等状态而非固定时长 wait_for / wait_for_function
trace 无法回放 没装 trace 回放或路径错 python -m playwright show-trace <无中文路径的.zip>

官方参考资料(外部)

用途 地址
Python 异步 API 参考 https://playwright.dev/python/docs/api/class-types
定位器概念 https://playwright.dev/python/docs/locators
动作 wait_for / fill https://playwright.dev/python/docs/input
  • 本项目的浏览器生命周期与登录检测 → [[Browser]]
  • 它的好友搜索定位(多级兜底) → [[DouyinChat]]
  • 它的发送与发送后确认 → [[Sender]]
  • 把所有选择器集中管理的隔离层 → [[Selectors]]
  • 失败排查现场 → [[DevOps]]
Playwright 入门
http://clxhxhhr.top/posts/457/
作者
clxstart
发布于
2026-09-04
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。