Playwright 是 Microsoft 推出的浏览器自动化框架:这是一个以 TypeScript 为核心、采用 Apache-2.0 许可的库,能够启动真实浏览器,通过同一套 API 驱动浏览器,并在 JavaScript 执行完成后把页面内容交还给你。它主打端到端测试框架,但很多人在遇到 HTTP 请求只返回一个空壳、真正数据却不见踪影时,都会默默转向它底层的这套引擎。它的形态和 Puppeteer、Selenium 很像——操控真实浏览器,而不是解析 HTTP 客户端返回内容。
我用一组固定的抓取测试对 microsoft/playwright 1.56.0 做了实测:包含带分页的静态目录、文章页、JavaScript 渲染目录、JSON API、报错的 500 页面、小型爬取图,以及两个公开练习站点。测试环境是 Node v22.22.3、macOS arm64,并且只测了 Chromium。渲染这一半表现干净利落;但网页抓取这一半根本不是它的职责,而这正是你在决定是否采用它之前最该先明白的一点。
最让我在意的点
结果里最亮眼的有两项,而且它们分别指向了不同的方向。
第一项是符合预期的浏览器结果,但前提是我人为设置了等待条件。在 page.goto(..., { waitUntil: 'domcontentloaded' }) 之后,动态页面测试继续等待 #dynamic-products article.product-card,公开的 Quotes to Scrape 测试则等待 .quote。这些应用专属的选择器出现后,测试分别拿到了 8/8 个 fixture 条目和 10 条公开站点引言;fixture 的召回率相对 8 条真值结果达到 1.0。这里并不需要额外写轮询循环,但 Playwright 也没有替你消除“页面是否已准备好”的问题——完成条件还是要由测试来定义。第一次调用时,全页截图也成功保存了。
第二项结果使用的是 browserContext.request:也就是在 Chromium 已经启动、浏览器上下文已经存在后,通过 ctx.request.get(...) 直接请求 fixture 的 JSON 接口,返回了 8/8 个商品,而且没有创建或渲染任何页面。这样绕开的是 DOM 工作,而不是这个测试框架里的浏览器进程成本。和浏览器页面共享 cookie 状态的是上下文绑定的 request 客户端;而单独的 playwright.request.newContext() 虽然不需要现成的浏览器上下文,但不会自动共享那个会话。这个测试只覆盖了前者路径。
Playwright 也不提供爬虫队列、数据集写入器或自动限速。我的网页抓取图测试——遍历内部链接、记录深度并避免重复访问——最终走到了 12 个页面,深度分布为 {0:1, 1:4, 2:7},但广度优先遍历这部分逻辑是我自己写的。Playwright 负责打开并检查页面;前沿队列持久化、URL 策略、重试和调度则属于别的层。
Playwright 本质上是什么
这个工具——GitHub 上的 microsoft/playwright——使用 TypeScript 编写,采用 Apache-2.0 许可,由 Microsoft 维护。这里测试的版本是 1.56.0,发布时间为 2026 年 7 月 9 日。本文所有结果都只针对这个版本,不应被理解为对后续版本兼容性的承诺。
官方定位很明确:它是一个用于网页测试和自动化的框架,可以通过统一 API 驱动 Chromium、Firefox 和 WebKit。Playwright 的主线是测试运行器,包含 fixtures、断言和 trace 查看器。把它当成抓取原语使用时,应遵循官方文档里的 Library mode:先 chromium.launch(),再创建 context,再创建 page,脱离测试框架本身使用。这篇评测中的所有内容都基于这套公开 API。我没有做跨版本兼容性测试,所以这里不能声称我测试到的行为在所有版本里都稳定。
文档里强调的广度,确实是它在其他工具里最突出的卖点,所以这里我谨慎说明。我这次实际测试的是:Playwright 通过一套 API 驱动三种浏览器引擎——Chromium、Firefox 和 WebKit——并且在 JavaScript 之外还提供 Python、Java 和 .NET 的一流客户端支持。以上都是官方文档明确写明的,也是它在形态上的最大差异点。不过这次实测真正覆盖到的范围更窄:
| 能力 | 本次评测中的状态 |
|---|---|
| Chromium 引擎 | 已实测——本文所有测试都在 Chromium 上运行 |
| Firefox 引擎 | 官方文档支持,但本文未验证 |
| WebKit 引擎 | 官方文档支持,但本文未验证 |
| 三种引擎统一 API | 官方文档支持,但本文未验证 |
| Python、Java、.NET 客户端 | 官方文档支持,但本文未验证 |
| 代理设置 | 未测试 |
| 多上下文并行规模 | 未测试 |
| 面向 API 优先抓取的网络拦截 | 未测试 |
如果目标站在 Safari 的 WebKit 下表现不同,或者你的团队主要写 Python,那么 Playwright 的核心卖点正是这份广度——但不要把我的结果当成 Firefox 或 WebKit 也同样可用的证明,因为我没有测它们。
它的底层工作方式
可以把它理解成一个由你来脚本控制的浏览器引擎。chromium.launch() 会启动一个浏览器进程。context 是一个隔离会话,拥有自己独立的 cookie、存储和缓存;page 则是这个上下文里的一个标签页。你先调用 page.goto(url),再等待代表应用已就绪的条件,最后用 page.$$eval 之类的辅助方法读取 DOM。它比解析 HTTP 响应更接近真实用户在浏览器里的操作,但它并不等于环境完全一致:无头模式信号、视口、语言环境、字体、用户配置状态、TLS/网络路径以及站点防护机制,仍然可能改变页面返回内容。本文没有测试反爬行为,也没有测试和生产浏览器的完全一致性。
page.screenshot() 可以截取渲染后的页面,支持全页或局部;这次测试里我第一次调用就成功了。前面提到的 request API——context.request.get——会沿用同一上下文的 cookie,但不需要渲染页面,因此你可以在同一个脚本里同时做“打开页面并读取 DOM”和“直接请求 JSON 接口”,而不必切换工具。
它底下没有爬取机器这一层。没有请求调度器、没有持久化的已访问集合、没有礼貌策略、也没有导出流水线。做一个有限范围的遍历并不难画出来,但要真正稳定地维护前沿队列,还需要 URL 归一化、重定向处理、重试、作用域规则、限速和故障恢复。你要么自己搭这一层,要么用一个包了浏览器引擎的框架。
安装与部署的现实情况
安装分两步,而且第二步才是真正决定部署成本的地方。npm install playwright 只会拉取库本身;npx playwright install 则会下载浏览器构建版本(我这里是 Chromium)。你需要把磁盘空间、下载时间、CI 环境中的浏览器缓存以及进程清理都算进去,而不是只把 npm 包当成整个可运行系统。
如果你是按测试教程来理解 Playwright 并把它当成爬虫,你起步时会看到测试文件和 expect() 断言;而真正的抓取代码则是直接调用库 API。这两种用法都在官方文档里,但在搜索示例和决定部署命令时,这个区别非常重要。
这次实测中,值得一提的易用性很具体:浏览器上下文可以隔离会话状态,异步调用拼接自然,一行就能截屏,HTTP 500 也可以通过 response 对象继续查看。真正麻烦的是运维层面,而不是语法层面:浏览器构建版本必须单独安装,生命周期也要独立管理。
实测结果

