用 Rust 写一个反检测浏览器自动化库:验证码 OCR、滑块缺口、自动过盾一把梭

做爬虫和自动化的同学,大概率都用过 Python 的 DrissionPage:语法顺手、上手快。但当业务往「高并发、低内存、还要过反爬」的方向走时,Python 的 GIL、运行时开销、以及「验证码得调第三方打码平台」这几件事,就开始变得别扭。

于是我用 Rust 把这套体验重写了一遍,做成了一个库:drission。一句话概括它:

Rust 编写的高性能浏览器自动化库 ,默认驱动 Camoufox 反检测浏览器,内置字符验证码 OCR 与图片滑块缺口距离识别,语法对齐 DrissionPage,面向高并发爬虫与自动化。

本文会从「为什么这么设计」讲到「怎么用」,并把验证码 OCR、滑块、过盾、高并发池这几个硬核能力的原理拆开讲清楚。代码都对应仓库里真实可跑的示例。

它解决了什么问题

先把痛点摆出来,再看 drission 怎么逐个回应:

传统做法的痛点 drission 的回应
验证码要花钱调打码平台,还得联网 内置离线 OCR,纯 Rust 推理,首次自动下模型
滑块缺口距离算不准、过不了 整块形状/颜色对齐算法,极验/顶象预设
反检测要自己拼一堆 stealth 补丁 默认 Camoufox 内核,webdriver=false + 指纹定制开箱即用
Cloudflare 盾拦在门口 pass_cloudflare() 自动过盾
Python 高并发吃内存、受 GIL 限制 tokio 异步 + 浏览器池,Driver/Session 双模省内存
换语言就得重学一套 API 语法对齐 DrissionPage:tab.get / tab.ele / ele.click

下面逐项展开。

技术选型:为什么是 Rust + Camoufox + Juggler

三个关键决策,先讲清楚,后面的能力都建立在它们之上:

  • 语言选 Rust :异步并发用 tokio,多标签可真正并行操作、各标签独立会话与 cookie;没有 GIL,内存可控,适合长跑的规模化采集。
  • 内核选 Camoufox :它是 Firefox 的反检测分支,把 navigator.webdriver、canvas/webgl/audio 指纹、WebRTC 泄漏这些「自动化特征」从底层做了处理,比在 Chromium 上层打 stealth 补丁更干净。首次运行会自动下载分发 对应平台的浏览器到 ~/.cache/camoufox,无需手动装。
  • 协议用 Juggler(而非 CDP) :Camoufox 只支持 Firefox 的 Juggler 协议,所以本库自研了一套 tokio 异步 Juggler 客户端。这也意味着它不是又一个 CDP 套壳。(同时提供可选的 cdp feature 驱动 Chromium 系,见后文。)

整体分层大致如此:

text 复制代码
你的业务代码  (DrissionPage 风格 API)
        │
  Browser / Tab / Element  ── 元素定位、交互、监听、截图
        │
  能力层:OCR · Slider · Cloudflare · BrowserPool · WebPage(双模)
        │
  Juggler 异步客户端  (codec: null 分隔 JSON 编解码 + protocol 消息)
        │
  transport:Unix(socket/pipe) · Windows(命名管道)
        │
  Camoufox / Firefox 进程   (默认自动下载分发)

快速开始

加依赖。核心默认精简,验证码能力按需开 feature:

toml 复制代码
[dependencies]
drission = "0.1"

# 需要验证码识别时再开(避免给不用的人引入重依赖):
# drission = { version = "0.1", features = ["ocr", "slider"] }

一个最小闭环:启动浏览器 → 开标签 → 访问 → 查元素读文本 → 退出。binary_path 留空即首次自动下载 Camoufox。

rust 复制代码
use drission::prelude::*;

#[tokio::main]
async fn main() -> drission::Result<()> {
    // 反检测内核 + 本地化指纹一起设好
    let opts = BrowserOptions::new()
        .headless(true)
        .locale("zh-CN")
        .timezone("Asia/Shanghai");
    let browser = Browser::launch(opts).await?;

    let tab = browser.latest_tab().await?;
    tab.get("https://example.com").await?;

    println!("标题 = {:?}", tab.title().await?);
    println!("h1   = {:?}", tab.ele("tag:h1").await?.text().await?);

    browser.quit().await?;
    Ok(())
}

元素定位沿用 DrissionPage 的前缀语法,迁移几乎零成本:

