1. 项目概述:为什么是Playwright?

如果你正在用Python做自动化测试或者网页数据抓取,大概率听说过Selenium,也可能被它那捉摸不定的元素定位、时快时慢的执行速度,以及各种浏览器驱动版本兼容问题折腾得够呛。几年前,当微软开源Playwright时,我抱着试试看的心态从一个Selenium老项目迁移过来,结果就像从手动挡换成了自动驾驶——效率提升是立竿见影的。Playwright不是一个简单的“更好的Selenium”,它是一个为现代Web而生的、从头设计的自动化与测试框架。它原生支持Chromium、Firefox和WebKit三大浏览器引擎,这意味着你可以用同一套脚本测试你的网站在Chrome、Firefox和Safari上的表现是否一致,这对于确保跨浏览器兼容性来说,简直是降维打击。

更核心的优势在于,Playwright的API设计极其人性化,它默认等待元素可交互,自动处理很多异步加载问题,大大减少了写“sleep”这种不靠谱等待的尴尬。对于页面交互,比如点击、输入、拖拽、文件上传,它提供了强大且稳定的方法。无论是构建端到端测试,还是编写复杂的网页爬虫来处理那些依赖大量JavaScript渲染的“单页应用”,Playwright都能让你事半功倍。接下来,我会结合我近两年的实战经验,从环境搭建到高级交互,手把手带你掌握这个利器,避开我当年踩过的那些坑。

2. 环境搭建与核心配置避坑指南

2.1 Python环境与Playwright安装

工欲善其事,必先利其器。一个干净的Python环境是高效工作的起点。我强烈建议使用 venv conda 创建独立的虚拟环境,避免包依赖冲突。这里以 venv 为例:

# 创建并激活虚拟环境
python -m venv playwright-env
# Windows
playwright-env\Scripts\activate
# macOS/Linux
source playwright-env/bin/activate

激活环境后,安装Playwright。官方推荐使用 pip 安装Python库,然后通过CLI命令安装浏览器。

# 安装Playwright的Python客户端库
pip install playwright
# 安装Playwright自带的浏览器(Chromium, Firefox, WebKit)
playwright install

注意 playwright install 这一步可能会比较慢,因为它需要下载几百MB的浏览器二进制文件。如果遇到网络问题导致下载缓慢或失败,可以尝试设置镜像源。例如,在命令前添加环境变量: PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright ,然后再执行安装命令。这是我解决安装慢问题最有效的方法。

2.2 浏览器启动与上下文配置实战

安装完成后,我们来编写第一个脚本。Playwright的核心对象是 Browser BrowserContext Page 。理解这三者的关系至关重要: Browser 对应一个浏览器进程; Context 是一个独立的会话环境,类似于隐身模式,隔离了cookies、本地存储等; Page 则代表一个标签页。

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        # 启动浏览器,headless=False表示显示浏览器界面
        browser = await p.chromium.launch(headless=False)
        # 创建一个新的上下文,可以在这里设置视窗大小、用户代理等
        context = await browser.new_context(
            viewport={'width': 1920, 'height': 1080},
            user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        )
        # 在上下文中打开一个新页面
        page = await context.new_page()
        # 导航到目标网址
        await page.goto('https://example.com')
        # 等待3秒以便观察
        await page.wait_for_timeout(3000)
        # 关闭浏览器
        await browser.close()

asyncio.run(main())

配置心得

  1. headless 模式选择 :调试阶段务必使用 headless=False ,亲眼看到浏览器的操作过程,能极大方便定位问题。在生产环境或追求速度的爬虫任务中,再切换为 headless=True
  2. 上下文(Context)的妙用 BrowserContext 是你进行会话隔离和全局配置的最佳场所。比如,你可以为一个上下文设置固定的Cookie,所有在这个上下文中创建的页面都会共享这些Cookie,非常适合模拟登录状态。你还可以为一个上下文设置代理,而另一个上下文不使用,实现多身份访问。
  3. 视窗(Viewport)设置 :有些网站的响应式布局会根据窗口大小显示不同内容。固定视窗大小能保证脚本运行环境的一致性,避免因窗口大小变化导致元素定位失败。

3. 元素定位与等待策略:从玄学到科学

元素定位是自动化的基石,不稳定的定位是脚本脆弱的首要原因。Playwright提供了多种强大且稳定的定位器。

3.1 核心定位器详解

# 假设我们有一个页面,其HTML片段如下:
# <button id="submit" class="btn primary">提交</button>
# <input name="username" placeholder="请输入用户名">
# <a href="/about">关于我们</a>
# <div data-testid="nav-menu">导航</div>

