Playwright 的 BrowserContext 与 Page:原理与实践指南
本文以 Playwright 的核心对象模型为主线,解释:
BrowserContext是什么、为什么存在、它隔离了什么、它的生命周期如何影响稳定性与性能Page(标签页)是什么、它内部有哪些关键子概念(Frame、Execution Context、网络栈)、以及常见抓取/自动化中的坑
适合做爬虫/渲染服务(尤其是"一个 worker 长驻 + 多请求并发")时作为设计参考。
1) 总览:Playwright 的三层架构(Client → Driver → Browser)
以 Python 为例(Node/Java/C# 类似):
- Client API
await page.goto(...) / page.click(...)这些调用。 - Playwright Driver(通常是 Node 进程)
Python 端会启动并与之通信。它负责把"高级语义"翻译成底层浏览器协议调用、实现自动等待、事件分发等。 - 真实浏览器进程(Chromium / Firefox / WebKit)
Chromium 还会再拆成多进程:browser 进程、renderer 进程、network 进程、GPU 进程等。
因此你看到的并发/CPU 分布,往往是:
- Python 负责调度与序列化/反序列化(通常单进程单线程事件循环)
- Driver(Node)是单线程事件循环
- 浏览器(Chromium)是多进程并行干活的主体
2) Browser / BrowserContext / Page:对象关系
可以把它们理解为:
Browser:一个浏览器实例(或一个到远端浏览器的连接)BrowserContext:浏览器里的一个"隔离的用户会话空间"(近似"无痕窗口/独立 Profile")Page:一个标签页(tab),属于某个 context
关系是:
Browser
├─ BrowserContext A
│ ├─ Page A1
│ └─ Page A2
└─ BrowserContext B
└─ Page B1
关键点:隔离通常发生在 Context 级别;Page 只是该 Context 内的一张"页面"。
3) BrowserContext:原理与知识点
3.1 BrowserContext 到底隔离了什么?
一个 BrowserContext 典型会隔离(或可配置):
- Cookies、LocalStorage、SessionStorage(同站点登录态的核心)
- Cache / Service Worker(不同浏览器实现细节不同,但语义上是"会话隔离")
- Permissions(地理位置、通知等授权)
- UA / Locale / Timezone / Viewport 等"设备指纹"配置(通常在创建 context 时指定)
- HTTP 认证信息(basic auth)、额外请求头(extraHTTPHeaders)
- 路由拦截(
context.route(...))------对该 context 内所有 page 的请求统一生效
简化理解:一个 context ≈ 一套独立的"用户环境/会话容器"。
这也是为什么爬虫服务通常选择"每个请求一个 context":避免 cookie 串号、状态污染、缓存污染、权限残留。
3.2 为什么不直接每个请求起一个 Browser?
因为 Browser 太重:
- 启动浏览器进程开销大(尤其是冷启动)
- 内存占用高
- 稳定性更差(频繁启动/退出更容易遇到资源竞争)
更典型的设计是:
- 复用 Browser(或复用 CDP 连接):减少冷启动
- 每请求创建/销毁 Context:保证隔离与可回收
你在仓库里的 scrape.py / chrome_configuration_fastapi.py 正是这种思路:复用 Playwright driver,尽量不复用 context。
3.3 Context 的生命周期为什么这么重要?
Context 是否及时关闭,会直接影响:
- 内存是否泄漏:页面、frame、JS heap、网络连接、下载句柄都挂在 context 下
- 并发稳定性 :大量残留 context/page 会导致
Target closed、Too many open files、连接复用异常、CDP 传输拥塞 - 性能抖动:缓存/ServiceWorker/Storage 污染会导致同站点行为变化、调试困难
经验法则:
- 对"服务端抓取 worker":强烈建议每个请求
context.close()(必要时还browser.close()或断开连接) - 对"需要登录保持"的场景:用 persistent context,但要限制复用范围(同租户/同账号),并对生命周期做强管理
3.4 Persistent Context vs Incognito Context
Playwright 有两类常用的 context:
-
普通 context(默认 / incognito-like)
browser.new_context(...)创建,生命周期内有效,关闭即清空(不落盘)。 -
持久化 context(persistent)
launch_persistent_context(user_data_dir=...)或等价方式创建。- 会把 profile 数据落到磁盘(cookies、storage 等)
- 非常适合"先人工登录一次,然后自动化复用登录态"
- 风险:更容易污染、膨胀、跨任务串号(不适合多租户共用)
爬虫/渲染服务如果要走 persistent:
- 需要按账号/租户隔离 user_data_dir
- 需要定期清理与回收(磁盘/缓存膨胀很快)
3.5 Context 级别的网络拦截:为什么比 Page 更适合"省资源"?
context.route("**/*", handler) 的优势:
- 一次设置,对 context 内所有 page 生效
- 可用于统一屏蔽图片/字体/媒体/广告域名,减少页面加载带宽
在 chrome_configuration_fastapi.py 里你能看到一个典型实现:
- 当不需要截图时
block_assets=True - 拦截
resource_type in {image, stylesheet, font, media}或 URL 后缀命中时直接route.abort()
这个思路要点是:要"省资源",必须在请求发出去之前拦截 ,单纯在 HTML 里删 <img> 并不能阻止浏览器已经下载资源。
4) Page:原理与知识点
4.1 Page 是什么?
Page 是一个标签页(tab),也是你与 DOM/JS/网络交互的主要入口:
page.goto(url):导航page.locator(...) / page.click(...):交互page.evaluate(...):在页面 JS 环境中执行脚本page.screenshot(...):截图
Page 属于一个 Context:它继承 context 的 cookies/storage/headers/proxy/route 等环境。
4.2 Page 的内部结构:Main Frame 与 Frames
一个页面不是只有一个 DOM:
page.main_frame:主文档的 frame- iframe 会生成子 frame
很多"为什么我拿不到元素/执行 JS 不生效"的问题,本质是:
- 元素在 iframe 里
- 你在错误的 frame 上执行脚本/定位
定位策略通常要用:
page.frame(...)/frame_locator(...)- 或在 DOM 里先定位 iframe 再进入其 frame
4.3 Page 的"执行上下文"(Execution Context)
页面 JS 的执行环境会随着:
- 导航(goto)
- SPA 路由切换
- frame 销毁/重建
发生变化。常见现象:
Execution context was destroyed
通常是你在页面还在导航时evaluate,或者页面被重定向/刷新导致上下文重建。
处理思路:
- 等待合适的 load state(
domcontentloaded通常比load更稳也更快) - 对 SPA 用更稳的信号(特定 selector 出现、网络空闲阈值、自定义 JS 判断)
4.4 Page 的导航与等待:wait_until 的真实含义
Playwright 导航常见的等待点(以 Chromium 为例):
domcontentloaded:DOM 构建完成(不等所有资源)load:window.onload 触发(资源更全,可能更慢)networkidle:网络在一段时间内"几乎空闲"(对现代站点不稳定,长连接/埋点会让它永远不 idle)
做爬虫服务时,最常用策略是:
- 默认
domcontentloaded - 需要更完整内容时,再配合:
page.wait_for_selector("...")- 或自定义"网络空闲阈值"(你项目里就实现了
_wait_for_network_idle_threshold这种思路)
4.5 Page 截图的成本与副作用
截图不是"读个 buffer"那么简单:
- full page 截图需要计算页面高度、可能触发滚动、触发懒加载
- 截图生成会消耗 CPU(浏览器侧)和内存(图片 buffer)
- base64 编码会消耗 CPU(你的应用侧)和放大内存峰值
因此常见的工程优化是:
- "不需要截图时拦截媒体资源"
- "截图用 PNG 还是 JPEG":PNG 更清晰但体积大;JPEG 可控质量但有损
- 返回结果尽量不要把大图塞进 JSON(改用对象存储 URL),否则 stdout/HTTP 带宽和内存会成为瓶颈
5) 并发与资源:为什么"多 Page"不等于"多核满载"
Playwright 的并发主要体现为:
- 浏览器侧:多个页面/渲染/网络并行(Chromium 多进程)
- driver/client 侧:仍然是事件循环调度 + IPC 通信
因此你经常看到:
- Python/Node 进程 1~2 个核较高
- 其它核主要由浏览器进程使用(或由远端 CDP 的那台机器使用)
工程上常见的并发模型是:
- 一个 worker 进程内:
Browser复用、Context按请求创建、并发由Semaphore控制 - 多进程扩展:用多个 worker 把 CPU/IO 吃满(尤其在本机渲染时)
6) 最佳实践清单(面向"抓取服务/worker")
- 每请求一个 Context(隔离会话,减少串号与污染)
- 确保 finally 里关闭 context/page(避免泄漏导致长期不稳定)
- 默认
wait_until="domcontentloaded",避免networkidle无限等待 - 不截图时在 context 层做资源拦截(image/font/media/css/广告域名)
- 截图要谨慎:full page 会触发懒加载,吞吐会显著下降
- 结果体积要控:大图不要长期塞 base64(能用 URL 就用 URL)
7) 一个最小化心智模型(帮助你"设计对")
Browser:重资源,尽量复用(或复用 CDP 连接)BrowserContext:隔离容器,请求级生命周期(可控、可回收)Page:一次抓取/一次交互的工作单元(Tab),属于某个 context
"高并发稳定服务",本质是:正确地选择"复用边界"和"隔离边界"。通常复用 Browser,隔离 Context。