一、先给结论:CloakBrowser 到底值不值得研究?
项目地址:https://github.com/CloakHQ/CloakBrowser
本文基于 2026 年 8 月初的
main分支与 v0.5.3 代码结构整理。CloakBrowser 更新频繁,具体二进制版本、授权方案和平台能力应以项目最新说明与cloakbrowser info输出为准。
值得,而且它最值得研究的部分,不是"换一行 import 就能绕过风控"这种过度简化的宣传,而是它把现代浏览器自动化中的四个难题放在了一个工程体系里:
- 浏览器指纹的底层一致性 :不是只改
navigator.webdriver,而是试图在 Chromium 源码与编译产物层面处理 Canvas、WebGL、WebGPU、Audio、字体、屏幕、硬件、WebRTC、网络时序、CDP 行为等信号。 - 网络身份的一致性:代理出口 IP、时区、语言、WebRTC 暴露地址必须匹配,不能只换 IP,却保留 UTC、错误语言和本机 WebRTC 地址。
- 行为轨迹的一致性:鼠标不能瞬移,键盘不能零延迟灌值,滚动不能一步跳到底,元素操作前还要处理可见、稳定、可点击等状态。
- 生产工程能力:二进制自动下载、缓存、版本固定、回滚、签名验证、跨平台、持久化 Profile、同步/异步 API、诊断 CLI、Playwright/Puppeteer/.NET 兼容。
但同样必须把边界说清楚:
- CloakBrowser 不是验证码识别器,它不能保证所有挑战都不出现。
- 它不是代理池,不会替你管理 IP 质量、轮换策略和出口信誉。
- 它不是"永远无法检测"的浏览器。反自动化是持续对抗,今天有效的组合未来可能失效。
- 仓库包装层开源,不等于定制 Chromium 的全部补丁开源。Python、TypeScript、.NET 包装代码采用 MIT;CloakHQ 的构建配置、补丁和已编译二进制采用独立许可。
- 它不应被用于未授权登录、撞库、批量注册、欺诈或绕过访问控制。本文代码以自有系统测试、授权 QA、监控、研究和合规自动化为场景。
如果你把它当成一个"浏览器身份一致性平台",它非常有研究价值;如果把它当成"一键通杀所有反爬"的魔法包,迟早会踩坑。
二、CloakBrowser 是什么:三层结构,而不是一个 Python 库
很多人安装后看到:
bash
pip install cloakbrowser
便以为它只是另一个 Playwright 插件。实际上,它至少包含三层。
2.1 API 包装层
包装层提供了三套主要入口:
- Python:同步 Playwright、异步 Playwright;
- Node.js:Playwright 与 Puppeteer;
- .NET 8:基于 Microsoft.Playwright 的客户端。
包装层负责:
- 解析
headless、proxy、geoip、humanize等参数; - 找到或下载 CloakBrowser Chromium;
- 组装启动参数;
- 修正 Playwright/Puppeteer 的默认行为;
- 注入 Humanize 行为层;
- 管理浏览器、上下文和 Playwright Driver 生命周期;
- 把许可证失败映射为清晰异常;
- 提供 CLI、缓存、诊断与更新能力。
2.2 定制 Chromium 二进制
真正影响底层指纹的是定制 Chromium。项目说明将其描述为"源码级补丁",覆盖包括:
- Canvas、ClientRects;
- WebGL、WebGPU、GPU 参数;
- AudioContext;
- 字体与字体测量;
- 屏幕、窗口、任务栏与视口几何;
- User-Agent、Client Hints、平台信息;
- CPU 核数、内存等硬件信号;
- WebRTC ICE 地址;
- 网络、代理与部分时序信号;
- 自动化特征与 CDP 输入行为。
这与传统 playwright-stealth 的关键差异在于:后者通常是在页面创建后执行 JavaScript,重写可见属性;CloakBrowser 的目标是让值从浏览器内部产生,而不是在页面世界里临时伪造。
2.3 行为模拟层 Humanize
humanize=True 不是二进制补丁,而是包装层对 Playwright/Puppeteer 交互接口的改写:
- 鼠标沿 Bézier 曲线移动;
- 引入加减速、微抖动、短暂停顿和小概率越过目标;
- 点击具有随机落点与按下持续时间;
- 输入按字符产生,带随机键程、思考停顿和可选误触纠正;
- 滚动模拟加速、巡航、减速与回调修正;
- 操作前检查元素是否可见、稳定、启用、可编辑、能接收事件。

三、现代反自动化到底在检测什么?
理解 CloakBrowser,先要理解一个事实:现代反自动化系统不是只检查一个布尔值,而是在做多层信号融合。
3.1 网络与信誉层
常见信号包括:
- 出口 IP、ASN、IDC/住宅/移动网络类型;
- IP 历史信誉、并发量、请求速率;
- TLS ClientHello、JA3/JA4 类特征;
- HTTP/2 参数、Header 顺序与优先级;
- DNS、连接、代理、缓存相关特征;
- IP 所在国家与浏览器时区、语言是否匹配。
即使浏览器指纹完全正常,低质量数据中心 IP 也可能直接被拒绝。反过来,住宅 IP 也不能修复明显的自动化行为。
3.2 浏览器静态指纹层
包括但不限于:
navigator.webdriver;navigator.plugins、window.chrome;- UA 与 Client Hints;
navigator.platform;hardwareConcurrency、deviceMemory;- 屏幕尺寸、可用区域、颜色深度、DPR;
- Canvas、WebGL、WebGPU、Audio;
- 字体集合与文本宽高;
- Codec、Speech、Media、Storage Quota;
- WebRTC 暴露的网络地址。
难点不在"每个值都像真的",而在"这些值组成了一台可能真实存在的设备"。例如:
- UA 说自己是 Windows 11;
- 字体却只有 Alpine Linux 默认字体;
- GPU 是 Apple Metal;
- 屏幕是手机尺寸;
- CPU 核数与内存组合极少见;
- 外层窗口比内层页面还小。
每个值单看都合法,组合起来却不可能。
3.3 自动化框架与协议层
检测目标不仅是 DOM 属性,还包括:
- Playwright/ChromeDriver 默认启动参数;
- Headless 特有差异;
- CDP 会话与命令使用模式;
page.evaluate()产生的调用痕迹;- 非可信事件
isTrusted=false; - 页面世界被注入或原生函数被重写;
- 默认空插件、默认视口、自动化扩展等。
3.4 行为层
常见行为模型会观察:
- 鼠标是否瞬移;
- 轨迹曲率、速度变化、停顿分布;
- 点击是否永远落在元素几何中心;
- 键盘是否以恒定间隔输入;
- 是否直接设置输入框 value;
- 滚动是否跳跃;
- 页面停留、导航顺序、交互节奏;
- 页面还未稳定就立即点击。
3.5 历史状态层
浏览器是否像"使用过的浏览器",也很重要:
- Cookie、LocalStorage、IndexedDB;
- Cache、Service Worker;
- 登录状态;
- Profile 中的访问历史与持久数据;
- 是否永远是一个全新的无痕环境。
因此,一次成功会话通常需要四层一致:
真正的目标不是"伪造更多值",而是减少矛盾。
四、从源码看 launch():一次启动经历了什么
Python 中最常见的代码只有几行:
python
from cloakbrowser import launch
browser = launch()
page = browser.new_page()
page.goto("https://example.com")
browser.close()
但 launch() 内部并不简单。其主流程可以概括为:

下面逐步拆解。
4.1 第一步:确定实际要运行的二进制
ensure_binary() 的优先级大致是:
- 检查
CLOAKBROWSER_BINARY_PATH,允许直接使用本地构建; - 读取显式
browser_version或CLOAKBROWSER_VERSION; - 解析许可证与发行通道;
- 查询本地缓存;
- 缓存不存在时下载正确平台的压缩包;
- 校验后解压到
~/.cloakbrowser/; - 返回
chrome、chrome.exe或 macOS App Bundle 中的可执行文件。
支持的平台标识包括 Linux x64、Linux ARM64、macOS x64/ARM64、Windows x64。具体平台可用的 Chromium 版本可能不同,因此不能只看一个全局版本号。
4.2 第二步:解析代理、GeoIP 和 WebRTC
当你提供:
python
browser = launch(
proxy="http://user:pass@proxy.example:8080",
geoip=True,
)
包装层需要完成多件事:
- 正确解析用户名、密码、主机、端口;
- 对特殊字符进行 URL 编码;
- 根据二进制能力决定使用 Chromium 原生内联认证,还是 Playwright 代理对象;
- 通过代理访问 IP Echo 服务,得到实际出口 IP;
- 使用 GeoLite2 City 数据库查找时区和国家;
- 将国家映射为 BCP 47 Locale;
- 把出口 IP复用于 WebRTC 地址一致性处理;
- 显式传入的 timezone/locale 优先于自动结果。
这里最容易犯的错误,是根据代理网关域名做地理定位。住宅代理或回连代理的网关可能在一个国家,而会话实际出口在另一个国家。CloakBrowser 优先通过代理访问 IP Echo 服务,拿到真正出口地址。
4.3 第三步:合并启动参数
默认参数不是简单列表拼接,而是按参数键去重:
- 默认 Stealth 参数优先级最低;
- 用户
args可以覆盖默认值; - 专用参数
timezone、locale优先级更高; - 扩展目录、窗口最大化等能力按条件加入。
默认会产生随机指纹种子,并根据宿主平台选择画像:
- macOS 倾向使用原生 macOS 画像;
- Linux/Windows 包装层默认使用 Windows 画像;
- 每次启动随机 seed,除非显式固定。
同时,包装层会移除 Playwright 的部分默认参数,例如与自动化标记、SwiftShader 行为相关的参数,减少明显冲突。
4.4 第四步:处理 Viewport 与窗口几何
这是非常专业、也很容易被忽略的一点。
Playwright 创建页面时可能默认使用模拟视口。若真实外层窗口尺寸、浏览器 UI 区域和内层 viewport 不能自洽,就可能出现物理上不合理的关系,例如 outerWidth < innerWidth。
CloakBrowser 的策略是:
- headed 模式默认使用真实窗口,不额外模拟 viewport;
- 新版本二进制若能在 headless 下保持几何一致,也使用
no_viewport; - 旧版本 headless 使用固定视口,保证可预测性;
- 用户显式传入 viewport 时尊重用户配置;
- 避免同时传入
viewport与no_viewport。
这说明它的设计重点不是"把某个值改掉",而是尽量让一组几何量彼此一致。
4.5 第五步:启动并补齐生命周期
包装层调用 Playwright:
python
pw.chromium.launch(
executable_path=binary_path,
headless=headless,
args=chrome_args,
ignore_default_args=IGNORE_DEFAULT_ARGS,
...
)
之后会替换 browser.close(),确保关闭 Chromium 的同时停止 Playwright Driver,防止进程与资源泄漏。
对于许可证限制,最新主分支还安装了运行期 guard:如果浏览器已完成 CDP 握手,随后因会话数限制退出,用户下一次调用本来只会看到模糊的 TargetClosedError。包装层通过每次启动独立的状态文件,把退出码转换为 CloakBrowserLicenseError,并覆盖 Browser、Context、Page 的关键调用面。
这类细节体现了一个成熟包装层与"几行启动脚本"的差距。
五、供应链安全:为什么不只是做一个 SHA-256
自动下载约数百 MB 的浏览器二进制,是一个高风险供应链动作。若下载服务器或镜像被入侵,攻击者可以替换二进制,同时替换同源的 SHA256SUMS。只检查同一个服务器提供的 Hash,并不能证明文件来自真正发布者。
CloakBrowser 的官方下载链路采用两阶段信任:
- 下载
SHA256SUMS; - 下载其分离签名
SHA256SUMS.sig; - 使用包装层内置的 Ed25519 公钥验证清单签名;
- 从已经认证的清单读取压缩包 Hash;
- 计算下载文件 SHA-256 并比较;
- 检查清单声明版本必须等于请求版本,防止镜像提供一个"签名合法但更旧"的版本;
- 验证通过后才解压。
伪代码如下:
python
def verify_release(archive, manifest_bytes, signature, requested_version):
public_key.verify(signature, manifest_bytes)
manifest = parse_manifest(manifest_bytes)
if manifest.version != requested_version:
raise VerificationError("possible forced downgrade")
expected = manifest.sha256[archive.name]
actual = sha256_file(archive)
if actual != expected:
raise VerificationError("archive checksum mismatch")
这解决了三个不同问题:
- 完整性:文件传输是否损坏;
- 真实性:清单是否由发布者签发;
- 新鲜度/版本绑定:是否被替换成旧但合法的版本。
下载时还使用临时文件,完成验证后再解压,避免半截文件进入缓存。对自建 CLOAKBROWSER_DOWNLOAD_URL,由于官方公钥无法证明第三方镜像内容,项目退回普通同源 Hash 机制;这时需要使用者自己建立可信发布链。
从架构角度看,这部分代码甚至比"指纹伪装"更值得复用到其他大型二进制分发系统中。
六、Humanize 不是随机 sleep:源码级拆解
官方给出的入口非常简单:
python
browser = launch(humanize=True)
真正实现分成鼠标、键盘、滚动、可操作性检查与 API Patch 五部分。