写法 含义
tab.ele("@id:kw") 按 id 定位
tab.ele("#submit") / tab.ele(".btn") CSS id / class 简写
tab.ele("css:form input") CSS 选择器
tab.ele("xpath://form//button") XPath
tab.ele("tag:h1") 标签名
tab.ele("text:登录") 按文本

亮点一:内置验证码 OCR(离线、纯 Rust 推理)

这是我最想做的能力。字母/数字、扭曲粘连的字符验证码,离线识别------不调第三方打码平台、不联网。

用法只有一行。ocr_image 会自动「定位元素 → 取图(<img>data: URL 直接解码,否则元素截图)→ 识别」:

rust 复制代码
use drission::prelude::*;

#[tokio::main]
async fn main() -> drission::Result<()> {
    let browser = Browser::launch(BrowserOptions::new().headless(true)).await?;
    let tab = browser.latest_tab().await?;
    tab.get("https://apizero.cn/login").await?;

    // 一步:定位验证码 <img> → 取图 → 识别(首次自动下载模型到缓存)
    let code = tab.ocr_image("xpath://form//button/img").await?;
    println!("验证码 = {code}");   // 例:"P38W"
    Ok(())
}

原理:ddddocr 模型 + tract 推理

它没有重造轮子,而是站在巨人肩上做了「纯 Rust 化」:

  • 模型 :复用 ddddocr 的预训练 common.onnx(~54MB,对常见 4~6 位扭曲验证码开箱即用),首次使用自动下载 到缓存目录;字符集(8210 字)内置在库里。可用 DRISSION_OCR_MODEL 指定本地模型、DRISSION_OCR_MODEL_URL 换源。
  • 推理引擎 :用纯 Rust 的 tract,不依赖原生 onnxruntime,跨平台交叉编译干净,没有 C++ 动态库的烦恼。
  • 流水线 (对齐 ddddocr):解码图 → 灰度 → 等比缩放到高 64 → 归一化 (p/255-0.5)/0.5CNN-LSTM 推理 → CTC 贪心解码

CTC 解码是关键一步:逐时间步取 argmax,再折叠连续相同字符、去掉 blank 。核心逻辑简化如下(库内置了对 [T,1,C]/[1,T,C] 等不同轴序的自适应):

rust 复制代码
// 每个时间步取概率最大的字符;与上一步相同则折叠,blank(索引 0)跳过
let mut out = String::new();
let mut prev = usize::MAX;
for t in 0..time_steps {
    let best = argmax_over_charset(t);          // 该步最可能的字符索引
    if best != 0 && best != prev {              // 非 blank 且非重复
        out.push_str(&charset[best]);
    }
    prev = best;
}

端到端实测

仓库里有个 apizero_login 示例:用本库打开 apizero.cn 登录页 → React 兼容方式填账号密码 → OCR 识别验证码并填入 → 点登录。判定标准很实在:用假账号,只要站点回的是「账号或密码错误」而不是「验证码错误」,就说明验证码识别对了。

bash 复制代码
HL=0 N=5 cargo run --example apizero_login --features ocr

实测有头/无头各 5/5、4/4 通过。注意一个细节:多数字母在验证码里大小写形同(S/s、W/w...),模型可能输出小写,而验证码登录通常大小写不敏感 ,按需 .to_uppercase() 即可。

亮点二:图片滑块缺口距离识别

滑块验证码的共性:一张带缺口的底图 + 一块拼图,把拼图水平拖到缺口即过。难点是两件事:① 算准「要移动多远」;② 拟人地把它拖到位 。drission 把这两件事做成了与厂商无关的通用能力,并内置极验/顶象预设。

rust 复制代码
use drission::prelude::*;

// 假设已拿到 tab: &Tab
// 极验 v4:canvas 三图模板匹配,缺口距离 + 闭环纠偏拖动,一把梭
let r = tab.solve_geetest_slide().await?;

// 顶象:拼图跨域 taint 读不到像素 → 截图 + 内容 NCC 匹配,只算缺口距离
let gap = tab.dingxiang_slide_gap(4).await?;
println!("需移动 {:.0}px,置信 {:.2}", gap.displace, gap.confidence);

缺口算法:不靠单侧边缘,靠整块对齐

这里有个踩过的坑值得说:早期版本用「缺口边缘 − 拼图边缘」做差,结果系统性偏移 ------因为缺口的低对比沿会被漏检、拼图的辉光又会外扩。后来改成整块形状/颜色对齐,误差互相抵消,才真正稳定过码。库会按手头素材自动选法:

