Playwright 完全指南
相关文档:Selenium完全指南(/selenium-wan-quan-zhi-nan/) DrissionPage完全指南(/drissionpage-wan-quan-zhi-nan/) asyncio异步编程完全指南(/asyncio-yi-bu-bian-cheng-wan-quan-zhi-nan/) Playwright 是微软开源的现代端到端测试和自动化框架,支持 Chromium、Firefox、WebKit 三种浏览器引擎。 Playwright 提供两套 API: Locator 有内置重试,比直接调用 page.click 更稳定
官方文档:https://playwright.dev/python/
适用版本:playwright 1.44+(2026-05-07 核实)
相关文档:Selenium完全指南 DrissionPage完全指南 asyncio异步编程完全指南
1. 基础概念
Playwright 是什么
Playwright 是微软开源的现代端到端测试和自动化框架,支持 Chromium、Firefox、WebKit 三种浏览器引擎。
| 特性 | Playwright | Selenium |
|---|---|---|
| 浏览器支持 | Chromium/Firefox/WebKit | 所有主流浏览器 |
| 异步支持 | 原生异步 API | 需要额外库 |
| 自动等待 | 内置智能等待 | 需手动等待 |
| 网络拦截 | 内置 | 需插件 |
| 截图/视频 | 内置 | 需插件 |
| CDP 支持 | 是 | 有限 |
| 并行执行 | 原生支持 | 需要 Grid |
安装
pip install playwright
# 安装浏览器(Chromium + Firefox + WebKit)
playwright install
# 只安装 Chromium(爬虫常用)
playwright install chromium
# 安装系统依赖(Linux)
playwright install-deps
2. 同步 vs 异步 API
Playwright 提供两套 API:
# 同步(适合脚本和简单爬虫)
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto("https://example.com")
print(page.title())
browser.close()
# 异步(适合高并发爬虫)
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
page = await browser.new_page()
await page.goto("https://example.com")
print(await page.title())
await browser.close()
asyncio.run(main())
3. 启动配置
from playwright.async_api import async_playwright
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True, # 无头模式
slow_mo=100, # 每步操作延迟 100ms(调试用)
args=[
"--no-sandbox",
"--disable-blink-features=AutomationControlled",
],
proxy={"server": "http://127.0.0.1:7890"},
)
# 创建浏览器上下文(隔离的 cookie/存储)
context = await browser.new_context(
user_agent="Mozilla/5.0 ...",
viewport={"width": 1280, "height": 720},
locale="zh-CN",
timezone_id="Asia/Shanghai",
geolocation={"latitude": 39.9, "longitude": 116.4},
permissions=["geolocation"],
ignore_https_errors=True, # 忽略 SSL 错误
java_script_enabled=True,
record_video_dir="videos/", # 录制视频
record_har_path="trace.har", # 录制 HAR
)
page = await context.new_page()
使用已有浏览器 Profile
context = await browser.new_context(
storage_state="state.json", # 从文件加载已保存的登录状态
)
# 保存当前登录状态
await context.storage_state(path="state.json")
4. 元素定位
# CSS 选择器(默认)
await page.click("#submit")
await page.fill("input[name='email']", "[email protected]")
# 文本内容
await page.click("text=登录")
await page.click("text=登录", exact=True)
# 角色(Accessible Role,推荐用于测试)
await page.click("role=button[name='提交']")
await page.click("role=link[name='首页']")
# 占位符
await page.fill("placeholder=请输入邮箱", "[email protected]")
# XPath
await page.click("xpath=//button[@type='submit']")
# 链式定位(在某个容器内查找)
form = page.locator("form#login")
await form.locator("input[name='email']").fill("[email protected]")
await form.locator("button[type='submit']").click()
Locator — 推荐的定位方式
# Locator 是惰性的,创建时不执行查找
email_input = page.locator("input[name='email']")
submit_btn = page.locator("button[type='submit']")
# 操作时才执行
await email_input.fill("[email protected]")
await submit_btn.click()
# 过滤
items = page.locator(".item")
active_items = items.filter(has_text="active")
item_with_btn = items.filter(has=page.locator("button.delete"))
# 获取第 n 个
first = items.first
last = items.last
nth = items.nth(2) # 第 3 个(从 0 开始)
# 断言(内置重试)
await expect(page.locator(".success-msg")).to_be_visible()
await expect(page.locator("#count")).to_have_text("42")
5. 页面操作
# 导航
await page.goto("https://example.com")
await page.goto("https://example.com", wait_until="networkidle") # 等到网络空闲
await page.go_back()
await page.go_forward()
await page.reload()
# 等待
await page.wait_for_selector("#result") # 等待元素出现
await page.wait_for_selector("#loading", state="hidden") # 等待元素消失
await page.wait_for_url("**/dashboard") # 等待 URL 变化
await page.wait_for_load_state("networkidle") # 等待网络空闲
await page.wait_for_timeout(1000) # 等待 1 秒
# 输入
await page.fill("#email", "[email protected]")
await page.type("#search", "python", delay=50) # 逐字符输入(模拟人工)
await page.press("#search", "Enter")
await page.keyboard.press("Tab")
# 点击
await page.click("#btn")
await page.click("#btn", button="right") # 右键
await page.dblclick("#item") # 双击
await page.hover("#menu") # 悬停
# 选择框
await page.select_option("select#city", "Beijing")
await page.select_option("select#city", label="北京")
# 上传文件
await page.set_input_files("input[type=file]", "photo.jpg")
await page.set_input_files("input[type=file]", ["file1.jpg", "file2.jpg"])
# 截图
await page.screenshot(path="page.png", full_page=True)
await page.locator("#chart").screenshot(path="chart.png")
# 执行 JS
result = await page.evaluate("() => document.title")
await page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
# 获取内容
text = await page.inner_text("#content")
html = await page.inner_html("#content")
value = await page.input_value("#email")
attr = await page.get_attribute("a.link", "href")
6. 网络拦截
# 拦截并修改请求
async def handle_route(route):
if route.request.resource_type == "image":
await route.abort() # 拦截图片请求(加速)
else:
await route.continue_() # 放行其他请求
await page.route("**/*", handle_route)
# 修改响应
async def mock_api(route):
await route.fulfill(
status=200,
content_type="application/json",
body='{"code": 200, "data": []}',
)
await page.route("**/api/list*", mock_api)
# 监听请求
page.on("request", lambda req: print(f">> {req.method} {req.url}"))
page.on("response", lambda res: print(f"<< {res.status} {res.url}"))
# 等待特定请求/响应
async with page.expect_response("**/api/data*") as response_info:
await page.click("#load-btn")
response = await response_info.value
data = await response.json()
7. 并发爬虫
import asyncio
from playwright.async_api import async_playwright
async def scrape_page(browser, url: str) -> dict:
context = await browser.new_context()
page = await context.new_page()
try:
await page.goto(url, timeout=30000)
await page.wait_for_selector(".content")
return {
"url": url,
"title": await page.title(),
"content": await page.inner_text(".content"),
}
except Exception as e:
return {"url": url, "error": str(e)}
finally:
await context.close()
async def main(urls: list[str], concurrency: int = 5):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
sem = asyncio.Semaphore(concurrency)
async def bounded_scrape(url):
async with sem:
return await scrape_page(browser, url)
results = await asyncio.gather(*[bounded_scrape(url) for url in urls])
await browser.close()
return results
8. 反检测
from playwright.async_api import async_playwright
async with async_playwright() as p:
browser = await p.chromium.launch(
args=["--disable-blink-features=AutomationControlled"],
)
context = await browser.new_context(
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
)
page = await context.new_page()
# 注入 stealth 脚本
await page.add_init_script("""
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3] });
Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh'] });
window.chrome = { runtime: {} };
""")
# 使用 playwright-stealth 库(更完整)
# pip install playwright-stealth
from playwright_stealth import stealth_async
await stealth_async(page)
9. 常用代码段
自动登录并保存状态
async def login_and_save(email: str, password: str, state_file: str = "state.json"):
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
context = await browser.new_context()
page = await context.new_page()
await page.goto("https://example.com/login")
await page.fill("input[name='email']", email)
await page.fill("input[name='password']", password)
await page.click("button[type='submit']")
await page.wait_for_url("**/dashboard")
await context.storage_state(path=state_file)
await browser.close()
print(f"登录状态已保存到 {state_file}")
处理弹窗
# 自动接受所有 confirm/alert
page.on("dialog", lambda dialog: asyncio.ensure_future(dialog.accept()))
# 或手动处理
async with page.expect_event("dialog") as dialog_info:
await page.click("#delete-btn")
dialog = await dialog_info.value
print(dialog.message)
await dialog.accept()
下载文件
async with page.expect_download() as download_info:
await page.click("#download-btn")
download = await download_info.value
await download.save_as("output/file.xlsx")
10. 最佳实践
优先使用 Locator 而非 page.click/fill
Locator 有内置重试,比直接调用 page.click 更稳定:
# 推荐
await page.locator("#submit").click()
# 不推荐(找不到时直接报错,没有重试)
element = await page.query_selector("#submit")
await element.click()
设置合理超时
# 全局超时(默认 30 秒)
page.set_default_timeout(60000)
page.set_default_navigation_timeout(60000)
# 单次操作超时
await page.click("#btn", timeout=5000)
爬虫禁用不必要的资源加载
await page.route("**/*.{png,jpg,jpeg,gif,svg,css,font}", lambda r: r.abort())
11. 踩坑与注意事项
异步上下文必须正确关闭
如果不关闭 browser/context,进程退出时会有资源泄漏,始终使用 async with 或显式 await browser.close()。
page.goto 的 wait_until 选项
| 值 | 触发时机 |
|---|---|
"commit" |
收到响应头(最快) |
"domcontentloaded" |
DOM 解析完成 |
"load" |
所有资源加载完成(默认) |
"networkidle" |
500ms 内无网络请求(最慢,SPA 常用) |
SPA 页面用 "networkidle" 或等待特定元素,而不是 "load"。
最佳实践
用 Page Object Model(POM)组织测试代码:将页面操作封装为类,测试用例只调用方法,不直接操作 locator。页面结构变化时只需修改 POM 类,测试用例无需改动。
class LoginPage:
def __init__(self, page):
self.page = page
self.email_input = page.get_by_placeholder("Email")
self.password_input = page.get_by_placeholder("Password")
self.submit_btn = page.get_by_role("button", name="Login")
async def login(self, email: str, password: str):
await self.email_input.fill(email)
await self.password_input.fill(password)
await self.submit_btn.click()
await self.page.wait_for_url("**/dashboard")
优先用语义化定位器(role、label、text)而非 CSS 选择器:get_by_role("button", name="Submit") 比 locator(".btn-primary") 更健壮,不受样式重构影响,也符合无障碍规范。
用 expect() 断言替代手动等待:Playwright 的 expect() 内置自动重试(默认 5 秒),比 wait_for_selector() + assert 更简洁,失败时提示信息也更友好。
from playwright.async_api import expect
await expect(page.get_by_text("Welcome")).to_be_visible()
await expect(page.get_by_role("alert")).to_have_text("Login successful")
CI 中使用 --headed=false + --browser=chromium 固定配置:无头模式减少 CI 资源消耗;固定浏览器避免不同环境的浏览器差异导致测试结果不一致。
录制脚本作为起点,再手工优化:playwright codegen 可生成初始代码,但生成的 CSS 选择器往往很脆,需要手动替换为语义化定位器。
常见陷阱
陷阱:未关闭 browser/context 造成资源泄漏
现象: 长时间运行后,系统中出现大量僵尸 Chromium 进程,内存持续增长。
原因: playwright.start() 后若不显式 browser.close(),进程会一直保留在内存中。
解决: 始终用 async with 上下文管理器,或在 finally 块中 close。
# 正确:上下文管理器自动清理
async with async_playwright() as pw:
browser = await pw.chromium.launch()
page = await browser.new_page()
await page.goto("https://example.com")
# browser 在退出 with 块时自动关闭
陷阱:wait_for_selector 等待超时在 SPA 页面失效
现象: 等待某个元素出现,但元素虽已渲染却仍超时。
原因: SPA 页面用 JS 动态更新 DOM,"load" 事件早于数据渲染完成触发,元素在网络响应返回前还未出现在 DOM 中。
解决: 使用 wait_for_load_state("networkidle") 或更具体地等待数据填充后才出现的元素。
# 等待网络空闲后再查找元素
await page.wait_for_load_state("networkidle")
await page.locator('[data-testid="product-list"]').wait_for()
陷阱:固定坐标点击在不同分辨率下失效
现象: 开发机上点击正确,CI 服务器上点击到错误位置。
原因: 使用 page.mouse.click(x, y) 硬编码坐标,不同屏幕分辨率或视口大小导致元素位置不同。
解决: 始终通过 locator 定位元素后调用 .click(),由 Playwright 自动计算元素中心坐标。
# 错误:硬编码坐标
await page.mouse.click(400, 300)
# 正确:通过 locator 点击
await page.get_by_role("button", name="Submit").click()