playwright 的 async api 必须在 asyncio event loop 中运行,不可跨线程;应复用 context、限制并发 page 数、设置超时与重试机制,并严格按顺序关闭 page、context 和 browser。

Playwright 的 async API 必须配合 asyncio.run() 或已存在的 event loop 使用
Playwright 的 Python 绑定默认只提供异步接口(async with async_playwright()),没有 sync 版本。直接在普通函数里调用 browser.new_context() 会报 RuntimeWarning: coroutine 'Browser.new_context' was never awaited —— 这不是忘了加 await,而是根本没在 async 上下文里运行。
常见错误是试图在多线程里启动 Playwright(比如用 ThreadPoolExecutor 包裹 async def),这会导致 RuntimeError: There is no current event loop in thread。Playwright 的 browser 实例**不跨线程安全**,也不能在非主线程里创建新 event loop。
- 正确做法:所有 Playwright 操作必须在同一个 asyncio event loop 中完成,用
asyncio.gather()并发调度多个async def task() - 若需与同步代码集成,用
asyncio.run()封装整个抓取流程,不要拆成“先 run loop,再 await”两段 - 避免在 Jupyter 中反复执行
asyncio.run():它每次都会新建 loop,而 Playwright 的 browser 可能未 clean up,导致Connection closed
并发数不能只靠增加 asyncio.gather() 里的任务数量
Playwright 自身有连接池和浏览器上下文限制。盲目传入 100 个 page.goto() 任务,大概率触发 TimeoutError: Timeout 30000ms exceeded 或页面加载失败,不是网络慢,而是 browser 实例扛不住。
关键控制点在 browser context 和 page 层级:
- 每个
browser.new_context()开销较大(内存 ~50–100MB),建议复用 context,用context.new_page()创建轻量 page 实例 - 单个 context 下并发 page 数建议 ≤ 10;若需更高并发,改用多个 context(但注意
browser.new_context()本身要 await,不能无节制创建) - 设置
context = await browser.new_context(accept_downloads=False, java_script_enabled=True)关闭非必要功能,减少资源占用 - 务必设置
page.set_default_timeout(10000),避免单个页面卡死拖垮整个 gather
处理反爬时 await 的顺序和重试逻辑直接影响成功率
很多网站对快速连续请求返回 429 或空白 HTML,这不是 Playwright 问题,而是服务端限流。单纯加 await asyncio.sleep(0.1) 在每个任务开头治标不治本——sleep 是协程挂起,不释放 CPU,且无法应对动态限速策略。
更稳妥的写法是把等待逻辑内聚到任务中,并区分失败类型:
async def fetch_with_retry(url, context, max_retries=3):
for i in range(max_retries):
try:
page = await context.new_page()
await page.goto(url, wait_until="networkidle")
html = await page.content()
await page.close()
return html
except Exception as e:
await page.close() if 'page' in locals() else None
if "429" in str(e) or "Timeout" in str(e):
await asyncio.sleep(1.5 ** i) # 指数退避
continue
raise e
- 不要在
asyncio.gather()外层统一 try-except:会吞掉具体哪次请求失败,难定位 - 避免用
page.wait_for_timeout()替代wait_until:前者是硬等待,后者是智能等待 DOM 稳定,更可靠 - 如需绕过 Cloudflare,必须在 context 创建时启用
user_agent和bypass_csp=True,否则page.content()可能返回空字符串
关闭 browser 的时机稍有不慎就会引发 Target closed
常见错误模式:async with async_playwright() as p: 块内启动 browser,但没等所有 asyncio.gather() 完成就退出上下文。此时还没 await 完的任务会抛出 Error: Target closed 或 Browser has been closed。
正确释放顺序不可颠倒:
- 先
await asyncio.gather(*tasks)确保所有页面操作结束 - 再显式
await context.close()(如果复用了 context) - 最后让
async with async_playwright()自动关闭 browser —— 不要手动调browser.close() - 若使用
chromium.launch(headless=True, args=["--no-sandbox", "--disable-setuid-sandbox"]),确保 args 里没有冲突参数,否则 browser 启动失败但异常被静默忽略
真正容易被忽略的是 context 生命周期和 page 的 close 配对:每个 new_page() 都该有对应 page.close(),哪怕出错了也要在 finally 里关,否则 context 内存持续增长,跑几小时后 OOM。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











