2026 年 10 月 3 日,我们做了一次小规模的隐身浏览器(stealth browser)基准测试:以原版 Playwright 为对照组,再测试 Patchright、nodriver、Camoufox 和 Zendriver,全部访问公开的机器人检测测试页,一次使用 Hetzner 数据中心 IP,一次通过美国住宅代理。所有自动化判定都由浏览器本身决定。原版 Playwright 在两种 IP 下的每一次运行中,都被 deviceandbrowserinfo、BrowserScan、Fingerprint 的 playground 和 Pixelscan 标记。以有头模式运行时,四款隐身工具在所有给出判定的运行中都通过了前三项检测,Pixelscan 在它们 39 次有头运行中标记了 2 次(另有 2 次为 n/a)。住宅 IP 只影响了那些检查网络层的项目,而且并不总是往好的方向变。
这是在一台主机、一个代理国家上记录的 54 次运行,针对的是专门用于测试的页面,并不是对任何生产环境反爬系统的排名。下文依次是:方法、原始判定结果、数据不能支持的结论,以及复现所需的代码。
隐身浏览器基准测试:简短结论
如果浏览器泄露了自动化特征,住宅 IP 也救不了它。如果浏览器本身干净,IP 主要影响网络层的检查:代理检测、时区一致性和 WebRTC。在这些页面上,影响有正有负:
- 自动化判定没有随 IP 改变。Playwright 在全部 10 次有头运行中、两种网络下,都在 deviceandbrowserinfo 上得到
isBot: true,在 BrowserScan 上得到 “WebDriver: Robot”,在 Fingerprint 的 playground 上得到 “bad bot (type: webdriver)”。Patchright、nodriver、Zendriver 和 Camoufox 在任何一次有头运行中都没有被打上这些标记。 - Pixelscan 在意的是数据中心 IP。来自 Hetzner IP 的 20 次运行中,它 20 次都显示 “Proxy detected”,尽管这些运行根本没用代理。通过住宅代理时,34 次运行中只有 3 次出现该提示。
- Fingerprint 的 playground 在意的是住宅 IP。它的代理信号在 34 次住宅运行中标记了 33 次,还经常点名代理网络(“residential proxy from SOAX”“from INFATICA”等)。对于基于 Chrome 的工具,它没有标记 Hetzner IP。三款基于 Chrome 的隐身工具,可疑分数在数据中心下为 0–6,通过粘性住宅代理时为 8–24。
- 五款工具中有三款在走代理时通过 WebRTC 暴露了服务器的真实 IP。Playwright、Patchright 和 nodriver 在每一次走代理的运行中都暴露了。Zendriver 默认屏蔽 WebRTC,Camoufox 则会伪装它。
- 无头模式比 IP 影响更大。在无头模式下,Patchright 和 nodriver 在有头时能通过的同一批页面上被标记了。
测试内容:版本、网络条件、目标页面
所有测试都在一个带 Xvfb 的 Debian 12 容器中进行,每次只运行一个浏览器,宿主机为 4 核 Linux。每款工具只使用其官方文档推荐的启动配置,别无其他:不自定义 User-Agent、不注入指纹、不模拟鼠标移动、不屏蔽资源。对 Patchright 来说,就是 README 中的“Best Practice”配置:持久化 context、真实 Chrome、有头模式、no_viewport=True。
| 工具 | 包 | 浏览器 | 启动方式 |
|---|---|---|---|
| Playwright(对照组) | playwright 1.62.0 | Playwright Chromium 151.0.7922.34 | chromium.launch(headless=False),默认设置 |
| Patchright | patchright 1.63.0 | Google Chrome 154.0.8037.97 | launch_persistent_context(channel="chrome", headless=False, no_viewport=True) |
| nodriver | nodriver 0.50.3 | Google Chrome 154.0.8037.97 | start(headless=False, sandbox=False) |
| Zendriver | zendriver 0.17.1 | Google Chrome 154.0.8037.97 | start(headless=False, sandbox=False) |
| Camoufox | camoufox 0.5.6 | Camoufox 152.0.4-beta.31 | AsyncCamoufox(headless=False, os="linux", geoip=True) |
先说明两处妥协。对照组用的是 Playwright 1.62.0 而不是当前的 1.63.0,因为 camoufox 0.5.6 要求 playwright<1.63,而所有工具共用一个环境。另外,Camoufox 以 os="linux" 运行且只带 Linux 字体,因为宿主机磁盘几乎已满,我们没有解压 macOS 和 Windows 字体包。Camoufox 的默认行为是随机选择操作系统。
网络条件:
- 数据中心:宿主机自身的 IP,AS24940 Hetzner Online GmbH,赫尔辛基(据 ipinfo.io)。
- 住宅、粘性:美国住宅出口,每次运行使用新的粘性会话。所有走代理的运行中,各工具都通过同一个小型本地中继连接。它负责添加代理认证并统计字节数,因为 nodriver 和 Zendriver 无法在 Chrome 启动参数里传递代理凭据。
- 三种附加条件:一轮轮换住宅测试(每个连接都换新出口 IP)、一轮预热测试(下文解释),以及每款工具在数据中心 IP 下的一次无头运行。
目标页面按以下顺序访问,每次运行都用全新浏览器,每个页面开一个新标签页:bot.sannysoft.com、BrowserScan 机器人检测、deviceandbrowserinfo “Are you a bot?”、CreepJS、Fingerprint 的 playground、nowsecure.nl(位于 Cloudflare 之后;直接用 curl 会得到 403 和 cf-mitigated: challenge),以及 Pixelscan 的机器人检测和指纹检测。Fingerprint 的机器人检测产品页被剔除了,因为它只展示一份静态 JSON 示例,而不是实时判定。
测量内容:页面是否加载、页面打印出的原样判定,以及判定出现所需的时间(每 0.5 秒轮询一次,超时 45 秒)。每次访问我们都保存了页面文本和整页截图。无法解析的判定记为“unparsed”,绝不猜测。有四次有头访问属于这种情况,其中三次页面超时,一次在页面更新中途被抓取。它们在表格中计为 n/a。
运行次数:数据中心、轮换住宅和粘性住宅条件下,每款工具各 3 次;无头和预热条件下每款工具各 1 次。共计 54 次运行,时间为 UTC 06:50 至 08:02。
结果:浏览器层面的自动化检测
表中统计的是运行次数:“3/3”表示页面在三次运行中都标记了该浏览器。每个单元格都来自解析后的结果文件。
| 工具 | 网络 | sannysoft 失败项 | BrowserScan 标记 | deviceandbrowserinfo isBot | CreepJS headless | Fingerprint “Bot” | Pixelscan 机器人横幅 |
|---|---|---|---|---|---|---|---|
| Playwright(对照组) | 数据中心(Hetzner) | WebDriver | 3/3 | 3/3 | 33% | 3/3:You are a bad bot (type: webdriver) | 3/3 |
| Playwright(对照组) | 美国住宅,粘性 | WebDriver | 3/3 | 3/3 | 33% | 3/3:You are a bad bot (type: webdriver) | 3/3 |
| Patchright | 数据中心(Hetzner) | 无 | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
| Patchright | 美国住宅,粘性 | 无 | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
| nodriver | 数据中心(Hetzner) | 无 | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
| nodriver | 美国住宅,粘性 | 无 | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/2(+1 n/a) |
| Zendriver | 数据中心(Hetzner) | 无 | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
| Zendriver | 美国住宅,粘性 | 无 | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
| Camoufox | 数据中心(Hetzner) | Chrome | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
| Camoufox | 美国住宅,粘性 | Chrome | 0/3 | 0/3 | 0% | Not detected (3/3) | 0/3 |
表格没体现的几个细节:
- Camoufox 在 sannysoft 上唯一的失败项是 “Chrome (New): missing”。对基于 Firefox 的浏览器来说这是意料之中的,并不是自动化泄露。
- CreepJS 还会给出一个 “% like headless” 分数。在每次有头运行中,Playwright 为 50%,三款基于 Chrome 的隐身工具为 44%,Camoufox 为 6%。它的 “% stealth” 分数对所有工具都是 0%。CreepJS 并不把这些数字标为通过或失败。
- Pixelscan 的机器人检测翻转过两次。Patchright 在唯一一次预热运行中得到 “Bot Behavior Detected”(CDP 一项),Zendriver 在轮换测试中出现过一次。两者在截图中都能看到。另有两次 Pixelscan 访问为 n/a:一次超时;另一次我们的文本抓取恰好赶上页面更新中途,片刻之后的截图显示 “You're Definitely a Human”。
- nowsecure.nl 在每次有头运行中都显示了页面内容,屏幕上没有残留挑战,包括从数据中心 IP 运行的原版 Playwright。在有头模式下,它完全区分不出这些工具。
这些检测为什么能抓到原版 Playwright(navigator.webdriver 标志、CDP Runtime.enable 的副作用、启动参数),请参阅我们的 Patchright 深度解析和浏览器指纹识别解析。
结果:与 IP 相关的信号,数据中心 vs 住宅
只有两个页面会对网络打分。Fingerprint 的 playground 展示 VPN、代理、IP 黑名单和可疑分数。Pixelscan 的指纹检测展示代理和位置一致性。deviceandbrowserinfo 明确表示其测试“不使用 IP 信誉或用户行为”,它的判定不随 IP 变化,与这一说法相符。
| 工具 | 网络 | Fingerprint 代理标记 | Fingerprint VPN 标记 | Fingerprint 可疑分数 | Pixelscan “Proxy detected” | Pixelscan 位置检查 | WebRTC 在代理后泄露服务器 IP |
|---|---|---|---|---|---|---|---|
| Playwright(对照组) | 数据中心(Hetzner) | 0/3 | 0/3 | 19 | 3/3 | Timezone spoofed | n/a(未用代理,显示真实 IP 属正常) |
| Playwright(对照组) | 美国住宅,粘性 | 2/3 | 0/3 | 25–27 | 0/3 | Timezone spoofed | 3/3 |
| Patchright | 数据中心(Hetzner) | 0/3 | 0/3 | 0–6 | 3/3 | Timezone spoofed | n/a(未用代理,显示真实 IP 属正常) |
| Patchright | 美国住宅,粘性 | 3/3 | 0/3 | 8–20 | 0/3 | Timezone spoofed | 3/3 |
| nodriver | 数据中心(Hetzner) | 0/3 | 0/3 | 0–6 | 3/3 | Timezone spoofed | n/a(未用代理,显示真实 IP 属正常) |
| nodriver | 美国住宅,粘性 | 3/3 | 2/3 | 10–23 | 0/3 | Timezone spoofed | 3/3 |
| Zendriver | 数据中心(Hetzner) | 0/3 | 0/3 | 0–6 | 3/3 | Timezone spoofed | 已屏蔽(3/3) |
| Zendriver | 美国住宅,粘性 | 3/3 | 0/3 | 8–24 | 0/3 | Timezone spoofed | 已屏蔽(3/3) |
| Camoufox | 数据中心(Hetzner) | 3/3 | 3/3 | 17 | 3/3 | Timezone spoofed | n/a(未用代理,显示真实 IP 属正常) |
| Camoufox | 美国住宅,粘性 | 3/3 | 0/3 | 4–14 | 1/3 | 匹配(显示美国城市) | 0/3 |
住宅 IP 起作用的地方
Pixelscan 不再显示 “Proxy detected”。在每次数据中心运行中它都显示了这条提示,尽管根本没有使用代理,这说明它把托管服务商的 ASN 当作代理。通过住宅代理池时,15 次粘性运行中只出现了 1 次。Camoufox 还得到了匹配的位置。它的 geoip=True 选项会根据出口 IP 设置时区、语言区域、地理位置和 WebRTC IP,因此 Pixelscan 显示的是一个美国城市,而不是 “Timezone spoofed”。
住宅 IP 帮倒忙或毫无作用的地方
Fingerprint 的代理信号在 34 次走代理的运行中,有 33 次把住宅出口识别为代理 IP,三种代理模式都是如此,而且很多还附上了服务商名称。我们的理解是 Fingerprint 掌握了住宅代理池的情报。这并不意味着每个 IP 都泄露了什么,也说明不了某个具体网站会如何使用这一信号。对于基于 Chrome 的隐身工具,可疑分数在走代理后不降反升。Playwright 从 19 升到 25–27,唯一一次预热运行得了 47 分。
其中一部分很可能是自己造成的。基于 Chrome 的工具保留了容器的 UTC 时区,所以美国 IP 配 UTC 时钟,在每次走代理的运行中都被 Pixelscan 判为 “Timezone spoofed”。Camoufox 从反方向展示了同样的机制。在赫尔辛基数据中心 IP 下,它的 GeoIP 查询把出口定位到 Europe/Berlin,而 ipinfo 显示的是赫尔辛基。于是 Fingerprint 在它的三次数据中心运行中都标记了 VPN(“timezone mismatch”)和一个低置信度的数据中心代理,而同一 IP 上的 Chrome 工具两项都没被标记。IP 与时区不一致,无论哪个方向都会被注意到。
WebRTC 泄露了代理背后的服务器
在 Playwright、Patchright 和 nodriver 每次走代理的运行中,CreepJS 和 Pixelscan 都把宿主机的 Hetzner 地址显示为 WebRTC IP。HTTP 流量走了住宅出口,但通过 UDP 发出的 STUN 请求没有。Zendriver 会传入 --webrtc-ip-handling-policy=disable_non_proxied_udp 和 --force-webrtc-ip-handling-policy,这是它的默认设置,CreepJS 在它的所有运行中都报告 WebRTC 已被屏蔽。Camoufox 则把地址替换成了代理出口。WebRTC 被屏蔽本身也是一些网站可能纳入评分的信号。我们没有测试这些页面是否会因此扣分,Pixelscan 没有扣分。反爬系统还会查找哪些其他不一致,请参阅反爬虫系统如何检测代理。
无头模式比 IP 暴露得更多
我们在数据中心 IP 下为每款工具做了一次无头运行。样本比其他部分小,但趋势很明确:
| 工具(无头,数据中心 IP,1 次运行) | sannysoft 失败项 | deviceandbrowserinfo isBot | CreepJS headless | Fingerprint “Bot” | nowsecure.nl |
|---|---|---|---|---|---|
| Playwright(对照组) | CHR_MEMORY:FAIL, HEADCHR_UA:FAIL, WebDriver | 1/1 | 100% | 1/1:You are a bad bot (type: headless_chrome) | 45 秒后仍卡在 “Just a moment...” |
| Patchright | CHR_MEMORY:FAIL, HEADCHR_UA:FAIL | 1/1 | 67% | 1/1:You are a bad bot (type: headless_chrome) | 45 秒后仍卡在 “Just a moment...” |
| nodriver | CHR_MEMORY:FAIL, HEADCHR_UA:FAIL | 1/1 | 67% | 1/1:You are a bad bot (type: headless_chrome) | 45 秒后仍卡在 “Just a moment...” |
| Zendriver | 无 | 1/1 | 33% | Not detected (1/1) | 45 秒后仍卡在 “Just a moment...” |
| Camoufox | Chrome | 0/1 | 0% | Not detected (1/1) | 页面已加载 |
仅仅切换到无头模式,Patchright 和 nodriver 在 deviceandbrowserinfo 和 Fingerprint 上就从干净变成被标记(“bad bot, type: headless_chrome”)。sannysoft 的 HEADCHR_UA 和 CHR_MEMORY 两项失败。所有基于 Chrome 的工具,包括 Zendriver,都卡在了 nowsecure.nl 的 Cloudflare “Just a moment...” 页面上。而在有头模式下,同样的工具,包括原版 Playwright,都直接通过了。Zendriver 躲过了 Fingerprint 的机器人标记,但仍被 deviceandbrowserinfo 标记。Camoufox 在所有这些页面上,无头与有头的表现完全相同。
两项运维发现:粘性会话与后台流量
并行的首批连接破坏了粘性会话
使用粘性会话 ID 时,基于 Chrome 的运行经常在第一个页面显示一个 IP,几个页面之后又变成另一个。12 次粘性运行中有 11 次出现这种情况。Camoufox 在 3 次运行中全部保持同一个 IP,可能是因为它的 GeoIP 查询在浏览器启动前只发一个请求。我们用 curl 复现了这一点:在全新的会话 ID 上并行发 6 个请求,两次试验分别得到 3 个和 5 个不同的 IP。之后的顺序请求则保持在同一个 IP 上。在启动浏览器之前先串行发一个预热请求,4 次运行中 4 次都解决了问题。我们第一轮代理测试还用了网关不认识的会话参数,导致每个连接都悄无声息地换了新 IP,这一轮在上文作为轮换条件报告。两件事给出的教训相同:信任粘性会话之前,先用两个顺序请求验证它。
部分真实 Chrome 方案会通过你的代理在后台下载
在按流量计费的住宅流量上,访问这 9 个页面,Playwright 每次运行约用 7 MB,Patchright 约 8 MB(有一次 12 MB),Camoufox 9–11 MB。nodriver 和 Zendriver 的大多数运行用了 40–61 MB。在我们按主机记录字节数的运行中,大部分流量来自 optimizationguide-pa.googleapis.com,这是 Chrome 的一项后台下载。有两次运行只用了约 9 MB 和 15 MB,所以并非每次都会发生。Patchright 同样在全新配置文件上运行真实 Chrome,却没有出现这种情况,可见这取决于工具如何启动 Chrome。如果你按 GB 付费,扩大规模前请按主机统计字节数。
这次基准测试不能说明什么
- 它不是生产级的反爬测试。这些页面主要检查浏览器端的痕迹。商业系统还会结合 TLS 和 HTTP/2 指纹、行为、IP 历史以及针对单个站点的模型。在这里拿到干净结果只是必要条件,不是充分条件。这些页面几乎没有触及的一层,可参阅我们的 TLS 指纹识别指南。
- 样本很小:每个单元格 3 次运行,一天之内,一台位于芬兰的数据中心主机,一个代理国家。像 Pixelscan 那样的单次翻转,无法与噪声区分开。
- 没有任何交互。没有点击、滚动、表单或鼠标轨迹,因此行为检测没有被触发。
- 部分检测会保留历史。Fingerprint 的 playground 会显示每台设备的历史(“1 IP, 2 Linked IDs in the past 24 hours”),因此重复访问可能影响了后续分数。
- 默认设置未经调优。基于 Chrome 的工具保留了 UTC 和 en-US,Camoufox 被限定为 Linux,除了各自 README 的默认配置外没有做任何加固。调优后的方案得分可能不同。那是我们下一步要测的内容,而不是已经测过的结论。
- 你的目标站点会不一样。用数据中心 IP 还是住宅 IP,取决于你采集什么、在哪里采集。成本方面请参阅住宅代理与数据中心代理的采集对比。对真实网站运行类似测试之前,请先查看 robots.txt、站点条款和个人数据相关规定。
复现方法
测试框架约 260 行 Python,外加一个 80 行的中继和一个只读取已保存页面文本的解析器。下面是在我们数据中表现干净的 Patchright 配置,并内置了上文的两项修复:在 Chrome 打开并行连接之前用预热请求固定粘性会话,以及 Zendriver 的 WebRTC 参数。我们通过美国住宅出口用它访问过一次 deviceandbrowserinfo("isBot": false),这次检查不计入 54 次运行。
import asyncio, os, tempfile, urllib.parse, urllib.request, uuid
from patchright.async_api import async_playwright
USER = os.environ["PROXY_USER"] # your proxy username
PASS = os.environ["PROXY_PASS"] # your proxy password, never hard-code it
GATE = "gate.proxyhat.com:8080"
def sticky_username() -> str:
# one fresh session per browser launch; -sid- keeps the exit IP between connections
return f"{USER}-country-US-sid-{uuid.uuid4().hex[:10]}-ttl-30m"
def warm_up(username: str) -> str:
# one serial request so the session is pinned before Chrome opens parallel connections
url = f"http://{urllib.parse.quote(username)}:{urllib.parse.quote(PASS)}@{GATE}"
opener = urllib.request.build_opener(urllib.request.ProxyHandler({"http": url, "https": url}))
return opener.open("https://api.ipify.org", timeout=30).read().decode()
async def main():
username = sticky_username()
print("session pinned to", warm_up(username))
async with async_playwright() as p:
ctx = await p.chromium.launch_persistent_context(
user_data_dir=tempfile.mkdtemp(),
channel="chrome", # real Google Chrome, as the Patchright README recommends
headless=False, # run under Xvfb on a server
no_viewport=True,
proxy={"server": f"http://{GATE}", "username": username, "password": PASS},
args=["--webrtc-ip-handling-policy=disable_non_proxied_udp",
"--force-webrtc-ip-handling-policy"],
)
page = await ctx.new_page()
await page.goto("https://deviceandbrowserinfo.com/are_you_a_bot")
await page.wait_for_function("document.body.innerText.includes('\"isBot\"')", timeout=45000)
print(await page.evaluate("document.body.innerText.match(/\"isBot\": (true|false)/)[0]"))
await ctx.close()
asyncio.run(main())
用户名后缀是 ProxyHat 的语法(-country-US 用于定位,-sid-…-ttl-30m 用于粘性会话),请换成你的服务商的格式。长时间运行之前若想先检查出口,免费的代理检测工具能显示网站看到的信息。按我们的方式给单元格打分:
- 每次运行启动全新浏览器,每个页面在新标签页中访问,等页面自己的判定文字出现在屏幕上再继续。不要用固定时长的 sleep。CreepJS 的分数出现后我们多等了 6 秒,因为它还会继续计算。
- 关闭标签页前保存
document.body.innerText和整页截图。 - 只解析页面打印出来的内容,例如 deviceandbrowserinfo 上的
"isBot"JSON、BrowserScan 上的Normal/Robot配对,以及 CreepJS 上的 “N% headless”。其他一律记为 “unparsed”。 - 在开始时记录出口 IP,并与最后一个页面显示的 IP 比较。如果不一致,说明你的会话并不粘性。
- 统计每次运行的代理字节数,让后台流量在账单到来之前就暴露出来。
该怎么用这些结论
先修好浏览器:原版 Playwright 在两种 IP 下都被抓到了,四款隐身工具在有头模式下两种 IP 都没被抓到。除非你实测过无头模式能通过目标站点,否则请使用有头模式(服务器上用 Xvfb)。然后让网络层的说法自洽:时区与出口 IP 匹配、屏蔽 WebRTC 或让它走代理、确认粘性会话真的粘性。做完这些再加住宅代理,并且要预期那些关注代理的厂商仍会识别出很多住宅 IP。IP 决定你一开始是否被归为数据中心流量,浏览器决定你是否被标记为机器人。






