aiohttp裸用asyncio.gather会瞬间并发导致429、连接耗尽、IP封禁;必须用Semaphore控实时并发数、全局复用ClientSession、校验响应状态与类型。

aiohttp 能并发爬取海量网页,但裸用会立刻触发 429、连接耗尽、IP 封禁、响应解析失败——关键不在“能不能发”,而在“怎么控节奏、怎么扛失败、怎么保连接”。
为什么不能直接 asyncio.gather(*[session.get(url) for url in urls])
这等于把所有请求瞬间塞进事件循环,不加任何节流。真实场景下,目标服务器通常会在 5–10 个并发后开始限流,更别说上千 URL 一起砸过去。
-
429 Too Many Requests或连接被重置(ClientOSError: [Errno 104] Connection reset by peer)是高频报错 - 系统级文件描述符打满:
OSError: [Errno 24] Too many open files,尤其在 Linux 上默认 limit=1024 -
ClientSession被反复新建,SSL 上下文和连接池无法复用,吞吐反而下降 - 没检查
resp.status和resp.content_type,直接await resp.text()可能解析到 302 重定向 HTML 或 JSON 错误体
必须用 asyncio.Semaphore 控制实时并发数
信号量是唯一靠谱的“并发闸门”,它限制的是**同时处于 session.get() 执行中**的请求数,不是任务提交数。
- 推荐起始值设为
10~30:先保守压测,再根据目标站响应延迟和稳定性上调 - 不要设为
0或None:那等于不限流,和裸用gather无异 - 每个请求前必须
async with semaphore:,且该语句要包裹住整个session.get()块 - 错误写法:
async with semaphore: await session.get(...)—— 缺少async with响应上下文,连接可能泄漏
semaphore = asyncio.Semaphore(20)
<p>async def fetch(session, url):
async with semaphore: # ✅ 正确位置
try:
async with session.get(url, timeout=15) as resp:
if resp.status == 200 and 'text/html' in resp.headers.get('Content-Type', ''):
return await resp.text()
except (aiohttp.ClientError, asyncio.TimeoutError):
pass
return None</p>ClientSession 必须全局复用,且配置 TCPConnector
每次新建 ClientSession 都会重建连接池、SSL 上下文和事件循环绑定,开销远超你想象。高频新建直接导致 Too many open files。
SkillSub Pro - Python 题解与代码注释双功能技能功能概述SkillSub Pro - Python 题解与代码注释双功能技能是一项面向实际任务的技能,主要用于SkillSub Pro 是一个 Python 题解生成与代码注释的 双功能合体技能 ,专为学生、算法学习者和开发者设计;✅ 一个技能,两种用途 :;核心要点📝 题解模式 :输入题目/题号,自动生成完整 Python 题解(含详细注释、解题思路、复杂度分析);💬 注释模式 :输入 Python 代码,自动添加详细中。它将相关步骤、
立即学习“Python免费学习笔记(深入)”;
- 整个爬虫生命周期只创建一次
ClientSession实例,传给所有协程共用 - 显式配置
TCPConnector:设limit_per_host=30(防止单域名打爆),ssl=False(跳过证书校验提速,仅测试期用),keepalive_timeout=30 - 超时必须分层设:
ClientTimeout(total=30, connect=10, sock_read=15),避免单个卡死拖垮全部 - 禁止在循环里写
async with aiohttp.ClientSession() as session:—— 这是新手最常写的致命错误
connector = aiohttp.TCPConnector(
limit_per_host=30,
keepalive_timeout=30,
ssl=False
)
timeout = aiohttp.ClientTimeout(total=30, connect=10, sock_read=15)
<p>async with aiohttp.ClientSession(
connector=connector,
timeout=timeout,
headers={"User-Agent": "Mozilla/5.0 (X11; Linux x86_64)"}
) as session:
tasks = [fetch(session, url) for url in urls]
results = await asyncio.gather(*tasks)</p>响应处理必须校验 status + content-type + 字符编码
aiohttp 不像 requests 自动 raise_for_status,也不自动猜解 charset。直接 await resp.text() 在遇到 404 页面、JSON API 错误、gzip 压缩未解包、或 GBK 编码页面时,大概率抛 UnicodeDecodeError 或返回乱码。
- 务必先判断
resp.status是否在200–299范围 - 检查
resp.headers.get('Content-Type'),过滤掉application/json、image/等非 HTML 类型 - 对 HTML 响应,优先用
await resp.read()拿 bytes,再交由lxml或BeautifulSoup解析(它们自带编码探测) - 若必须用
.text(),显式指定encoding='utf-8'并捕获UnicodeDecodeError
真正卡住项目的,往往不是并发逻辑本身,而是某次 resp.text() 在没校验的情况下突然炸开——这种错误不会立刻暴露,而是在跑了几千页后才批量报错。

