方法 适用素材 思路
TwoImage 双图法 bg + full_bg + piece(极验) 拼图真实颜色对完整底图找最小色差,叠加形状重叠互校,最准
PieceTemplate 模板法 只有 bg + piece 拼图轮廓在底图边缘幅度图上滑动,最大化重叠
Notch 缺口探测 只有 bg 取纵向边缘最强的列当缺口(兜底)
ContentNcc 内容相关 bg + 截图拼图(顶象) 绿环掩膜 + 彩色内容 NCC + 暗度门控 + 描边对齐

拖动用 minimum-jerk 拟人轨迹 ;给了拼图素材就开闭环纠偏 (标定把手位移比 + 读真实位置校正),否则按比例开环。换厂商基本只换 SliderConfig 配置:

bash 复制代码
cargo run --example geetest_slide --features slider   # 极验
cargo run --example dx_slide      --features slider   # 顶象(HL=0 看界面)

小贴士:滑块/拖拽前建议导航前 调用 tab.apply_pointer_stealth().await?,让指针行为更像真人。

亮点三:反检测与自动过盾

反检测分两层:底层指纹 交给 Camoufox 内核,上层策略由库提供配置与「过盾」动作。

rust 复制代码
use drission::prelude::*;

let opts = BrowserOptions::new()
    .headless(true)
    .locale("zh-CN")
    .timezone("Asia/Shanghai")
    .geolocation(31.23, 121.47)   // 经纬度与时区/语言自洽
    .block_webrtc(true)           // 堵 WebRTC 真实 IP 泄漏
    .humanize(true)               // 拟人化行为
    .proxy(Proxy::new("http://user:pass@host:port"));
let browser = Browser::launch(opts).await?;

要点:navigator.webdriver=false、canvas/webgl/audio 指纹定制、block_webrtc 防 IP 泄漏,这些都在内核层生效,不是 JS 注入能轻易识破的那种。

遇到 Cloudflare 盾,一行过:

rust 复制代码
use std::time::Duration;

// 交互式 Turnstile 复选框「可信点击」+ 非交互式自动放行
let ok = tab.pass_cloudflare(Duration::from_secs(20)).await?;
println!("过盾 = {ok}");

亮点四:网络监听与请求拦截

爬虫最常要的是「直接拿接口响应体」,而不是去解析渲染后的 DOM。drission 支持 XHR/Fetch 监听抓响应体:

rust 复制代码
use drission::prelude::*;

// 假设已拿到 tab: &Tab
tab.listen_start(&["api/data"]).await?;          // 先开监听(关键词匹配 URL)
tab.get("https://example.com").await?;            // 再触发请求
tab.ele("@id:kw").await?.input("drission").await?;
tab.ele("#submit").await?.click().await?;

let packet = tab.listen_wait().await?;            // 抓到目标 XHR(含响应体)
println!("{}", packet.response.body);

需要改写/伪造/拦截请求时,用拦截 API。每个被拦请求必须恰好决策一次 (resume / resume_with / fulfill / abort),类型系统在编译期就替你保证了这一点(决策方法消费 self):

rust 复制代码
use drission::prelude::*;

// 假设已拿到 tab: &Tab
tab.intercept_xhr(&["/track", "/ads"]).await?;
let req = tab.intercept_next().await?;
if req.url.contains("/ads") {
    req.abort("blockedbyclient").await?;          // 拦掉广告/埋点
} else {
    // 也可 req.fulfill(200, headers, body) 直接伪造响应
    req.resume().await?;                           // 原样放行
}

此外还支持 WebSocket 帧监听、控制台监听,以及「吐环境(补环境)」:采集 canvas/webgl/audio 真实指纹 + 定位签名 sink,一键导出可 node 运行的补环境工程。

亮点五:高并发浏览器池

规模化采集靠 BrowserPool:它管理一组浏览器 worker,提供并发限流、每任务轮换代理/指纹、失败重试、健康自愈、断点续抓 。总并发槽 = size × tabs_per_worker

rust 复制代码
use drission::prelude::*;

#[tokio::main]
async fn main() -> drission::Result<()> {
    let pool = BrowserPool::launch(
        PoolOptions::new()
            .size(4)                                  // 4 个浏览器进程
            .tabs_per_worker(3)                       // 每进程 3 标签 → 并发 12
            .base_options(BrowserOptions::new().headless(true))
            .fingerprints(FingerprintPool::presets()) // 每 context 轮换指纹
            .retry(RetryPolicy::new(2)),              // 失败重试 2 次
    )
    .await?;

    let urls: Vec<u32> = (0..100).collect();
    let results = pool
        .map(urls, |i, tab| async move {
            tab.get(&format!("https://example.com/p/{i}")).await?;
            let title = tab.title().await?;
            Ok::<String, drission::Error>(title)
        })
        .await;

    println!("完成 {} 个任务", results.len());
    pool.shutdown().await?;
    Ok(())
}

