在ASGI环境(如Uvicorn/Daphne)中,Web框架已托管唯一事件循环,视图内调用asyncio.run()会因试图启动第二个循环而必然报错;正确做法是将爬虫作为独立进程运行,通过Redis等中间存储与Web层解耦,视图内仅用await调用协程。

直接在 Django 或 FastAPI 视图里启动异步爬虫,几乎必然触发 RuntimeError: asyncio.run() cannot be called from a running event loop。这不是配置问题,而是架构错误——Web 框架(如 Uvicorn、Daphne)已托管主事件循环,你不能再用 asyncio.run() 去“另起炉灶”。
为什么 asyncio.run() 在 ASGI 环境中必报错
ASGI 服务器(Uvicorn/Daphne)启动时就创建并运行了唯一的事件循环,所有视图、中间件、路由都在这个循环内调度执行。此时调用 asyncio.run(),等于试图在同一线程里启动第二个事件循环,Python 明确禁止该行为。
- 常见误用:在
async def view(request)中写asyncio.run(some_scraper()) - 隐性陷阱:某些封装库(如旧版
g4f同步接口)内部仍调用asyncio.run(),混入 ASGI 流程即崩溃 - 错误堆栈里一定含
cannot be called from a running event loop,这是最明确的诊断信号
正确解耦:爬虫必须独立进程,不共享 Web 事件循环
爬虫和 Web 层必须物理隔离:爬虫是长期运行的后台进程,Web 是无状态响应层。二者通过中间存储通信,而非共享协程或循环。
- 爬虫端:单独运行
scraper.py,用httpx.AsyncClient+asyncio.sleep()控制频率,结果存redis.set("scraper:result", json.dumps(data)) - Web 端:Django/FastAPI 视图只做读取(
redis.get("scraper:result"))和返回,不发起任何网络请求 - 部署时:爬虫用
systemd或supervisord独立管理,与 Web 进程完全解耦
实时推送不要用 WebSocket,优先选 SSE
若需“爬完立刻推给前端”,别碰 WebSocket——它要求双向长连接、状态管理复杂,且极易因事件循环混用导致 Task attached to a different loop 错误。
立即学习“Python免费学习笔记(深入)”;
- SSE(Server-Sent Events)天然适配:单向流、HTTP/2 友好、自动重连、无需额外协议层
- Django 实现只需一个 ASGI 路由函数,用
async def sse_view(scope, receive, send)直接写响应流 - 关键点:SSE 响应体里不能调用任何阻塞操作(如
time.sleep()),但可安全轮询 Redis(redis.client.pubsub()或短间隔get())
嵌套调用时,永远用 await,而不是 asyncio.run()
当你不得不在已有协程中触发另一个异步操作(比如从爬虫结果触发通知),唯一合法方式是 await,不是 asyncio.run()。
- 错误:
asyncio.run(send_notification())—— 即使send_notification是async def,也会报循环冲突 - 正确:
await send_notification()—— 复用当前事件循环 - 例外情况极少:仅当代码运行在纯同步上下文(如 CLI 脚本、Jupyter)且无法改造成协程时,才考虑
nest_asyncio.apply()补丁,但生产环境禁用
最容易被忽略的点:爬虫进程和 Web 进程的 Python 版本、依赖版本(尤其是 asyncio 相关库)必须严格一致。一个用 uvloop、一个用默认循环,或 aiohttp 版本差一个大号,都可能引发静默的事件循环不兼容——表现为偶发卡死或连接复位,而非明显报错。


















