0. 引言
今天来看一下这个困扰我很久的问题。是关于在FastAPI / NiceGUI 等基于Uvicorn环境下使用Async Playwright 提示NotImplementedError的问题。
本解决方案仅适用基于Uvicorn的异步环境,若需解决在Jupyter中无法使用Async Playwright的问题,请参阅:Running Playwright in JupyterLab Notebook Problem - Not implemented Error - JupyterLab - Jupyter Community Forum
1.reload来背锅吧
根本原因分析
1. Async Playwright 在 --reload 下的 NotImplementedError
- 触发条件:
- Uvicorn 使用
--reload时,会启动一个 文件监视子进程(通过asyncio.create_subprocess_exec)。 - Windows 上,
asyncio子进程管理依赖ProactorEventLoop。 - Async Playwright 启动浏览器时,内部也会尝试创建子进程(浏览器进程),但 Windows 的事件循环策略冲突导致
NotImplementedError。
- Uvicorn 使用
- 为什么?
Windows 的SelectorEventLoop不支持子进程操作,而ProactorEventLoop需要显式设置。Uvicorn 的重载机制和 Playwright 的子进程创建可能使用了不同的事件循环策略,导致冲突。
2. Sync Playwright 在 --reload 下的 it looks like you are using playwright sync api inside the asyncio loop
- 触发条件:
- Uvicorn 运行在异步事件循环中(ASGI 服务器必须是异步的)。
- 如果在 FastAPI 路由或生命周期事件(如
@app.on_event("startup"))中直接调用 Sync Playwright,会阻塞事件循环。 - Playwright 检测到你在异步环境中使用同步 API,抛出此错误。
- 为什么?
Sync Playwright 会尝试在同步上下文中运行,但 Uvicorn 的--reload模式已经运行在asyncio事件循环中,二者无法兼容。
2.要怎么解决
方案 1:禁用 --reload(最简单)
- 适用场景:开发/生产环境均可,但失去代码热更新功能。
- 启动方式:bash复制下载uvicorn main:app --host 0.0.0.0 --port 8000 # 去掉 --reload(或者在入口中使用
Uvicorn.run("main:app",host="0.0.0.0",port=8000,reload=False)) - 优点:无需修改代码,直接解决问题。
- 缺点:开发时需手动重启服务。
方案 2:显式设置 WindowsProactorEventLoopPolicy(仅限 Async Playwright)
- 适用场景:必须使用
--reload+ Async Playwright。 - 修改入口文件(
main.py):
Python
import sys
import asyncio
from fastapi import FastAPI
if sys.platform == "win32":
asyncio.set_event_loop_policy(asyncio.WindowsProactorEventLoopPolicy())
app = FastAPI()
@app.on_event("startup")
async def startup():
from playwright.async_api import async_playwright
playwright = await async_playwright().start()
# ... 其余初始化代码- 优点:保留
--reload功能。 - 缺点:仅适用于 Async Playwright,且需确保所有 Playwright 操作都是异步的。
方案 3:Sync Playwright + 线程隔离(推荐)
- 适用场景:必须使用 Sync Playwright +
--reload。 - 实现方式:
Python
from fastapi import FastAPI
import threading from playwright.sync_api
import sync_playwright
app = FastAPI()
def run_sync_playwright():
with sync_playwright() as playwright:
browser = playwright.chromium.launch()
# ... Sync Playwright 操作
@app.on_event("startup")
async def startup():
# 在单独线程中运行 Sync Playwright,避免阻塞事件循环
thread = threading.Thread(target=run_sync_playwright) thread.start()- 优点:
- 兼容
--reload。 - 不依赖事件循环策略。
- 兼容
- 缺点:需要管理线程生命周期。
方案 4:换用 Linux 开发(终极方案)
- Windows 的
asyncio子进程管理存在限制,而 Linux/Mac 无此问题。 - 适用场景:长期项目,可切换开发环境。
- 优点:一劳永逸,无需处理兼容性问题。
- 缺点:需要调整开发环境。
3.总结一下
- 如果只是临时开发,禁用
--reload最简单。 - 如果必须用
--reload,优先 Async Playwright +ProactorEventLoop。 - 如果坚持用 Sync Playwright,用线程隔离。
发表回复