Playwright 实战指南 — 从入门到 Web 自动化测试

2026/06/05 dev 11865 字 · 约 34 分钟 阅读
基于 AlbumDownloader 项目实际使用经验整理的 Playwright 完整实战指南,涵盖安装配置、元素定位、Browser Context、网络拦截 Mock、CDP 直连、Chrome 扩展配合、反爬虫对抗、测试框架集成等全部核心能力

Playwright 实战指南

从入门到 Web 自动化测试 — 基于 AlbumDownloader 项目的实际使用经验


一、Playwright 是什么

Playwright 是微软开发的开源 Web 自动化框架(Apache-2.0 协议),GitHub ⭐70,000+。

核心能力

能力说明
多浏览器Chromium、Firefox、WebKit(Safari)三引擎统一 API
多语言TypeScript/JavaScript、Python、Java、C#
自动等待定位元素时自动等待到可操作,无需手动 sleep
网络拦截mock API、修改请求/响应、等待特定请求
无头/有头支持 headless(无界面)和 headed(有界面)模式
截图/录屏全页截图、元素截图、操作视频录制
跨域处理自带 Cookie/Storage 管理,支持多 Context 隔离
CDP 直连可连接已有浏览器(connect_over_cdp

与 Puppeteer 的区别

对比项PlaywrightPuppeteer
浏览器支持Chromium + Firefox + WebKit仅 Chromium
语言支持TS/JS/Python/Java/C#仅 TS/JS
自动等待✅ 原生内置❌ 需手动
网络拦截✅ 完整 API✅ 完整 API
移动端模拟✅ 内置设备模拟✅ 需额外配置
性能✅ 略优(并行架构)✅ 成熟稳定
维护方微软(活跃)Google(维护)

二、安装

2.1 使用 npm(推荐项目级)

# 在项目目录中
npm init -y
npm install playwright

# 安装浏览器(Chromium + Firefox + WebKit)
npx playwright install

# 或仅安装 Chromium(最快)
npx playwright install chromium

# 安装后验证
npx playwright --version

2.2 使用 Python

pip install playwright
playwright install           # 安装所有浏览器
playwright install chromium  # 仅 Chromium

2.3 在 Windows 上的注意事项

  • 安装路径:浏览器二进制文件默认下载到 %USERPROFILE%\AppData\Local\ms-playwright\
  • Windows 防火墙:首次启动浏览器时可能会弹出防火墙提示,允许即可
  • 中文输入法page.type() 在中文输入法开启时可能异常,建议用 page.fill() 替代

2.4 验证安装

# TypeScript
npx playwright test --help

# 运行内置 demo
npx playwright test example.spec.ts

# Python
python -c "from playwright.sync_api import sync_playwright; print('OK')"

三、基础使用

3.1 启动浏览器与页面

TypeScript

import { chromium } from 'playwright';

// 启动浏览器
const browser = await chromium.launch({
  headless: false,  // false = 有界面,true = 无头
  slowMo: 100,      // 操作间延迟(毫秒),调试时好用
});

// 创建页面
const page = await browser.newPage();

// 导航
await page.goto('https://example.com');

// ... 执行操作 ...

// 关闭
await browser.close();

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')
    # ... 操作 ...
    browser.close()

3.2 导航与等待

// 基本导航
await page.goto('https://example.com');
await page.goto('https://example.com', { waitUntil: 'networkidle' });

// 导航等待选项
// 'load'       — load 事件触发(默认)
// 'domcontentloaded' — DOM 解析完成
// 'networkidle' — 网络空闲(500ms 无请求)
// 'commit'     — 收到响应头

// 后退/前进
await page.goBack();
await page.goForward();

// 刷新
await page.reload();

// 等待特定 URL
await page.waitForURL('**/login**');

3.3 截图

// 全页截图
await page.screenshot({ path: 'screenshot.png', fullPage: true });

// 元素截图
const element = page.locator('.main-content');
await element.screenshot({ path: 'element.png' });

// 指定区域
await page.screenshot({
  path: 'clip.png',
  clip: { x: 100, y: 100, width: 500, height: 300 }
});

四、元素定位(Locator 系统)

Playwright 推荐使用 locator() API 定位元素,支持自动等待和重试。

4.1 定位策略

策略示例说明
文本page.getByText('登录')按可见文本匹配
角色page.getByRole('button', { name: '提交' })按 ARIA 角色+名称
标签page.locator('button')CSS 选择器
占位符page.getByPlaceholder('请输入密码')input placeholder
标题page.getByTitle('关闭')title 属性
alt 文本page.getByAltText('logo')img alt 属性
标签关联page.getByLabel('用户名')label for / aria-labelledby
测试 IDpage.getByTestId('submit-btn')data-testid 属性

4.2 组合定位

// 链式定位
const table = page.locator('table.data-table');
const row = table.locator('tr').filter({ hasText: '完成' });
const cell = row.locator('td').nth(2);

// 过滤
page.locator('li').filter({ hasText: '待办' });
page.locator('button').filter({ has: page.locator('.icon-download') });

// 包含文本
page.locator('text=确认删除');
page.locator('button:has-text("确认")');

4.3 操作

// 点击
await button.click();
await button.click({ force: true });       // 强制点击(绕过可操作性检查)
await button.click({ timeout: 5000 });      // 自定义超时

// 输入
await input.fill('Hello World');             // 清空后输入(推荐,更快)
await input.type('Hello', { delay: 50 });    // 模拟键盘输入(较慢)

// 选择
await select.selectOption('option-value');

// 勾选
await checkbox.check();
await checkbox.uncheck();

// 键盘
await page.keyboard.press('Enter');
await page.keyboard.press('Control+A');

// Hover
await element.hover();

4.4 自动等待规则

Playwright 定位元素的默认超时是 30 秒。在 30 秒内,系统会自动重试直到元素满足以下所有条件:

  1. 元素附着到 DOM
  2. 可见:非 display:none、非 visibility:hidden、在视口内
  3. 稳定:位置不再变化(两次 getBoundingClientRect 相同)
  4. 可接收事件:未被其他元素遮挡
  5. 已启用:非 disabled

这才是 Playwright 最核心的优势 — 不需要写 sleep(1000) 等页面加载。


五、Browser Context 与多页管理

5.1 Browser Context

每个 Context 相当于一个独立的浏览器会话,隔离 Cookie、Storage、缓存:

const browser = await chromium.launch();

// 创建两个隔离的会话
const ctx1 = await browser.newContext();
const ctx2 = await browser.newContext();

// 每个 Context 有自己的页面
const page1 = await ctx1.newPage();
const page2 = await ctx2.newPage();

// Context 间数据完全隔离
await page1.goto('https://example.com/login');
// ctx2 完全不共享 ctx1 的登录态

5.2 模拟环境

// 移动端模拟
const iphone = devices['iPhone 13 Pro'];
const ctx = await browser.newContext({
  ...iphone,
  locale: 'zh-CN',
  timezoneId: 'Asia/Shanghai'
});

// 地理位置模拟
await browser.newContext({
  geolocation: { longitude: 120.15, latitude: 30.28 },
  permissions: ['geolocation']
});

// 用户代理
await browser.newContext({
  userAgent: 'Mozilla/5.0 ... Chrome/120.0.0.0'
});
// 获取所有 Cookie
const cookies = await context.cookies();

// 添加 Cookie(如注入登录态)
await context.addCookies([
  {
    name: 'session',
    value: 'your-session-token',
    domain: '.example.com',
    path: '/'
  }
]);

// 清除 Cookie
await context.clearCookies();

// 持久化(保存认证状态)
await context.storageState({ path: 'auth.json' });

// 恢复认证状态
const ctx = await browser.newContext({ storageState: 'auth.json' });

六、网络拦截与 Mock

6.1 拦截请求

// 拦截并打印所有请求
page.on('request', request => {
  console.log(`>> ${request.method()} ${request.url()}`);
});

page.on('response', response => {
  console.log(`<< ${response.status()} ${response.url()}`);
});

// 等待特定请求完成
const [response] = await Promise.all([
  page.waitForResponse(resp => resp.url().includes('/api/data')),
  page.click('#load-data-btn')
]);
const data = await response.json();

6.2 Mock API

// 拦截并返回 mock 数据
await page.route('**/api/user', async route => {
  await route.fulfill({
    status: 200,
    contentType: 'application/json',
    body: JSON.stringify({ id: 1, name: 'Mock User' })
  });
});

// 修改请求(如添加 Referer)
await page.route('**/*.jpg', async route => {
  const headers = {
    ...route.request().headers(),
    'Referer': 'https://weibo.com/'
  };
  await route.continue({ headers });
});

// 阻塞特定请求(如图片)
await page.route('**/*.{png,jpg,jpeg}', route => route.abort());

6.3 等待特定 API 数据

// ⭐ 模式:等待 API 返回后再操作
const [apiResponse] = await Promise.all([
  page.waitForResponse(response =>
    response.url().includes('/ajax/profile/getImageWall')
  ),
  page.click('#load-albums')     // 触发 API 调用
]);
const albums = await apiResponse.json();
console.log('相册列表:', albums);

七、CDP 直连 — 连接已有浏览器

7.1 场景

当需要操作用户已经在使用的浏览器(已有登录态、已打开页面),而不是新启动浏览器时。

7.2 启动 Chrome 调试模式

# 关闭所有 Chrome 进程
taskkill /F /IM chrome.exe

# 以调试模式启动(Windows)
"C:\Program Files\Google\Chrome\Application\chrome.exe" \
  --remote-debugging-port=9222 \
  --restore-last-session

# macOS
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
  --remote-debugging-port=9222

# Linux
google-chrome --remote-debugging-port=9222

7.3 Connect Over CDP

import { chromium } from 'playwright';

const browser = await chromium.connectOverCDP('http://localhost:9222');

// 获取已有页面
const context = browser.contexts()[0];
const pages = context.pages();
console.log(`已有 ${pages.length} 个标签页`);

// 也可创建新标签页
const page = await browser.newPage();
await page.goto('https://example.com');
// 注意:新页面在用户浏览器中直接打开

7.4 获取所有标签页

# 查看所有打开的页面
curl http://localhost:9222/json | python3 -c "
import sys, json
data = json.load(sys.stdin)
for i, d in enumerate(data):
    print(f'[{i}] {d[\"title\"][:60]}')
    print(f'    {d[\"url\"][:80]}')
"

7.5 限制与注意事项

限制说明
需要预先启动Chrome 必须以 --remote-debugging-port 启动
首次会关窗口taskkill 关闭 Chrome 后再重启,所有标签页会自动恢复
安全问题9222 端口默认只监听 localhost,远程访问可能有安全风险
内容脚本CDP 连接的页面中,Chrome 扩展的 content script 仍然运行

项目经验:在 AlbumDownloader 项目中,我们用 CDP 直连用户已有的 Chrome 来获取内网页面内容(网易内网文章),以及验证扩展在真实浏览器中的行为。注意直接 npx playwright test 方式运行会被微博等网站的反爬机制拦截。


八、与 Chrome 扩展配合使用

8.1 加载未打包扩展

const path = require('path');

const browser = await chromium.launch({
  headless: false,
  args: [
    `--disable-extensions-except=${path.resolve('./dist')}`,
    `--load-extension=${path.resolve('./dist')}`,
  ],
});

8.2 处理扩展页面

// 获取扩展的背景页面(Service Worker)
const backgroundUrl = 'chrome-extension://your-extension-id/background.html';
const context = browser.contexts()[0];
const bgPage = await context.newPage();
await bgPage.goto(backgroundUrl);

// 或者监听扩展的日志
page.on('console', msg => {
  if (msg.text().includes('[AlbumDownloader]')) {
    console.log('扩展日志:', msg.text());
  }
});

8.3 连接已有扩展

// 通过 CDP 连接已有 Chrome(扩展已安装)
const browser = await chromium.connectOverCDP('http://localhost:9222');

// 获取扩展的 Service Worker 控制台
for (const ctx of browser.contexts()) {
  for (const p of ctx.pages()) {
    const url = p.url();
    if (url.startsWith('chrome-extension://')) {
      console.log('扩展页面:', url);
    }
  }
}

九、测试框架集成

9.1 作为测试运行器

# 安装 Playwright Test
npm init playwright@latest

# 运行所有测试
npx playwright test

# 运行特定文件
npx playwright test tests/weibo.spec.ts

# 有界面运行(调试用)
npx playwright test --headed

# 显示测试报告
npx playwright show-report

9.2 与 Vitest 集成

// weibo-adapter.test.ts
import { test, expect } from '@playwright/test';
import { chromium } from 'playwright';

test('should detect weibo albums from API', async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();

  // Mock API
  await page.route('**/ajax/profile/info?screen_name=**', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        ok: 1,
        data: { user: { id: 2352174263 } }
      })
    });
  });

  // Mock 相册 API
  await page.route('**/ajax/profile/getImageWall**', async route => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({
        ok: 1,
        data: {
          album_list: [
            { containerid: '231826...', pic_title: '自拍', pic_count: 128 },
            { containerid: '231826...', pic_title: '原创', pic_count: 256 },
          ]
        }
      })
    });
  });

  await page.goto('https://weibo.com/n/testuser?tabtype=album');
  // ... 验证扩展行为 ...
  await browser.close();
});

