Playwright多页面并发需用asyncio.gather或create_task配合Semaphore限流,避免单context阻塞;应为每个请求新建context隔离状态,用evaluate高效提取数据,并配置headless="new"及禁用GPU防崩溃。

Playwright 多页面并发不是靠 page.goto() 串行调用
直接在一个 browser 实例里连续 page.goto() 是串行的,哪怕开了多个 Page 对象,若没显式并发控制,实际仍是排队执行。真正并发依赖的是异步任务调度 —— Playwright 的 async API 必须配合 asyncio.gather() 或 asyncio.create_task() 才能压满浏览器上下文吞吐。
常见错误是写成同步风格的循环:for url in urls: page = context.new_page(); await page.goto(url),这会阻塞后续页面创建,性能比单页还差。
- 必须用
context.new_page()创建独立Page实例,每个实例对应一个真实标签页 - 所有
goto、evaluate、screenshot等操作都得是await调用 - 并发数受
context和浏览器进程资源限制,一般单context支持 20–50 个活跃页面,再多易 OOM
用 asyncio.Semaphore 控制并发数量防崩
无节制创建页面会迅速耗尽内存或触发 Chromium 的进程限制(如 “Failed to create a new page” 或 TimeoutError: Page.goto: Timeout 30000ms exceeded)。不能只靠 asyncio.gather() 把所有任务扔进去 —— 得加信号量限流。
比如抓 100 个 URL,但只允许最多 10 个页面同时加载:
立即学习“Python免费学习笔记(深入)”;
sem = asyncio.Semaphore(10)
async def fetch_one(url):
async with sem: # 这里阻塞,直到有空槽位
page = await context.new_page()
await page.goto(url, wait_until="networkidle")
title = await page.title()
await page.close()
return title-
async with sem必须包裹context.new_page()和整个生命周期,否则页面创建不受控 - 别在
sem外提前await context.new_page(),那等于白限流 -
wait_until="networkidle"比"load"更稳,避免 JS 渲染未完成就取数
browser.new_context() 比复用 context 更适合高并发隔离
很多人想“省资源”,把所有页面塞进同一个 context,结果遇到 cookie 冲突、localStorage 干扰、或某个页面卡死拖垮全部请求。其实 new_context() 开销远小于想象 —— Playwright 会复用底层浏览器进程,只是逻辑隔离。
典型误用:context = await browser.new_context(); for _ in range(20): page = await context.new_page() —— 一旦某个页面触发无限重定向或弹窗,整个 context 可能 hang 住。
- 对无状态抓取(如纯 HTML 渲染),优先用
await browser.new_context(user_agent=...)配合ignore_https_errors=True - 需要登录态复用时,才考虑单
context+ 多page,但必须加超时和异常兜底:await page.goto(..., timeout=15000) - 每个
context最好配独立user_agent和viewport,降低被风控概率
渲染后提取数据别用 page.content() 全量拉取
page.content() 返回完整 HTML 字符串,对大页面(尤其含大量内联 JS/CSS)非常慢,且容易因编码问题损坏中文。真正高效的方式是直接在浏览器上下文中执行提取逻辑,只传回必要字段。
例如取标题和文章正文文本:
data = await page.evaluate('''() => ({
title: document.title,
text: document.querySelector("article")?.innerText || "",
})''')-
page.evaluate()比page.inner_text()或page.query_selector()+.text_content()快 3–5 倍,减少 Python ↔ 浏览器 IPC 开销 - 避免用
page.wait_for_selector()等 DOM 出现再取 —— 如果 selector 永远不出现,会卡死;改用page.evaluate("() => !!document.querySelector('article')")做存在性判断 - 如果要截图或 PDF,
page.screenshot()和page.pdf()必须在wait_until="networkidle"后调用,否则截到白屏
实际跑起来最易忽略的点:Chromium 默认启用 GPU 加速,在无界面服务器(如 Linux Docker)上可能崩溃。启动时务必加 headless="new" 和 args=["--no-sandbox", "--disable-gpu"] —— 少一个参数,整批页面都会静默失败。


















