Appearance
章节 3:Playwright 自动化采集
学习目标
- 掌握 Playwright 的安装配置与多浏览器驱动
- 理解 Browser/Context/Page 三层生命周期的关系
- 熟练使用 CSS、XPath、文本和角色定位器选择元素
- 掌握页面交互、表单填写与智能等待策略
- 理解无头模式与浏览器指纹伪装技术
- 实现异步 Playwright 并发采集
3.1 Playwright 概述与安装
3.1.1 什么是 Playwright
定义:Playwright 是微软开发的开源自动化测试库,支持 Chromium、Firefox、WebKit 三大浏览器引擎,提供统一的 API 进行浏览器自动化操作。相比 Selenium,Playwright 具有以下优势:
- 原生异步支持(async/await)
- 自动等待元素就绪,减少显式 sleep
- 多浏览器同一套 API
- 网络拦截与路由控制
- 浏览器上下文隔离(类似独立会话)
- 内置 Trace Viewer、Codegen 等调试工具
3.1.2 安装与环境配置
bash
# 安装 Playwright 库
pip install playwright
# 安装浏览器驱动(首次需要)
playwright install
# 只安装特定浏览器
playwright install chromium
playwright install firefox
playwright install webkit
# 查看版本
playwright --version验证安装:
python
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://www.baidu.com")
print(page.title())
browser.close()3.2 Browser / Context / Page 生命周期
3.2.1 三层架构
定义:Playwright 的三层架构从粗到细管理浏览器资源,理解生命周期是高效使用的基础。
Playwright ─→ Browser ─→ BrowserContext ─→ Page
(入口) (浏览器进程) (独立会话) (标签页)各层职责:
| 层级 | 说明 | 典型操作 |
|---|---|---|
Playwright | 入口点,管理浏览器启动 | sync_playwright() 或 async_playwright() |
Browser | 浏览器进程实例,可启动多个 | launch(), close() |
BrowserContext | 隔离的浏览器会话(类似无痕窗口) | Cookie/Storage 隔离、权限设置 |
Page | 一个标签页,处理页面交互 | goto(), click(), fill() |
3.2.2 Browser——浏览器实例
python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
# 启动 Chromium(带配置)
browser = p.chromium.launch(
headless=False, # 有头模式,方便调试
slow_mo=500, # 操作间延迟 500ms(调试用)
args=[ # 启动参数
"--disable-blink-features=AutomationControlled",
"--disable-dev-shm-usage",
],
)
print(browser.version)
# 浏览器可创建多个上下文
context1 = browser.new_context()
context2 = browser.new_context()
browser.close() # 关闭浏览器3.2.3 BrowserContext——独立会话
定义:BrowserContext 相当于一个隔离的浏览器会话(类似无痕窗口),每个 Context 有独立的 Cookie、localStorage、缓存和代理设置。
python
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=False)
# 创建上下文——指定窗口大小、语言、时区等
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
locale="zh-CN",
timezone_id="Asia/Shanghai",
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36",
# 设置 Cookie
storage_state=None,
# 忽略 HTTPS 错误
ignore_https_errors=False,
)
# 上下文可以持久化 Cookie
page = context.new_page()
page.goto("https://example.com")
# ... 登录操作 ...
context.storage_state(path="auth.json") # 保存登录状态
# 下次启动时加载
context2 = browser.new_context(storage_state="auth.json")
context.close() # 清理上下文(清除 Cookie 等)3.2.4 Page——页面操作
python
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", wait_until="networkidle")
# 页面信息
print(page.url) # 当前 URL
print(page.title()) # 页面标题
print(page.content()) # 完整 HTML
# 截图与 PDF
page.screenshot(path="screenshot.png", full_page=True)
# page.pdf(path="page.pdf") # 仅 Chromium 支持
# 执行 JavaScript
result = page.evaluate("() => document.title")
print(result)
# 获取 Cookie
cookies = page.context.cookies()
print(cookies)
browser.close()3.3 元素定位
3.3.1 CSS 定位器
定义:Playwright 内置了强大的定位器 API,支持 CSS、XPath、文本、角色等多种方式。page.locator() 是推荐的定位方式。
python
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")
# 基础 CSS 定位器
locator = page.locator("#main") # ID
locator = page.locator(".product-list") # class
locator = page.locator("div.product-item") # 标签 + class
locator = page.locator("[data-id='123']") # 属性
locator = page.locator("ul > li:first-child") # 子选择器
locator = page.locator(":has-text('确认')") # 包含文本(非标准)
# 定位器支持链式调用
product = page.locator(".product-item").first # 第一个
product = page.locator(".product-item").nth(2) # 第三个
products = page.locator(".product-item").all() # 所有(返回列表)
# 条件定位器
visible_button = page.locator("button").filter(has_text="提交")
enabled_button = page.locator("button").filter(has=page.locator(":enabled"))
browser.close()3.3.2 XPath 定位器
python
# 使用 XPath 定位(推荐用 // 开头)
page.locator("//div[@id='main']")
page.locator("//input[@type='text']")
page.locator("//button[contains(text(), '登录')]")
page.locator("//div[@class='list']/div[position()<=5]")
# 按文本精确匹配
page.locator("text=登录") # 精确文本
page.locator("text='确认提交'") # 精确文本(含引号)
# 按文本模糊匹配
page.get_by_text("登录", exact=True) # 精确匹配
page.get_by_text("登录", exact=False) # 模糊匹配(默认)3.3.3 角色定位器(ARIA)
定义:Playwright 支持基于 ARIA 角色(role)的定位方式,更接近用户感知,适合可访问性测试。
python
# 角色定位器:get_by_role(role, **kwargs)
page.get_by_role("button", name="提交")
page.get_by_role("link", name="下一页")
page.get_by_role("heading", name="商品列表")
page.get_by_role("textbox", name="用户名")
page.get_by_role("combobox", name="城市")
page.get_by_role("checkbox", name="同意协议")
page.get_by_role("listitem") # 列表项
# 常用角色
# button, link, heading, textbox, checkbox, radio, combobox
# list, listitem, table, row, cell, img, form, navigation3.3.4 其他定位方式
python
# 占位符定位(placeholder)
page.get_by_placeholder("请输入用户名")
# 标签文本定位(label for)
page.get_by_label("用户名")
# 标题属性定位(title)
page.get_by_title("帮助")
# Alt 文本定位(图片)
page.get_by_alt_text("产品图片")
# 测试 ID 定位
page.get_by_test_id("product-123")
# 组合使用
page.locator("div.product-card").filter(
has=page.locator(".price", has_text="¥")
).first3.4 页面交互与智能等待
3.4.1 基本交互操作
python
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/login")
# 填写输入框
page.fill("#username", "admin") # 清空后填入
page.type("#password", "123456", delay=50) # 模拟键盘输入(带延迟)
# 点击
page.click("#login-btn") # 左键点击
page.dblclick("#item") # 双击
page.click("#menu", button="right") # 右键
# 选择下拉
page.select_option("#city", "北京") # 按标签文本
page.select_option("#city", "110000") # 按 value
page.select_option("#city", index=2) # 按索引
# 复选框与单选
page.check("#agree") # 勾选
page.uncheck("#agree") # 取消勾选
# 键盘操作
page.keyboard.press("Enter")
page.keyboard.press("Control+A") # 全选
page.keyboard.type("Hello") # 在焦点元素上输入
# 鼠标操作
page.mouse.move(100, 200) # 移动鼠标
page.mouse.click(100, 200) # 点击指定坐标
page.mouse.dblclick(100, 200)
page.mouse.wheel(0, 500) # 向下滚动
page.evaluate("window.scrollTo(0, document.body.scrollHeight)") # 滚动到底部
browser.close()3.4.2 智能等待机制
定义:Playwright 的定位操作自带智能等待——在执行操作前会自动等待元素可见、稳定、可交互,无需显式添加 time.sleep()。
python
# 定位器操作的默认超时(30 秒可配置)
page.set_default_timeout(10000) # 10 秒
# 等待元素可见
page.locator("#result").wait_for(state="visible") # 默认值
page.locator("#result").wait_for(state="attached") # 已附加到 DOM
page.locator("#result").wait_for(state="hidden") # 隐藏
page.locator("#result").wait_for(state="detached") # 已从 DOM 移除
# 等待特定条件
page.wait_for_load_state("networkidle") # 网络空闲
page.wait_for_load_state("domcontentloaded") # DOM 就绪
page.wait_for_url("**/order/success") # URL 匹配
page.wait_for_function("() => window.loaded === true") # 自定义 JS 条件
# 等待元素
page.wait_for_selector("#loading-spinner", state="hidden")
# 等待弹窗
with page.expect_popup() as popup_info:
page.click("#open-new-window")
new_page = popup_info.value
# 等待请求
with page.expect_request("**/api/data") as req_info:
page.click("#load-data")
request = req_info.value
print(request.url, request.response().status)3.4.3 表单交互实战
python
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://httpbin.org/forms/post")
# 填写表单
page.fill("#custname", "张三")
page.fill("#custtel", "13800001111")
page.fill("#custemail", "zhangsan@example.com")
page.select_option("#size", "large")
page.check("#topping-cheese")
page.check("#topping-mushroom")
page.click("button:has-text('提交')")
# 等待响应
page.wait_for_load_state("networkidle")
print(page.content())
browser.close()3.5 无头模式与浏览器指纹伪装
3.5.1 无头模式
定义:无头模式在后台运行浏览器,不显示 GUI 界面,适用于服务器端数据采集。
python
from playwright.sync_api import sync_playwright
# 无头模式(headless=True)
with sync_playwright() as p:
# Chromium 无头
browser = p.chromium.launch(headless=True)
# Firefox 无头
# browser = p.firefox.launch(headless=True)
# WebKit 无头
# browser = p.webkit.launch(headless=True)
page = browser.new_page()
page.goto("https://example.com")
print(page.title())
browser.close()3.5.2 浏览器指纹伪装
定义:浏览器指纹是网站通过 JavaScript 检测到的浏览器特征集合,包括 User-Agent、WebGL、Canvas、字体等。伪装指纹可以降低被反爬机制识别的风险。
python
from playwright.sync_api import sync_playwright
def create_stealth_context(browser):
"""创建经过伪装处理的浏览器上下文"""
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
user_agent=(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"
),
locale="zh-CN",
timezone_id="Asia/Shanghai",
permissions=["geolocation"],
geolocation={"longitude": 116.4074, "latitude": 39.9042}, # 北京坐标
color_scheme="light",
)
# 注入 JavaScript 修改 navigator 属性
page = context.new_page()
page.add_init_script("""
// 隐藏 WebDriver 特征
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
});
// 修改 navigator.languages
Object.defineProperty(navigator, 'languages', {
get: () => ['zh-CN', 'zh'],
});
// 修改 navigator.plugins 长度
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5],
});
// 修改 chrome 对象
window.chrome = {
runtime: {},
loadTimes: function() {},
csi: function() {},
app: {},
};
""")
return context, page
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
# 检测是否被识别为自动化工具
context, page = create_stealth_context(browser)
page.goto("https://bot.sannysoft.com/")
page.screenshot(path="fingerprint_test.png")
print("截图已保存,检查指纹信息")
browser.close()3.5.3 常见反检测参数
python
browser = p.chromium.launch(
headless=True,
args=[
"--disable-blink-features=AutomationControlled", # 隐藏自动化标记
"--disable-dev-shm-usage", # 避免 /dev/shm 不足
"--no-sandbox", # Docker 环境需要
"--disable-setuid-sandbox",
"--disable-infobars", # 隐藏信息栏
"--window-size=1920,1080",
"--disable-web-security", # 关闭同源策略
"--disable-features=IsolateOrigins,site-per-process",
],
)3.6 异步 Playwright 并发采集
3.6.1 异步 API 基础
定义:Playwright 提供 async_playwright() 原生异步 API,配合 asyncio 可以实现高效的并发采集。
python
import asyncio
from playwright.async_api import async_playwright
async def scrape_page(url):
"""异步采集单个页面"""
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context()
page = await context.new_page()
await page.goto(url, wait_until="networkidle")
title = await page.title()
content = await page.content()
await context.close()
await browser.close()
return {"url": url, "title": title, "length": len(content)}
async def main():
urls = [
"https://example.com",
"https://httpbin.org",
"https://httpbin.org/get",
]
tasks = [scrape_page(url) for url in urls]
results = await asyncio.gather(*tasks)
for r in results:
print(f"{r['url']} -> {r['title']} ({r['length']} chars)")
asyncio.run(main())3.6.2 浏览器实例复用(高效并发)
定义:创建浏览器实例开销较大,应复用同一个 Browser 实例,为每个页面创建独立的 Context。
python
import asyncio
from playwright.async_api import async_playwright
class AsyncCrawler:
def __init__(self, max_pages=5):
self.max_pages = max_pages
self.semaphore = asyncio.Semaphore(max_pages) # 并发控制
async def scrape_one(self, browser, url):
"""使用共享浏览器实例采集一个 URL"""
async with self.semaphore: # 限流
context = await browser.new_context(
user_agent=("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
"Chrome/120.0.0.0 Safari/537.36"),
)
page = await context.new_page()
try:
await page.goto(url, wait_until="networkidle", timeout=30000)
title = await page.title()
text = await page.evaluate(
"() => document.body.innerText"
)
return {"url": url, "title": title, "text_length": len(text)}
except Exception as e:
return {"url": url, "error": str(e)}
finally:
await context.close()
async def run(self, urls):
async with async_playwright() as p:
browser = await p.chromium.launch(
headless=True,
args=["--disable-blink-features=AutomationControlled"],
)
tasks = [self.scrape_one(browser, url) for url in urls]
results = await asyncio.gather(*tasks)
await browser.close()
return results
# 运行
async def main():
urls = [
"https://httpbin.org/delay/2",
"https://httpbin.org/delay/3",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/4",
"https://httpbin.org/delay/2",
]
crawler = AsyncCrawler(max_pages=3)
results = await crawler.run(urls)
for r in results:
if "error" in r:
print(f"❌ {r['url']} -> {r['error']}")
else:
print(f"✅ {r['url']} -> {r['title']} ({r['text_length']} chars)")
asyncio.run(main())3.6.3 网络拦截与请求修改
python
import asyncio
from playwright.async_api import async_playwright
async def intercept_requests():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
page = await browser.new_page()
# 路由拦截——修改请求
async def handle_route(route):
headers = route.request.headers
headers["X-Custom-Header"] = "MyCrawler"
await route.continue_(headers=headers)
await page.route("**/*", handle_route)
# 阻止特定资源加载(图片、CSS、字体)
async def block_resources(route):
resource_type = route.request.resource_type
if resource_type in ["image", "stylesheet", "font", "media"]:
await route.abort()
else:
await route.continue_()
await page.route("**/*", block_resources)
# 监听响应
async def handle_response(response):
if response.status == 200 and "json" in response.headers.get("content-type", ""):
print(f"JSON 响应: {response.url}")
page.on("response", handle_response)
await page.goto("https://example.com", wait_until="networkidle")
await asyncio.sleep(2)
await browser.close()
asyncio.run(intercept_requests())小结
- 三层架构:Playwright(入口)→ Browser(浏览器进程)→ Context(独立会话)→ Page(标签页),复用 Browser 和 Context 提高效率
- 元素定位:推荐使用
page.locator()统一 API,角色定位器get_by_role()语义化最高 - 智能等待:Playwright 自动等待元素可交互,无需显式 sleep;
networkidle是导航等待的推荐状态 - 指纹伪装:
add_init_script()注入 stealth JS +--disable-blink-features=AutomationControlled参数是基础伪装方案 - 异步并发:复用 Browser 实例 + asyncio.Semaphore 限流是最高效的并发采集模式
练习
- 基本导航:使用 Playwright 打开百度首页,搜索"Python 爬虫",截取搜索结果页面的全屏截图。
- 表单填写:使用 Playwright 自动填写并提交
https://httpbin.org/forms/post表单,验证返回数据。 - 列表分页:爬取一个分页列表页(如豆瓣电影 Top250),使用
page.locator(".next").click()翻页,提取所有页面的数据。 - 指纹检测:访问
https://bot.sannysoft.com/,分别在普通模式和经过伪装模式(注入 stealth JS)下截图对比检测结果。 - 异步并发:编写异步爬虫,采集 10 个页面,控制并发数为 3,记录总耗时并与串行执行对比。