断点续抓

长跑任务最怕中途挂掉重来。map_resumable 配合 Checkpoint 落盘:首跑完成的 key 记账,再跑只补未完成项。

rust 复制代码
// 假设已有 pool: &BrowserPool
let ckpt = Checkpoint::load("checkpoint.jsonl").await?;
let results = pool
    .map_resumable(
        (0..1000).collect::<Vec<_>>(),
        |i| format!("page-{i}"),      // 每个任务的去重 key
        &ckpt,
        |i, tab| async move {
            tab.get(&format!("https://example.com/p/{i}")).await?;
            Ok::<u32, drission::Error>(i)
        },
    )
    .await;
println!("本轮新增完成 {} 项,累计 {}", results.len(), ckpt.done_count().await);

代理池还能做出口 IP 地理 ↔ 指纹自洽 (ProxyGeo / ProxyHealth):住宅代理落在哪个国家,语言/时区就跟着对齐,降低被风控的概率。

亮点六:Driver + Session 双模

不是所有请求都值得开一个浏览器。drission 借鉴 DrissionPage 的 WebPage,提供 Driver(浏览器)/ Session(纯 HTTP)双模 ,且共享 cookie :重活(登录、过盾、JS 渲染)走 Driver,海量轻请求走 Session,省内存、对旧机器友好

rust 复制代码
use drission::prelude::*;

#[tokio::main]
async fn main() -> drission::Result<()> {
    // 先用浏览器把登录/过盾这种重活干了
    let mut page = WebPage::new_driver(BrowserOptions::new().headless(true)).await?;
    page.get("https://site/login").await?;
    // ... 这里做登录交互 ...

    // 切到会话模式高速抓列表:自动把浏览器 cookie 同步过去,登录态无缝衔接
    page.change_mode(PageMode::Session).await?;
    page.get("https://site/api/list?page=1").await?;
    let rows = page.s_eles("css:.item").await?;
    println!("抓到 {} 行", rows.len());

    page.quit().await?;
    Ok(())
}

change_mode 切换时会自动双向同步 cookie,你不用手动搬运登录态。

feature 设计:核心精简,按需加料

库的依赖做了分层,默认只编核心,重依赖按需开:

feature 能力 依赖 默认
camoufox Camoufox/Juggler 后端(核心) 始终编译 ✅ 开
ocr 字符验证码识别 image + tract-onnx
slider 滑块缺口距离识别 纯 JS + std,零额外依赖
cdp Chromium(CDP)后端(实验) 无额外重依赖

不需要 OCR 的项目不会被 tract-onnx(几十 MB 级)拖累编译时间,这点对 CI 很友好。

适用与不适用

为了不让你踩坑,把边界说清楚:

  • 适合:Rust 技术栈的爬虫/自动化、需要反检测过盾、需要离线识别字符验证码或滑块、要长跑高并发规模化采集。
  • 暂不适合:点选/文字点选类验证码(在路线图上)、强 MMTLS/证书 pinning 的私有协议、把过盾当「万能钥匙」的预期(反爬是持续对抗,没有银弹)。
  • 平台:macOS(arm64,主力)、Linux、Windows(命名管道传输已打通);Rust ≥ 1.85(edition 2024)。

路线图

接下来重点在:点选/文字点选与计算题验证码、滑块行为轨迹模型化、OCR 自训模型接入(dddd_trainer)、更多深指纹注入与「吐环境」补全、WS 接管多路复用与 wss:// TLS、更多厂商滑块/盾预设。

写在最后

drission 想做的,是把「反检测浏览器 + 验证码识别 + 高并发采集 」这三件常常要东拼西凑的事,收进一个 Rust 库里,同时保留 DrissionPage 那种「打开就会用」的手感。

如果你正好在找 Rust 的验证码识别 / 滑块缺口距离 / 反检测浏览器 / 高并发爬虫方案,欢迎试试:

⚠️ 免责声明 :本项目仅供学习与合法、非盈利用途。使用者须遵守目标站点的 robots 协议与当地法律法规,禁止用于任何违法、侵害他人利益或采集受保护数据的行为。使用本库产生的一切后果由使用者自行承担。

如果觉得有用,点个 star、评论区聊聊你的使用场景,我会持续更新。