Skip to content

章节 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, navigation

3.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="¥")
).first

3.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())

小结

  1. 三层架构:Playwright(入口)→ Browser(浏览器进程)→ Context(独立会话)→ Page(标签页),复用 Browser 和 Context 提高效率
  2. 元素定位:推荐使用 page.locator() 统一 API,角色定位器 get_by_role() 语义化最高
  3. 智能等待:Playwright 自动等待元素可交互,无需显式 sleep;networkidle 是导航等待的推荐状态
  4. 指纹伪装add_init_script() 注入 stealth JS + --disable-blink-features=AutomationControlled 参数是基础伪装方案
  5. 异步并发:复用 Browser 实例 + asyncio.Semaphore 限流是最高效的并发采集模式

练习

  1. 基本导航:使用 Playwright 打开百度首页,搜索"Python 爬虫",截取搜索结果页面的全屏截图。
  2. 表单填写:使用 Playwright 自动填写并提交 https://httpbin.org/forms/post 表单,验证返回数据。
  3. 列表分页:爬取一个分页列表页(如豆瓣电影 Top250),使用 page.locator(".next").click() 翻页,提取所有页面的数据。
  4. 指纹检测:访问 https://bot.sannysoft.com/,分别在普通模式和经过伪装模式(注入 stealth JS)下截图对比检测结果。
  5. 异步并发:编写异步爬虫,采集 10 个页面,控制并发数为 3,记录总耗时并与串行执行对比。

Python 学习资料