采集限速与合规:别把对方站点打挂
几年前做过一个比价的小项目,目标站不大,我压了 200 并发。当天下午对方站点开始 502,第二天我的整段 IP 被机房封了,任务停了三天。那次之后我才认真去想一个问题:代理解决的是"我是谁",限速解决的是"我有多克制"——这两件事都得做,缺一个都不行。
换 IP 绕开的是限流,不是容量
很多人有一种错觉:只要 IP 池够大,就能无限提速。实际上目标站面对的不只是"单 IP 限流",还有数据库连接数、带宽、后端服务承载能力。你把请求摊到 500 个 IP 上,对方的机器该崩还是崩,只不过日志里看不到同一个来源 IP 而已。
而一旦把对方服务搞出故障,后果通常比被封 IP 麻烦得多:可能是法务函,也可能是整个 IP 段被拉黑,连带你的其他业务一起受影响。
单站限速:先定一个能自我约束的数字
我的默认起点是:
- 对同一域名,总并发 不超过 10,QPS 不超过 5;
- 对方站点规模小(个人站、老系统)就降到并发 2~3;
- 夜间可以适当放宽,白天业务高峰期收紧。
注意"总并发"是跨 IP 汇总的。加了代理之后,很多人只盯着单 IP 的并发,结果 50 个 IP 各开 5 并发,对目标站就是 250 并发——等于没限速。
用 Redis 做全局令牌桶
多进程、多机器的时候,本地计数器各算各的,汇总起来就失控了。把限速器放到 Redis 里,全局共用一份配额:
import time
import redis
TOKEN_BUCKET_LUA = """
local key = KEYS[1]
local rate = tonumber(ARGV[1]) -- 每秒补充多少令牌
local burst = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3])
local data = redis.call('HMGET', key, 'tokens', 'ts')
local tokens = tonumber(data[1]) or burst
local ts = tonumber(data[2]) or now
tokens = math.min(burst, tokens + (now - ts) * rate)
if tokens < 1 then
redis.call('HMSET', key, 'tokens', tokens, 'ts', now)
return 0
end
tokens = tokens - 1
redis.call('HMSET', key, 'tokens', tokens, 'ts', now)
return 1
"""
client = redis.Redis(decode_responses=True)
script = client.register_script(TOKEN_BUCKET_LUA)
def allow(domain, rate=5, burst=10):
"""对 domain 这个站点申请一个令牌,拿到才能发请求。"""
ok = script(keys=["rl:%s" % domain],
args=[rate, burst, time.time()])
return bool(ok)
用法就是发请求前先问一句 if not allow(domain): time.sleep(0.2),简单但有效。桶的粒度按域名算,别按 URL 算,否则等于没限。
退避重试:指数 + 抖动
被限流时立刻重试是最糟的反应,它会把"偶发限流"变成"持续被封"。正确姿势是指数退避,并且加随机抖动——否则几百个任务会在同一时刻一起重试,形成新的流量尖峰:
import random
import time
def backoff_sleep(attempt, base=1.0, cap=60.0):
"""第 attempt 次失败后的等待时间:1s, 2s, 4s ... 最多 60s,并加随机抖动。"""
delay = min(cap, base * (2 ** attempt))
delay = delay * (0.5 + random.random() * 0.5) # 抖动,避免同时重试
time.sleep(delay)
另外要区分错误类型:403/429 这种明确被拒的,退避要长;超时这种可能只是线路抖动,退避可以短。一刀切的重试策略基本都是错的。
缓存与增量:最省流量的优化
比"抓得更快"更有效的是"抓得更少"。三个做法按性价比排序:
- 列表页做增量:用上次抓到的最大时间戳/ID 做条件,别每次全量翻页;
- 详情页走缓存:URL 级别的响应缓存(带 TTL),重复的 URL 直接读本地;
- 用条件请求:带上
If-Modified-Since/ETag,对方返回 304 时几乎没有代价。
我在一个日更的任务里加完这三条,每天的请求量从 40 万降到 6 万,目标站的响应时间也明显变好了。
合规这条线要自己心里有数
- robots.txt 是底线不是上限。它没禁止,不等于你可以在对方高峰时段压满它。
- 别采个人隐私数据。手机号、身份证、住址这类信息,即使页面上有也别落库,风险远大于收益。
- 别绕过付费墙。用技术手段拿到本该付费的内容,属于典型的侵权场景。
- 标明身份。给爬虫起个能联系到你的 User-Agent,真有纠纷时对方更愿意先联系你而不是直接封禁。
- 公开数据也注意频率。有公开 API 的优先用 API,通常比爬页面稳定得多。
小结:代理管"我是谁",限速管"我有多克制",缓存管"我能不能少抓点"。三者配上,采集才能长期稳定地跑下去——被封一次停三天,比一开始慢一点代价大得多。
顺便说下代理的角色:把请求分散到多个出口 IP,本质是为了让单 IP 的请求频率保持在"正常用户"的量级,而不是为了把并发堆到对方身上去。我用的是 星月代理,短效 IP 会自动轮换,配合上面这套全局限速用起来比较稳。具体的池子实现和 18 种语言示例都在 github-xydaili-examples 里。