浅谈现代爬虫与反爬虫

写这篇文章,纯粹是无聊来的,这部分好像也有点用处,但是不深入,标题我里的“浅谈”也是我的小巧思,因为确实是“浅谈”。

这几年再聊爬虫,已经很难只停留在代理池、UA 伪造和登录态复用这些老话题上了。真正把一批中低质量自动化流量筛掉的,往往不是某一条规则,而是一整套”环境可信度”判断体系。浏览器是不是像真的浏览器,设备参数之间是否自洽,行为轨迹有没有人的味道,网络层指纹是否符合它自称的客户端,这些问题加在一起,才构成了今天主流的反爬能力。

很多人第一次接触这类思路,都会把注意力集中在 navigatorcanvasWebGL Fingerprint 这些关键词上。它们当然重要,但如果把它们理解成”一招制敌”的开关,往往就会高估单一特征的作用。现代反爬更像是在做风控,而不是在做一道是非题。

先从 navigator 说起

navigator 本质上是浏览器暴露给 JavaScript 的一个全局对象,用来告诉页面当前环境的一部分信息。前端在控制台里直接输入 navigator,通常就能看到浏览器语言、平台、核心数、插件、自动化状态等内容。对业务页面来说,这些信息原本只是兼容性和体验优化的辅助数据;但对风控系统来说,它们天然就是判断”你到底像不像一个真实用户”的第一层样本。

console.log({
  userAgent: navigator.userAgent,
  language: navigator.language,
  languages: navigator.languages,
  platform: navigator.platform,
  hardwareConcurrency: navigator.hardwareConcurrency,
  deviceMemory: navigator.deviceMemory,
  webdriver: navigator.webdriver,
  plugins: navigator.plugins.length
});

其中最有名的当然是 navigator.webdriver。在最朴素的检测模型里,它几乎就是自动化浏览器的代名词。Selenium、Puppeteer、Playwright 之类的工具如果没有额外处理,通常都会在这个点上露出痕迹。也正因为它太显眼,几乎所有稍微讲究一点的自动化框架都会优先把它改掉,于是问题也就随之出现了:如果一个特征可以被轻易伪造,那它就永远不可能单独承担最终判定。

这也是为什么今天的反爬不会停留在”看一眼 webdriver 就下结论”这种阶段。navigator 仍然重要,但它更像一份自述材料,而不是铁证。真正有价值的,是这份自述是否和其他证据彼此吻合。

为什么 WebGL 指纹这么受重视

如果说 navigator 更偏向”浏览器自己说了什么”,那么 WebGLCanvasAudio 这些特征,更接近”浏览器实际上做出了什么”。它们的价值在于,结果往往会受到硬件、驱动、操作系统、浏览器内核乃至字体环境的共同影响,所以更难靠简单的字符串替换蒙混过关。

WebGL 为例,网页可以主动创建一个画布上下文,然后读取底层渲染相关参数:

const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");

const vendor = gl && gl.getParameter(gl.VENDOR);
const renderer = gl && gl.getParameter(gl.RENDERER);

console.log({ vendor, renderer });

在真实设备上,这里返回的值通常带有明显的硬件和图形栈特征,比如 Apple GPU、NVIDIA、AMD,或者系统图形中间层 ANGLE 组合出来的一串描述信息。而在很多廉价自动化环境里,尤其是跑在无 GPU 的云主机、容器或虚拟化环境里的无头浏览器,渲染结果里经常会出现 SwiftShader 这类软件渲染痕迹。对反爬系统来说,这就像你嘴上说自己开着跑车来,转头却发现发动机声音像电瓶车。

Canvas 指纹的思路也类似。页面让浏览器画一段特定文字、渐变、阴影或路径,然后对输出结果做编码或哈希。由于不同平台在字体栈、抗锯齿、图形库实现上的细微差异,最终生成的图像结果并不会完全一样。这些差异不一定足以精确标识某一台设备,但足以帮助风控系统区分”正常用户群体”和”一批从相同模板环境里批量跑出来的自动化实例”。

所以 WebGL Fingerprint 的强,不在于它能直接读出”你是爬虫”,而在于它能比较稳定地暴露环境的真实性。它本质上是一种旁证。

但 WebGL 远远不够

刚接触这个方向时,很容易产生一个判断:既然 WebGL 这么难伪造,那是不是靠它就能解决绝大多数爬虫?直觉上好像没问题,但现实里这件事并没有这么简单。

首先,今天的高级自动化环境根本不一定运行在那种简陋的无头服务器上。攻击者完全可以使用带 GPU 的远程桌面环境、云真机、移动设备农场,甚至直接操作一批真实的桌面浏览器。这样一来,WebGL 返回的值就会非常像正常用户。其次,就算原始环境不够真实,也可以通过 Hook 接口、注入脚本或补丁框架来伪造部分输出。虽然这类伪造往往做不到天衣无缝,但至少足以骗过只看单一字段的检测逻辑。

真正拉开差距的,不是某个字段真假,而是整组信息是否一致。假设一个浏览器告诉你自己拥有高端显卡,却只暴露 1 核 CPU、1GB 内存、空插件列表和一个很不协调的屏幕分辨率,这种组合本身就已经说明问题了。现代反爬越来越强调的,正是这种浏览器环境一致性检测。