9.3 测试报告

Playwright Test 自动生成 HTML 报告,包含:

  • 成功/失败/跳过统计
  • 每个测试的截图(失败时自动截)
  • 执行时间线
  • 追踪信息(Trace Viewer)
npx playwright show-report

十、反爬虫对抗实践

10.1 常见反爬机制

机制检测方式Playwright 应对
WebDriver 检测navigator.webdriverPlaywright 默认不暴露
User-Agent检查浏览器标识browser.newContext({ userAgent }) 覆盖
IP 频率限制单位时间请求数超限控制操作间隔、代理池
行为验证鼠标轨迹/操作模式非 headless 模式 + human-like 鼠标
CDN 防盗链检查 Referer/Origin请求拦截添加 Referer 头
验证码图灵测试人工介入 / OCR 服务

10.2 降低被检测的技巧

// 1. 非无头模式(headed 有界面模式)
const browser = await chromium.launch({ headless: false });

// 2. 模拟真人操作
await page.mouse.move(100, 200);  // 移动鼠标
await page.waitForTimeout(500);   // 停顿(仅在需要时用)
await page.mouse.click(100, 200);

// 3. 自定义 User-Agent
await browser.newContext({
  userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ' +
    '(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36'
});

// 4. 设置 Cookie/Storage(模拟已登录状态)
await context.addCookies([/* ... */]);

