
本文详解 aiohttp.ClientResponse 在 async with 语境下自动关闭的机制,指出 response.text() 被延迟调用时引发的 Connection closed 异常,并提供安全返回 HTML 内容的重构方案。
本文详解 `aiohttp.clientresponse` 在 `async with` 语境下自动关闭的机制,指出 `response.text()` 被延迟调用时引发的 `connection closed` 异常,并提供安全返回 html 内容的重构方案。
在使用 aiohttp 进行异步 HTTP 请求时,一个常见却容易被忽视的陷阱是:ClientResponse 对象在 async with session.get(...) 退出上下文后会自动关闭连接并释放响应体缓冲区。这意味着,若你在 make_request() 中仅返回 response 对象,而将 await response.text() 推迟到后续协程(如 get_image_url)中执行,此时响应已关闭,response.text() 将永远挂起(或抛出 RuntimeError: Connection closed),导致整个程序“冻结”。
根本原因在于 aiohttp 的设计原则:响应体必须在响应对象存活期间读取完毕。async with 块确保了资源及时清理,但也要求所有读取操作(如 .text(), .read(), .json())必须在该块内完成。
✅ 正确做法:按需读取响应内容
最直接、安全的解决方案是 让 make_request() 根据调用意图决定返回响应对象还是已解析的文本内容。通过新增布尔参数 get_html 控制行为:
import asyncio
import aiohttp
from bs4 import BeautifulSoup
from concurrent.futures import ProcessPoolExecutor
async def make_request(url, session, get_html=False):
async with session.get(url) as response:
if response.ok:
if get_html:
return await response.text() # ✅ 在上下文内完成读取
return response
else:
print(f'{url} returned: {response.status}')相应地,调整 get_image_page 和 get_image_url 的调用逻辑:
-
get_image_page仍需response.url,因此传入get_html=False(默认),获取response对象; -
get_image_url需要 HTML 内容进行解析,因此显式传入get_html=True,直接获得已读取的字符串:
async def get_image_page(queue, session):
url = "https://c.xkcd.com/random/comic/"
response = await make_request(url, session) # 返回 response 对象
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) # ✅ 返回 str
loop = asyncio.get_running_loop()
with ProcessPoolExecutor() as pool:
image_url = await loop.run_in_executor(
pool, parse_link, html
)
await image_urls_queue.put(image_url)
pages_queue.task_done()⚠️ 注意事项与最佳实践
-
避免跨协程传递未读取的
ClientResponse:它不是可序列化或可跨任务安全持有的对象; -
ProcessPoolExecutor中解析 HTML 是合理选择:BeautifulSoup是 CPU 密集型操作,用进程池可避免阻塞事件循环; -
务必调用
pages_queue.task_done():这是queue.join()正常工作的前提; -
补充异常处理更健壮:实际项目中应在
make_request和parse_link中加入try/except,捕获网络错误、解析失败等; -
考虑超时与重试:为
session.get()添加timeout=参数,并对失败请求做指数退避重试。
✅ 总结
异步编程中,“资源生命周期”意识至关重要。aiohttp 的 async with 不仅是语法糖,更是强制你明确响应读取边界的契约。将 response.text() 移入 make_request 并由调用方按需触发,既符合异步 I/O 最佳实践,又彻底规避了连接关闭导致的死锁。掌握这一模式,是构建高可靠异步爬虫与 API 客户端的关键一步。

















