先问自己一个问题:你是想换得勤,还是想换得巧
见过不少团队,一开始的思路很朴素——把IP池开得越大越好,一遇到内容拿不全就更换IP,听见「限额」两个字就再换一次。跑了两天回头看日志,成功率没涨,出口IP的消耗倒是翻了三倍,还额外背了一堆没必要的封禁。
问题往往不在池子大小,而在节奏。同样的 200 个出口,A 团队每个 IP 平均扛 40 次请求就被拦,B 团队每个 IP 平均扛到 300 次还稳得住——差别就在于谁在控制「一个 IP 上短时间内挤了多少次请求」,以及碰壁之后怎么退。
所以这篇不聊虚的,直接拆两件事:调度节奏怎么排,以及失败之后怎么重试才不浪费 IP。这两件事做对了,同样的预算能多跑出两三倍的活儿。
调度节奏:并发、密度、间隔,三根旋钮
把调度想象成放水。并发是水管粗细,单IP请求密度是水压,轮换间隔是换桶的频率。任何一根拧过头,水就溢出来——表现出来就是 429、503、连接被重置,或者干脆返回一堆空白页。
举个例子。你要抓某平台 8000 个详情页,手上 60 个可用出口。新手常写的配置是 60 路全开、请求之间不加等待,结果前 10 分钟很爽,第 11 分钟开始大面积 5xx。其实改成这样就行:并发压到 30,每个 IP 连续跑 10 到 15 个页面就归还,两次请求之间插入 0.6 到 1.8 秒的随机等待——总耗时只多了不到 20%,成功率能从 60% 出头提到 95% 以上。
间隔里有个细节值得单独说:等待时间一定要带随机。固定 1000 毫秒的间隔,跑几百次之后形成的就是一条完美等距的节奏线,机器味太重。改成随机区间,看起来就像正常的页面浏览。
| 采集目标类型 | 单IP连续请求 | 轮换间隔 | 建议并发 |
|---|---|---|---|
| 商品列表页 | 15–25 次 | 3–8 分钟 | 20–40 |
| 商品详情页 | 8–15 次 | 2–5 分钟 | 30–50 |
| 搜索结果页 | 3–8 次 | 30–90 秒 | 10–20 |
| 公开文档 / 接口 | 50–100 次 | 10–30 分钟 | 50–100 |
这张表里的数字不是标准答案,是起点。真正落地时,先按表跑 300 次请求,把成功率记下来,再把并发加 20% 跑一遍。成功率掉头往下走了,说明到边界了,退回上一档就是你这套环境的最优节奏。
失败重试:先分清是谁的错,再决定怎么退
重试最怕的是「无差别重试」——不管什么原因失败,统统换一个 IP 再打一次。看起来暴力有效,实际上浪费极大。因为失败的原因大概分三类,处理方式完全不同:
第一类,出口的错。连接超时、握手失败、连接被重置、返回的页面明显是代理异常页。这类失败跟目标站点没关系,换个出口立刻就好,不需要等待,直接换下一个 IP 重来最省时间。
第二类,节奏的错。429、503,或者页面里出现「访问过于频繁」的提示。这是目标在告诉你「慢一点」。这时候换 IP 也能过,但等于用新 IP 去填旧坑,池子消耗会很快。更划算的做法是让这个 IP 冷却 60 秒左右,同时把整批任务的并发临时降一档,等限速窗口过去再恢复。
第三类,目标的错。404、页面结构变了、返回内容解析不出来。这类失败重试十次也没用,该做的是标记下来扔进单独一个队列,回头人工看一眼页面是不是改版了。一直死磕只会把 IP 白烧掉。
还有一个容易被忽略的:重试上限一定要写死。建议单个任务最多 3 次,超过就标记为失败并记录最后一次的错误类型。没有上限的重试会在网络抖动的那几个小时里,把你的池子变成一个黑洞。
另外,重试必须做退避。第一次失败等 1 到 2 秒,第二次等 4 秒左右,第三次等 8 秒以上,中间再加点随机。这条曲线能保证偶发的网络抖动被自然消化掉,不会一下打出一串无效请求。
把节奏写成代码:一次请求的完整闭环
下面这段是最小可用的骨架,去掉业务解析,只保留取 IP、发请求、分类失败、退避重试这四件事。可以直接套进你现有的采集脚本里改。
import random, time
def fetch_with_retry(url, pool, max_try=3):
for attempt in range(1, max_try + 1):
ip = pool.get() # 从池里取一个可用出口
try:
resp = request(url, proxy=ip, timeout=8)
if resp.status == 200:
pool.mark_ok(ip) # 这个出口当时是干净的
return resp.text
if resp.status in (429, 503):
# 目标在限速:让它冷却,别急着用新 IP 去填坑
pool.cool_down(ip, seconds=60)
time.sleep(2 ** attempt + random.random())
elif resp.status in (403, 404):
# 大概率是目标侧问题,重试意义不大
pool.release(ip)
return None
else:
pool.mark_bad(ip)
time.sleep(2 ** attempt)
except (TimeoutError, ConnectionResetError):
pool.mark_bad(ip) # 出口本身有问题,直接下线
time.sleep(random.uniform(0.6, 1.5))
return None
几行细节值得留意。timeout=8 这个值不要设太大,超时时间太长会让整个队列被几个慢出口拖住;cool_down 和 mark_bad 分开记录,你才能事后算清楚「这个池子到底是被目标限速拖垮的,还是被坏出口拖垮的」;random.random() 那一点随机,别省。
跑起来之后,每天花两分钟看三个数:总成功率、失败原因分布、单个 IP 的平均使用次数。前两个决定你要不要调节奏,第三个决定你的池子是不是买多了或者买少了。
节奏排得开,前提是池子跟得上
道理都懂,但节奏这东西有物理上限——你手上 20 个出口,无论怎么调,并发也超不过去。所以到一定规模,工具层的优化到顶了,就该看 IP 资源本身。
神龙IP在这块比较适合做采集类任务:200+ 城市精准定位,做各地搜索排名测试时能精确到城市级别取出口;1000 万+ 纯净 IP 加自营机房,30ms 级别的响应,意味着单次请求的握手耗时才不至于吃掉你辛苦排好的间隔。带宽 6–15M 可定制,跑图片和长文档时不会卡在传输环节。
具体到套餐,两种比较常用。动态高级适合短周期高频轮换的活:6Mbps 峰值、日更 200 万+ IP、2–360 小时时效,任务跑多久就开多久,用完即走,不会囤一堆闲置资源。动态独享则是 10Mbps 峰值、2–360 小时时效,出口是你一个人在用,带宽稳定,适合对单任务吞吐要求高、不能和别人抢通道的场景。两个都支持 IKEv2、PPTP、L2TP、SSTP、SOCKS5,数据传输全程加密,接进上面那段代码基本不用改结构。
选哪个的判断标准很简单:任务是「很多小请求分散打」,走动态高级;任务是「少数长连接持续跑」,走动态独享。拿不准就先小批量试跑两小时,看成功率曲线和出口消耗速度,数据会告诉你答案。
常见问题
Q:轮换间隔到底设多少秒合适?有没有通用值?
没有通用值,但有个起手式:先按单 IP 连续 10 次请求、间隔随机 1 到 2 秒跑 300 次,看成功率。如果 95% 以上,说明还能再压紧,把连续次数提到 15 次再试;如果掉到 80% 以下,就把连续次数降到 5 到 8 次,或者把间隔拉长到 3 秒以上。用数据逼近,比照抄别人的配置靠谱。
Q:遇到 429 到底该换 IP 还是该等?
优先等。429 是目标在限速,说明你这一路请求太密了,换个出口继续打只是把压力转嫁给下一个 IP,池子会被迅速消耗掉。让当前出口冷却 60 秒左右,同时把整批并发降一档,通常比无脑换 IP 的效率高得多。只有连续多次 429 且降并发也没用的时候,才说明出口本身被划进了受限范围,那时候换才是对的。
Q:重试次数设 3 次够不够?
对绝大多数采集任务够了。3 次意味着最坏情况下单个 URL 消耗 3 个出口,超过这个数还没拿到内容,基本可以判断是 URL 本身有问题或者目标侧改了规则,继续试只会白烧 IP。真正该做的是把失败记录留全——URL、时间、错误类型、用的哪个出口,回头一统计,问题根源基本自己就浮出来了。
Q:采集任务跑一半成功率突然掉下来,先查什么?
按这个顺序看:先看失败原因分布,如果集中是超时,多半是出口质量出问题,检查池子里有没有大量失效 IP;如果集中是 429,是节奏问题,降并发拉间隔;如果集中是解析失败,那就是目标页面改版了,跟 IP 没关系。三类问题的处理方向完全相反,不看分布就动手调参数,经常越调越差。