所有本地数据都在 127.0.0.1 的 fixture 服务器上跑出,且在网页抓取前就已经写好了真值。完整测试脚本在 run_playwright_material_tests.mjs,已提交的 raw artifacts 里包含真值和每项测试的输出。这些都只是作者在一台机器上的一次实测记录;链接提供的是可复现路径,而不是把它变成广义基准测试。
| 测试 | 目标 | 结果 |
|---|---|---|
| 静态目录 + 分页 | 本地 fixture | 12/12 个商品,召回率 1.0 |
| 文章抽取 | 本地 fixture | 标题 + 3/3 段正文,且保留了 boilerplate 区域 |
| 动态 JS 页面(原生渲染) | 本地 fixture | 8/8,召回率 1.0,全页截图已保存 |
动态 JSON API(page.request) | 本地 fixture | 8/8,召回率 1.0,无需渲染 DOM |
| HTTP 500 处理 | 本地 fixture | 可查看 500 状态,导航未抛错 |
| 爬取图(手写 BFS) | 本地 fixture | 12 个页面,深度分布 {0:1, 1:4, 2:7} |
| Books to Scrape | 公开演示站点 | 20 个商品 |
| Quotes JS(JS 渲染) | 公开演示站点 | 10 条引言,原生渲染 |
分页循环是明确跟着下一页链接走的;Playwright 并不会自动帮你发现页面。文章选择器把导航和页脚文本挡在正文结果之外。对于失败路径,导航返回了一个状态码为 500 的 response 对象,而不是直接抛错,至于后续是记录日志、重试还是继续,则由调用者决定。两个公开练习目标都返回了表格里写明的数量。
这里有一个边界必须说清楚:以上所有内容都只在一台机器上的 Chromium 上运行了一次。能力表把“文档支持的广度”和“本次实测到的行为”分开列出。我没有在另一个 Playwright 版本上重新跑这套测试,所以这里不能推导出跨版本结论。每项测试的耗时也没有作为基准值保留,因为单台笔记本的一次计时无法支撑速度对比。
准备就绪,本身就是抽取合同的一部分
动态结果依赖的等待条件是“数据真的准备好了”,而不只是浏览器完成了跳转。对于本地目录,测试先以 waitUntil: 'domcontentloaded' 完成导航,再用 15 秒超时等待 #dynamic-products article.product-card。公开的 Quotes JS 测试也是相同的导航状态,并等待 .quote,超时 20 秒。只有这些选择器出现后,才开始抽取数据。
在改写脚本时,这个区别非常关键。domcontentloaded 只表示初始文档已解析,并不意味着延迟返回的 API 已经到达、hydration 已完成、无限列表停止增长,或者虚拟列表中的某一行已经进入视口。若“页面上出现某个匹配元素”就足够,那么选择器等待就很有用;但如果完整性取决于某个已知响应、项目数量、应用状态或网络空闲窗口,就应该改为等待那个条件。等待条件应该和输出契约绑定: “至少出现一张卡片” 和 “所有预期页面都已加载完成” 本来就是两种不同的断言。

