你可以清除所有 Cookie,切换到全新的 IP 地址,并打开无痕窗口,但网站在下一次请求时仍然可以识别出你。这就是浏览器指纹识别的工作原理。网站不是在你的设备上存储 ID,而是通过对你的设备提出上百个小问题来推导出一个 ID:你运行的是什么浏览器,你的 GPU 如何绘制曲线,你的音频栈如何对数字取整,你的 TLS 握手是什么形态。将足够多的答案组合起来,你就得到了一个跨会话稳定、且几乎独一无二的值。
对于从事抓取工作的工程师而言,这是一种不关心你代理的防御手段。轮换 IP 解决了一个问题,却对指纹识别毫无影响,这正是为什么一个"干净"的住宅 IP 仍然可能给你返回一个封锁页面。本文解释了指纹实际上是什么,构成它的信号有哪些,为什么这种组合能够识别你身份,以及对抓取工作最重要的一点:如何保持指纹的一致性,让 IP 和浏览器彼此相互印证。
什么是浏览器指纹
浏览器指纹是一个派生标识符,由你的浏览器向页面暴露的配置和行为构建而成。脚本读取数十个属性,对其组合进行哈希处理,得到一个每次访问时都倾向于保持不变的值。没有任何内容被写入你的设备。网站不需要你的许可,也不需要在你这端存储任何东西,因为该 ID 在每次访问时都是从你的环境中重新计算出来的。
有一个重要的心智模型:指纹是无状态且派生的,而 Cookie 是被存储的。Cookie 是网站植入你设备上的一个令牌,因此删除它就切断了关联。指纹则是每次从你的硬件和软件中新鲜计算出来的,因此你这端没有任何东西可以删除。这一个区别解释了为何指纹识别能够绕过人们通常用于保持匿名的三种方式:
- 清除 Cookie。没有存储的令牌可以清除;ID 会被重新计算。
- 切换 IP。IP 只是众多信号中的一个,大多数信号来自浏览器,而非网络。
- 无痕或隐私模式。私密窗口会屏蔽历史记录和 Cookie,但它们仍然暴露相同的屏幕、字体、GPU 和 TLS 栈,因此指纹几乎不会改变。
Cookie 是网站给你的、你可以丢弃的东西。指纹是网站对你进行测量得到的、在每次访问时都会重新计算的值。你无法删除一次测量,只能改变被测量的对象,而这远比清空缓存要困难得多。
构成指纹的信号
指纹不是单一的值,而是在不同层次收集的一组信号的叠加。有些信号直接体现在请求本身中,有些在页面加载后由 JavaScript 读取,还有一个信号在你的任何代码运行之前就已经确定了。了解每个信号属于哪个层次,正是让你之后能够推理一致性的基础。
请求层信号
最廉价的信号直接来自 HTTP 请求本身,在任何一行 JavaScript 执行之前就已存在:
- User-Agent 与请求头。你声称使用的浏览器和版本,以及请求所携带的请求头的具体集合和顺序。真实浏览器发送的是一致且可预测的请求头集合;裸 HTTP 客户端通常发送的请求头更少,且顺序不同,这很容易被识别。
- Accept-Language 与时区。你的浏览器声明的语言,以及通过 JavaScript 报告的系统时区。一个美国数据中心 IP,配上莫斯科时区和越南语 Accept-Language 请求头,是明显的不匹配。
- 屏幕与颜色深度。分辨率、可用屏幕区域、设备像素比和颜色深度。常见值由数百万人共享;不寻常的值则能很快缩小范围。
JavaScript 渲染信号
这些信号需要一个真实的渲染引擎才能产生。普通的 HTTP 客户端根本无法生成它们,而其缺失本身就是一个信号。
- 字体。通过测量测试字符串的渲染方式来探测系统上安装的字体集合。这种组合具有出乎意料的辨识度。
- Canvas 指纹。页面在一个不可见的 HTML5 canvas 上绘制文本和形状,然后读回像素数据。你特定的 GPU、驱动程序、抗锯齿设置和字体光栅化组合会产生微小的每设备差异,对像素数据进行哈希处理会得到一个稳定的签名。
- 音频指纹。Web Audio API 通过振荡器和压缩器生成一个音调,然后读取产生的缓冲区。确切的浮点输出取决于你的音频栈和硬件,因此派生出的值在你的设备上是一致的,而在不同设备上是各不相同的。
- WebGL 与 GPU。WebGL API 报告你的渲染器和供应商字符串,以及它如何绘制 3D 场景,从而暴露浏览器背后的 GPU 和驱动程序。
canvas 读取的过程足够简单,值得展示。页面从不显示这个 canvas;它在屏幕外绘制,并将结果序列化:
const canvas = document.createElement("canvas") const ctx = canvas.getContext("2d") ctx.textBaseline = "top" ctx.font = "14px Arial" ctx.fillText("Crawlbase fingerprint \u{1F4A1}", 2, 2) // Same code, different pixels per GPU/driver/font stack. const signature = canvas.toDataURL() const fp = hash(signature)
大多数客户端搞错的信号:TLS / JA3
握手发生在 HTTP 之前,在 JavaScript 之前,在你代码中能控制的任何事情之前。当你的客户端建立 TLS 连接时,它会发送一个 Client Hello,其中以特定顺序列出了它支持的密码套件、扩展和椭圆曲线。这种形态对于给定的客户端库和版本是一致的,对其进行哈希处理会产生一个 JA3 指纹。Chrome 的握手看起来像 Chrome;Python 的 requests 看起来像 Python。
这一层是绊倒大多数爬虫的因素,也是任何 User-Agent 字符串都无法修复的。你可以设置每一个请求头来声称自己是 iPhone 上的 Safari,但如果你的 TLS 握手与 Linux 上的 Python HTTP 库相匹配,这两个层次就产生了矛盾,而比较它们的防御方会立即看穿这个谎言。握手是由你的网络栈设定的,而非你的请求头,因此欺骗它意味着要更改客户端本身。
为什么组合能够识别你
没有任何单一信号是独一无二的。数百万人在 1920x1080 的分辨率下运行相同的浏览器版本。力量在于组合:将你的浏览器、操作系统、字体、时区、canvas 哈希、音频哈希、WebGL 渲染器和 TLS 形态叠加在一起,联合值就会变得足够罕见,可以将你单独识别出来。对真实流量的研究将设备识别率置于 90-99% 的范围内,你应该将这些视为来自特定数据集的观测数字,而非每个浏览器都是唯一可识别的保证。
使指纹识别对欺诈预防和机器人检测有用的同一特性,也使其成为一个隐私问题:它无需同意、无需在你设备上存储任何东西就能跨会话追踪用户。对于爬虫工作者而言,结论更为直接而尖锐。你的工具发射的是一种信号组合,如果这种组合看起来像自动化行为,或者内部存在矛盾,那么无论你的 IP 有多干净,你都会被标记。
指纹识别如何封锁爬虫
反爬虫系统从你的爬虫处收集与从真实访客处收集的相同信号栈,然后提出两个问题。第一,这看起来像一个真实的浏览器吗?第二,这些层次彼此一致吗?大多数爬虫即使通过了第一个问题,也会在第二个问题上失败,而这正是改变你构建方式的洞察。
陷阱就在这里。你购买了轮换住宅 IP,设置了令人信服的 User-Agent,却仍然遭到封锁。IP 轮换完成了它的工作,但指纹识别从未孤立地看待 IP。它看的是整体图景,而整体图景前后矛盾:
- 轮换 IP 背后固定不变的指纹。如果每个请求在 IP 不断变化的同时共享同一个 canvas 哈希和 TLS 签名,你就像一台"设备"在全球各地传送。这种模式本身就是一个标记。
- 内部不一致的层次。Linux 数据中心的 TLS 签名,通过 User-Agent 声称是 iPhone 上的 Safari。iOS Safari 不会产生那种握手,也不会在那种硬件上运行。这种矛盾就是破绽所在。
- 真实浏览器始终具备、而爬虫缺失的信号。没有 canvas,没有 WebGL,没有音频上下文,请求头集合单薄。真实浏览器会产生所有这些;裸 HTTP 客户端一个都没有,这种差距显而易见。
因此规则不是"更努力地轮换"。规则是跨层一致性。IP 来源、请求头、TLS 握手和 JavaScript 渲染信号都必须描述同一个在同一台可信设备上的可信用户。这与绕过 Cloudflare 和避免机器人检测背后的一致性问题相同,也是爬虫在抓取时遭遇 CAPTCHA 的重要原因之一:挑战往往由不一致的指纹触发,而非单纯由 IP 引起。
实际上该怎么做
你有三条现实可行的路径,它们在投入精力与控制力之间有所权衡。
- 运行一个真实或无头浏览器进行渲染。真正的浏览器引擎会产生真实的 canvas、WebGL、音频和字体信号,其 TLS 握手与其 User-Agent 相匹配,因为两者是同一个软件。这一举措同时弥补了"缺失信号"和"TLS 与浏览器不匹配"这两个问题。代价是速度和资源:渲染比原始抓取重得多。
- 使用反检测浏览器。这类工具为每个会话提供刻意设计的、内部一致的配置文件,同步变化各类信号,使各层保持可信。适合规模较小、以会话为重的工作;在规模化时管理许多一致的配置文件本身就是另一项工程。
- 使用托管解决方案。由服务为你保持整个指纹的一致性,呈现一个可信的浏览器并将其与匹配的 IP 配对,这样你只需指向一个端点,而无需自己维护整个栈。
请对第二个最诱人的选项保持诚实:用原始 HTTP 请求手工构建一个一致的指纹。从原理上说是可行的,但这很难且很脆弱。你必须将 TLS 握手与声称的浏览器相匹配,发送真实浏览器所发送的确切请求头集合和顺序,并随着浏览器版本迭代和检测手段演进而保持所有内容同步更新。一个过时的值就会让各层再次产生矛盾。对于大多数团队而言,维护负担超过了节省的成本,这正是渲染或托管层通常胜出的原因。如果你想了解更广泛的应对方法,请参阅如何在不被封锁的情况下抓取网站。
无论选择哪条路径,IP 仍然必须与其他部分保持一致。住宅来源被视为真实用户,而数据中心地址范围则不然,这是数据中心代理与住宅代理权衡的核心,轮换必须分散负载,而不能让同一个指纹在大陆之间跳跃。轮换住宅代理处理了 IP 这一侧的问题,但只有在此之上保持一致的指纹,才能使整个请求变得可信。
难点在于在规模化时保持 IP 和浏览器指纹的协调一致。Smart AI Proxy 是单一的回连端点,它将真实用户的住宅 IP 与一致的浏览器指纹配对轮换,使握手、请求头和渲染信号描述同一个可信访客,而非相互矛盾。将你的客户端指向单一主机,并在免费层上试用。
核心要点
- 指纹是派生的,而非存储的。它在每次访问时从你的环境中重新计算,因此能够在清除 Cookie、切换 IP 和使用无痕模式之后依然有效。
- 它是一组信号的叠加。请求头、屏幕、字体、canvas、音频、WebGL 和 TLS/JA3 握手共同组成一个近乎唯一的值,在真实数据集中识别率达到 90-99%。
- TLS 是爬虫最容易搞错的信号。握手由你的网络栈设定,而非你的 User-Agent,因此声称自己是浏览器的 Python 客户端会在 TLS 层暴露。
- 单纯轮换 IP 毫无用处。固定或内部不一致的指纹,无论 IP 多么干净,都会被标记。
- 跨层一致性才是制胜之道。IP、请求头、TLS 和渲染信号必须描述同一台可信设备;渲染或托管层是保持它们一致的实际方法。
常见问题
什么是浏览器指纹?
浏览器指纹是一种技术,它从浏览器暴露的配置和行为(如 User-Agent、屏幕、已安装字体、GPU、音频栈和 TLS 握手)中为你的设备构建唯一标识符。这些信号的组合被哈希处理成一个在各次访问中倾向于保持不变的值。没有任何内容存储在你的设备上,因为 ID 每次都是从你的环境中重新计算的。
指纹与 Cookie 有什么区别?
Cookie 是网站存储在你设备上的令牌,因此删除它就切断了关联。指纹是派生的,在每次访问时从你的硬件和软件中新鲜测量,因此你这端没有任何东西可以删除。这就是为什么指纹识别能够在清除 Cookie、切换 IP 和使用私人浏览模式之后依然奏效。
我能通过清除 Cookie 或使用无痕模式来避免指纹识别吗?
不能。这些操作会删除存储的数据,但指纹不是存储的;它是从你的屏幕、字体、GPU 和 TLS 栈等信号中重新计算的,而无痕模式并不会改变这些。私密窗口会隐藏历史记录和 Cookie,但它们仍然暴露相同的设备特征,因此指纹几乎没有变化。
为什么即使使用了轮换代理,我的爬虫仍然被封锁?
因为指纹识别不会孤立地看待 IP。如果每个请求在 IP 轮换的同时携带相同的 canvas 哈希和 TLS 签名,你看起来就像一台设备在全球各地传送。如果你的 TLS 握手显示的是 Linux 上的 Python,而你的 User-Agent 声称是 iPhone 上的 Safari,这些层次就相互矛盾。无论 IP 多么干净,这两种模式都会被标记。
什么是 TLS 或 JA3 指纹?
当你的客户端建立 HTTPS 连接时,TLS Client Hello 会以特定顺序列出支持的密码套件、扩展和椭圆曲线。对这种形态进行哈希处理会产生一个 JA3 指纹,该指纹是客户端库和版本的特征。它由你的网络栈设定,而非你的请求头,因此往往会暴露 User-Agent 声称自己是某个浏览器、实则并不是的爬虫。
绕过指纹识别进行抓取的最佳方式是什么?
保持指纹在每一层的一致性。运行一个真实或无头浏览器,使渲染信号和 TLS 握手真正与你声称的浏览器相匹配;或者使用托管层,将可信的指纹与匹配的真实用户 IP 配对。用原始 HTTP 请求手工构建一致的指纹在理论上是可行的,但随着浏览器和检测手段的演进,维护起来非常困难。
大规模爬取任何站点,无需与基础设施对抗。
Crawlbase 负责处理代理、指纹和 CAPTCHA,让你的团队专注于交付数据流水线,而非维护爬取管道。1,000 次请求免费,无需信用卡。
