Playwright 入门
#module-devops #pattern-context-manager
[!important] 这里不是泛泛的 Playwright 教程,而是围绕本项目怎么写来教——每个概念都从
app/browser.py、app/douyin.py、app/sender.py的真实调用讲起。看完你就能读懂本项目的浏览器层,也能自己写新的自动化脚本。
1. Playwright 是什么
微软开源的跨浏览器自动化框架,用一套 API 驱动真实浏览器(Chromium / Firefox / WebKit)去模拟人的操作:点按钮、填表单、输键盘、上传文件、读页面内容。
| 关键词 | 一个比喻 |
|---|---|
| 无头(headless) | 浏览器"隐身"跑在后台,不弹窗口(本项目 GHA 用 headless=true) |
| 有头(headed) | 弹出真实窗口,看得见、也方便调试扫码(本项目 login.py 用它) |
| 录制回放(trace) | Playwright 录下每步操作+页面快照,事后逐帧回放查 bug(本项目失败产物 traces/*.zip) |
本项目用到它的三样东西:Chromium 浏览器 + async(异步)API + trace。
另有 同步 API(
sync_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] 关闭顺序必须逆序:
context→browser→playwright。本项目干脆用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 |
Related Notes
- 本项目的浏览器生命周期与登录检测 → [[Browser]]
- 它的好友搜索定位(多级兜底) → [[DouyinChat]]
- 它的发送与发送后确认 → [[Sender]]
- 把所有选择器集中管理的隔离层 → [[Selectors]]
- 失败排查现场 → [[DevOps]]