超时处理也应该由调用方来负责。测试里用了有限时长的 selector 超时,但没有研究重试策略,也没有去区分“页面只是慢”还是“选择器已经永久变了”。真正上线的包装层应该记录到底是哪一个就绪条件失败了,抓取足够的页面状态用于排查,并判断是否适合再次尝试导航。Playwright 能提供事件和 DOM,但它无法替你判断这份工作里的“完整数据”到底意味着什么。
这个边界应该写在每个抽取器旁边,而不是留给隐含的超时去表达。
API 路径也有类似的契约。之所以使用 ctx.request.get,是因为浏览器上下文已经存在,而且共享会话状态很有价值。如果某个任务发现自己的数据接口在完全没有浏览器会话的情况下也能工作,那么单独的 request 上下文就是另一种架构,它有不同的生命周期和 cookie 行为。本次测试没有对这两种方式做对比。应当把“没有渲染 DOM”视为本次测得的事实,至于是否需要浏览器进程,则由更大的工作流来决定。
关于网页抓取这一层的问题
爬取图的结果,最能决定你应该如何看待 Playwright。12 个页面、3 个深度、结果正确——但所有“走图”的逻辑都是我自己写的。Playwright 提供的是“打开这个 URL 并读取内容”的那一半;队列、已访问集合和深度追踪则由我来提供。
对于规模不大的有限任务,这不是问题。但一旦进入网页抓取级别,就意味着你要么是在浏览器库之上自己写爬虫,要么是在和一个已经具备爬虫能力的组件搭配使用。文档里常见的方案是 Crawlee,它把 Playwright(以及 Puppeteer)包装成真正的请求队列、数据集存储和自动限速层——你保留 Playwright 的渲染能力,借用它的调度系统。如果你更希望队列能力直接内建在框架里,而不是后面再外挂一层,那就是 Scrapy 的设计思路,不过 Scrapy 是以 HTTP 为先,并不会自己渲染 JavaScript。重点不是 Playwright 不够好,而是“浏览器自动化”和“网页抓取”本来就是两件事,Playwright 只负责其中一件事。

