Python用Playwright异步控制浏览器打开网站示例

Alice
Alice
发布时间:2026-11-08 | 8人浏览过
原创

详细介绍Python使用Playwright异步API(async_playwright)控制浏览器打开网站的完整步骤、核心代码、多页面并发示例、等待策略及常见报错排查方法。

在 Python 中使用 Playwright 异步打开网站,核心做法是导入 playwright.async_api 模块中的 async_playwright,在 async with 上下文管理器中依次启动浏览器实例、创建隔离上下文与页面,并通过 await page.goto() 访问目标网址,最后使用标准库 asyncio.run() 驱动整个协程函数。本文将完整演示从环境准备、单页面异步打开、多网站并发访问到常见异常排查的全过程。

一、适用条件与异步方案的判断依据

Playwright 同时提供同步(Sync)和异步(Async)两套 API,但并非所有自动化脚本都需要写成异步形式。如果脚本只是按顺序打开一两个网页、点击按钮并导出单份数据,同步 API 代码更简短,调试也更直观;只有当任务满足以下条件之一时,采用异步 API 才有实际收益:

  • 需要同时打开或巡检多个网页:网页加载的主要耗时在于网络传输和前端渲染等待。异步模式可以在等待第一个网页响应的同时发起其余网页的请求,显著缩短总耗时。
  • 需要将浏览器操作嵌入已有的异步框架:当项目本身运行在 FastAPI、aiohttp 或自定义的 asyncio 事件循环中时,直接调用同步 Playwright API 会阻塞整个事件循环或触发运行报错,此时必须使用 async_playwright。
  • 需要监听页面异步事件流:例如持续捕获页面发出的特定 XHR/Fetch 响应、WebSocket 消息或控制台日志,异步回调配合协程更为自然。

反过来需要注意,异步并发并不能突破单机的内存与 CPU 上限。每个 Chromium 浏览器上下文和标签页都会占用独立的渲染进程内存,如果在同一台机器上无节制地并发打开上百个页面,反而会因内存耗尽导致浏览器崩溃。因此,异步控制浏览器的核心是“受控并发”而非无限并发。

二、环境准备与操作流程总览

要运行异步 Playwright 脚本,本地需要 Python 3.8 及以上版本(Python 3.7 已停止维护,建议使用 Python 3.10+ 以获得更清晰的异步异常栈信息)。安装过程分为两步:先安装 Python 第三方库,再下载 Playwright 专用驱动与浏览器内核。

Python用Playwright异步控制浏览器打开网站示例
图 1:Python 使用 Playwright 异步控制浏览器打开网站的标准四步流程

打开系统终端(Windows 使用 PowerShell 或 CMD,macOS/Linux 使用 Terminal),在激活的虚拟环境中依次执行以下两条命令:

pip install playwright
playwright install chromium
注意:仅执行 pip install playwright 只会安装 Python 接口包,不会下载浏览器二进制文件。如果跳过第二条命令直接运行代码,会报错提示 Executable doesn't exist。若只需要使用 Chromium 内核,指定 playwright install chromium 即可,无需下载 Firefox 和 WebKit,能节省约数百兆磁盘空间与下载时间。

三、基础示例:异步启动浏览器并打开单个网站

我们先编写一个最小可用的完整脚本,演示如何以可见窗口模式(有头模式)启动 Chromium 浏览器、打开示例网站 https://example.com、读取页面标题与状态码,并在完成后安全关闭浏览器。

在工作目录下新建文件 open_single_async.py,写入以下代码:

import asyncio
from playwright.async_api import async_playwright