# 1. 按文本定位:最符合人类直觉的方式
await page.click('text=提交')
await page.click('text=/关于我们/')  # 使用正则表达式模糊匹配

# 2. 按CSS选择器定位:最通用、最强大的方式
await page.click('#submit')  # ID选择器
await page.click('.btn.primary')  # 类选择器
await page.click('button >> text=提交')  # 组合选择器:先找button元素,再在其中找文本

# 3. 按属性定位:特别是对于没有ID或Class的元素
await page.fill('[name="username"]', 'my_username')
await page.click('[data-testid="nav-menu"]')  # 推荐使用自定义的测试属性,如data-testid

# 4. 按XPath定位:在复杂DOM结构中有时不可替代,但应谨慎使用
await page.click('//button[contains(text(), "提交")]')

定位策略黄金法则

  • 优先级 data-testid > id > text > CSS Selector > XPath data-testid 是专为测试设计的属性,最稳定。 id 通常是唯一的。文本定位直观但受语言和样式影响。CSS选择器性能好且灵活。XPath虽然强大,但易受DOM结构微小变动的影响,维护成本高。
  • 使用 page.locator() page.click(selector) 是简写,更推荐使用 locator = page.locator(selector) 先创建定位器对象,然后对其进行多次操作(如 locator.click() locator.fill() )。这样代码更清晰,且Playwright会对同一 locator 执行的操作进行优化。

3.2 智能等待:告别time.sleep

Playwright最大的优点之一是其内置的自动等待机制。大多数操作(如 click , fill , wait_for_selector )都会自动等待元素满足可操作条件(如可见、启用、稳定等)。

# 不好的做法:使用固定休眠
await page.wait_for_timeout(5000)  # 盲目等待5秒,浪费时间且不可靠

# 好的做法:使用Playwright的智能等待
# 等待元素出现在DOM中并可见
await page.wait_for_selector('#submit', state='visible')
# 等待元素被隐藏或从DOM中移除
await page.wait_for_selector('.loading-spinner', state='hidden')
# 等待导航完成
await page.goto('https://example.com', wait_until='networkidle')  # 等待到网络基本空闲
# 等待某个条件成立,例如URL包含特定路径
await page.wait_for_url('**/dashboard')

# 更复杂的等待:等待API响应
async with page.expect_response('https://api.example.com/data') as response_info:
    await page.click('#load-data-btn')
response = await response_info.value
print(await response.json())

等待策略心得

  • wait_until 参数在 page.goto 中非常有用。 'load' 等待 load 事件触发; 'domcontentloaded' 等待DOMContentLoaded事件触发,更快; 'networkidle' 等待网络活动基本停止,适合单页应用。根据页面类型选择。
  • page.wait_for_load_state() 是另一个有用的方法,可以在点击某个可能引发导航的按钮后调用。
  • 对于动态加载的内容,结合 wait_for_selector page.expect_response page.expect_request 是最佳实践,它能精准地等待数据加载完成,而不是猜测一个固定的时间。

4. 高级页面交互与实战技巧

掌握了定位和等待,我们就可以施展拳脚,模拟各种复杂的用户交互了。

4.1 处理输入框、下拉框与文件上传

# 1. 输入框
await page.fill('#username', '张三')  # fill会先清空再输入
await page.type('#username', '张三', delay=100)  # type模拟逐个字符输入,delay是毫秒间隔
await page.press('#username', 'Enter')  # 在输入框内按回车键

# 2. 下拉选择框(Select)
# <select id="city">
#   <option value="bj">北京</option>
#   <option value="sh">上海</option>
# </select>
await page.select_option('#city', value='sh')  # 通过value选择
await page.select_option('#city', label='上海')  # 通过显示文本选择
await page.select_option('#city', index=1)  # 通过索引选择(从0开始)

# 3. 复选框(Checkbox)和单选框(Radio)
await page.check('#agree-terms')  # 勾选复选框
await page.uncheck('#newsletter')  # 取消勾选
await page.set_checked('#gender-male', True)  # 更通用的设置方法,也适用于单选框

# 4. 文件上传(这是Playwright的强项)
# 方法一:直接设置文件路径(适用于input[type=file]元素)
await page.set_input_files('input[type="file"]', 'path/to/my/file.pdf')
# 方法二:上传多个文件
await page.set_input_files('input[type="file"]', ['file1.pdf', 'file2.jpg'])
# 方法三:先打开选择文件对话框(较少用,更模拟人工)
file_chooser = await page.wait_for_event('filechooser')
await file_chooser.set_files('path/to/file.pdf')

4.2 模拟键盘、鼠标与拖拽操作

