1. 从零开始认识Playwright:一个现代Web自动化测试框架
如果你正在寻找一个能稳定、高效地操作浏览器,进行自动化测试、数据抓取或者网页监控的工具,那么Playwright绝对值得你花时间深入了解。它不是一个新概念,但绝对是近年来在开发者社区中口碑迅速崛起的一个。简单来说,Playwright是一个由微软开源的Node.js库,它提供了一套统一的API,让你可以用同样的代码去驱动Chromium(Chrome、Edge)、Firefox和WebKit(Safari)三大浏览器引擎。这意味着你写一次脚本,就能在几乎所有主流浏览器上运行,这对于需要做跨浏览器兼容性测试的场景来说,简直是福音。
我最初接触它,是为了解决一个老项目里用Puppeteer和Selenium时遇到的痛点:动态内容加载不稳定、iframe难以处理、等待逻辑复杂导致脚本脆弱。Playwright在设计之初就充分考虑了现代Web应用的复杂性,它对单页应用(SPA)、网络请求拦截、文件上传下载、甚至移动设备模拟都提供了原生支持,用起来非常顺手。无论是前端开发者想给自己的项目做端到端(E2E)测试,还是数据工程师需要应对那些反爬机制复杂的网站,Playwright都能提供强大的能力。它的学习曲线相对平缓,特别是如果你有JavaScript/TypeScript或Python的基础,上手会非常快。接下来,我会从一个实际使用者的角度,带你一步步拆解它的核心用法,避开我当初踩过的那些坑。
2. 环境搭建与核心概念解析
2.1 安装与初始化:一步到位还是按需索取?
Playwright的安装非常灵活,主要取决于你的使用语言和场景。最主流的方式是通过Node.js的包管理器npm或yarn,当然,Python版本也同样强大。
对于Node.js项目,你可以在项目目录下执行:
npm init playwright@latest这个命令是一个交互式的初始化工具,它会问你几个问题,比如是用JavaScript还是TypeScript,测试目录放在哪,是否需要安装GitHub Actions工作流等。对于新手,我强烈推荐使用这个方式,它能帮你搭建一个结构清晰、开箱即用的项目骨架。
如果你已经有一个项目,只想添加Playwright依赖,可以:
npm install playwright需要注意的是,仅仅安装playwright包并不会自动下载浏览器。浏览器二进制文件是通过一个单独的@playwright/browser包来管理的。更常见的做法是使用上面提到的init命令,或者直接安装带有浏览器的完整包:
npm install playwright @playwright/test这里引入了@playwright/test,这是Playwright官方的测试运行器,它比直接用playwright库写脚本然后node执行要强大得多,内置了断言、测试钩子、并行执行和报告生成等功能。对于做自动化测试,这是首选。
对于Python用户,安装同样简单:
pip install playwright playwright installplaywright install命令会下载所有三大浏览器的二进制文件。如果你只想安装Chromium以节省磁盘空间,可以使用playwright install chromium。
注意:浏览器的下载可能会比较慢,因为它需要从官方CDN拉取几百兆的文件。如果遇到网络问题,可以考虑配置环境变量
PLAYWRIGHT_DOWNLOAD_HOST使用国内镜像源,或者耐心等待。第一次安装成功后,后续使用就无需再下载了。
安装完成后,一个关键的概念是Browser Context。你可以把它理解为一个独立的“隐身会话”。每个Context拥有独立的cookie、缓存和本地存储,相互隔离。这非常有用,比如你可以用一个Context模拟登录用户A,另一个Context模拟未登录用户B,两者互不干扰,而它们都共享同一个Browser实例的资源。这比为每个场景都启动一个全新的浏览器要高效得多。
2.2 Playwright的核心架构:Browser, Context, Page与Frame
理解Playwright的四个核心对象层级关系,是写出健壮脚本的基础。它们的关系就像俄罗斯套娃:
- Browser:代表一个浏览器进程实例。通过
await chromium.launch()或await playwright.chromium.launch()启动。你可以在这里配置是否使用无头模式、指定浏览器路径、设置代理等全局参数。 - Context:浏览器上下文。由Browser创建(
await browser.newContext())。这是隔离的关键层级。你可以为Context设置视口大小、用户代理(User-Agent)、地理位置、权限(如摄像头、通知)等。一个Browser可以创建多个Context。 - Page:标签页。由Context创建(
await context.newPage())。这是我们最常打交道的对象,代表一个网页。绝大多数操作,如跳转URL、点击元素、输入文本、获取内容,都在Page对象上进行。 - Frame:页面框架。一个Page至少包含一个主Frame(
page.mainFrame())。如果页面内有<iframe>或<frame>标签,就会产生子Frame。Playwright可以非常方便地定位和操作这些子Frame,这对于处理那些把登录框或核心内容放在iframe里的网站至关重要。
这种层级设计带来了巨大的灵活性。例如,在数据抓取中,你可以为每个任务创建一个新的Context,任务结束后关闭Context,这样就能彻底清除痕迹,避免被网站通过浏览器指纹关联。而在测试中,你可以利用Context的隔离性来并行执行多个独立的测试用例。
3. 基础操作与元素交互实战
3.1 启动浏览器与导航控制
一切始于启动浏览器。下面是一个最基础的脚本,它启动一个Chromium浏览器(默认是无头模式,即不显示图形界面),打开一个新页面,访问百度,然后截图并关闭。
const { chromium } = require('playwright'); // 或 import { chromium } from 'playwright'; (async () => { // 1. 启动浏览器 const browser = await chromium.launch({ headless: false, // 设置为 true 则在后台运行,不显示窗口 slowMo: 500, // 操作间延迟500毫秒,方便观察 }); // 2. 创建一个浏览器上下文 const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, // 设置视口大小 userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...', // 可自定义UA }); // 3. 打开一个新页面 const page = await context.newPage(); // 4. 导航到目标网址 await page.goto('https://www.baidu.com'); // 等待页面达到“网络空闲”状态,对于SPA尤其重要 await page.waitForLoadState('networkidle'); // 5. 进行一些操作,比如截图 await page.screenshot({ path: 'baidu.png', fullPage: true }); // 6. 关闭资源(务必关闭,释放内存) await context.close(); await browser.close(); })();page.goto()是导航的核心方法。它默认会等待页面触发load事件。但对于大量使用Ajax的现代网站,load事件触发后页面内容可能还在加载。因此,Playwright提供了更强大的等待选项:
await page.goto(url, { waitUntil: 'domcontentloaded' }):等待HTML文档加载完成。await page.goto(url, { waitUntil: 'networkidle' }):等待网络活动基本停止(大约500ms内没有超过2个网络请求)。这是最常用也最可靠的选项。await page.goto(url, { waitUntil: 'commit' }):当接收到响应时立即返回,不等待页面加载。
在实际项目中,我习惯在goto后再加上page.waitForLoadState('networkidle'),双保险确保页面完全加载。对于某些特别“重”的页面,你可能还需要等待某个特定元素出现,这就要用到下面介绍的选择器和等待机制。
3.2 元素定位与等待策略:告别脆弱的脚本
定位元素是自动化的基石。Playwright支持多种选择器引擎,最常用的是CSS选择器和XPath,但它也内置了非常方便的文本定位和角色定位。
1. CSS选择器与文本定位:
// 通过CSS ID点击搜索框 await page.click('#kw'); // 通过CSS类名输入文本 await page.fill('.s_ipt', 'Playwright教程'); // 通过文本内容点击按钮(模糊匹配) await page.click('text=百度一下'); // 通过文本内容精确匹配 await page.click('text="新闻"');text=选择器极其好用,尤其是当元素没有稳定的ID或Class时,直接通过肉眼可见的文本定位,直观又省事。
2. XPath定位:对于复杂的DOM结构,XPath有时是唯一的选择。
// 点击第一个包含“知道”文本的a标签 await page.click('//a[contains(text(), "知道")]');3. 角色定位(ARIA):这是Playwright的一大特色,对于可访问性友好的网站,通过角色定位非常语义化。
// 点击一个按钮(role=button) await page.click('role=button[name="登录"]'); // 点击一个链接(role=link) await page.click('role=link');等待策略是自动化脚本稳定性的生命线。Playwright的API大多是“自动等待”的,比如click、fill会在执行前检查元素是否可交互(可见、未被禁用、在视口内)。但有时你需要显式等待。
page.waitForSelector(selector, options): 等待某个元素出现在DOM中。
// 等待搜索按钮出现,最多等10秒 await page.waitForSelector('#su', { timeout: 10000, state: 'visible' });state可以是'attached'(存在于DOM)、'visible'(可见)、'hidden'等。
page.waitForFunction(predicate): 等待一个JavaScript条件成立。
// 等待页面标题变成包含“百度”字样 await page.waitForFunction(() => document.title.includes('百度'));page.waitForTimeout(ms): 强制等待指定毫秒数。这是最后的手段,应尽量避免使用,因为它会让脚本变得低效且不可靠。优先使用基于条件的等待。
实操心得:处理动态加载内容时,最稳健的模式是“导航 -> 等待网络空闲 -> 等待关键元素出现”。例如,在点击一个可能触发新内容加载的按钮后,立即跟上
await page.waitForLoadState('networkidle')和await page.waitForSelector('.new-content')。另外,Playwright的locator模式(如page.locator('#kw'))比直接使用page.$或page.$$更强大,它支持链式调用和更丰富的等待选项,是现代Playwright脚本的推荐写法。
3.3 模拟用户输入与事件
除了简单的点击和输入,Playwright能模拟几乎所有用户交互。
键盘操作:
await page.fill('#kw', 'Playwright'); // 模拟按下回车键 await page.press('#kw', 'Enter'); // 组合键,如Ctrl+A全选 await page.keyboard.press('Control+A'); // 输入特殊字符 await page.type('#kw', 'Hello World!', { delay: 100 }); // 每个字符间隔100ms,模拟真人输入鼠标操作:
const button = page.locator('button'); // 悬停 await button.hover(); // 右键点击 await button.click({ button: 'right' }); // 双击 await button.dblclick(); // 拖放(从source元素拖到target元素) await page.dragAndDrop('#source', '#target');文件上传:这是很多自动化工具的痛点,Playwright处理起来非常优雅。
// 方法1:对于input[type="file"]元素,直接设置文件路径 await page.setInputFiles('input[type="file"]', './my-file.pdf'); // 方法2:上传多个文件 await page.setInputFiles('input[type="file"]', ['./file1.png', './file2.png']); // 方法3:通过文件选择器对话框(更接近真实用户操作) const [fileChooser] = await Promise.all([ page.waitForEvent('filechooser'), // 监听文件选择事件 page.click('text=Upload'), // 点击触发文件选择对话框的按钮 ]); await fileChooser.setFiles('./my-file.pdf');方法3尤其适用于那些不是通过标准input元素触发上传的复杂前端组件。
下拉选择:
// 通过value选择 await page.selectOption('#city', 'beijing'); // 通过label文本选择 await page.selectOption('#city', { label: '北京' }); // 选择多个值 await page.selectOption('#multi-select', ['opt1', 'opt2']);4. 处理高级场景与疑难杂症
4.1 征服动态Iframe与多页面
现代网站,特别是那些包含第三方登录(如微信、支付宝)、富文本编辑器或地图服务的页面,大量使用iframe。Playwright处理iframe的思路很清晰:先定位到iframe元素,然后获取其对应的Frame对象,之后的所有操作都在这个Frame对象上进行。
// 方法1:通过iframe的元素句柄获取Frame对象 const iframeElement = await page.$('iframe[name="editor"]'); const iframe = await iframeElement.contentFrame(); await iframe.fill('body', 'Hello from inside iframe!'); // 方法2:使用page.frame()系列方法(更推荐) // 通过名称或URL定位iframe const iframeByName = page.frame({ name: 'editor' }); const iframeByUrl = page.frame({ url: /.*login-dialog.*/ }); await iframeByUrl.click('button:has-text("登录")'); // 方法3:使用Locator的frameLocator方法进行链式操作(最新、最优雅的方式) await page .frameLocator('iframe[title="富文本编辑器"]') .locator('body') .fill('直接在iframe里编辑内容');frameLocator是处理嵌套iframe的利器,它返回一个作用域被限定在该iframe内的定位器,后续的locator调用都会在这个iframe的DOM中进行查找。
处理多页面(新标签页)也很简单:
// 点击一个会打开新标签页的链接,并等待新页面出现 const [newPage] = await Promise.all([ context.waitForEvent('page'), // 监听新page事件 page.click('a[target="_blank"]'), // 点击链接 ]); // 现在可以操作新页面了 await newPage.bringToFront(); // 切换到该页 console.log(await newPage.title()); await newPage.close(); // 关闭新页面4.2 拦截与修改网络请求
Playwright强大的网络拦截能力,可以用于性能测试、屏蔽广告、修改请求/响应、模拟API返回等。
// 拦截所有请求,并打印URL await page.route('**/*', route => { console.log('Request:', route.request().url()); route.continue(); // 继续请求 }); // 拦截图片请求,并阻止加载以加快速度 await page.route('**/*.{png,jpg,jpeg,svg}', route => route.abort()); // 拦截特定API请求,并返回模拟数据 await page.route('**/api/user/profile', async route => { const mockData = { name: 'Mock User', id: 12345 }; // 以JSON格式返回模拟数据 await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockData), }); }); // 修改请求头(例如添加Token) await page.route('**/api/**', route => { const headers = { ...route.request().headers(), 'Authorization': 'Bearer fake-token-123', }; route.continue({ headers }); });这个功能在测试中极其有用,你可以轻松模拟后端API的各种响应(成功、失败、超时),而无需搭建复杂的测试环境。
4.3 应对反爬与复杂验证
Playwright能模拟真人浏览器环境,本身就绕过了许多基于简单HTTP库的检测。但对于更高级的反爬措施(如一些动态加密的验证参数),需要更多技巧。
伪装成普通浏览器:创建Context时,使用完整的、更新的User-Agent字符串,并设置合理的视口大小、时区、语言等。
const context = await browser.newContext({ viewport: { width: 1920, height: 1080 }, userAgent: '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', timezoneId: 'Asia/Shanghai', });注入JavaScript代码:有些网站会检测浏览器环境变量(如
navigator.webdriver)。Playwright启动的浏览器默认会将此属性设为true,可能被检测。可以在创建Page时注入脚本修改它。const page = await context.newPage(); await page.addInitScript(() => { Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); // 也可以修改其他属性,如plugins, languages等 });重要警告:关于修改
webdriver属性以绕过检测,其效果因网站而异,且可能涉及合规性问题。请仅将此技术用于对自己拥有权限的网站进行自动化测试,或明确允许自动化的公开数据获取。用于规避他人网站的访问限制可能违反服务条款。慢速操作与随机等待:避免脚本行为过于规律。在关键操作之间加入随机延迟。
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); await page.type('#kw', 'test', { delay: Math.random() * 100 + 50 }); // 50-150ms随机延迟 await delay(Math.random() * 2000 + 1000); // 1-3秒随机等待处理Canvas指纹等:对于极其严格的检测,可能需要更复杂的方案,如使用浏览器插件或更底层的CDP(Chrome DevTools Protocol)命令。但这已超出基本使用范畴,且维护成本很高。
关于“过瑞数”等特定反爬:网络热词中提到的“playwright过瑞数”指的是应对瑞数(一种动态安全技术)的挑战。这通常需要分析其JavaScript混淆代码,找到生成动态令牌(如cookie或参数)的逻辑,并在Playwright中复现或直接执行相关JS代码。这是一个非常深入的话题,涉及逆向工程,没有通用的一键解决方案。核心思路是使用page.evaluate()在页面上下文中执行JS,获取生成的令牌,再将其设置到后续请求中。
5. 测试集成与高级工具链
5.1 使用Playwright Test进行结构化测试
@playwright/test框架让编写和组织测试用例变得专业且轻松。它提供了测试夹具(Fixtures)、断言、并行执行和精美报告。
一个基本的测试文件如下:
// tests/example.spec.js const { test, expect } = require('@playwright/test'); test('百度搜索Playwright', async ({ page }) => { // `page` fixture由框架自动提供 await page.goto('https://www.baidu.com'); await page.fill('#kw', 'Playwright'); await page.click('#su'); // 等待搜索结果出现 await page.waitForSelector('.result.c-container'); // 使用Playwright Test内置的断言 await expect(page).toHaveTitle(/Playwright/); // 更灵活的断言:检查页面是否包含特定文本 await expect(page.locator('text=微软开源')).toBeVisible(); }); test('测试文件上传', async ({ page }) => { await page.goto('https://the-internet.herokuapp.com/upload'); await page.setInputFiles('#file-upload', './test-file.txt'); await page.click('#file-submit'); await expect(page.locator('h3')).toHaveText('File Uploaded!'); });你可以通过命令行运行测试:
npx playwright test # 运行所有测试 npx playwright test example.spec.js # 运行特定文件 npx playwright test --headed # 在UI模式下运行(显示浏览器) npx playwright test --project=chromium # 指定在Chromium上运行 npx playwright test --grep "搜索" # 运行标题包含“搜索”的测试运行后会生成HTML报告,直观展示通过/失败的测试、步骤、截图和追踪信息。
5.2 代码生成与调试利器:Playwright CLI
Playwright自带一个强大的命令行工具,对于快速探索和调试网站非常有帮助。
1. 代码生成器 (playwright codegen):这是新手的神器。运行它会在打开浏览器的同时,记录你的所有操作,并实时生成对应的Playwright代码。
npx playwright codegen https://www.baidu.com执行后,一个浏览器窗口和一个代码生成器窗口会同时打开。你在浏览器里的点击、输入等操作,都会自动转换成代码显示在另一个窗口。你可以直接复制这些代码到你的脚本中使用。这对于学习API和快速编写脚本原型至关重要。
2. 调试工具 (playwright debug):以调试模式运行脚本,会启动一个支持“时间旅行”的调试器。
npx playwright debug your-script.js你可以暂停执行、检查元素、查看调用栈、单步执行,并且浏览器界面会高亮显示Playwright正在操作的元素。这对于排查“元素找不到”、“点击没反应”这类问题效率极高。
3. 追踪查看器 (playwright show-trace):在测试配置中启用追踪后,每次测试失败都会生成一个.zip追踪文件。
// playwright.config.js module.exports = { use: { trace: 'on-first-retry', // 仅在第一次重试时记录追踪(节省资源) // trace: 'on', // 始终记录 // trace: 'retain-on-failure', // 仅在失败时保留 }, };运行测试失败后,使用以下命令查看追踪:
npx playwright show-trace trace.zip追踪查看器提供了一个图形化界面,你可以逐帧回放测试执行的全过程,查看每个时刻的DOM快照、控制台日志、网络请求,是分析偶发性失败的终极武器。
5.3 与CI/CD集成与打包部署
Playwright测试可以无缝集成到GitHub Actions、GitLab CI、Jenkins等持续集成流程中。官方甚至提供了预配置的GitHub Actions工作流模板。
关于“python 打包 playwright”的热词,这里有一个常见问题:当你用PyInstaller、PyOxidizer等工具将Python脚本打包成独立可执行文件(exe)时,Playwright的浏览器二进制文件可能不会被包含进去。解决方案通常是在打包后,通过代码在首次运行时检查并自动下载浏览器,或者将浏览器二进制文件手动复制到打包目录的特定位置。Playwright Python库提供了playwright install命令,你可以在你的打包后脚本或安装程序中调用它。
对于“cursor怎么配置playwright mcp”和“dify平台中使用playwright mcp”,这涉及到Playwright作为一个工具被集成到更上层的AI辅助编程(Cursor)或AI应用开发平台(Dify)中。MCP(Model Context Protocol)是一种协议,允许AI模型使用外部工具。在这些平台中配置Playwright MCP,本质上是让AI(如Claude Code)能够调用Playwright来浏览网页、执行操作,从而实现更复杂的AI智能体功能。这通常需要在平台的插件或工具配置页面,指定Playwright的安装路径和启动命令。
6. 常见问题排查与性能优化
6.1 高频问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
Error: page.goto: Navigation timeout | 页面加载超时(默认30秒)。网络慢、页面过重、或目标网站有阻塞。 | 1. 增加超时时间:page.goto(url, { timeout: 60000 })。2. 检查网络或代理设置。 3. 尝试使用 waitUntil: 'domcontentloaded'代替'networkidle'。 |
Error: Element is not visible | 元素存在但不可见(被遮挡、样式display: none、visibility: hidden)。 | 1. 确保元素在视口内:await element.scrollIntoViewIfNeeded()。2. 等待元素变为可见状态: await page.waitForSelector(selector, { state: 'visible' })。3. 检查是否有弹窗、遮罩层覆盖。 |
Error: No node found for selector | 选择器找不到元素。元素尚未加载、选择器写错、或在iframe内。 | 1. 在操作前增加等待:await page.waitForSelector(selector)。2. 使用浏览器开发者工具检查选择器是否正确。 3. 确认元素是否在iframe中,使用 frameLocator。 |
点击/输入没有效果 | 元素可能被其他透明元素覆盖,或者页面状态未就绪。 | 1. 使用page.click(selector, { force: true })强制点击(不检查可交互性)。2. 先尝试 element.hover()再点击。3. 在操作前等待页面状态稳定: await page.waitForLoadState('networkidle')。 |
文件上传不工作 | 文件选择器不是标准的<input type="file">,可能是自定义的div模拟的。 | 1. 使用page.waitForEvent('filechooser')方法(见上文)。2. 如果页面是React/Vue组件,可能需要触发特定的事件。 |
脚本在无头模式下运行正常,但非无头(headed)下失败 | 无头模式和有头模式在渲染、计时上可能存在细微差异。 | 1. 在非无头模式下添加slowMo参数,放慢操作速度便于观察。2. 检查是否有依赖视觉的逻辑(如懒加载),增加等待时间。 3. 统一测试环境,尽量在CI中也使用无头模式。 |
浏览器无法启动 | 浏览器二进制文件损坏、权限问题、或端口冲突。 | 1. 重新安装浏览器:npx playwright install或playwright install。2. 检查是否有其他进程占用了Playwright使用的端口。 3. 在Linux/Mac上,检查浏览器二进制文件是否有执行权限。 |
6.2 性能优化与最佳实践
复用Browser实例:启动和关闭浏览器的开销很大。对于需要执行多个独立任务的脚本,应该只启动一个Browser实例,然后为每个任务创建独立的Context。任务完成后关闭Context,但保留Browser。
const browser = await chromium.launch(); // 任务1 const context1 = await browser.newContext(); const page1 = await context1.newPage(); // ... 执行任务1 await context1.close(); // 任务2 const context2 = await browser.newContext(); // ... 执行任务2 await context2.close(); await browser.close(); // 所有任务完成后关闭浏览器并行执行:利用
Promise.all并行执行多个独立操作,可以大幅缩短总耗时。// 串行(慢) await page.goto(url1); await page.goto(url2); // 并行(快)- 注意:这需要多个Page,通常在一个Context下创建 const [page1, page2] = await Promise.all([ context.newPage(), context.newPage(), ]); await Promise.all([ page1.goto(url1), page2.goto(url2), ]);禁用不必要的资源加载:如果不需要图片、样式、字体等来执行你的操作(例如只做API接口测试),可以拦截并阻止它们加载,极大提升页面加载速度。
await page.route('**/*.{png,jpg,jpeg,svg,gif,css,woff,woff2}', route => route.abort());合理使用等待,避免
sleep:始终使用条件等待(waitForSelector,waitForFunction)而非固定等待(page.waitForTimeout)。条件等待在条件满足后立即继续,而固定等待总是消耗固定时间,效率低下且不可靠。清理资源,避免内存泄漏:确保在脚本结束时(包括发生异常时)关闭Page、Context和Browser。使用
try...catch...finally块是个好习惯。const browser = await chromium.launch(); try { const context = await browser.newContext(); const page = await context.newPage(); // ... 你的主要逻辑 } catch (error) { console.error('执行出错:', error); } finally { await browser.close(); }
从我自己的使用经验来看,Playwright最大的优势在于其稳定性和开发者体验。它的错误信息通常很清晰,调试工具链完善,社区活跃。相比于Selenium,它不需要额外的WebDriver管理;相比于Puppeteer,它提供了多浏览器支持和更丰富的内置功能。对于任何涉及浏览器自动化的任务,无论是测试、爬虫还是监控,它目前都是我的首选工具。刚开始可能会觉得API繁多,但一旦熟悉了Browser-Context-Page-Frame这个核心模型和Locator模式,你会发现它既强大又直观。