代理池、限速与请求指纹
限速是对目标站点的基本礼貌,也是保护自己的手段;代理与指纹一致性的讨论,前提是"合规地分散自建出口的负载",而不是"隐藏身份继续高频抓取"。本章给可落地的限速实现、代理的使用边界与健康检查,以及一张能直接抄的配置表。
为什么必须限速
两个理由权重相同:对别人(不因为你的采集影响正常用户)、对自己(稳定的低速任务比忽快忽慢的高频任务更容易跑完)。
| 不限速的后果 | 具体表现 | 限速后的收益 |
|---|---|---|
| 触发限流 | 429、响应变慢、返回空数据 | 数据完整度稳定 |
| 被临时封禁 | 403、IP 段被拉黑、账号受限 | 任务可长期无人值守运行 |
| 影响对方业务 | 高峰期响应变慢,容易引发投诉 | 大幅降低被投诉与被封的概率 |
令牌桶:控制平均速率,允许小幅突发
漏桶严格匀速,令牌桶允许攒够令牌后连发几次。对第三方站点,建议直接用漏桶式的固定间隔。
import threading
import time
class TokenBucket:
"""令牌桶:平均 rate 个/秒,容量 burst,线程安全。"""
def __init__(self, rate: float, burst: int = 1) -> None:
self.rate = rate
self.capacity = max(burst, 1)
self._tokens = float(self.capacity)
self._updated_at = time.monotonic()
self._lock = threading.Lock()
def acquire(self, tokens: int = 1) -> None:
while True:
with self._lock:
now = time.monotonic()
self._tokens = min(self.capacity, self._tokens + (now - self._updated_at) * self.rate)
self._updated_at = now
if self._tokens >= tokens:
self._tokens -= tokens
return
wait = (tokens - self._tokens) / self.rate
time.sleep(min(wait, 0.5)) # 短暂放锁,避免长时间持有
并发上限:信号量比"心里有数"靠谱
并发与速率是两个闸门,缺一不可;连接池大小与信号量保持一致,避免"限了并发却开了一堆连接":
import asyncio
import aiohttp # pip install aiohttp
sem = asyncio.Semaphore(4) # 同一域名最大并发
connector = aiohttp.TCPConnector(limit=4, limit_per_host=4)
async def fetch(session: aiohttp.ClientSession, url: str) -> str:
async with sem: # 并发闸门
await asyncio.sleep(0.5) # 速率闸门
async with session.get(url, timeout=aiohttp.ClientTimeout(total=15)) as resp:
resp.raise_for_status()
return await resp.text()
代理的作用与使用边界
代理解决的是"单一出口 IP 负载过于集中",它不解决"你不该抓"这件事。
| 场景 | 是否合适 | 说明 |
|---|---|---|
| 自建代理分摊多台机器的出口 | 合适 | 自己控制带宽与日志,行为可审计 |
| 公司统一出口被限流,换自建出口 | 合适 | 先与站点沟通,再调整出口 |
| 用商业代理池隐藏高频抓取 | 不合适 | 只是转移风险,行为性质未变 |
| 用代理绕过地区限制访问付费内容 | 违法 | 绕过访问控制,可能涉及刑事责任 |
| 用代理访问隐私数据源 | 违法 | 与是否用代理无关,行为本身即违法 |
判断标准很简单:如果你需要向对方解释"为什么不能知道是我在请求",那这件事就不该做。
代理池的健康检查与轮换
import random
import requests
def check_proxies(proxies: list[str], check_url: str, timeout: float = 5.0) -> list[str]:
"""逐个探测代理可用性,只返回可用的那部分。"""
alive = []
for proxy in proxies:
try:
resp = requests.get(check_url, proxies={"http": proxy, "https": proxy}, timeout=timeout)
if resp.ok:
alive.append(proxy)
except requests.RequestException:
continue # 不可用直接剔除,不做无谓重试
return alive
def pick(alive: list[str]) -> str | None:
"""从可用代理中轮换取一个;返回 None 时调用方应回退直连并降频。"""
return random.choice(alive) if alive else None
请求指纹的一致性
指纹不一致本身就是异常信号:UA 说是 Windows 上的 Chrome,Accept-Language 却是别的语种,时区对不上,连接行为又像脚本,组合起来比高频更容易被拦。
| 维度 | 一致的做法 | 不一致的后果 |
|---|---|---|
| User-Agent | 全流程固定同一个自定义 UA | 每次请求换 UA 是典型脚本特征 |
| Accept-Language | 与访问内容的语言匹配 | 与地区、UA 矛盾时易被判异常 |
| Cookie | 同一会话内保持一致 | 一半带一半不带,像多客户端混用 |
| 连接复用 | 用 Session 复用 TCP 连接 | 每次新建连接,握手特征过于突出 |
| 请求节奏 | 稳定间隔,允许轻微波动 | 毫秒级等间隔是最明显的时间指纹 |
| 出口 IP | 一个任务固定一个出口 | 同一会话频繁换 IP 会触发风控 |
可直接抄的限速配置表
| 目标 | 单域名并发 | 请求间隔 | 每小时上限 | 失败处理 |
|---|---|---|---|---|
| 小型个人站点 | 1 | 3 秒 | 约 1000 | 失败即停,人工确认 |
| 一般公开站点 | 2 | 1~2 秒 | 约 2000 | 指数退避,最多 3 次 |
| 大型站点 + 官方 API Key | 按配额 | 按配额 | 按配额 | 按官方文档的重试策略 |
| 自己可控的服务 | 压测决定 | 不限 | 不限 | 在压测目标内自由调整 |
常见坑
| 坑 | 说明 |
|---|---|
| 只限并发不限速率 | 并发 4 也能在一秒内打出几十个请求 |
| 代理池不做健康检查 | 大量时间耗在不可用代理上,整体更慢 |
| 用代理代替降频 | 换 IP 继续高频,仍然在影响对方服务 |
| 为"更像人"而随机 sleep | 随机不等于合理,稳态低速比忽快忽慢更好 |
忽略 Retry-After | 站点已给出等待时间,硬顶只会换来更长的封禁 |
小结:限速对站点和对自己都是最划算的投入,用令牌桶控速率、信号量控并发,代理只用于自建与合规场景并配合健康检查,请求指纹要保持前后一致;能靠降频解决的,不要用代理去绕。