
本文详解 asyncio 中因过早关闭 HTTP 响应连接而导致 response.text() 永久阻塞的根本原因,并提供安全返回 HTML 内容、避免资源泄漏的正确实践。
本文详解 asyncio 中因过早关闭 http 响应连接而导致 `response.text()` 永久阻塞的根本原因,并提供安全返回 html 内容、避免资源泄漏的正确实践。
在使用 aiohttp 进行异步网络请求时,一个常见却隐蔽的陷阱是:在 async with session.get(...) 上下文管理器退出后,ClientResponse 对象自动关闭底层连接,此时再调用 await response.text() 将永远挂起(freeze),而非抛出异常。这是因为 aiohttp 的 response.text() 在连接已关闭时会陷入等待不可达的 EOF,导致协程无法继续执行——这正是你代码中 html = await response.text() 卡死的根源。
问题核心在于 make_request() 函数的设计:它用 async with session.get() 获取响应,但仅返回 response 对象本身。一旦该函数执行完毕,async with 上下文即退出,响应被关闭;而后续在 get_image_url() 中尝试读取已关闭响应的 .text(),便触发无限等待。
✅ 正确解法是:确保 HTML 内容在响应仍处于活跃状态时完成读取。推荐采用参数化策略,让 make_request() 支持按需返回原始响应对象或预读取的 HTML 字符串:
import asyncio
from aiohttp import ClientSession
from concurrent.futures import ProcessPoolExecutor
from bs4 import BeautifulSoup
async def make_request(url, session, *, get_html=False):
async with session.get(url) as response:
if response.ok:
return await response.text() if get_html else response
else:
print(f'{url} returned: {response.status}')
return None if get_html else response
注意:我们添加了 * 强制关键字参数,提升可读性与健壮性;同时对非 2xx 响应统一返回 None(当 get_html=True 时),避免下游解析空内容。
相应地,调整消费者逻辑:
async def get_image_page(queue, session):
url = "https://c.xkcd.com/random/comic/"
response = await make_request(url, session)
if response is not None:
await queue.put(str(response.url))
async def get_image_url(pages_queue, image_urls_queue, session):
while True:
url = await pages_queue.get()
html = await make_request(url, session, get_html=True)
if not html:
pages_queue.task_done()
continue
# CPU 密集型解析移交至进程池,避免阻塞事件循环
loop = asyncio.get_running_loop()
with ProcessPoolExecutor() as pool:
try:
image_url = await loop.run_in_executor(
pool, parse_link, html
)
await image_urls_queue.put(image_url)
except (AttributeError, TypeError, ValueError) as e:
print(f"Failed to parse image URL from {url}: {e}")
pages_queue.task_done()
? 关键注意事项:
- ❌ 禁止跨
async with边界使用response对象(如.text(),.read(),.json()); - ✅ 所有 I/O 解析操作(
.text())必须在async with块内完成; - ⚠️
ProcessPoolExecutor实例不应在协程内部重复创建(当前示例为简化演示)。生产环境建议复用全局executor或通过asyncio.to_thread()(Python 3.9+)替代; - ? 增加异常处理,防止单个页面解析失败导致整个 worker 协程崩溃;
- ? 调用
pages_queue.task_done()必须放在try/except后或确保始终执行,否则queue.join()将永不返回。
最后,在 main() 中,你还需要补全 download_image() 的异步实现,并合理取消未完成任务(当前 page_getters 取消逻辑多余,因它们已在 gather 后自然结束)。完整可运行的最小修正版可基于上述原则快速迭代。
遵循“响应生命周期即作用域”的原则,你的异步爬虫将稳定、高效且易于维护。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











