跑采集的都知道这个场景:脚本刚上线的时候顺风顺水,跑到第 40 分钟开始,返回的页面越来越空,日志里全是 ProxyError 和 ReadTimeout。IP 池里明明躺着几万个地址,任务却卡在那里一动不动。问题通常不在 IP 数量够不够,而在于池子没人管——谁去取、取哪个、同时取几个、失败了怎么办,这四件事没定规矩,池子再大也是一盘散沙。
这篇就按工程落地的顺序,把这几件事一件件拆开说。
一、先给池子分格,别把动态和静态混着用
很多人的 IP 池就是一个大列表,取的时候 random.choice() 一把梭。前半小时没问题,一旦任务里出现需要保持会话的页面(比如要连着走"列表页 → 详情页 → 分页"的流程),这套就崩了:第二跳换了个 IP,服务端看到的会话是新的,前面那一步白跑。
实操上建议把池子分成两格来管:
动态格——短效动态 IP,量大利于摊薄压力,适合列表页、搜索结果页、公开详情页这种一次请求一个页面的任务。这类请求彼此独立,换 IP 没有副作用,用起来最放得开。
静态格——长效静态 IP 和固定 IP,数量少、贵、但稳。留给需要连续会话的流程、需要稳定出口的接口轮询、以及"重试了两次还是失败、必须上一个靠得住的出口"的兜底场景。
配比不用太精确,二八开是个好起点:八成流量走动态,两成留给静态和兜底。跑一周看日志,如果静态池的占用长期贴着上限,说明你的任务里"会话型请求"比想象的多,该往上调了。
二、分配策略:抽哪个 IP,得有明确的规则
取 IP 这个动作看起来最简单,其实决定了整个任务的请求指纹分布。常见的几种打法差别挺大:
| 策略 | 适合什么任务 | 好处 | 容易踩的坑 |
|---|---|---|---|
| 轮询 | 页数固定的列表、详情页遍历 | 压力摊得匀,每个 IP 用到的次数可预估 | 池子中间有死 IP 时会周期性卡一下 |
| 随机 | 请求量波动大的任务 | 打散请求指纹,不容易被盯上 | 短时间可能反复抽到同一个 IP |
| 加权 | 池里混着快慢不一的 IP | 好 IP 多跑几趟,整体吞吐更高 | 权重要靠真实成功率来更新,得配监控 |
| 按城市定向 | 同城生活、区域价格、地方站点 | 就近命中目标城市,减少内容错配 | 小城市的池子薄,要留降级方案 |
| 会话粘性 | 登录态流程、多步表单 | 整个流程同一个出口,逻辑不中断 | 只能用在静态池上,用久了要主动下线 |
落到代码里,不需要搞得多复杂,一个带场景判断的取用方法就够用:
import random, itertools
class IpPool:
def __init__(self, dynamic, static):
self.dynamic = dynamic # 短效动态池:量大、换得快
self.static = static # 长效静态池:量少、要稳
self._rr = itertools.cycle(dynamic)
def pick(self, scene="list"):
# 需要连续会话的流程,只走静态池,并固定同一个出口
if scene == "session":
return random.choice(self.static)
# 目标城市明确的页面,先从动态池里筛同城
if scene == "city":
city = self.task_city
same_city = [ip for ip in self.dynamic if ip.city == city]
if same_city:
return random.choice(same_city)
return next(self._rr) # 该城市没货,退回轮询
# 普通页面,轮询到底,压力最均匀
return next(self._rr)
注意 scene == "city" 那个分支里的 if same_city。定向不是硬性要求,城市池子薄的时候必须允许降级,不然一个三线城市的详情页能把整个队列堵死。神龙HTTP 的城市级定位能覆盖 300 多个城市,日常够用,但代码里这层兜底还是留着更安心。
三、并发控制:闸门开多大,先算再开
并发数凭感觉设,是最常见的事故来源。设成 500,看着挺猛,实际池里能用的 IP 就 2000 个,每个 IP 一秒挨 3 次请求,目标站点的风控第一分钟就把你标记了;设成 5,任务跑三天跑不完。
给一个可落地的算法:先定目标吞吐,再反推在途并发。
假设你要采 30 万个页面,希望 1 小时内收工,那就是每秒 83 个请求,取整按 90 QPS 算。再假设单个 IP 在这类页面上每秒扛 2 次请求比较安全(保守点按 1.5 算),那需要的 IP 数是 90 ÷ 1.5 = 60 个。留 30% 的余量给抽到失效 IP 的情况,在途并发就定在 80 上下。
数字定好之后,闸门要靠信号量来管,不要靠 sleep:
import asyncio
SEM = asyncio.Semaphore(80) # 全局在途请求上限
async def fetch(url, pool):
async with SEM: # 超过 80 个就在这儿排队,不会压垮池子
ip = pool.pick(url.scene)
try:
return await request(url, proxy=ip, timeout=8)
except Exception as e:
return on_fail(url, ip, e) # 交给下面的回退逻辑
还有两个细节容易被忽略。一是超时别设太长,代理类请求 8 秒不回基本就废了,设 30 秒等于把并发槽位白白占住。二是并发要能动态调,从日志里算出最近 5 分钟的失败率,超过 15% 就自动把信号量下调两成,稳定十分钟再慢慢放回去。这套升降机制比写死一个数字管用得多。
四、失败回退:把"失败"拆成三档再处理
所有异常都走同一条重试路径,是另一个大坑。连接超时和被目标站限流是两回事,处理方式完全相反——前者换个 IP 立刻重来就行,后者越重试越糟,你等于在告诉对方"我还在"。
按来源分三档:
第一档:连接层失败。连不上代理、握手超时、返回 502/503 这类代理自身的错。这种最好办,把当前 IP 打个标记丢进冷却队列,隔两分钟再看,请求换个 IP 重发。重试 1 次就够,不用纠缠。
第二档:代理被目标拒绝。返回 403、407,或者页面内容变成一张拦截页。说明这个 IP 在目标站那边已经上黑名单了。处理办法是直接下线,不要进冷却队列——它在这个站点上已经没救了。同时把这个情况记到 IP 的健康分上,同一个 IP 在多个站点被拒,就该彻底剔除。
第三档:目标侧限流。返回 429、验证码页、或者返回 200 但正文是空的。这是最需要克制的一档。正确做法是先降速:把该域名下的并发砍一半,等三分钟再试探性放几个请求;连续两次还是这样,就切换到静态池,用低频慢跑的方式把剩下的页面啃完。硬刚只会把整个域名打成不可用。
回退逻辑写成函数大概长这样,重点是三档分开走:
MAX_RETRY = 2
def on_fail(url, ip, err):
if isinstance(err, (ProxyError, ConnectTimeout)):
ip.cooldown(120) # 冷却两分钟,可能是临时抖动
return retry(url, retry=url.retry + 1)
if err.status in (403, 407):
ip.blacklist(url.domain) # 该站点永久拉黑
return retry(url, retry=url.retry + 1)
if err.status == 429 or err.empty_body:
throttler.slow_down(url.domain, ratio=0.5) # 先降速
if url.retry >= MAX_RETRY:
return move_to_static_queue(url) # 转静态池低频慢跑
return retry(url, retry=url.retry + 1, delay=180)
return mark_failed(url)
另外给每个任务设一个全局重试上限(比如单个 URL 最多 3 次),不然一条死链能把队列拖住一整天。
五、让池子自己转起来:监控和日常维护
上面这些策略能跑起来,前提是你知道池子里现在是什么状态:哪些 IP 在冷却、哪些被某个站点拉黑、哪个城市的池子快见底了、失败率曲线有没有突然抬头。这些光靠翻日志很累。
神龙HTTP 的个人中心里有可视化的用量统计和实时监控,能看到提取量、并发情况和使用趋势,异常一眼能看出来——比如某个时间点失败率突然跳到 20%,基本就是某个城市池子被打空了,或者目标站刚改了风控规则,你顺着时间点去查就能定位。池子本身 3000 万+ 的正规授权 IP 储备、99.9% 的可用率和 99.8% 的纯度,意味着你不用花太多精力去筛"这个 IP 到底能不能用",重心可以放在调度逻辑上。
接入侧也不折腾,API 兼容主流爬虫语言,拿文档里的示例代码改几行就能跑;HTTP/HTTPS/SOCKS5 都支持,短效动态、长效静态、固定 IP 三条产品线可以按上面说的"分格管理"思路各取一部分。真遇到调不通的情况,技术团队 7×24 小时在线,比自己对着报错猜要快。
最后补一句实践里的经验:别追求一开始就把策略调到最优。先按轮询 + 固定并发 + 简单重试跑通,观察一周日志,看看失败主要集中在哪一档、哪个城市、哪个时段,再针对性地加权重、加定向、加动态升降。池子调优是个持续的事,不是一次性配置。
常见问题
Q:IP 池里 IP 越多,是不是采集就越稳?
不一定。池子大了如果分配还是纯随机,可能同一秒有几个请求打到同一个 IP 上,单个 IP 的压力反而更集中。决定稳定性的是"每个 IP 每秒挨几次请求"这个数字,把它控制住,几万个 IP 和几十万个 IP 的效果差别不大;控制不住,再多也没用。
Q:短效动态 IP 和长效静态 IP 该怎么选?
看请求之间有没有关联。彼此独立的一次性页面,用短效动态最划算,压力摊得开;需要连续会话、要保持登录状态、或者重试多次必须上个靠得住的出口,就用长效静态或固定 IP。很多任务其实是混合的,两种都开一部分,按场景分流最省心。
Q:遇到验证码或者返回空页面,一直重试能解决吗?
大概率会让情况更糟。这类信号通常意味着目标站已经在限流,你重试得越勤,它对你这个域名的整体容忍度就越低。正确顺序是先降并发、加延时,观察几分钟;还不行就换成静态池低频慢跑,把节奏放下来。硬冲的结局往往是整个域名一段时间内都拿不到正常内容。
Q:并发数到底设多少比较合适?
倒推着算:目标总吞吐 ÷ 单个 IP 能安全承受的每秒请求数 = 需要的 IP 数量,再留三成余量作为在途并发。比如目标 90 QPS、单 IP 按 1.5 QPS 算,需要 60 个 IP,并发就定在 80 左右。算完之后别定死,让失败率来决定升降,跑两周基本就能摸到适合自己任务的区间。






