我们收到最多的工单大意都是这样:「我们换到更大的 IP 池了,还是被封。」几乎每一次,IP 都不是原因。它被拿来背锅,只是因为它是最容易改的那个东西。
现代的机器人检测栈,在第一个 HTML 字节发出之前,大概会对十几个信号打分。IP 是其中之一,而且往往不是区分度最高的那个。下面按「实际上更常出卖你」的大致顺序,把其余信号过一遍。
TLS 指纹
在任何 HTTP 通信发生之前,客户端会发出一个 TLS ClientHello。这条消息包含:它支持的密码套件及其顺序、它声明的扩展及其顺序、它接受的椭圆曲线与签名算法,以及它提供的 ALPN 协议。
每个 HTTP 库产生的组合都有辨识度。Python 的 requests 基于 OpenSSL 产生一种指纹,Go 的 net/http 产生另一种,Chrome 产生第三种,而且 Chrome 的指纹每个大版本都在变。把这些字段哈希成 JA3 或 JA4 字符串,检测方就得到了一个稳定的标识,且与你的 IP 地址完全无关。
这就是为什么一个爬虫可以轮换一万个住宅 IP,然后每一个都被封。在检测方看来,那不是一万个访客,而是一个客户端库配了一万个地址。
解决办法是使用能复现真实浏览器 ClientHello 的客户端。在 Python 里,curl_cffi 可以模拟特定的 Chrome 与 Safari 构建:
from curl_cffi import requests
response = requests.get(
"https://example.com",
impersonate="chrome124",
proxies={"https": PROXY},
timeout=30,
)在 Node 里,配置了自定义 TLS 的 undici 或无头浏览器能达到同样效果。无论选哪个,都要保持更新:模拟两年前的 Chrome 构建,本身就是一种异常。
HTTP/2 设置与请求头顺序
如果 TLS 握手协商出了 HTTP/2——面对任何大型站点都会——连接会以一个 SETTINGS 帧开场。这个帧里的参数、它们的顺序、初始窗口大小,以及客户端构建的优先级树,全都与具体的库相关。它们构成了第二个指纹,稳定程度不亚于 TLS 指纹。
请求头顺序同样重要。真实浏览器按固定顺序发送请求头,而且 Chrome、Firefox、Safari 各不相同。大多数 HTTP 库要么按字母序排序请求头,要么按字典插入顺序发出,两者都不匹配任何浏览器。
加一个 User-Agent 字符串解决不了这个问题。一个声称自己是 Chrome 124、却按 Python 插入顺序发送请求头的请求,比完全不带 User-Agent 的信号还要明确——因为它证明的是「有意伪装」,而不只是「自动化」。
要么用一个能正确控制请求头顺序的客户端,要么干脆别声称自己是浏览器。
请求头内容的一致性
假设传输层已经处理妥当,请求头本身还需要彼此一致、并与出口 IP 一致。我们最常见到的不匹配有:
Accept-Language 与 IP 地理位置不符。 华沙的住宅 IP 发送 Accept-Language: en-US,en;q=0.9 就是不匹配。请从你在代理上固定的国家推导这个头。
User-Agent 与 Sec-CH-UA 不符。 Chrome 会在 User-Agent 之外同时发送客户端提示。如果你伪造了其中一个却漏掉另一个,或者版本号对不上,那就是直接的自相矛盾。
缺少 Sec-Fetch-* 请求头。 现代浏览器会发送 Sec-Fetch-Site、Sec-Fetch-Mode、Sec-Fetch-Dest 和 Sec-Fetch-User。一个自称来自 Chrome 的请求缺了它们,非常显眼。
深层页面没有 Referer。 真实用户是从列表页进入商品页的。一个反复直接落在深层 URL、且不带来源的请求,就是在描述一种抓取模式。
声明了自己处理不了的 Accept-Encoding。 声明支持 br 却无法解码 Brotli,是很容易被抓到的破绽。
时序与行为模式
单请求层面的信号都过关之后,跨请求的模式就接手了。
人类浏览是突发且不规则的:加载一个页面、读十一秒、点击、读四秒、开一个新标签页、九十秒后回来。一个每 500 毫秒抓一次、永不停歇、标准差接近零的爬虫,在任何 IP 池规模下都不像人。
加抖动有帮助,但前提是抖动要真实。在 400 到 600 毫秒之间均匀随机,依然明显是机器产生的——分布形状不对。人类的交互间隔大致服从对数正态分布:大部分很短,带一条长尾。按这个形状采样成本很低,说服力却强得多。
请求顺序也很重要。真实会话是顺着链接走的。一个按数字升序、以恒定速率抓取 /product/1、/product/2、/product/3 的爬虫,是在准确地自我描述。
最后,真实浏览器会加载子资源。一个只请求 HTML 文档、从不请求其中引用的 CSS、字体和图片的客户端,产生的是任何浏览器都不会产生的流量形状。
Cookie 与会话状态
一个在中途更换出口 IP、却沿用同一份 Cookie 的会话,等于告诉目标站:有一个身份瞬移了。这是个很强的信号,而且完全是自己造成的。
规则很简单:Cookie 罐和出口 IP 具有相同的生命周期。要么一起轮换,要么都不换。我们的 Python 轮换代理完整设置指南里有一套能保持两者对齐的粘性会话写法。
凡是涉及登录的场景,干脆不要轮换。使用静态 ISP 代理或独占的静态移动代理,让账号长期保持同一个地址。一个每天从不同城市登录的账号,无论每个 IP 本身多干净,都会不断累积验证挑战。
IP 依然重要的地方
以上这些并不意味着出口地址无关紧要。它在三个具体方面仍然关键。
类型。 被归类为托管的地址会被做信誉检查的目标直接拒绝。这就是住宅代理变成必需品的分界线,详见住宅代理与数据中心代理该怎么选。
地理位置。 本地化定价、库存和搜索结果都取决于请求看起来来自哪里。这一点搞错,产出的就是「自信的错误数据」。
历史。 一个已经连续冲击目标站六小时的地址会带着这段历史。轮换会重置它——这才是轮换真正提供的价值。
而 IP 无法做到的,是弥补一个暴露了你所用客户端库的指纹。
排查的先后顺序
当请求开始失败时,按这个顺序排查,而不是先去换更大的 IP 池:
- 完整抓取一个失败响应。 请求头、响应体、状态码,都读一遍。页面上经常直接写着原因。
- 在自己电脑的浏览器里、不走代理访问同一个 URL。 如果同样失败,那是目标站变了,不是你的配置。
- 通过代理、用真实浏览器再访问一次。 如果浏览器成功而你的客户端失败,问题出在客户端指纹,不是 IP。
- 把你的 TLS 指纹与 Chrome 的对比。 公开的 JA3/JA4 检测端点能让这一步在两分钟内完成。
- 把你的请求头与真实浏览器对同一 URL 的请求做 diff,包括顺序。
- 最后才考虑出口类型。 如果浏览器走同一个代理也失败,那 IP 类型才确实是问题所在。
第三步和第四步能解决大多数情况,剩下的通常归结为时序问题。
一致性检查清单
上面所有内容都可以归结为一条原则:请求的每一层都应该对「是谁在发起这个请求」给出一致的答案。
- TLS 指纹与当前的浏览器构建匹配
- HTTP/2 设置与请求头顺序与同一浏览器匹配
User-Agent、客户端提示与Sec-Fetch-*请求头彼此自洽Accept-Language与出口国家一致- 依赖时区的参数与出口国家一致
- Cookie 生命周期与出口 IP 生命周期一致
- 请求间隔服从真实的分布形状
- 导航顺着链接走,而不是枚举 ID
- 登录态任务使用静态出口,绝不使用轮换出口
一个满足全部九条的爬虫,用远小得多的 IP 池就能活下来,比只满足最后一条的爬虫强得多。而且通常,这也是更便宜的投入。
关于作者
UUIProxy 解决方案架构师
周美琳与亚太地区的增长团队和研究团队合作,负责账号基础设施与多市场数据采集方案。UUIProxy 的大部分入门资料出自她手,她花了很多时间解释一件事:被封禁时,答案很少是「买更贵的代理」。