Playwright可以精确模拟几乎所有的用户输入设备行为。

# 键盘操作
await page.keyboard.type('Hello, World!')  # 在当前焦点元素输入
await page.keyboard.press('ArrowDown')
await page.keyboard.down('Shift')  # 按下Shift不放
await page.keyboard.press('KeyA')  # 按下A(此时是Shift+A,输入大写A)
await page.keyboard.up('Shift')  # 释放Shift

# 鼠标操作
await page.hover('nav > a')  # 鼠标悬停,常用于触发下拉菜单
await page.click('#button', button='right')  # 右键点击
await page.dblclick('#item')  # 双击
# 更精细的控制:使用`page.mouse` API
await page.mouse.move(100, 200)  # 移动鼠标到坐标(100, 200)
await page.mouse.down()  # 按下鼠标左键
await page.mouse.move(300, 400)  # 拖动
await page.mouse.up()  # 释放鼠标左键

# 拖拽操作
# 将元素 #draggable 拖拽到元素 #droppable 上
source = page.locator('#draggable')
target = page.locator('#droppable')
await source.drag_to(target)
# 或者使用更底层的mouse API模拟复杂拖拽路径

4.3 处理弹窗、框架与多页面

现代网页应用充满了弹窗和嵌入式内容。

# 1. 处理对话框(alert, confirm, prompt)
# 在触发对话框之前,先监听对话框事件
page.on('dialog', lambda dialog: dialog.accept())  # 自动接受(确定)所有对话框
# 或者更精确地处理
def handle_dialog(dialog):
    print(f'对话框消息: {dialog.message}')
    if dialog.type == 'confirm':
        dialog.accept()  # 点击“确定”
    else:
        dialog.dismiss()  # 点击“取消”
page.on('dialog', handle_dialog)
await page.click('#btn-confirm')  # 点击会触发confirm对话框的按钮

# 2. 处理新打开的标签页(Popup)
async with page.context.expect_page() as new_page_info:
    await page.click('a[target="_blank"]')  # 点击一个会打开新窗口的链接
new_page = await new_page_info.value
await new_page.wait_for_load_state()
print(await new_page.title())
# 操作完成后,可以关闭新页面
await new_page.close()

# 3. 处理iframe(内嵌框架)
# 先定位到iframe元素
frame = page.frame_locator('iframe[name="my-frame"]').first
# 然后在frame的上下文中操作元素
await frame.locator('button').click()
# 或者通过URL或名称获取frame
frame = page.frame(url='**/login-form')
await frame.fill('input', 'data')

5. 网络请求拦截与Mock实战

拦截和修改网络请求是Playwright的高级功能,对于测试和爬虫都极其有用。你可以模拟API响应、阻断不必要的资源加载以提升速度,或者捕获请求数据。

5.1 拦截请求与修改响应

# 拦截所有图片请求并阻止加载,加快页面速度
await page.route('**/*.{png,jpg,jpeg,webp,gif}', lambda route: route.abort())

# 拦截特定API请求,并返回自定义的Mock数据
async def handle_route(route):
    # 获取原始请求
    request = route.request
    print(f'拦截到请求: {request.url}')
    # 伪造一个JSON响应
    await route.fulfill(
        status=200,
        content_type='application/json',
        body=json.dumps({'success': True, 'data': 'mocked data'})
    )

await page.route('https://api.example.com/user/*', handle_route)

# 修改请求头(例如添加认证Token)
await page.route('**/*', lambda route: route.continue_(headers={
    **route.request.headers,
    'Authorization': 'Bearer fake-token-123'
}))

5.2 捕获请求与响应数据

# 监听所有请求和响应
page.on('request', lambda request: print(f'>> {request.method} {request.url}'))
page.on('response', lambda response: print(f'<< {response.status} {response.url}'))

# 捕获特定请求的响应数据
async def capture_api_data(response):
    if 'api/data' in response.url:
        try:
            data = await response.json()
            print(f'捕获到数据: {data}')
        except:
            pass # 响应可能不是JSON

page.on('response', capture_api_data)

网络拦截心得

  • route.continue_() 用于继续发出被拦截的请求(可修改)。
  • route.fulfill() 用于直接返回一个自定义响应,而不发送真实请求。
  • route.abort() 用于中止请求。
  • 在爬虫场景中,合理使用 route.abort() 阻断图片、字体、CSS等非必要资源,可以显著提升脚本运行速度和降低带宽消耗。但在测试场景下,为了更真实地模拟用户环境,通常不需要阻断。

6. 调试技巧与常见问题排查实录

即使再熟练,编写自动化脚本也难免遇到问题。高效的调试能力能帮你快速定位症结。

