asyncio.Semaphore仅限并发数而非QPS,需结合令牌桶或动态sleep实现真正限流;同时须轮换User-Agent、完善请求头,并根据WAF响应精准应对。

asyncio.Semaphore 控制并发数,但不等于限流
很多人以为 asyncio.Semaphore 设成 5 就能保证每秒最多发 5 个请求,其实不是。它只限制同时运行的协程数量,如果每个请求耗时 200ms,那实际 RPS 是 5 ÷ 0.2 = 25,远超预期。真正控频得靠时间维度的节拍器。
- 用
asyncio.Semaphore防服务器被打垮(避免瞬间 100 并发),不是防被封 - 必须搭配
asyncio.sleep或时间戳计数,才能实现“每秒 N 次”语义 - 注意:sleep 时间不能写死(比如固定
await asyncio.sleep(1)),否则吞吐量会随单次请求变慢而暴跌
用令牌桶逻辑做软限流,比 sleep 更稳
硬 sleep 容易让爬虫节奏僵化,尤其在响应延迟波动大时。推荐用带时间窗口的令牌桶模拟——每次请求前检查是否“有令牌”,没有就等差值时间。
- 维护一个
last_request_time和单位时间配额(如 1 秒 3 个),每次请求前计算应等待:max(0, 1/3 - (now - last_request_time)) - 这样既能平滑控频,又不会因某次请求慢就卡住后续所有请求
- 别用
time.time(),统一用asyncio.get_event_loop().time(),避免时钟精度问题或阻塞
# 示例片段(非完整类) rate_limit = 3 # RPS interval = 1.0 / rate_limit last_request = 0 <p>async def limited_fetch(url): global last_request now = asyncio.get_event_loop().time() wait = max(0, interval - (now - last_request)) if wait > 0: await asyncio.sleep(wait) last_request = asyncio.get_event_loop().time() return await aiohttp.ClientSession().get(url)</p>
反爬封禁常和 User-Agent + 请求头模式强相关
光控频率不够,很多站点会比对请求头指纹。同一 IP 下所有请求头完全一致、且 User-Agent 是默认值(如 aiohttp/3.x),比高频本身更容易触发风控。
图片提示词生成器?不止如此。 马甲系统 —— 把脑海中的画面,翻译成AI能理解的专业表达。 用得越多,它越懂你:首次需要多问几句确认方向,用久了几乎一说就懂。 用得越多,它越快:缓存机制让后续对话越来越省。 RAG进化:成功案例持续入库,越跑越聪明。 输入「新手指南」查看完整功能介绍
- 务必替换
User-Agent,用真实浏览器 UA,并轮换(至少 3–5 个) - 加上
Accept、Accept-Language、Sec-Ch-Ua等现代字段,否则请求头过于“干净”会被标记为脚本 - 禁用
Connection: keep-alive或强制短连接有时反而更安全——某些 WAF 对长连接行为更敏感
被封后别急着重试,先确认是哪一层拦截
返回 403 不一定代表 IP 被封,可能是 503(WAF 限速)、429(明确限流)、甚至 200 但 HTML 里塞了验证码或跳转 JS。直接重试只会加重惩罚。
立即学习“Python免费学习笔记(深入)”;
- 打印响应状态码 +
response.headers.get("Server")+response.headers.get("X-Served-By"),快速判断是源站还是 Cloudflare / Akamai / 华为云 WAF - 检查响应体是否含
"cf-challenge"、"akamai"、"security_code"等关键词 - 如果是 WAF 拦截,降频 + 换 UA + 加 Referer 可能比换代理更有效;IP 封禁则必须走代理池,且要控制代理间切换节奏
实际跑起来你会发现,最麻烦的不是写限流逻辑,而是不同目标站对“正常人访问节奏”的定义差异极大——有的允许 10RPS 但拒绝连续 3 次相同 Referer,有的放行固定间隔请求却封掉任何 Accept-Encoding: gzip, deflate 缺失的请求。得一个个站去试,留日志,别信通用配置。

