function simpleRiskScore(env) {
  let score = 0;

  if (env.webdriver) score += 40;
  if (String(env.renderer).includes("SwiftShader")) score += 20;
  if (env.plugins === 0) score += 10;
  if (!env.hasMouseMovement) score += 30;
  if (!env.isConsistent) score += 25;

  return score;
}

上面这种示意代码当然很粗糙,但足够说明问题:现实中的反爬通常不是”命中一条规则就封”,而是不断累积分数。webdriver 可能只是一个信号,SwiftShader 是另一个信号,鼠标轨迹、页面停留时间、时区与语言组合、TLS 指纹与浏览器声明是否一致,又是另外几组信号。只有把这些特征放在一起看,系统才会变得更稳。

一个更贴近真实世界的案例

如果拿 Cloudflare 这一类厂商的 Bot Protection 思路来举例,最值得关注的并不是某个单点检测,而是它为什么能在很短时间里完成判断。很多人把这件事理解成”它有一个特别厉害的 JavaScript 特征”,其实更准确的说法是,它把网络层、浏览器环境层和行为层串成了一条很紧凑的检测链。

当请求刚到边缘节点时,JavaScript 甚至还没执行,服务端已经先看到了一部分网络侧信息。TLS Client Hello 里的 cipher suites、extensions、ALPN 组合,HTTP/2 的某些协商参数,以及连接建立时的细节,本身就能形成一套客户端指纹。真正的 Chrome、Firefox、curlpython requests,乃至某些嵌入式 HTTP 客户端,在这一层的表现差异都很明显。如果流量在网络层就已经露出”脚本味”,后面的浏览器挑战只是进一步确认,而不是从零开始判断。

接下来才轮到浏览器侧的挑战脚本。这个阶段里,页面通常会采集一批基础环境数据,例如 navigator.webdrivernavigator.pluginsmimeTypeswindow.chrome 对象完整性、语言与时区组合、屏幕特征,以及 CanvasWebGL 输出结果。只看这些字段本身,其实都不算特别神秘,但难点在于它们会彼此交叉验证。一个经过临时修补的自动化环境,往往能骗过前两项,却会在别处暴露修补痕迹。

最典型的例子就是对象完整性检测。很多 Puppeteer 或 Playwright 的伪装方案,都会通过 Object.defineProperty 去覆盖 navigator.webdriverpermissions 或其他敏感属性。看上去页面拿到的值已经变正常了,但反爬脚本并不只看结果,它还会去看这个属性是不是”以原生浏览器的方式存在”。如果描述符、getter 的 toString() 输出、原型链挂载位置和真实浏览器不一致,那这份伪装本身就会变成证据。

同样的思路也可以延伸到异常栈、事件循环时序和行为采样。举个很简单的例子,真实用户在打开页面后的几秒里,往往会伴随滚动、鼠标移动、焦点切换、甚至短暂的停顿;而很多自动化脚本的动作则过于平滑、过于规则,点击间隔像是拿尺子量出来的一样。再加上无头环境的任务调度通常比真实桌面环境更稳定,某些时间侧信号也会显得不自然。单独拿出任何一个点,都不一定足以定罪,但它们叠在一起,判断就会越来越稳。

这也是为什么 Cloudflare 一类系统经常能在几秒内完成识别。它并不是在这几秒里做了某种玄学分析,而是因为能采集的信息本来就很多,而且大多数都不需要等待太久。页面加载完成、挑战脚本执行、几个事件被触发,再加上一开始就拿到的网络侧特征,往往已经足够构成一次风险评估。

从攻防两端看这件事

站在防守方视角,现代反爬的核心目标并不是”绝对阻止一切爬虫”,而是尽量提高自动化成本,让大规模滥用变得不划算。只要攻击者需要为更真实的浏览器环境、更高质量的代理、更复杂的行为模拟和更贵的设备资源付费,业务方就已经达到了相当一部分目的。

站在对抗方视角,难点也恰恰在这里。伪造一个字符串很容易,伪造一整个自洽的浏览器生态却极其困难。你不只要骗过 navigator,还得骗过图形栈、字体栈、插件、权限对象、异常栈、时序行为和网络指纹,而且这些层次跨越了 JavaScript、浏览器内核、操作系统和传输协议。任何一个环节补得不自然,都可能成为下一轮检测的突破口。

所以与其说今天的高水平反爬依赖某项”黑科技”,不如说它依赖的是多信号融合。WebGL 很重要,Canvas 很重要,navigator 仍然重要,但它们真正的价值不在于单独命中,而在于共同构成一幅环境画像。反爬做到最后,拼的不是某个 API 会不会调,而是谁能更准确地理解”一个真实用户环境到底应该长什么样”。

总结

navigator 只是浏览器向页面提供的一层可见信息,WebGL Fingerprint 也只是环境真实性检测中的一个侧面证据。它们都很有用,但都不足以单独解释整个反爬体系。真正有效的系统,往往把浏览器指纹、环境一致性、行为轨迹和网络层特征放在一起,最后得到的不是一个简单的真假判断,而是一套风险评分模型。

这也是我认为现代反爬最值得研究的地方:它早就不只是”识别脚本”这么简单,而是在逼近一种更完整的客户端可信度判断。