反爬虫到底在检测什么?我用Xvfb绕过了所有Headless标记
---title: 反爬虫到底在检测什么?我用Xvfb绕过了所有Headless标记---我说的服务器,不是那种机房里的物理机,是我租的一台 2C4G 的云服务器,装了个 Ubuntu,没装桌面环境。但我想在上面跑浏览器自动化——发文章、登录、截图那种。第一次尝试很简单:`google-chrome-stable --headless`,搞定。然后发现一个残酷的事实——网站根本不认
title: 反爬虫到底在检测什么?我用Xvfb绕过了所有Headless标记
我说的服务器,不是那种机房里的物理机,是我租的一台 2C4G 的云服务器,装了个 Ubuntu,没装桌面环境。但我想在上面跑浏览器自动化——发文章、登录、截图那种。
第一次尝试很简单:google-chrome-stable --headless,搞定。
然后发现一个残酷的事实——网站根本不认。
有返回人机验证的,有直接返回 403 的,有页面能打开但关键操作"带不动"的。最离谱的是某个后台管理页面,无头模式下表单提交永远报 token 过期,换有头模式就一切正常。
这就引出一个问题:反爬虫到底在检测什么?
现代反爬已经不是简单地查 User Agent 带没带 Headless 了。我用 Chrome DevTools Protocol(CDP)逐步提取了无头模式和有头模式下的浏览器状态,整理出一份比较完整的对比:
| 检测维度 | 无头模式 | 有头模式(正常桌面) |
|---|---|---|
| User Agent | HeadlessChrome/148.0.7778.167 |
Chrome/148.0.7778.167 |
navigator.webdriver |
true |
undefined |
window.chrome 对象 |
部分属性缺失 | 完整存在 |
chrome.runtime |
不存在 | 存在 |
| WebGL 渲染器 | Google SwiftShader(CPU) |
真实 GPU 型号 |
navigator.plugins |
长度为 0 | 通常 5 个以上 |
| 系统字体数量 | ~5 个(只读 minimal) | ~30-60 个 |
navigator.languages |
可能为 ["en-US"] |
按系统配置 |
| 屏幕分辨率 | 800×600(默认) | 正常 1920×1080 |
navigator.hardwareConcurrency |
可能异常 | 正常逻辑核数 |
其中 navigator.webdriver 是最致命的——W3C WebDriver 规范要求无头浏览器必须把这个属性设为 true,而这恰恰是反爬 SDK(如 Akamai、PerimeterX、易盾)最直接的检测点。
Chrome 官方还提供了一个参数 --disable-blink-features=AutomationControlled 来隐藏 navigator.webdriver,但我们实测发现它只能骗过浅层检测——深层的反爬引擎会同时交叉验证 WebGL、字体列表、插件数量等多个维度,单一参数的伪装很容易被识破。
二、服务器没有显示器,但有 Xvfb
云服务器没有物理显示器,$DISPLAY 环境变量为空,有头模式的 Chrome 启动就会报:
[ERROR:env.cc(255)] The platform failed to initialize.
Missing X server or $DISPLAY
这是一个经典的工程问题:没有真实显示设备,怎么让 Chrome 以为自己有显示环境?
答案是 Xvfb(X Virtual Framebuffer)——一个在内存中运行的虚拟显示服务器。
安装只需要一行:
sudo apt-get install xvfb
启动虚拟屏幕:
Xvfb :99 -screen 0 1920x1080x24 &
export DISPLAY=:99
然后,不加 --headless 启动 Chrome:
google-chrome-stable --no-sandbox --disable-gpu \
--dump-dom https://httpbin.org/headers
Chrome 会认为自己运行在一个有 1920×1080×24bit 显示屏的桌面环境中。它加载了完整的渲染管线、完整的 JavaScript 引擎状态、完整的插件列表。从浏览器的角度看——这就是一台正常的电脑。
三、实测对比:三种模式的实验结果
我写了一个对照脚本,分别测试三种模式:
#!/bin/bash
echo "=== 模式 A: 纯无头 ==="
google-chrome-stable --headless --no-sandbox --disable-gpu \
--dump-dom https://httpbin.org/headers 2>/dev/null
echo "=== 模式 B: 无头 + 伪装参数 ==="
google-chrome-stable --headless --no-sandbox --disable-gpu \
--disable-blink-features=AutomationControlled \
--user-agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36" \
--dump-dom https://httpbin.org/headers 2>/dev/null
echo "=== 模式 C: Xvfb + 有头 ==="
xvfb-run --server-args="-screen 0 1920x1080x24" \
google-chrome-stable --no-sandbox --disable-gpu \
--dump-dom https://httpbin.org/headers 2>/dev/null
结果:
| 模式 | UA 标记 | navigator.webdriver |
反爬友好度 |
|---|---|---|---|
| A 纯无头 | ❌ HeadlessChrome |
true |
⭐ |
| B 伪装无头 | ✅ 正常 | true |
⭐⭐ |
| C Xvfb 有头 | ✅ 正常 | undefined |
⭐⭐⭐⭐⭐ |
模式 B 虽然 UA 看着正常了,但 navigator.webdriver 依然亮着红灯。Chrome 在无头模式下即使改了 UA,内部状态标记也不会消失。
模式 C 就完全不同了——浏览器认为自己就是一台正常的、带显示器的桌面 Chrome,没有任何自动化痕迹。
我专门用一个使用了易盾验证码的服务做测试。模式 A 和 B 在 3 步操作内必然触发验证码弹窗,模式 C 连续运行 20 个会话均未触发。
四、Xvfb + CDP:让 AI 智能体接管浏览器
深入一点。在实项目里(比如我这边的自动化发文系统),不只是 dump DOM,还需要完整的浏览器控制——点击元素、填表单、拦截请求、注入脚本。
这就要用到 Chrome DevTools Protocol(CDP)。
CDP 是一个 WebSocket 接口,可以对 Chrome 进行"内窥镜级"的控制。加上 Xvfb 虚拟显示器,就得到了一个没有任何自动化标记、可被程序完全操控的浏览器:
# 第一步:启动 Xvfb 虚拟显示器
Xvfb :99 -screen 0 1920x1080x24 &
# 第二步:在有头模式下启动 Chrome,开启调试端口
export DISPLAY=:99
google-chrome-stable --no-sandbox --disable-gpu \
--remote-debugging-port=9222 \
--no-first-run about:blank &
# 第三步:验证 CDP 连接
curl -s http://localhost:9222/json/version
返回:
{
"Browser": "Chrome/148.0.7778.167",
"webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/..."
}
有了这个 WebSocket 通道,我们可以直接用 Python 发送 CDP 命令,绕过了 Playwright、Puppeteer 等中间层:
import json, websocket
# 获取页面目标
pages = json.loads(urllib.request.urlopen("http://localhost:9222/json").read())
ws_url = pages[0]["webSocketDebuggerUrl"]
# 连接 WebSocket
ws = websocket.create_connection(ws_url)
# 发送 CDP 命令:获取 DOM
ws.send(json.dumps({
"id": 1,
"method": "DOM.getDocument"
}))
result = json.loads(ws.recv())
更强大的功能是请求拦截——在 CDP 层面改写请求头、注入 cookie、mock 响应。比如有些 CSDN 文章发布接口要求 quality-score 达到一定阈值才不报错,我们就可以在 CDP 层拦截校验响应,直接返回成功:
# 拦截 Network.responseReceived
ws.send(json.dumps({
"id": 2,
"method": "Network.enable"
}))
# 对特定 URL 模式进行响应改写
ws.send(json.dumps({
"id": 3,
"method": "Network.setRequestInterception",
"params": {
"patterns": [{"urlPattern": "*quality*", "interceptionStage": "HeadersReceived"}]
}
}))
这种操控能力,让 AI 智能体可以直接接管浏览器的完整行为——登录、发帖、翻页、截图,而不需要在 DOM 层面跟各种反爬框架打架。
五、Xvfb 的资源和稳定性
实测两张 2C4G 云服务器的表现:
| 指标 | 无头模式 | Xvfb + 有头模式 |
|---|---|---|
| 额外内存占用 | 0(基准) | ~60-80MB |
| CPU 开销 | 基准 | 略高(渲染管线激活) |
| 启动时间 | 0.8s | 1.2s |
| 稳定性(24h 连续) | 偶有崩溃 | 稳定 |
| 反爬触发率 | ~35% | <1% |
可以忽略不计的开销,换来了数量级的反爬通过率提升。
注意: 如果进程退出后没有清理 Xvfb 进程,下次启动会报 Fatal server error: Server is already active for display :99。建议用 xvfb-run 替代手动启动,或者加 killall Xvfb 做清理。
六、结论与选型建议
讲到底,无头和有头的差距不是一个参数,而是一个完整渲染链路的差距。反爬引擎透过 UA、JavaScript API、WebGL、字体列表等多维度交叉验证,断定你是不是自动化工具。
而 Xvfb 的思路不是"伪装"——是"替身"。它给了 Chrome 一个它相信的真实环境。
根据实际场景选型:
| 你的场景 | 推荐方案 | 理由 |
|---|---|---|
| 临时跑个脚本 | 无头 + 伪装参数 | 够用、省资源 |
| 数据采集 | 无头 + 代理 + --disable-blink-features |
叠加多个绕过手段 |
| 登录 / 发文章 / 表单提交 | Xvfb + 有头模式 | 反爬最少、行为最自然 |
| AI 智能体操控浏览器 | Xvfb + CDP WebSocket | 完整的协议级控制 |
| 需要截图或 PDF 渲染 | Xvfb + 有头模式 | 渲染效果和真机一致 |
最后说一句:反爬对抗不是"找到一个参数就能一劳永逸"的事。网站的反爬 SDK 在不断升级,检测维度在不断增加。但让浏览器像浏览器——这可能是最基础的,也是最重要的防线。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)