这次 12 页的 BFS 很清楚地说明了责任边界。它为一个受控图提供了队列、已访问集合和深度追踪;但真正生产环境中的前沿层还必须定义 URL 规范化、重定向处理、允许的主机、去重键、重试、并发、按主机延迟、持久化和重启语义。导出本身也是另一个选择:fixture 写出 JSON 和 CSV,只是因为测试脚本这么做了,而不是因为 Playwright 自带数据集抽象。
会话设计也会影响包装层。一个浏览器可以包含多个上下文,每个上下文都有独立的 cookie 和存储;但本次评测没有测多上下文并行规模,也没有测故障隔离。复用上下文可以保留登录状态并减少初始化开销;创建独立上下文则可以防止任务之间的状态泄漏。尽管这些都是爬虫层面的策略,Playwright 还是把 context 这个原语提供给你了。真正要比较的是你在实际部署环境和浏览器构建版本下选择的生命周期方案。
优缺点
优点:
- 通过真实 Chromium 引擎执行 JavaScript;两个动态目标都顺利等到了用于就绪判断的选择器。
- 第一次调用就成功截取了全页截图。
- 在受控 fixture 上,选择器可以提取预期的静态目录和文章字段。
browserContext.request能在不渲染页面的情况下直接命中 JSON 接口,同时浏览器进程仍然处于运行状态。- 对错误响应比较稳:HTTP 500 可以查看,导航也不会抛错。
- 官方文档支持通过同一 API 驱动三种引擎(Chromium、Firefox、WebKit),并提供 Python、Java、.NET 客户端(本文仅实测 Chromium,但这些能力是官方支持的)。
- Apache-2.0 许可,并由 Microsoft 维护。
- 一旦进入 library mode,开发体验很顺:统一 API、原生异步、截屏简单。
缺点:
- 没有内置网页抓取队列、数据集或自动限速——要做大规模网页抓取,只能自己写,或者用 Crawlee 这样的封装层。
- 浏览器本身很“重”:下载二进制和每页运行成本,才是相较于纯 HTTP 工具的主要代价。
- 默认叙事是测试运行器;要用它做抓取,得先知道 library mode 的存在,并主动跳出官方主打的路径。
- 这里只测了 Playwright 1.56.0 和 Chromium,跨版本、跨引擎一致性都没有验证。
- 它本身不提供基于 schema 的结构化 JSON 输出;选择器和数据形状都得自己写。
适合谁,不适合谁
如果你的问题是:页面里的数据要等 JavaScript 跑完才出现,或者你除了 DOM 数据还想一起拿到截图,那么 Playwright 是一个值得对目标站复现实测的候选方案。已经在用 Playwright 做测试的团队,也可以直接把相同的概念和选择器技巧迁移到 library mode。Python、Java 和 .NET 客户端都是文档支持的选项,但这篇评测只实测了 Node 和 Chromium。
以下三种情况,建议考虑别的层。如果所需数据本来就已经存在于 HTTP 响应里,那么 HTTP-first 工具能省掉浏览器启动和渲染开销;Colly 属于这类爬虫库,而 Trafilatura 更偏向文章抽取。如果你需要队列、持久化和限速,就该用爬虫框架或 Playwright 的封装层。如果你想要的是无需维护选择器的 schema 形输出,那就去比较托管式抽取服务。这些替代方案都没有在本文中做基准测试。
如果你现在是在 Playwright 和 Puppeteer 之间做选择,那是另一个单独的对比问题;我们的 并排比较 会用同一套 fixture 跑两者,并覆盖最终差异到底落在哪里。
替代方案,以及托管式抽取适合放在哪
Playwright 是免费、Apache-2.0、可自托管的。浏览器部署、选择器、就绪条件、网页抓取代码、更新和故障处理,都得由你负责。本次评测没有测试反爬表现,也没有把总运营成本和托管服务做对比。
在开源方案里,更有价值的比较方式是按任务来分。对于浏览器上的大规模网页抓取,Crawlee 给 Playwright 补上了它缺少的队列和数据集能力。如果你的目标输出不是手工拼出来的行,而是更适合 LLM 使用的 Markdown,那么 Crawl4AI 会运行浏览器并把结果产出为 Markdown,适合那类流水线。如果你想一次性横向比较这些工具,我们的 开源爬虫总览 已经把各类方案并排整理好了。
披露说明:Thunderbit 是发布方的产品,本次没有放进这套 Playwright fixture 里跑。它代表的是托管式抽取这一类:服务方负责渲染,返回页面文本或 schema 化记录;而 Playwright 则把浏览器操作和选择器逻辑留给开发者。因此,这里的比较是托管模式、输出形态和成本模型的差异,而不是这次评测里跑出来的性能结果。
结论
当你的目标需要浏览器引擎,而且你也准备自己负责就绪条件、选择器和网页抓取调度时,Playwright 就很合适。本次作者实测的结果表明,它的 Chromium library mode 能按预期处理受控的静态、动态、API、截图和失败场景。
但要保留证据边界:这次只跑了 Chromium 和 Node;那条 12 页的遍历依赖手写 BFS;browserContext.request 虽然跳过了页面渲染,但仍然依赖已启动的浏览器进程;每一次动态抽取都明确使用了就绪选择器。基于这些限制,在本文的语境里,Playwright 只是一个浏览器原语,而不是一个经过实测的端到端网页抓取系统。
试试 Thunderbit,轻松提取网页数据 Get Started Free
常见问题
用 Playwright 抓取时,还需要等待吗?
需要。浏览器执行完成,并不会自动告诉你的脚本应用数据什么时候才真正准备好。本次测试是先导航到 domcontentloaded,再等待目标站点特定的选择器,最后才抽取数据。对于生产页面,可能需要别的信号,比如 response、定位器状态或应用事件。
Playwright 能自己抓取完整个网站吗? 开箱即用不行。它没有内置请求队列、数据集写入器或自动限速——我之所以能把网页抓取图跑到 12 个页面、深度分布 {0:1, 1:4, 2:7},是因为我自己手写了广度优先搜索。要做大规模网页抓取,可以把 Playwright 和 Crawlee 搭配使用,或者直接上爬虫框架。
什么时候该用 browserContext.request,什么时候该用独立 request 上下文?
如果 HTTP 请求需要和现有浏览器上下文里的页面共享 cookie,就用 browserContext.request。如果你只想要 API-only 上下文,不打算启动浏览器,也不需要和浏览器页面自动共享 cookie,那就用 playwright.request.newContext()。本文只测试了第一种路径。
这里测试 Firefox 和 WebKit 了吗? 没有。本文所有测试都只在 Chromium 上跑了一次,且只在一台机器上完成。Playwright 文档支持的三引擎能力(Chromium、Firefox、WebKit)以及 Python、Java、.NET 客户端,我这里是按官方说明来报告,而不是实测验证——Firefox 和 WebKit 的一致性、代理、多上下文规模以及网络拦截,都不在本文数据范围内。
这篇评测覆盖了什么环境? Playwright 1.56.0、Node v22.22.3、macOS arm64,且只测了 Chromium。Firefox、WebKit、代理、多上下文规模、反爬行为以及更高版本的 Playwright 都不在这次运行范围内。安装时需要先装库,再单独下载浏览器构建版本。


