跳转到主要内容
UUIProxy

爬虫被封 IP 怎么办:请求指纹实战排查指南

为什么换 IP 会失效,以及你的爬虫还在广播什么:TLS 指纹、请求头顺序、HTTP/2 设置、时序模式,以及能让会话活下去的一致性规则。

周美琳阅读约 6 分钟
请求指纹信号叠加在 IP 地址之上的抽象插画

我们收到最多的工单大意都是这样:「我们换到更大的 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-AgentSec-CH-UA 不符。 Chrome 会在 User-Agent 之外同时发送客户端提示。如果你伪造了其中一个却漏掉另一个,或者版本号对不上,那就是直接的自相矛盾。

缺少 Sec-Fetch-* 请求头。 现代浏览器会发送 Sec-Fetch-SiteSec-Fetch-ModeSec-Fetch-DestSec-Fetch-User。一个自称来自 Chrome 的请求缺了它们,非常显眼。

深层页面没有 Referer 真实用户是从列表页进入商品页的。一个反复直接落在深层 URL、且不带来源的请求,就是在描述一种抓取模式。

声明了自己处理不了的 Accept-Encoding 声明支持 br 却无法解码 Brotli,是很容易被抓到的破绽。

时序与行为模式

单请求层面的信号都过关之后,跨请求的模式就接手了。

人类浏览是突发且不规则的:加载一个页面、读十一秒、点击、读四秒、开一个新标签页、九十秒后回来。一个每 500 毫秒抓一次、永不停歇、标准差接近零的爬虫,在任何 IP 池规模下都不像人。

加抖动有帮助,但前提是抖动要真实。在 400 到 600 毫秒之间均匀随机,依然明显是机器产生的——分布形状不对。人类的交互间隔大致服从对数正态分布:大部分很短,带一条长尾。按这个形状采样成本很低,说服力却强得多。

请求顺序也很重要。真实会话是顺着链接走的。一个按数字升序、以恒定速率抓取 /product/1/product/2/product/3 的爬虫,是在准确地自我描述。

最后,真实浏览器会加载子资源。一个只请求 HTML 文档、从不请求其中引用的 CSS、字体和图片的客户端,产生的是任何浏览器都不会产生的流量形状。

一个在中途更换出口 IP、却沿用同一份 Cookie 的会话,等于告诉目标站:有一个身份瞬移了。这是个很强的信号,而且完全是自己造成的。

规则很简单:Cookie 罐和出口 IP 具有相同的生命周期。要么一起轮换,要么都不换。我们的 Python 轮换代理完整设置指南里有一套能保持两者对齐的粘性会话写法。

凡是涉及登录的场景,干脆不要轮换。使用静态 ISP 代理或独占的静态移动代理,让账号长期保持同一个地址。一个每天从不同城市登录的账号,无论每个 IP 本身多干净,都会不断累积验证挑战。

IP 依然重要的地方

以上这些并不意味着出口地址无关紧要。它在三个具体方面仍然关键。

类型。 被归类为托管的地址会被做信誉检查的目标直接拒绝。这就是住宅代理变成必需品的分界线,详见住宅代理与数据中心代理该怎么选

地理位置。 本地化定价、库存和搜索结果都取决于请求看起来来自哪里。这一点搞错,产出的就是「自信的错误数据」。

历史。 一个已经连续冲击目标站六小时的地址会带着这段历史。轮换会重置它——这才是轮换真正提供的价值。

而 IP 无法做到的,是弥补一个暴露了你所用客户端库的指纹。

排查的先后顺序

当请求开始失败时,按这个顺序排查,而不是先去换更大的 IP 池:

  1. 完整抓取一个失败响应。 请求头、响应体、状态码,都读一遍。页面上经常直接写着原因。
  2. 在自己电脑的浏览器里、不走代理访问同一个 URL。 如果同样失败,那是目标站变了,不是你的配置。
  3. 通过代理、用真实浏览器再访问一次。 如果浏览器成功而你的客户端失败,问题出在客户端指纹,不是 IP。
  4. 把你的 TLS 指纹与 Chrome 的对比。 公开的 JA3/JA4 检测端点能让这一步在两分钟内完成。
  5. 把你的请求头与真实浏览器对同一 URL 的请求做 diff,包括顺序。
  6. 最后才考虑出口类型。 如果浏览器走同一个代理也失败,那 IP 类型才确实是问题所在。

第三步和第四步能解决大多数情况,剩下的通常归结为时序问题。

一致性检查清单

上面所有内容都可以归结为一条原则:请求的每一层都应该对「是谁在发起这个请求」给出一致的答案。

  • TLS 指纹与当前的浏览器构建匹配
  • HTTP/2 设置与请求头顺序与同一浏览器匹配
  • User-Agent、客户端提示与 Sec-Fetch-* 请求头彼此自洽
  • Accept-Language 与出口国家一致
  • 依赖时区的参数与出口国家一致
  • Cookie 生命周期与出口 IP 生命周期一致
  • 请求间隔服从真实的分布形状
  • 导航顺着链接走,而不是枚举 ID
  • 登录态任务使用静态出口,绝不使用轮换出口

一个满足全部九条的爬虫,用远小得多的 IP 池就能活下来,比只满足最后一条的爬虫强得多。而且通常,这也是更便宜的投入。

分享这篇文章

周美琳 的头像

关于作者

周美琳

UUIProxy 解决方案架构师

周美琳与亚太地区的增长团队和研究团队合作,负责账号基础设施与多市场数据采集方案。UUIProxy 的大部分入门资料出自她手,她花了很多时间解释一件事:被封禁时,答案很少是「买更贵的代理」。

高性价比、长期稳定的全球代理 IP

为你的数据业务打好基础设施底座,随业务一起扩张。

  • 多协议支持
  • 自动 IP 轮换
  • 无带宽限制

无需信用卡,即时开通。