必须复用 ClientSession 并配置 TCPConnector:禁用 DNS 缓存、设 limit_per_host=30;响应体优先 read() 后显式解码,大文件流式处理;避免 gather 列表推导式,改用 create_task 动态调度。

用 aiohttp + asyncio 写爬虫,单机并发 100+ 请求很常见,但实际吞吐常卡在 DNS 解析、连接复用或响应体读取上,不是加了 async 就自动快 —— 关键在配置和生命周期控制。
为什么 asyncio.run() 启动后反而比 requests 慢?
典型表现:发 50 个请求,总耗时比同步还长。根本原因是没复用 aiohttp.ClientSession,每次 async with aiohttp.ClientSession() as session: 都新建 TCP 连接池、SSL 上下文和 DNS 缓存。
- 必须把
ClientSession提到最外层,作为协程的共享上下文,而不是在每个fetch()里新建 - 禁用默认的 DNS 缓存(
connector = aiohttp.TCPConnector(use_dns_cache=False))反而可能提速 —— 当目标域名多且冷门时,内置缓存会阻塞协程等待解析完成 - 设置
limit_per_host=30(而非默认的 100),避免单域名触发服务端限流或连接拒绝
如何避免 await response.text() 成为性能瓶颈?
response.text() 默认用 chardet 自动探测编码,CPU 占用高、不可控;而 response.read() 返回 bytes 极快,但后续解码要手动处理。
- 优先用
await response.read(),再按response.charset或response.headers.get('content-type')中的charset显式解码 - 若确定是 UTF-8,直接
(await response.read()).decode('utf-8'),跳过所有自动探测 - 对大响应体(>1MB),避免一次性
.read(),改用content = response.content+async for chunk in content.iter_chunked(8192):流式处理
asyncio.gather() 和 asyncio.create_task() 的选择陷阱
错误写法:await asyncio.gather(*[fetch(session, url) for url in urls]) —— 表面并发,实则列表推导式先全部生成协程对象,内存占用陡增,且无法做中间取消或超时分粒度控制。
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
- 小批量(gather 简洁,但务必加
return_exceptions=True,否则一个异常导致全盘失败 - 大批量或需容错:用
asyncio.create_task()批量提交,配合asyncio.as_completed()按完成顺序处理结果,可随时task.cancel() - 永远不要在循环里写
await fetch(...)—— 这是伪异步,退化成串行
超时与重试必须手动精细控制
aiohttp 的 timeout 参数只管连接和读响应头,不包括 response.text() 或大响应体下载 —— 这部分超时得自己套 asyncio.wait_for()。
- 连接/首字节超时用
aiohttp.ClientTimeout(total=10, connect=3, sock_read=5) - 全文本解析超时:用
await asyncio.wait_for(response.text(), timeout=8) - 重试别依赖第三方库,手写三段式逻辑更可控:
for attempt in range(3): try: ... break except aiohttp.ClientError: continue - 注意重试时不要重复
session.request()—— 失败请求的连接可能已半关闭,应新建request调用
真正卡住效率的,往往不是并发数,而是 DNS 解析策略、连接复用粒度、响应体处理方式这三处。它们不报错,但会让并发优势彻底失效。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!