6.1 鼠标:三次 Bézier 曲线 + 缓动
三次 Bézier 曲线公式为:
B ( t ) = ( 1 − t ) 3 P 0 + 3 ( 1 − t ) 2 t P 1 + 3 ( 1 − t ) t 2 P 2 + t 3 P 3 , t ∈ \[ 0 , 1 \] \] \[ B(t)=(1-t)\^3P_0+3(1-t)\^2tP_1+3(1-t)t\^2P_2+t\^3P_3,\\quad t\\in\[0,1\] \] \[B(t)=(1−t)3P0+3(1−t)2tP1+3(1−t)t2P2+t3P3,t∈\[0,1\]
其中:
P0是起点;P3是目标点;P1、P2是随机控制点;- 控制点沿路径法线方向加入偏移,产生自然曲率。
CloakBrowser 还叠加了:
ease-in-out缓动,开始和结束慢,中段快;- 轨迹中段微小 wobble,起点和终点趋近于零;
- 按 burst 成组发送移动事件,中间随机暂停;
- 小概率越过目标数像素,再回到目标附近;
- 点击落点不是固定中心,输入框更偏左侧,按钮在中部随机;
mouse.down()和mouse.up()之间有随机保持时间。
因此它模拟的不只是路径形状,还包括事件时间分布。
6.2 键盘:逐字符、邻键误触与可信事件
键盘层为每个字符执行:
- 按键按下;
- 随机 key hold;
- 按键释放;
- 字符间随机延迟;
- 小概率长停顿;
- 小概率选择键盘附近字符误触;
- 延迟后 Backspace;
- 再输入正确字符。
对中文、Emoji、Cyrillic 等非 ASCII 字符,使用 insert_text,避免错误模拟物理键盘映射。
对需要 Shift 的符号,优先通过 CDP Input.dispatchKeyEvent 发送事件,使事件具备浏览器认可的可信属性,同时避免在主页面世界中执行一段明显的合成事件脚本。
6.3 滚动:加速、巡航、减速
Humanize 滚动不会直接 scrollIntoView():
- 计算目标元素 bounding box;
- 检查它是否已经处于视口合理区域;
- 将鼠标移动到页面滚动区域;
- 计算目标中心与期望落点的距离;
- 前几步使用较慢、较小的滚轮增量;
- 中间进入较快巡航;
- 临近目标减速;
- 每隔几步重新读取元素位置;
- 小概率越过,再反向纠正;
- 最后等待页面稳定。
6.4 Isolated World 与 Actionability
Humanize 需要判断元素是不是输入框、是否聚焦、是否可操作。直接大量调用 page.evaluate() 会进入主页面执行环境,可能被页面重写的 querySelector 或监控代码观察。
项目为此建立 CDP Isolated World:
- 为主 Frame 创建隔离执行上下文;
- 在隔离世界读取 DOM;
- 导航后使 context ID 失效并自动重建;
- CDP 不可用时才回退到
page.evaluate()。
同时,它在点击、输入前进行 Actionability 检查,接近 Playwright 自身的"可见、稳定、启用、可接收事件"语义。这样既能提高成功率,也避免元素仍在动画时立即点击产生机械行为。
6.5 Humanize 的代价
Humanize 会显著降低吞吐量。生产系统不应无脑对每个动作启用最慢 preset。更合理的策略是:
- 页面导航、静态读取不必全部模拟;
- 登录、表单、挑战前后的关键交互使用 Humanize;
- 内部后台、无行为检测页面使用原生高速 API;
- 通过
page._original或 per-call 配置进行分级; - 对延迟敏感场景使用自定义参数,而不是一律
careful。
七、安装与诊断:先把环境跑对,再谈效果
7.1 Python
bash
python -m venv .venv
# Linux / macOS
source .venv/bin/activate
# Windows PowerShell
# .\.venv\Scripts\Activate.ps1
python -m pip install -U pip
pip install cloakbrowser
需要 GeoIP:
bash
pip install 'cloakbrowser[geoip]'
Playwright 的浏览器无需另装,但 Linux 仍可能需要系统依赖:
bash
playwright install-deps chromium
7.2 Node.js
bash
npm install cloakbrowser playwright-core
Puppeteer:
bash
npm install cloakbrowser puppeteer-core
7.3 .NET
bash
dotnet add package CloakBrowser
7.4 CLI 诊断
bash
cloakbrowser install
cloakbrowser info
cloakbrowser info --json
cloakbrowser update
推荐把 info --json 纳入部署探针,记录:
- Wrapper 版本;
- 实际启动的 Chromium 版本;
- 平台标签;
- 缓存路径;
- 许可证层级;
- 字体情况;
- GeoIP 数据库状态;
- 缺失的 Linux 共享库;
- Stable/Preview 实际解析结果。
不要只在本机测试通过后,就假设 Docker、CI、Kubernetes 中完全相同。字体、显示服务、共享库和 CPU 架构都会造成差异。
八、最小可运行示例:授权页面采集
下面使用 example.com,展示同步 API、异常处理和输出文件。
python
from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import Any
from cloakbrowser import launch
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
logger = logging.getLogger("cloak-demo")
def collect_page(url: str, output_dir: Path) -> dict[str, Any]:
output_dir.mkdir(parents=True, exist_ok=True)
browser = launch(
headless=True,
humanize=False,
)
try:
page = browser.new_page()
response = page.goto(url, wait_until="domcontentloaded", timeout=30_000)
result = {
"url": page.url,
"title": page.title(),
"status": response.status if response else None,
"h1": page.locator("h1").first.text_content(),
}
page.screenshot(path=str(output_dir / "page.png"), full_page=True)
(output_dir / "result.json").write_text(
json.dumps(result, ensure_ascii=False, indent=2),
encoding="utf-8",
)
return result
finally:
browser.close()
if __name__ == "__main__":
data = collect_page("https://example.com", Path("artifacts/example"))
logger.info("result=%s", data)
这段代码没有使用"万能重试",因为浏览器自动化中的失败需要分类:
- DNS/网络超时:可以有限重试;
- 401/403:不应盲目重试,应先检查授权、代理信誉或访问政策;
- 选择器失效:属于页面结构变化;
- 许可证限制:应释放并发或调整套餐;
- 浏览器崩溃:需要诊断二进制、共享库、内存和 GPU;
- 业务数据为空:应做语义校验,而不只是 HTTP 200。
九、生产级示例:持久化 Profile、固定身份与重试分类
下面构建一个更完整的"授权站点监控器"。它具备:
- Profile 持久化;
- 固定指纹 seed;
- 浏览器与页面生命周期管理;
- 只对可恢复错误重试;
- JSON 与截图输出;
- 代理通过环境变量注入,避免写入源码;
- 每个 Profile 只允许一个进程使用。
python
from __future__ import annotations
import json
import logging
import os
import random
import time
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any
from cloakbrowser import launch_persistent_context
from playwright.sync_api import Error as PlaywrightError
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError
logger = logging.getLogger("authorized-monitor")
@dataclass(frozen=True)
class MonitorConfig:
url: str
profile_dir: Path
output_dir: Path
fingerprint_seed: int
proxy: str | None = None
headless: bool = True
max_attempts: int = 3
class RetryableNavigationError(RuntimeError):
pass
def is_retryable_status(status: int | None) -> bool:
return status is None or status in {408, 425, 429, 500, 502, 503, 504}
def backoff(attempt: int) -> float:
base = min(2 ** (attempt - 1), 8)
return base + random.uniform(0.1, 0.8)
def run_once(cfg: MonitorConfig) -> dict[str, Any]:
cfg.profile_dir.mkdir(parents=True, exist_ok=True)
cfg.output_dir.mkdir(parents=True, exist_ok=True)
context = launch_persistent_context(
cfg.profile_dir,
headless=cfg.headless,
proxy=cfg.proxy,
geoip=bool(cfg.proxy),
humanize=True,
human_preset="default",
args=[f"--fingerprint={cfg.fingerprint_seed}"],
)
try:
page = context.pages[0] if context.pages else context.new_page()
response = page.goto(
cfg.url,
wait_until="domcontentloaded",
timeout=45_000,
)
status = response.status if response else None
if is_retryable_status(status):
raise RetryableNavigationError(f"retryable status: {status}")
if status is not None and status >= 400:
raise RuntimeError(f"non-retryable HTTP status: {status}")
page.locator("body").wait_for(state="visible", timeout=15_000)
result = {
"url": page.url,
"title": page.title(),
"status": status,
"captured_at": int(time.time()),
}
stamp = str(result["captured_at"])
page.screenshot(
path=str(cfg.output_dir / f"{stamp}.png"),
full_page=True,
)
(cfg.output_dir / f"{stamp}.json").write_text(
json.dumps(result, ensure_ascii=False, indent=2),
encoding="utf-8",
)
return result
finally:
context.close()
def run_with_retry(cfg: MonitorConfig) -> dict[str, Any]:
last_error: Exception | None = None
for attempt in range(1, cfg.max_attempts + 1):
try:
return run_once(cfg)
except (PlaywrightTimeoutError, RetryableNavigationError) as exc:
last_error = exc
if attempt == cfg.max_attempts:
break
delay = backoff(attempt)
logger.warning(
"attempt %s/%s failed: %s; retry in %.2fs",
attempt,
cfg.max_attempts,
exc,
delay,
)
time.sleep(delay)
except PlaywrightError:
# 浏览器协议错误不一定可恢复,保留完整堆栈直接退出。
logger.exception("browser protocol failure")
raise
raise RuntimeError("monitor failed after retries") from last_error
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
config = MonitorConfig(
url=os.environ.get("TARGET_URL", "https://example.com"),
profile_dir=Path("profiles/site-a"),
output_dir=Path("artifacts/site-a"),
fingerprint_seed=42069,
proxy=os.environ.get("HTTPS_PROXY"),
headless=os.environ.get("HEADED") != "1",
)
logger.info("config=%s", asdict(config) | {"proxy": bool(config.proxy)})
logger.info("result=%s", run_with_retry(config))
为什么 Profile、IP、Seed 应绑定?
固定 seed 只固定浏览器指纹,不会自动固定:
- Cookie;
- LocalStorage;
- 代理出口;
- 账号;
- 语言和时区;
- 页面历史。
生产系统应把它们建模为一个 Session Identity:
text
session_id
├── profile_dir
├── fingerprint_seed
├── proxy_id / exit_region
├── locale + timezone
├── account_id(若有)
└── last_used_at
同一 Profile 今天从新加坡访问、明天突然从德国访问;或同一账号每次都换设备指纹,都可能比固定身份更异常。
十、异步并发:重点不是"开得越多越好"
浏览器实例非常重。与普通 HTTP 客户端不同,一个 Chromium 会占用多个进程、内存、文件描述符和共享内存。并发应受三层约束:
- 许可证允许的会话数;
- 机器资源;
- 目标系统授权的请求速率。
下面示例使用一个浏览器、多个独立 Context,并用 Semaphore 限制并发。适用于你自有站点的批量回归测试;若不同任务需要完全独立的代理和进程级指纹参数,应改为多个 Browser Worker。
python
from __future__ import annotations
import asyncio
import json
from pathlib import Path
from typing import Iterable
from cloakbrowser import launch_async
async def fetch_one(browser, url: str, sem: asyncio.Semaphore) -> dict:
async with sem:
context = await browser.new_context()
try:
page = await context.new_page()
response = await page.goto(
url,
wait_until="domcontentloaded",
timeout=30_000,
)
return {
"url": page.url,
"title": await page.title(),
"status": response.status if response else None,
}
finally:
await context.close()
async def collect(urls: Iterable[str], concurrency: int = 3) -> list[dict]:
browser = await launch_async(headless=True)
sem = asyncio.Semaphore(concurrency)
try:
tasks = [asyncio.create_task(fetch_one(browser, url, sem)) for url in urls]
return await asyncio.gather(*tasks)
finally:
await browser.close()
async def main() -> None:
urls = [
"https://example.com",
"https://example.com/?case=2",
"https://example.com/?case=3",
]
results = await collect(urls, concurrency=2)
Path("results.json").write_text(
json.dumps(results, ensure_ascii=False, indent=2),
encoding="utf-8",
)
if __name__ == "__main__":
asyncio.run(main())
一个 Browser 多 Context,还是多个 Browser?
一个 Browser,多 Context:
- 启动快、内存低;
- Context 间 Cookie 隔离;
- 但进程级启动参数共享;
- 某些时区、Locale、平台画像属于进程级;
- 一个浏览器崩溃会影响所有 Context。
多个 Browser:
- 每个 Worker 可拥有独立代理、seed、时区和启动参数;
- 故障隔离更好;
- 资源消耗大;
- 启动成本高;
- 必须受许可证和机器容量约束。
生产中通常采用"固定数量 Browser Worker + 每个 Worker 少量 Context"的混合模型,而不是每个 URL 临时启动一个 Chromium。
十一、自建一致性审计:不要只看一个检测网站
下面代码收集常见浏览器信号,适合在你自己的测试页面或受控环境中做回归对比。
python
from __future__ import annotations
import json
from pathlib import Path
from cloakbrowser import launch
COLLECT_SCRIPT = r"""
() => {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
const dbg = gl && gl.getExtension('WEBGL_debug_renderer_info');
return {
userAgent: navigator.userAgent,
platform: navigator.platform,
webdriver: navigator.webdriver,
languages: navigator.languages,
language: navigator.language,
hardwareConcurrency: navigator.hardwareConcurrency,
deviceMemory: navigator.deviceMemory,
plugins: navigator.plugins.length,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
screen: {
width: screen.width,
height: screen.height,
availWidth: screen.availWidth,
availHeight: screen.availHeight,
colorDepth: screen.colorDepth,
pixelDepth: screen.pixelDepth,
},
window: {
innerWidth,
innerHeight,
outerWidth,
outerHeight,
devicePixelRatio,
},
webgl: gl ? {
vendor: dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : null,
renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null,
} : null,
};
}
"""
def audit() -> dict:
browser = launch(headless=True)
try:
page = browser.new_page()
page.goto("https://example.com", wait_until="domcontentloaded")
data = page.evaluate(COLLECT_SCRIPT)
warnings: list[str] = []
win = data["window"]
if win["outerWidth"] < win["innerWidth"]:
warnings.append("outerWidth < innerWidth")
if win["outerHeight"] < win["innerHeight"]:
warnings.append("outerHeight < innerHeight")
if data["webdriver"] is True:
warnings.append("navigator.webdriver=true")
if data["plugins"] == 0:
warnings.append("empty plugin list")
return {"signals": data, "warnings": warnings}
finally:
browser.close()
report = audit()
Path("fingerprint-audit.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2),
encoding="utf-8",
)
print(json.dumps(report, ensure_ascii=False, indent=2))
更完整的企业级审计应同时采集:
- 浏览器 JS 信号;
- 服务器看到的 Header;
- TLS/HTTP2 指纹;
- 出口 IP 与 ASN;
- DNS 与 WebRTC 地址;
- 页面行为事件序列;
- 同一 seed 多次启动是否稳定;
- 不同 seed 是否出现不可能组合;
- headed/headless/Docker/裸机之间的差异。
不要用一个站点的一次"绿色"结果宣布系统成功。检测服务自身也会更新,且不同服务使用的模型不同。
十二、JavaScript / TypeScript:Playwright 用法
typescript
import { launchPersistentContext } from 'cloakbrowser';
import fs from 'node:fs/promises';
async function main(): Promise<void> {
const context = await launchPersistentContext({
userDataDir: './profiles/site-a',
headless: true,
humanize: true,
humanPreset: 'default',
args: ['--fingerprint=42069'],
});
try {
const pages = context.pages();
const page = pages[0] ?? await context.newPage();
const response = await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
const result = {
url: page.url(),
title: await page.title(),
status: response?.status() ?? null,
};
await page.screenshot({ path: 'example.png', fullPage: true });
await fs.writeFile('example.json', JSON.stringify(result, null, 2), 'utf8');
} finally {
await context.close();
}
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
Node 包还暴露两个很有价值的组合式函数:
buildLaunchOptions():只构建 CloakBrowser 的 Playwright LaunchOptions,不直接启动;humanizeBrowser():对已有 Browser 应用 Humanize。
这意味着你可以把它集成到已有的浏览器框架,而不必完全交出启动控制权:
typescript
import { chromium } from 'playwright-core';
import { buildLaunchOptions, humanizeBrowser } from 'cloakbrowser';
const cloakOptions = {
headless: true,
humanize: true,
args: ['--fingerprint=20260801'],
};
const launchOptions = await buildLaunchOptions(cloakOptions);
const browser = await chromium.launch({
...launchOptions,
timeout: 60_000,
});
await humanizeBrowser(browser, cloakOptions);
这是良好的库设计:既提供"一键入口",又提供可组合的低层构件。
十三、Puppeteer 可以用,但要理解协议差异
入口为:
typescript
import { launch } from 'cloakbrowser/puppeteer';
const browser = await launch({
headless: true,
humanize: true,
});
try {
const page = await browser.newPage();
await page.goto('https://example.com');
} finally {
await browser.close();
}
Puppeteer 包装层同样处理:
- 默认 viewport;
- 二进制路径;
- 指纹参数;
- HTTP/SOCKS 代理;
- 旧二进制的代理认证回退;
- GeoIP 与 WebRTC;
- Humanize;
- 持久化
userDataDir; - 许可证错误映射。
但 Playwright 与 Puppeteer 的 CDP 使用方式、页面初始化行为和默认配置不同。项目文档本身也更推荐在高敏感场景优先使用 Playwright。工程上不要因为两者 API 都能启动,就假设其网络与协议痕迹完全等价。
十四、Docker、CI 与云环境
最小 smoke test:
bash
docker run --rm cloakhq/cloakbrowser cloaktest
在企业部署中,更推荐把二进制下载放到镜像构建阶段,而不是每个容器启动时下载:
dockerfile
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
CLOAKBROWSER_CACHE_DIR=/opt/cloak-cache
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
fonts-noto-color-emoji \
fonts-freefont-ttf \
fonts-unifont \
fonts-wqy-zenhei \
&& rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir cloakbrowser \
&& playwright install-deps chromium \
&& cloakbrowser install \
&& cloakbrowser info --quick
WORKDIR /app
COPY app.py /app/app.py
CMD ["python", "/app/app.py"]
Docker 中必须关注的资源
/dev/shm:Chromium 多进程共享内存不足会崩溃;- 内存 Limit:不能只看主进程 RSS;
- PID Limit:一个浏览器会派生多个子进程;
- 文件描述符;
- 字体;
- 时区数据库;
- Xvfb/显示服务,如果必须 headed;
- Profile 持久卷;
- 缓存卷;
- 容器架构与二进制平台是否匹配。
建议:
bash
docker run --rm \
--shm-size=1g \
-v cloak-cache:/root/.cloakbrowser \
-v "$PWD/profiles:/app/profiles" \
your-image:latest
不要多个容器同时写同一 Profile
Chromium Profile 不是并发数据库。多个进程共享一个 userDataDir 容易出现:
- SingletonLock 冲突;
- Cookie DB 锁;
- Profile 损坏;
- 状态互相覆盖;
- 身份串线。
正确做法是为每个 Session 分配独占 Profile,并由调度器维护租约。
十五、推荐的生产架构
一个稳健的浏览器自动化平台可以拆成以下组件:
text
API / Scheduler
│
▼
Task Queue ──────────────── Dead Letter Queue
│
▼
Session Allocator
├── profile lease
├── fingerprint seed
├── proxy binding
├── locale/timezone
└── license slot
│
▼
Browser Worker Pool
├── Browser 1 -> Context/Page
├── Browser 2 -> Context/Page
└── Browser N -> Context/Page
│
├── Artifact Store:截图、HAR、HTML、JSON
├── Metrics:启动耗时、导航耗时、崩溃、内存
└── Audit Log:授权目标、任务来源、会话身份
15.1 Session Allocator 的职责
它不能只"拿一个代理",而应原子分配:
json
{
"session_id": "sess_01J...",
"profile_dir": "/profiles/sess_01J...",
"fingerprint_seed": 42069,
"proxy_id": "proxy_sg_008",
"timezone": "Asia/Singapore",
"locale": "en-SG",
"browser_version": "resolved-at-runtime",
"expires_at": 1785628800
}
15.2 Worker 池应有背压
不要让请求直接无限创建浏览器。至少需要:
- 有界队列;
- Browser 启动并发限制;
- 页面任务超时;
- 每个 Worker 最大任务数;
- 内存水位驱逐;
- 浏览器崩溃自动重建;
- Profile 锁与租约;
- 优雅停机。
15.3 可观测性指标
建议记录:
browser_launch_seconds;page_navigation_seconds;browser_crash_total;page_timeout_total;license_denial_total;binary_download_total;binary_verify_failure_total;geoip_resolution_seconds;worker_memory_bytes;active_browser_sessions;profile_lock_conflict_total。
把"被挑战/被拒绝"当成普通业务指标,而不是只写在日志文本里。
十六、性能与容量评估
16.1 冷启动成本
首次启动包括:
- 下载约数百 MB 浏览器;
- 校验签名与 Hash;
- 解压;
- 首次 GeoIP 数据库下载约几十 MB;
- Chromium 初始化;
- Profile 初始化;
- 字体缓存加载。
因此:
- CI 镜像应预下载;
- 运行节点应保留缓存卷;
- 不要把冷启动延迟计入普通页面 SLA;
- GeoIP 数据库最好在镜像或初始化阶段准备。
16.2 Humanize 成本
假设输入 50 个字符,平均每字符 70ms,再叠加键程、停顿和偶发误触,可能需要 4~8 秒;一次滚动与点击也可能增加数百毫秒到数秒。它不适合所有页面全部开启。
16.3 内存模型
浏览器内存近似:
text
总内存 ≈ Browser 基础开销
+ Renderer 数量 × 单 Renderer 内存
+ 页面资源
+ Profile / Cache
+ GPU / Shared Memory
页面是否包含视频、Canvas、WebGL、大量图片和前端框架,差异很大。容量规划必须用真实业务页面压测,不能拿 example.com 推算。
16.4 版本更新风险
CloakBrowser 同时受三条版本链影响:
- Wrapper 版本;
- 定制 Chromium 二进制版本;
- Playwright/Puppeteer 版本。
升级前应做矩阵回归:
| 维度 | 示例 |
|---|---|
| OS | Linux x64 / Windows / macOS |
| 模式 | Headless / Headed |
| 代理 | 无代理 / HTTP / SOCKS5 / 认证代理 |
| Profile | 临时 / 持久化 |
| API | Python Sync / Async / Node Playwright / Puppeteer |
| 业务 | 登录、表单、文件下载、Shadow DOM、Iframe、上传 |
生产环境应支持 CLOAKBROWSER_VERSION 固定版本和快速回滚。
十七、常见问题与真正原因
17.1 安装成功,但第一次运行很慢
正常。可能正在下载并解压二进制;启用 GeoIP 后还会下载数据库。查看日志和缓存目录,不要在下载过程中重复启动多个进程。
17.2 Linux 上效果比 Windows 差
重点检查字体。Linux 默认画像若表现为 Windows,却缺少 Segoe UI、Calibri、Consolas 等 Windows 字体,会形成明显矛盾。基础 Noto/CJK/Emoji 字体只能解决最小渲染问题,不能等价于完整 Windows 字体环境。
17.3 Headed 在服务器上无法启动
需要 Xvfb 或真实显示服务:
bash
sudo apt-get install -y xvfb
Xvfb :99 -screen 0 1920x1080x24 &
export DISPLAY=:99
同时确认容器内字体、共享内存和沙箱配置。
17.4 geoip=True 仍然是错误地区
检查:
- 代理是否真正生效;
- 认证信息是否包含未编码特殊字符;
- IP Echo 是否能通过代理访问;
- 代理网关 IP 与实际出口 IP 是否不同;
- GeoIP 数据库是否存在且未损坏;
- 是否显式传入了 timezone/locale 覆盖自动值。
17.5 同一 Node 进程启动多个不同代理,却得到相同地区
历史版本出现过连接池复用导致出口身份串线的问题,v0.5.3 已修复相关逻辑。生产环境仍应将"实际出口 IP"写入日志,并在每次会话启动后做一致性校验。
17.6 持久化 Profile 无法启动
通常是同一目录已被另一个 Chromium 占用。不要删除锁文件强行启动;先确认是否存在活进程,再通过租约管理释放 Profile。
17.7 指纹 seed 固定了,但身份仍不稳定
Seed 只影响一部分浏览器画像。以下仍可能变化:
- IP;
- 时区/语言;
- 浏览器版本;
- 字体环境;
- Headed/Headless;
- 视口;
- Profile;
- 扩展;
- GPU/宿主机。
必须固定完整 Session Identity,而不是只固定一个数字。
17.8 下载校验失败
不要通过关闭验证"先跑起来"。官方路径的签名验证失败意味着真实性无法确认,应:
- 检查系统时间、网络与代理;
- 清理临时下载后重试;
- 检查是否被企业代理替换内容;
- 核对版本;
- 查看项目 Issue;
- 在可信网络重新下载。
17.9 CLOAKBROWSER_BINARY_PATH 指向普通 Chrome,可以吗?
包装层能够启动,但普通 Chrome 不具备 CloakBrowser 定制补丁。此选项更适合本地开发、调试或自有构建,不应把"API 能跑"误认为"具备相同指纹能力"。
十八、许可证与商业部署:最容易被误读的一章
18.1 Wrapper 与 Binary 是两套许可
- 仓库中的 Python、JavaScript、.NET 包装代码:MIT;
- CloakHQ 发布的定制 Chromium Binary、构建配置和补丁:独立 Binary License。
这意味着你可以自由学习和修改包装层,但不能自然推导出你有权:
- 重新分发官方二进制;
- 把它打包进面向第三方销售的产品;
- 作为 Browser-as-a-Service 提供;
- 逆向、反编译定制二进制;
- 绕过许可证或并发限制。
18.2 内部使用与 SaaS 的分界
独立许可允许企业在内部基础设施中运行,用于自己的自动化、研究、数据处理和业务交付;但如果把浏览器能力本身暴露给第三方控制,或作为托管浏览器、客户可配置工作流、OEM 能力提供,通常需要单独授权。
举例:
- 你的公司使用 CloakBrowser 采集公开、获授权的数据,最终向客户交付报告:可能仍属于内部使用;
- 客户可以在你的平台输入任意 URL、代理、脚本并远程控制浏览器:更接近 SaaS/OEM,需要额外许可;
- 在内部 Docker 镜像中保存二进制:许可文本允许内部基础设施使用,但不能因此向外分发该镜像。
上线前应由法务结合最新 License 审核,而不是只看仓库首页的 MIT Badge。
18.3 数据与隐私
启用 GeoIP 时会访问第三方 IP Echo 服务;许可证验证与会话计数可能与 CloakHQ 服务通信。企业环境应评估:
- 出口 IP 是否属于敏感信息;
- 代理凭据是否会出现在日志;
- License 请求是否需走代理;
- Profile 是否包含账号 Cookie;
- 截图、HTML、HAR 是否含个人数据;
- 缓存和 Profile 的保留期限;
- 是否需要磁盘加密与密钥管理。
十九、与其他方案的架构对比
| 方案 | 浏览器引擎 | 主要改动位置 | API 生态 | 典型优势 | 典型限制 |
|---|---|---|---|---|---|
| 原生 Playwright | Chromium/Firefox/WebKit | 无 Stealth | 非常成熟 | 稳定、测试能力强 | 自动化默认信号明显 |
| playwright-stealth | Chromium | JS 注入/属性覆盖 | Playwright | 轻量、安装快 | 页面世界可观察、维护跟随浏览器变化 |
| undetected-chromedriver | Chrome | Driver/配置/启动修补 | Selenium | Selenium 生态成熟 | 与 Playwright 不兼容,版本耦合明显 |
| Camoufox | Firefox | 定制 Firefox | Playwright 方向 | Firefox 路线、指纹能力强 | 与 Chromium 行为和站点兼容性不同 |
| CloakBrowser | Chromium | 定制二进制 + 包装层 | Playwright/Puppeteer/.NET | Chromium 生态、跨语言、Humanize、供应链校验 | Binary 非完全开源、许可与更新依赖、无法保证长期不被识别 |
这里没有绝对赢家。选择取决于:
- 目标站点是否要求 Chromium;
- 是否依赖 Chrome 扩展;
- 是否需要 Playwright;
- 是否接受专有 Binary;
- 是否能持续跟进版本;
- 是否需要多语言 SDK;
- 是否需要行为模拟;
- 是否有合规授权。
二十、对项目的技术评价:亮点与不足
20.1 真正优秀的地方
第一,工程完整度高。
不是只提供一个二进制下载地址,而是把版本、缓存、签名、回滚、GeoIP、代理、Profile、CLI、三种语言包装起来。
第二,重视一致性。
Viewport、时区、Locale、WebRTC、字体、平台画像等细节说明开发者理解"矛盾比单值更危险"。
第三,Humanize 源码可读。
鼠标、键盘、滚动算法不是黑盒,可调参数集中,便于二次开发和性能取舍。
第四,错误处理在持续成熟。
最近版本持续修复代理身份串线、Windows 控制台、许可证握手后错误等边缘问题,说明项目已经进入生产工程打磨阶段。
第五,供应链验证设计认真。
Ed25519 签名、Hash、版本绑定、临时文件和失败关闭,比多数自动下载型工具更完善。
20.2 必须保持清醒的地方
第一,"源码级补丁"不等于补丁源码全部公开。
仓库主要可审计的是包装层;定制 Chromium 的核心补丁与构建产物受独立许可。安全敏感企业需要评估供应商依赖和二进制审计能力。
第二,官方检测结果属于项目方自测。
截图和表格可以作为参考,但不能替代你的真实目标、真实代理、真实部署环境和持续回归。
第三,反自动化没有永久解。
浏览器升级、检测模型、代理信誉和目标业务规则都会变化。
第四,平台画像可能与宿主环境冲突。
例如 Linux 默认模拟 Windows,需要字体、GPU、屏幕和窗口几何共同配合。
第五,Humanize 不是行为身份的全部。
真实用户还有阅读节奏、页面路径、历史状态、内容偏好和任务上下文。随机曲线只能覆盖其中一部分。
第六,商业许可要单独核对。
Wrapper 的 MIT 并不覆盖 Binary 的再分发、SaaS 和 OEM 使用。
二十一、什么时候适合使用 CloakBrowser?
适合:
- 自有网站的反自动化回归测试;
- 获授权的第三方页面自动化;
- 需要 Chromium 与 Playwright 生态的 QA;
- 合规的数据采集、价格监控、内容监控;
- 浏览器 Agent 的底层执行器;
- 需要持久化 Profile、扩展和复杂页面交互的任务;
- 研究浏览器指纹一致性;
- 在受控环境中比较不同浏览器自动化方案。
不适合:
- 未授权账户登录;
- Credential Stuffing;
- 批量虚假注册;
- 绕过付费、身份验证或访问控制;
- 欺诈、广告作弊;
- 不允许自动化访问的高风险系统;
- 无法接受专有二进制和持续订阅依赖的项目;
- 只需要普通网页测试、完全没有 Stealth 需求的场景。
普通内部系统 E2E 测试,原生 Playwright 往往更简单、更可控;只有当你确实面对指纹一致性、Headless 环境差异、授权的反自动化测试时,CloakBrowser 的复杂度才有价值。
二十二、总结
CloakBrowser 的核心价值可以浓缩成一句话:
它不是在页面加载后"伪装几个 JavaScript 属性",而是试图把浏览器二进制、启动参数、网络位置、窗口几何、用户行为和持久状态组合成一个自洽会话。
从技术学习角度,最值得掌握的不是某几个 --fingerprint-* 参数,而是下面五条原则:
- 一致性优先于随机性:随机越多不一定越自然,固定身份往往更合理;
- 网络、浏览器、行为、状态必须统一建模;
- 包装层也需要生产级工程:下载、验证、缓存、回滚、错误映射缺一不可;
- Humanize 应按风险分级,不应成为全局性能税;
- 检测能力和授权边界都在变化,必须持续测试与持续合规。
如果要把它用于生产,推荐从下面这条最小路线开始:
text
固定 Wrapper 版本
→ 运行 cloakbrowser info
→ 在受控测试页建立一致性基线
→ 绑定 Profile + Seed + Proxy + Locale
→ 小并发 Worker 池
→ 记录出口 IP、版本、崩溃与挑战指标
→ 灰度升级与快速回滚
→ 定期复核 Binary License
做到这些,你使用的才不是一个"反检测脚本",而是一套可维护、可审计、可回滚的浏览器自动化基础设施。
附录 A:常用配置速查
bash
# 缓存目录
export CLOAKBROWSER_CACHE_DIR=/data/cloak-cache
# 本地二进制覆盖
export CLOAKBROWSER_BINARY_PATH=/opt/cloak/chrome
# 固定二进制版本
export CLOAKBROWSER_VERSION=146.0.7680.177.5
# Preview 通道
export CLOAKBROWSER_RELEASE_CHANNEL=preview
# 关闭自动更新
export CLOAKBROWSER_AUTO_UPDATE=false
# GeoIP 超时
export CLOAKBROWSER_GEOIP_TIMEOUT_SECONDS=5
# 许可证
export CLOAKBROWSER_LICENSE_KEY=cb_xxxxxxxx
附录 B:常用 Python 入口
python
from cloakbrowser import (
launch,
launch_async,
launch_context,
launch_context_async,
launch_persistent_context,
launch_persistent_context_async,
)
附录 C:官方 README 中的测试截图
以下图片来自项目官方 README,是维护者给出的特定版本、特定环境测试结果。它们不构成对任意目标、任意部署环境的保证。

项目方展示的 Turnstile 测试

项目方展示的 reCAPTCHA v3 分数

项目方展示的 BrowserScan 结果

项目方展示的 FingerprintJS 测试

项目方展示的行为检测结果
参考资料
- CloakBrowser GitHub Repository:https://github.com/CloakHQ/CloakBrowser
- README:https://github.com/CloakHQ/CloakBrowser/blob/main/README.md
- Changelog:https://github.com/CloakHQ/CloakBrowser/blob/main/CHANGELOG.md
- Wrapper MIT License:https://github.com/CloakHQ/CloakBrowser/blob/main/LICENSE
- Binary License:https://github.com/CloakHQ/CloakBrowser/blob/main/BINARY-LICENSE.md