6.1 内置调试工具

# 1. 慢动作模式:让你看清每一步操作
browser = await p.chromium.launch(headless=False, slow_mo=1000)  # 每个操作延迟1秒

# 2. 录制操作生成代码(神器!)
# 在终端运行:playwright codegen https://example.com
# 会打开一个浏览器和代码录制窗口,你的所有操作都会实时生成Playwright代码。

# 3. 打开开发者工具
browser = await p.chromium.launch(headless=False, devtools=True)  # 启动时自动打开DevTools

# 4. 在脚本中暂停
await page.pause()  # 脚本运行到这里会暂停,并打开一个Playwright Inspector界面,可以检查元素、继续执行单步调试。

6.2 常见问题与解决方案速查表

问题现象 可能原因 排查与解决方案
TimeoutError (超时) 1. 元素定位器找不到匹配项。
2. 页面加载太慢或网络问题。
3. 等待条件永远无法满足(如弹窗未出现)。
1. 检查定位器 :使用 playwright codegen 重新录制或手动在浏览器控制台测试 $$('你的选择器')
2. 增加超时时间 await page.wait_for_selector('...', timeout=30000)
3. 检查页面状态 :在超时前手动截图 await page.screenshot(path='debug.png') ,查看页面是否如预期加载。
4. 使用更宽松的等待 :尝试 state='attached' (存在于DOM)而非 'visible'
元素无法点击/交互 1. 元素被遮挡(如弹窗、其他元素)。
2. 元素未处于可交互状态(disabled, readonly)。
3. 需要滚动到视图内。
1. 截图查看 await page.screenshot(path='before_click.png')
2. 强制点击 (慎用): await page.click('...', force=True) 。这能绕过可交互性检查,但可能破坏测试逻辑。
3. 滚动到元素 await element.scroll_into_view_if_needed()
4. 检查元素状态 await page.is_enabled('...') , await page.is_visible('...')
脚本在无头模式下失败,但非无头模式成功 1. 视窗大小不同导致布局变化。
2. 某些功能(如WebGL)在无头模式下被禁用。
3. 浏览器指纹检测。
1. 统一视窗 :在 new_context 中明确设置相同的 viewport
2. 添加启动参数 browser = await p.chromium.launch(headless=True, args=['--disable-web-security', '--disable-features=IsolateOrigins'] ,但需谨慎。
3. 使用更真实的上下文 context = await browser.new_context(viewport=..., user_agent=...)
文件上传失败 1. 文件路径错误。
2. 上传控件是自定义的,非原生 <input type="file">
1. 使用绝对路径
2. 对于自定义上传 :可能需要先点击“上传按钮”触发文件选择对话框,然后用 page.wait_for_event('filechooser') 捕获。如果还不行,可能需要考虑使用 page.evaluate() 注入JS来设置文件。
页面卡住或无响应 1. 陷入无限循环或长耗时JS。
2. 内存泄漏。
1. 设置全局超时 browser = await p.chromium.launch(timeout=60000)
2. 定期检查并重启 :对于长时间运行的爬虫,定期关闭页面和上下文,新建一个干净的上下文可以避免内存累积。

6.3 我的调试工作流

  1. 第一步:重现问题 。确保能在本地稳定复现,这是调试的前提。
  2. 第二步:可视化 。将 headless 设为 False ,亲眼看看脚本到底在哪一步出了问题。配合 slow_mo 使用效果更佳。
  3. 第三步:定位元素 。在出问题的步骤前插入 await page.pause() ,使用弹出的Inspector工具检查目标元素的状态、CSS选择器是否正确。
  4. 第四步:截图与日志 。在关键步骤前后截图,并打印出当前的URL、页面标题或关键元素的内容,形成日志。
  5. 第五步:简化与隔离 。如果问题复杂,尝试写一个最小的、能复现问题的代码片段。这往往能帮你排除无关干扰,聚焦核心问题。

7. 项目结构优化与最佳实践

当脚本越来越多,一个清晰的项目结构能让你和你的团队长期受益。

7.1 推荐的项目目录结构

my_playwright_project/
├── requirements.txt          # 项目依赖
├── config.py                 # 配置文件(URL、账号、超时时间等)
├── conftest.py               # (如果使用pytest)共享的fixture和hook
├── pages/                    # 页面对象模型(Page Object Model)
│   ├── __init__.py
│   ├── login_page.py
│   ├── home_page.py
│   └── search_page.py
├── locators/                 # 定位器集中管理(可选)
│   ├── __init__.py
│   └── login_locators.py
├── tests/                    # 测试用例
│   ├── test_login.py
│   └── test_search.py
├── utils/                    # 工具函数
│   ├── __init__.py
│   ├── helpers.py
│   └── logger.py
└── main.py                   # 主运行脚本(用于爬虫或独立任务)

7.2 使用页面对象模型

页面对象模型是一种设计模式,将每个页面的元素定位和交互操作封装成一个类。这极大地提高了代码的可读性和可维护性。

# pages/login_page.py
from playwright.async_api import Page

class LoginPage:
    def __init__(self, page: Page):
        self.page = page
        self.username_input = page.locator('#username')
        self.password_input = page.locator('#password')
        self.submit_button = page.locator('button[type="submit"]')
        self.error_message = page.locator('.alert-error')

    async def navigate(self):
        await self.page.goto('https://example.com/login')

    async def login(self, username: str, password: str):
        await self.username_input.fill(username)
        await self.password_input.fill(password)
        await self.submit_button.click()

    async def get_error_message(self) -> str:
        await self.error_message.wait_for(state='visible')
        return await self.error_message.text_content()

# 在测试或主脚本中使用
async def test_login():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        context = await browser.new_context()
        page = await context.new_page()
        login_page = LoginPage(page)
        await login_page.navigate()
        await login_page.login('wrong_user', 'wrong_pass')
        error = await login_page.get_error_message()
        assert 'Invalid credentials' in error
        await browser.close()

7.3 集成测试框架

对于自动化测试,强烈建议集成 pytest 。Playwright官方也提供了 pytest-playwright 插件,使得编写测试用例更加方便。

pip install pytest pytest-playwright
# test_sample.py
import pytest
from playwright.async_api import Page, expect

@pytest.mark.asyncio
async def test_homepage_has_title(page: Page):  # page fixture由插件自动提供
    await page.goto('https://example.com')
    await expect(page).to_have_title('Example Domain')

@pytest.mark.asyncio
async def test_click_link(page: Page):
    await page.goto('https://example.com')
    await page.get_by_text('More information...').click()
    await expect(page).to_have_url('https://www.iana.org/domains/example')

使用 pytest 可以方便地管理用例、生成报告、并行执行等。 expect 断言语法是Playwright提供的一种更优雅、自带等待的断言方式。

8. 性能优化与资源管理

编写稳定的脚本不仅要功能正确,还要高效、节省资源。

8.1 并行执行与上下文复用

Playwright支持并行执行,充分利用多核CPU。

import asyncio
from playwright.async_api import async_playwright

async def run_task(url, context):
    """一个独立的任务,使用共享的浏览器上下文"""
    page = await context.new_page()
    await page.goto(url)
    title = await page.title()
    print(f'{url}: {title}')
    await page.close()

async def main():
    urls = ['https://example.com/1', 'https://example.com/2', 'https://example.com/3']
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        # 创建一个上下文,供多个任务共享(注意:会话不隔离)
        context = await browser.new_context()
        tasks = [run_task(url, context) for url in urls]
        await asyncio.gather(*tasks)  # 并行执行所有任务
        await browser.close()

asyncio.run(main())

注意 :共享 BrowserContext 意味着Cookies、LocalStorage是共享的。如果需要完全隔离的会话,每个任务应创建自己的上下文( await browser.new_context() ),但这会消耗更多资源。

8.2 资源清理与内存管理

不正确的资源管理会导致内存泄漏,尤其是在长时间运行的爬虫中。

async def safe_operation():
    browser = None
    try:
        browser = await p.chromium.launch()
        context = await browser.new_context()
        page = await context.new_page()
        # ... 你的操作 ...
    except Exception as e:
        print(f'操作出错: {e}')
    finally:
        # 确保资源被关闭,顺序很重要:先关页面,再关上下文,最后关浏览器
        if page and not page.is_closed():
            await page.close()
        if context:
            await context.close()
        if browser:
            await browser.close()

# 对于循环任务,定期重启浏览器是释放内存的好方法
async def long_running_crawler():
    async with async_playwright() as p:
        for i in range(100):
            # 每处理10个任务重启一次浏览器
            if i % 10 == 0:
                if 'browser' in locals():
                    await browser.close()
                browser = await p.chromium.launch()
                context = await browser.new_context()
            page = await context.new_page()
            # ... 处理任务 ...
            await page.close()
        await browser.close()

遵循这些实践,你的Playwright脚本将不仅功能强大,而且健壮、高效、易于维护。从简单的页面操作到复杂的单页应用爬取和自动化测试,Playwright都能提供一流的开发体验。记住,多动手实践,善用 codegen pause() 进行调试,遇到问题时参考官方文档和社区,你会发现用Python操控浏览器从未如此轻松愉快。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