async def open_website() -> None:
    # 1. 启动 Playwright 异步上下文管理器
    async with async_playwright() as p:
        # 2. 启动 Chromium 浏览器,headless=False 表示显示图形界面窗口
        browser = await p.chromium.launch(headless=False, slow_mo=300)

        # 3. 创建独立的浏览器上下文(隔离 Cookie 与本地存储)
        context = await browser.new_context(
            viewport={"width": 1280, "height": 800}
        )

        # 4. 在当前上下文中打开一个新标签页
        page = await context.new_page()

        # 5. 访问目标网站,等待 DOM 内容加载完成
        target_url = "https://example.com"
        response = await page.goto(target_url, wait_until="domcontentloaded", timeout=15000)

        # 6. 验证打开结果:读取响应状态码、最终 URL 与网页标题
        status_code = response.status if response else None
        page_title = await page.title()
        print(f"目标地址: {page.url}")
        print(f"响应状态: {status_code}")
        print(f"网页标题: {page_title}")

        # 7. 停留 2 秒便于肉眼观察窗口,随后关闭上下文与浏览器
        await asyncio.sleep(2)
        await context.close()
        await browser.close()


if __name__ == "__main__":
    asyncio.run(open_website())

在终端中进入 open_single_async.py 所在目录,执行如下命令运行脚本:

python open_single_async.py
示例输出(终端打印结果)目标地址: https://example.com/ 响应状态: 200 网页标题: Example Domain

运行过程中,屏幕上会弹出一个分辨率为 1280×800 的 Chromium 窗口,自动跳转到 https://example.com,停留 2 秒后自动关闭,同时终端输出状态码 200 和网页标题 Example Domain。

核心对象层级与为什么要显式创建 Context

不少初学者会直接在 browser 对象上调用 await browser.new_page(),虽然这样也能打开网页,但在异步任务中更推荐遵循“Playwright -> Browser -> BrowserContext -> Page”的四层结构。

Python用Playwright异步控制浏览器打开网站示例
图 2:Playwright 四层对象关系——单个 Browser 进程可派生多个互不干扰的 BrowserContext

如上图所示,启动一个 Browser 实例需要拉起操作系统级别的浏览器进程,耗时在数百毫秒到一秒以上;而 BrowserContext 相当于一个轻量级的“无痕浏览窗口”,创建耗时仅需几毫秒,且不同 Context 之间的 Cookie、LocalStorage 和会话缓存完全隔离。在后续的多网站并发任务中,复用同一个 Browser 并为不同任务创建独立的 BrowserContext 或 Page,是兼顾性能与隔离性的关键设计。

四、进阶示例:异步并发打开多个网站并限制并发数

异步控制的真正优势体现在同时打开多个网站。下面的示例将使用 asyncio.Semaphore(信号量)控制最大同时打开的页面数,配合 asyncio.gather() 并发访问多个网址。

Python用Playwright异步控制浏览器打开网站示例
图 3:同步顺序打开与异步并发打开多个网站的时间线对比

创建文件 open_multiple_async.py,填入以下完整代码。代码中使用了 return_exceptions=True 参数,确保即使某一个网站超时或域名无法解析,也不会中断其余网站的正常打开:

import asyncio
from playwright.async_api import async_playwright, Browser


async def fetch_page_info(browser: Browser, url: str, semaphore: asyncio.Semaphore) -> dict:
    # 使用信号量限制同时打开的页面数量
    async with semaphore:
        context = await browser.new_context()
        page = await context.new_page()
        try:
            response = await page.goto(url, wait_until="domcontentloaded", timeout=15000)
            title = await page.title()
            return {
                "url": url,
                "status": response.status if response else None,
                "title": title.strip(),
                "error": None,
            }
        except Exception as exc:
            return {
                "url": url,
                "status": None,
                "title": None,
                "error": str(exc).splitlines()[0],
            }
        finally:
            # 无论成功还是报错,都确保关闭当前上下文释放内存
            await context.close()


async def main() -> None:
    urls = [
        "https://example.com",
        "https://www.iana.org/domains/reserved",
        "https://www.python.org",
    ]
    # 限制同一时刻最多打开 2 个并发页面
    semaphore = asyncio.Semaphore(2)

    async with async_playwright() as p:
        # 批量任务通常开启无头模式 headless=True 以节省图形渲染开销
        browser = await p.chromium.launch(headless=True)

        tasks = [fetch_page_info(browser, url, semaphore) for url in urls]
        results = await asyncio.gather(*tasks)

        for item in results:
            if item["error"]:
                print(f"[失败] {item['url']} -> {item['error']}")
            else:
                print(f"[成功] {item['url']} (状态码: {item['status']}) -> 标题: {item['title']}")

        await browser.close()


