asyncio.Semaphore是控制并发请求数的核心工具,需定义为全局单例以确保限流有效;应结合随机延迟、指数退避及状态码处理来规避风控,而非依赖Python版本升级。

asyncio.Semaphore 是控制并发请求数的核心工具
直接用 asyncio.Semaphore 限制同时发出的请求数,比在请求间加 asyncio.sleep() 更可靠——后者只控频次,不控并发量,高延迟时仍可能瞬间涌出大量请求被风控。
典型错误是把 Semaphore 实例定义在函数内(每次调用新建一个),导致限流失效。必须作为共享对象传入或定义在模块/类作用域:
import asyncio import aiohttp <h1>✅ 正确:全局单例,所有协程共用</h1><p>sem = asyncio.Semaphore(3) # 最多3个并发</p><p>async def fetch(url): async with sem: # 进入前等待,退出后自动释放 async with aiohttp.ClientSession() as session: async with session.get(url) as resp: return await resp.text() </p>
- 数值设为 1–5 比较稳妥,具体看目标站点反爬强度;超过 10 很容易触发频率拦截
- 不要在
async with sem块里做耗时同步操作(如解析、写文件),否则占用信号量时间过长,拖慢整体吞吐 - 若需动态调整并发数(比如根据响应头里的
Retry-After),得换用自定义锁或配合asyncio.Queue
如何结合随机 delay 避开固定节拍检测
纯固定间隔(如每 1.2 秒发一次)容易被识别为脚本行为。asyncio 本身不提供随机延迟,但 asyncio.sleep() 支持浮点秒数,配合 random.uniform() 即可:
import random
import asyncio
<p>async def fetch_with_jitter(url):
async with sem:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
data = await resp.text()</p><h1>请求完成后,再随机等待 0.8–2.5 秒</h1><pre class="brush:php;toolbar:false;"> await asyncio.sleep(random.uniform(0.8, 2.5))
return data- 务必把
await asyncio.sleep()放在async with sem外面,否则它也占着并发名额 - 下限别低于 0.5 秒,否则和无延迟差别不大;上限别超 3 秒,否则爬取效率断崖下跌
- Python 3.12 对
asyncio.sleep()的精度有优化,但底层仍受系统调度影响,不必追求毫秒级精确
遇到 429 或 503 时该立刻退避,而不是硬扛
很多爬虫忽略响应状态码,继续用原节奏重试,结果被 IP 封禁。asyncio 中要主动捕获异常并延长等待:
async def fetch_with_backoff(url, max_retries=3):
for i in range(max_retries):
try:
async with sem:
async with aiohttp.ClientSession() as session:
async with session.get(url) as resp:
if resp.status == 429:
retry_after = int(resp.headers.get("Retry-After", "1"))
await asyncio.sleep(retry_after * (2 ** i)) # 指数退避
continue
elif resp.status >= 400:
raise Exception(f"HTTP {resp.status}")
return await resp.text()
except Exception as e:
if i == max_retries - 1:
raise e
await asyncio.sleep(1.5 * (2 ** i))
-
Retry-After头优先级最高,必须尊重;没有时才 fallback 到指数退避 - 不要用
time.sleep()替代asyncio.sleep(),会阻塞整个事件循环 - 注意
aiohttp默认不自动 raise 异常,resp.raise_for_status()可以补上,但需自己处理 429
Python 3.12 的新特性对爬虫限流没直接帮助
3.12 加了 asyncio.timeout() 和更细粒度的 task introspection,但它们不改变限流逻辑本身。真正要注意的是:
-
asyncio.to_thread()不适合用在请求环节——网络 I/O 本就是异步的,丢进线程池反而增加开销 - 3.12 默认启用 per-task GC,如果解析 HTML 用了大量临时对象(如 BeautifulSoup 构建树),可能轻微拖慢响应,建议用
lxml或selectolax替代 - 别迷信 “3.12 更快”,实际瓶颈永远在 DNS 解析、TCP 握手、远端响应速度,不是 Python 版本
真正决定是否被封的,是你请求的节奏模式、User-Agent 真实性、Cookie 复用逻辑,以及是否模拟了浏览器的资源加载顺序——这些 asyncio 控不了,得靠业务层设计。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











