爬虫跑着跑着就开始收到403、验证码或者空数据,通常说明目标站点已经根据IP地址、请求频率或者请求特征启动了风控策略。单纯更换User-Agent并不能解决核心问题,更可靠的做法是从网络出口和访问节奏两个维度同时调整。代理池负责把请求分散到多个出口IP,频率控制则让每个IP的请求行为更接近真实用户。两者配合之后,封禁概率会明显下降,但前提是理解各自的原理和限制。

一、先弄清楚封禁是如何触发的
大部分站点并非一开始就封禁某个IP,而是通过一段时间内的统计特征做判断。例如同一个IP在五秒内请求了二十次列表页,或者请求间隔高度固定,就会被识别为脚本行为。还有的平台会结合登录状态、Referer、Cookie、设备指纹等做综合评分。即使代理IP本身是干净的,如果每个代理IP都只使用一次,但切换速度过快,也可能因为访问节奏异常触发整个IP段的风控。
代理池的价值不在于让请求完全匿名,而在于把高频访问分散开。假设目标站对单个IP限制每分钟不超过十次请求,而爬虫需要每分钟抓取一百个页面,理论上只需要十个可用代理IP就能把频率压到安全线以下。但代理IP的质量参差不齐,免费代理经常不可用或者延迟很高,所以才需要先验证再使用。下面是一个简单的代理可用性检测脚本,它通过访问一个回显IP的接口来判断代理是否正常工作。
import requests
proxy_list = [
"http://127.0.0.1:7890",
"http://127.0.0.1:7891",
]
def check_proxy(proxy):
try:
resp = requests.get(
"https://httpbin.org/ip",
proxies={"http": proxy, "https": proxy},
timeout=5
)
if resp.status_code == 200:
print("可用代理:", proxy)
return True
except Exception as exc:
print("代理失效:", proxy, exc)
return False
valid_proxies = [p for p in proxy_list if check_proxy(p)]
这段代码只验证了代理能不能连通目标接口,实际项目中还需要检查响应内容是否包含被封禁的标记。比如有些站点虽然返回200,但页面里会出现验证码或者异常提示。因此验证逻辑不能只看状态码,还要检查返回文本中的关键字段。
二、请求频率控制不只是加延时
最简单的频率控制方式是固定延时,也就是每次请求前休眠固定秒数。这种方式实现简单,但缺点非常明显:固定间隔会形成规律,很容易被风控模型识别。更好的做法是加入随机延时,让每次请求间隔在一个区间内浮动。进一步的做法是根据失败率动态调整延时,例如连续出现几次429响应后自动增加基础等待时间。
除了时间间隔,还可以从请求速率的角度建模。令牌桶算法就是一种常用的频率控制手段。它允许突发请求,但长期来看会限制平均速率。桶中按照固定速度放入令牌,每次请求需要消耗一个令牌,没有令牌时就需要等待或者丢弃请求。相比单纯延时,令牌桶更适合需要同时控制峰值和平均速度的场景。
| 控制方式 | 实现难度 | 行为特征 | 适用场景 |
|---|---|---|---|
| 固定延时 | 低 | 间隔固定,容易被识别 | 低频抓取 |
| 随机延时 | 低 | 间隔浮动,更接近人类 | 中小规模爬取 |
| 令牌桶 | 中 | 平滑平均速率 | 需要控制QPS的接口 |
下面是一个随机延时和令牌桶的简单实现。随机延时使用uniform生成区间内的随机浮点数,令牌桶则通过时间差计算新增令牌数量。
import time
import random
def random_delay(min_seconds=1, max_seconds=3):
delay = random.uniform(min_seconds, max_seconds)
time.sleep(delay)
return delay
class TokenBucket:
def __init__(self, rate, capacity):
self.rate = rate
self.capacity = capacity
self.tokens = capacity
self.last = time.time()
def get_token(self):
now = time.time()
self.tokens = min(self.capacity, self.tokens + (now - self.last) * self.rate)
self.last = now
if self.tokens >= 1:
self.tokens -= 1
return True
return False
实际使用时通常不会只依赖某一种方式。例如可以先通过令牌桶限制总请求速率,再对同一个代理IP设置最低间隔。两者叠加可以避免因为突发流量导致代理被临时封禁,也能防止总请求量超过目标站承受范围。
三、代理池与频率控制如何协同工作
单独维护代理池或者单独控制频率,都无法稳定应对封禁。正确做法是把代理轮换、失败退避、请求限速放在同一个请求器里。每次请求前选择代理,再经过频率校验,确认可以发起后再真正发送请求。请求结束后根据响应状态更新代理健康度,如果某个代理连续失败几次,就暂时从可用池中剔除。
下面的示例定义了一个SafeCrawler类,它从代理列表中随机选取代理,并为每个代理记录失败次数。当响应状态为403或429时,认为该代理已经被目标站限制,增加失败计数。失败次数超过阈值的代理会被移出可用集合,等待后续定时检测后恢复。
import time
import random
import requests
class SafeCrawler:
def __init__(self, proxies, min_delay=1, max_delay=3):
self.proxies = proxies
self.min_delay = min_delay
self.max_delay = max_delay
self.failed = {}
def get_proxy(self):
available = [p for p in self.proxies if self.failed.get(p, 0) < 3]
if not available:
raise RuntimeError("没有可用代理")
return random.choice(available)
def fetch(self, url, headers=None):
proxy = self.get_proxy()
time.sleep(random.uniform(self.min_delay, self.max_delay))
try:
resp = requests.get(
url,
proxies={"http": proxy, "https": proxy},
headers=headers,
timeout=10
)
if resp.status_code in (403, 429):
self.failed[proxy] = self.failed.get(proxy, 0) + 1
raise PermissionError("请求被封禁: %s" % resp.status_code)
return resp
except Exception:
self.failed[proxy] = self.failed.get(proxy, 0) + 1
raise
这个示例只是一个基础框架,实际项目中还需要加入代理池定时补充、响应内容校验、请求重试等模块。代理数量不足时,宁可降低抓取速度,也不要频繁复用已经出现失败记录的代理。另一个容易被忽略的问题是代理协议,有些代理只支持HTTP,有些支持HTTPS,如果抓取的是HTTPS页面,必须确保代理本身支持CONNECT隧道。
除了代理和频率,请求头也需要做适当伪装。同一种User-Agent在大量请求中出现,会形成明显的指纹。可以维护多个常见的浏览器User-Agent,并在每次请求时随机选择。同时可以随机调整Accept-Language、Referer等字段,但不要频繁改变一个会话内的核心指纹,否则反而会显得异常。
四、更高级的反爬与应对边界
代理池和频率控制只能解决基础层面的IP封禁问题。如果目标站启用了验证码、JavaScript计算或浏览器指纹检测,单纯换IP和限速可能仍然无法绕过。验证码通常意味着目标站已经识别到异常行为,此时继续暴力请求只会让封禁范围扩大。遇到这类情况,更合适的做法是降低抓取频率、更换数据源,或者使用官方的数据接口。
浏览器指纹是另一个难处理的问题。目标站可能通过Canvas渲染结果、WebGL信息、字体列表等方式识别访问环境。代理池可以改变IP,但无法改变这些指纹。如果需要处理这类站点,通常要引入无头浏览器或者专门的浏览器自动化工具。但这种方式复杂度高,而且会更重地消耗资源,不适合所有爬虫项目。
最后需要明确一点,技术手段不能成为破坏目标站正常服务的理由。爬虫应当在尊重robots协议、遵守网站服务条款和相关法律法规的前提下运行。代理池与频率控制的目的是降低对目标站的压力,而不是逃避规则。合理设置抓取间隔、限制并发数、优先使用公开数据接口,才能让爬虫项目长期稳定运行。

