结论:用 async def + yield + await 写异步生成器,再配 async for 消费,就能在不爆内存、不阻塞事件循环的前提下,边请求边处理海量网页数据。

直接说结论:用 async def + yield + await 写异步生成器,再配 async for 消费,就能在不爆内存、不阻塞事件循环的前提下,边请求边处理海量网页数据。
为什么不能用普通生成器处理网页流?
普通生成器(def + yield)无法 await,遇到 aiohttp.get() 或 asyncio.sleep() 这类协程调用会直接报 SyntaxError: 'await' outside async function。更关键的是,它会在 I/O 等待时卡住整个事件循环——一个请求卡 3 秒,后面所有请求都得干等,吞吐量归零。
常见错误现象:
- 写了个
def fetch_urls(urls): for u in urls: yield requests.get(u).text,结果一跑就报错或阻塞 - 强行用
asyncio.run_in_executor包裹同步请求,线程池打满、上下文丢失、异常难捕获
async for 要求类必须实现 __aiter__
你写的异步生成器函数(如 async def stream_pages(...))返回的是 async_generator 对象,它内部自动实现了 __aiter__ 和 __anext__,所以能被 async for 直接消费。但如果你手动写类来封装异步迭代逻辑,就必须显式实现 __aiter__,否则 async for 会抛 TypeError: 'YourClass' object is not an async iterator。
要点:
-
__aiter__必须返回一个实现了__anext__的对象(通常是self),且该方法必须是async - 不要在
__aiter__里做耗时操作(比如预取第一页),它只应做初始化 - CPython 3.12 不会为类自动补
__aiter__,漏写就是运行时报错
实战:分页爬取并流式解析 HTML
典型场景是爬取分页列表(如新闻聚合页),每页含 20 条链接,总页数未知。目标是边请求、边解析、边入库,全程不缓存全部响应体。
仅使用 Python 将 Claw Code 虚拟化框架与 OpenClaw 集成,提供 184 种工具和 200 多条命令,用于安全分析和代码质量...
示例结构(省略异常重试和 UA 处理):
import aiohttp
import asyncio
from bs4 import BeautifulSoup
<p>async def fetch_page(session, url):
async with session.get(url) as resp:
return await resp.text()</p><p>async def stream_article_urls(base_url: str, max_pages: int = 100):
async with aiohttp.ClientSession() as session:
for page in range(1, max_pages + 1):
url = f"{base_url}?page={page}"
html = await fetch_page(session, url)
soup = BeautifulSoup(html, "lxml")
for link in soup.select("a.title"):
yield link["href"] # 注意:这里 yield 的是字符串,不是 response 对象</p><h1>消费端</h1><p>async def main():
async for url in stream_article_urls("<a href="https://www.php.cn/link/ca14cd6c279d15639a51915b4b7917bc">https://www.php.cn/link/ca14cd6c279d15639a51915b4b7917bc</a>"):</p><h1>每拿到一个 URL 就立刻发起详情页请求或丢进队列</h1><pre class="brush:php;toolbar:false;"><code> print(f"Got URL: {url}")</code>
关键细节:
- 必须用
async with aiohttp.ClientSession()管理连接池,避免每页新建 session 导致端口耗尽 -
yield前的await是必须的;yield 后的内容不会被自动 await,要自己控制生命周期 - 如果解析 HTML 很重(如大量
select),考虑用asyncio.to_thread把 BS4 调用移出事件循环
容易被忽略的资源泄漏点
异步生成器退出时(比如消费者提前 break 或抛异常),Python 3.12 会自动调用 aclose() 关闭生成器,但不会自动关闭你创建的 aiohttp.ClientSession 或未完成的 async with 上下文。
所以务必:
- 把
ClientSession的生命周期严格限制在async with块内(如上例),不要传入外部 session - 若需复用 session,改用
Runner或手动管理aclose(),并在生成器中捕获GeneratorExit或监听asyncio.CancelledError - 避免在生成器里开文件(
aiofiles.open)却不aclose(),否则句柄泄漏比内存泄漏更隐蔽
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










