同步模式下启动无头浏览器必须显式设置headless=true,playwright默认在macos等系统上启动有头模式,不设该参数会导致ci环境因缺少显示服务器而报错failed to launch browser。

同步模式下启动无头浏览器必须显式设置 headless=True
Playwright 默认在某些系统(如 macOS)上启动的是有头模式,即使你没看到窗口,也可能因 GUI 依赖导致 CI 环境失败。不加 headless=True,playwright.sync_api.sync_playwright() 启动的浏览器实际会尝试连接显示服务器——这在 Docker 或 GitHub Actions 里直接报错:Failed to launch browser: Error: spawn Xvfb ENOENT。
正确写法是:先获取 playwright 实例,再调用 chromium.launch() 时传入 headless=True(注意不是 headless="new",那是 Chromium 117+ 的新参数,旧版 Playwright 不识别)。
常见误操作:
- 漏掉
headless=True,只靠环境变量PLAYWRIGHT_HEADLESS=1—— 不生效 - 写成
headless="true"(字符串)—— 类型错误,必须是布尔值 - 在
launch_persistent_context()中忘记设,结果上下文仍可能弹窗
异步模式需用 asyncio.run() 包裹入口函数,不能直接 await 在模块顶层
Python 不允许在模块作用域直接使用 await,所以不能写 await page.goto(...) 在 import 下面。必须把抓取逻辑封装进 async 函数,再用 asyncio.run() 启动。
同时要注意:Playwright 的异步 API(playwright.async_api.async_playwright)和同步 API 完全隔离,不能混用 sync_playwright() 和 await。
典型结构:
import asyncio
from playwright.async_api import async_playwright
async def run():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
page = await browser.new_page()
await page.goto("https://example.com")
print(await page.title())
await browser.close()
asyncio.run(run())
page.wait_for_selector() 比 time.sleep() 可靠,但超时时间要设合理
很多人习惯用 time.sleep(2) 等页面加载,结果在慢网或高负载下不稳定,在快环境下又拖慢速度。Playwright 提供的等待机制才是真解耦。
page.wait_for_selector() 默认超时 30 秒,可传 timeout=5000 改为 5 秒;若目标元素动态渲染(如 React/Vue),建议加 state="visible" 或 state="attached" 明确状态预期。
容易踩的坑:
- 等一个不存在的选择器,没设
timeout就卡死 30 秒 - 用
page.wait_for_load_state("networkidle")却忽略单页应用(SPA)根本不会触发该状态 - 在异步模式下误用同步的
page.wait_for_selector()(它没有 async 版本,但异步实例的page对象自带异步方法,名称一样,自动适配)
关闭浏览器前务必调用 browser.close(),尤其在异步循环中易漏
Playwright 不会自动回收浏览器进程。不手动关,每次 launch() 都会起新 Chromium 进程,跑几次就内存爆满、端口占满,报错:Unable to connect to browser 或 OSError: [Errno 24] Too many open files。
同步模式可用 with 语句确保关闭;异步模式必须用 async with,且不能只关 context 或 page——它们不释放底层浏览器进程。
特别注意循环抓取场景:
- 别在 for 循环里反复
launch()+ 忘记close() - 想复用浏览器?那就只
launch()一次,多次new_context()和new_page() - Docker 环境中,
ps aux | grep chromium能快速验证是否残留进程
真正麻烦的从来不是启动浏览器,而是关不干净——尤其是异步任务被 KeyboardInterrupt 中断后,browser.close() 没机会执行。这时候得加 try/finally 或用信号处理器兜底。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