if __name__ == "__main__":
    asyncio.run(main())

在终端执行该脚本:

python open_multiple_async.py
示例输出(终端打印结果)[成功] https://example.com (状态码: 200) -> 标题: Example Domain [成功] https://www.iana.org/domains/reserved (状态码: 200) -> 标题: IANA-managed Reserved Domains [成功] https://www.python.org (状态码: 200) -> 标题: Welcome to Python.org

五、决定打开成败的关键参数:等待策略与启动选项

使用 await page.goto(url) 打开网站时,最常见的问题是“网页明明已经显示出来了,却一直卡住直到报错 Timeout 30000ms exceeded”,或者“goto 刚返回就去读取元素,结果读到空内容”。这通常是因为没有根据目标网站的类型选择合适的 wait_until 参数。

wait_until 取值 触发完成时机 适用网站与判断依据
"load"(默认值) HTML、样式表、脚本及所有图片/iframe 等静态资源全部加载完毕 适合传统静态页面。若页面包含加载缓慢的第三方广告、统计脚本或大图,容易导致无意义的长时间等待甚至超时。
"domcontentloaded" HTML 文档解析完成且 DOM 树构建完毕,不等待图片和样式表 大多数场景的首选。响应速度快,若后续需要等待特定动态组件,可在 goto 之后配合 await page.wait_for_selector() 精确定位。
"networkidle" 连续 500 毫秒内没有任何网络连接(或仅剩极少连接) 适合依赖首屏异步接口渲染的单页应用(SPA)。反例警告:若网站存在轮询请求、WebSocket 推送或视频流,networkidle 会永远无法触发直至超时。
"commit" 接收到网络响应头且开始加载文档的一瞬间 仅用于快速检测链接连通性或状态码,此时页面 DOM 尚未生成,不能直接提取正文。

六、常见问题与报错排查

1. 报错:Playwright Sync API inside the asyncio loop

具体表现:控制台抛出 Error: It looks like you are using Playwright Sync API inside the asyncio loop. Please use the Async API instead.

原因与修复:在 async def 函数内部,或者在 Jupyter Notebook、FastAPI 等自带事件循环的环境中误导入了 from playwright.sync_api import sync_playwright。将导入语句改为 from playwright.async_api import async_playwright,并在所有浏览器操作方法前加上 await 即可解决。

2. 在 Jupyter Notebook 中运行报 asyncio.run() cannot be called from a running event loop

具体表现:把上述脚本原封不动复制到 Jupyter 单元格中执行时,在最后一行 asyncio.run(main()) 处抛出 RuntimeError。

原因与修复:Jupyter 内核本身已经运行在一个活动的 asyncio 事件循环中,而 asyncio.run() 会尝试创建一个新的事件循环。在 Jupyter 单元格中,应删掉 if __name__ == "__main__": asyncio.run(main()),直接在单元格顶层调用 await main()。

3. Windows 环境下结合子进程报错 NotImplementedError

具体表现:在 Windows 系统的特定 Web 框架(如部分旧版 Tornado 或自定义 SelectorEventLoop 配置)中启动 Playwright 时,由于 Playwright 底层需要通过标准输入输出管道(Subprocess Pipe)与 Node.js 驱动进程通信,若当前事件循环策略为 WindowsSelectorEventLoopPolicy,会抛出 NotImplementedError。

原因与修复:Windows 下支持子进程管道的事件循环是 WindowsProactorEventLoopPolicy(Python 3.8+ 默认即是此策略)。如果第三方框架篡改了默认策略,可在调用 asyncio.run() 之前显式恢复:

import sys
import asyncio

if sys.platform == "win32":
    asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())

七、操作与参数配置总结

为方便在实际编写代码时快速对照,下图汇总了 Python 使用 Playwright 异步打开网站的生命周期对应关系、核心配置项与排错检查点:

Python用Playwright异步控制浏览器打开网站示例
图 4:Python 用 Playwright 异步控制浏览器打开网站的核心配置与排查要点总结

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表