// 5. 随机操作间隔(避免固定频率)
const delay = () => new Promise(r => setTimeout(r, 1000 + Math.random() * 2000));

10.3 Playwright 被拦截后的替代方案

项目教训:用 Playwright 直接 page.goto() 打开微博相册页会被反爬拦截,返回空白页或验证码页。替代方案:

方案适用场景说明
CDP 直连用户已有浏览器和登录态通过 connectOverCDP 连接用户正在使用的 Chrome
Chrome 扩展需要长驻页面操作扩展的 content script 在页面原生环境中执行,不走 WebDriver
真实浏览器安装开发测试直接把 unpacked extension 加载到 Chrome,手动操作验证

十一、项目实战经验

11.1 AlbumDownloader 项目中的 Playwright 应用

用途方式是否成功
获取内网页面内容CDP 直连用户 Chrome
自动化测试 Weibo 适配器Playwright 直接启动浏览器❌(被反爬拦截)
验证扩展下载逻辑用户手动在 Chrome 加载扩展✅(手动操作)
Web 自动化框架调研全局框架对比✅(纳入文档)

11.2 具体代码片段 — CDP 直连

# 步骤 1:关闭用户 Chrome 并以调试模式重启
taskkill /F /IM chrome.exe
"C:\Program Files\Google\Chrome\Application\chrome.exe" \
  --remote-debugging-port=9222 --restore-last-session

