用 aiohttp 而不是 requests 是因为其基于 asyncio 实现真正的异步 i/o 并发,能高效复用连接、降低 cpu 和内存开销,适合高并发网络请求场景,但需合理配置 clientsession、并发控制和错误重试策略。

为什么用 aiohttp 而不是 requests?
requests 是同步阻塞的,发一个请求就得等响应回来才能发下一个;而 aiohttp 基于 asyncio,能在一个线程里并发处理几十甚至上百个请求,真正节省的是网络等待时间。这不是“多开几个线程”那种粗暴并发,而是操作系统级 I/O 复用,CPU 占用低、内存开销小。
- 真实瓶颈通常在 DNS 解析、TCP 握手、SSL 握手和服务器响应延迟,这些阶段
requests只能干等,aiohttp可以切走做别的事 - 不适合 CPU 密集型任务(比如解析大 HTML 后做复杂计算),它只加速 I/O 等待
- 如果目标网站有反爬限制(如限速、封 IP),并发太快反而触发风控,这时候加
asyncio.sleep比换库更管用
aiohttp.ClientSession 必须复用,不能每次请求都新建
新建 ClientSession 会重新初始化连接池、DNS 缓存、SSL 上下文,相当于每次重装浏览器——不仅慢,还可能耗尽本地端口或触发 TIME_WAIT。正确做法是在整个异步任务生命周期内只创建一次。
- 在
async def main()里用async with aiohttp.ClientSession() as session:包裹所有请求 - 不要在循环里写
async with aiohttp.ClientSession() as session:,那等于每个请求都重建会话 - 需要自定义超时或 headers,统一在 session 初始化时传入,比如
timeout=aiohttp.ClientTimeout(total=10) - 若需不同配置(如部分请求带 token、部分不带),建多个 session,但别为每个 URL 新建一个
并发数设多少才不翻车?
没有固定值。设太高,本地机器扛不住(大量 socket、内存占用飙升);设太低,又没发挥异步优势。常见错误是直接 asyncio.gather(*[fetch(session, url) for url in urls]),urls 一上千就崩。
- 用
asyncio.Semaphore控制并发上限,比如sem = asyncio.Semaphore(20),然后每个协程里async with sem:再发请求 - 初始建议从 10–30 开始调,观察系统负载(
htop看 CPU/内存,netstat -an | grep :443 | wc -l看连接数) - 某些网站对单 IP 并发敏感,即使你本地扛得住,也可能被返回 429 或直接断连,此时得降并发 + 加随机 delay
- 注意 DNS 解析默认不缓存,高频请求同一域名时,可考虑用
aiohttp.TCPConnector(limit_per_host=30)避免连接争抢
遇到 ClientConnectorError 或 ServerDisconnectedError 怎么办?
这类错误在高并发下极常见,本质是连接层不稳定,不是代码写错了。盲目重试或加大 timeout 往往治标不治本。
- 不要 catch 所有
Exception,只捕获aiohttp.ClientError子类,避免吞掉真正的逻辑 bug - 对临时性错误(如
ClientOSError、ServerDisconnectedError)做有限重试,比如最多 2 次,且每次间隔递增(await asyncio.sleep(0.5 * (2 ** attempt))) - 设置合理的 connector 参数:
TCPConnector(limit=100, limit_per_host=30, ttl_dns_cache=300),能显著减少连接失败率 - 如果大量报
ClientConnectorError: Cannot connect to host,先检查是不是 DNS 解析失败,可尝试加family=socket.AF_INET强制走 IPv4
异步不是万能油,它只解决 I/O 等待,但 DNS、TCP、TLS、服务端响应质量这些环节,每一步都可能成为隐形瓶颈。调并发数时盯着的不该是“跑得多快”,而是“连接稳不稳、错误多不多、本地资源撑不撑得住”。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