# 步骤 2:连接并获取页面
python3 -c "
import json, urllib.request
tabs = json.loads(urllib.request.urlopen('http://localhost:9222/json').read())
for t in tabs:
    print(f'[{t[\"id\"][:20]}] {t[\"title\"][:60]}')
    print(f'    {t[\"url\"][:80]}')
"
// 步骤 3:用 Playwright 操作
const browser = await chromium.connectOverCDP('http://localhost:9222');
const pages = browser.contexts()[0].pages();
const targetPage = pages.find(p => p.url().includes('netease.com'));
const content = await targetPage.evaluate(() =>
  document.querySelector('article')?.innerText || 'not found'
);

11.3 踩坑总结

说明解决方案
微博/淘宝反爬直接 Playwright page.goto() 被拦截改用 CDP 直连用户真实浏览器
无头模式更易被检测headless: true 时反爬检测率极高headless: false
中文输入异常page.type() 在中文输入法下异常page.fill() 替代
扩展 Service Workerpage.waitForURL() 不匹配 chrome-extension://browser.contexts() 遍历
CDP 端口 9222 被占用之前启动的调试进程未关闭taskkill 清除
Chrome 版本不匹配Playwright 下载的 Chromium 与系统版本不同channel: 'chrome' 使用系统安装的浏览器

十二、调试技巧

# 1. 使用 PWDEBUG 环境变量(DevTools 模式)
PWDEBUG=1 npx playwright test

# 2. 慢速运行(看清每一步)
const browser = await chromium.launch({ slowMo: 500 });

# 3. 录制操作脚本
npx playwright codegen https://example.com

# 4. 保留浏览器(调试不关闭)
const browser = await chromium.launch({ headless: false });
await page.pause();  // 在此暂停,手动检查

# 5. Trace Viewer(记录完整操作回放)
npx playwright test --trace on
npx playwright show-trace trace.zip

# 6. 截图比较
await expect(page).toHaveScreenshot('homepage.png');

十三、快速参考

安装速查

# 方式	命令
npm 	npm install playwright
npx 	npx playwright install chromium
Python 	pip install playwright && playwright install

常用命令

# 命令	说明
npx playwright test	运行所有测试
npx playwright test --headed	有界面运行
npx playwright show-report	查看测试报告
npx playwright codegen	录制操作脚本
npx playwright test --debug	调试模式

最佳实践总结

  1. 优先用 locator API,而不是 $(),自动等待省去 sleep
  2. page.fill() 而不是 page.type(),更快更稳定
  3. Promise.all 模式等待 API 和点击同时完成
  4. 不要用固定 waitForTimeout,用 waitForSelectorwaitForResponse 替代
  5. 每个测试创建独立 Context,避免状态污染
  6. 失败截图默认开启,用 show-report 查看
  7. 对强反爬站点,放弃 Playwright 纯自动化,改用 CDP 直连或浏览器扩展

文档信息

加载评论…