这是 drission 的一篇能力深挖。如果你还不认识它:它是用Rust写的高性能反检测浏览器自动化库,内置字符 OCR、图片滑块缺口距离识别与点选/文字点选验证码,API 对齐 DrissionPage。前面几篇聊过「默认 Chrome / TLS 指纹」和「吐环境编译成单二进制」,这一篇专攻一块过去最容易东拼西凑的硬骨头——文字点选验证码(「请依次点击 全、验、体」那种),并顺手讲清它编译出来到底有多干净。一句话先把结论摆出来:drission 把点选验证码的识别链路——目标检测、逐框 OCR、全局指派——全做成了纯 Rust 推理(用tract,不引原生 onnxruntime);整条「监听取图 → 检测 → 受约束识别 → 全局最优指派 → 拟人可信点击 → check 铁证」跑通后,编译产物是一个单文件:默认 release strip 后约26MB,换上体积优先 profile 实测能压到~12MB,且otool -L看依赖只有系统库——没有 onnxruntime、没有 Python、没有node_modules。一、点选验证码到底难在哪字符 OCR 是「一张小图 → 一串字」,点选不是。点选要回答四个问题,错一个就点不中:子问题为什么难字在图里哪个位置一张大图上散布 5~9 个艺术字 干扰,得先定位出每个字的框每个框是什么字艺术体 / 扭曲 / 低对比,单字 OCR 直接argmax极易误判按提示先点谁后点谁提示「依次点击 库、扩、类」,要把识别结果和提示顺序对齐成点击序列点下去算不算数验证码认isTrusted真实鼠标事件,JSclick()不认;还要拟人轨迹避开行为风控ddddocr 的detTrue解决了第一问(目标检测),但「检测 逐框识别 按提示配序 可信点击 结果核验」串成一条工程链路,才是真正能自动点选的东西。drission 做的就是这条链路,而且每一环都讲究。二、关键认知:为什么要「监听接口」而不是「截图」这是踩过坑才换来的经验,先说,因为它决定了后面一切。很多人做点选第一反应是「截个图喂模型」。但以易盾文字点选为例,它的干净底图根本不在img.src上——而是由接口c.dun.163.com/api/v3/get以JSONP下发:data.bg[0]是干净 JPEG,data.front直接给出要依次点击的字 顺序。截图会踩两个坑:把工具栏拍进去:验证码弹窗右上角有刷新 / 语音 / 反馈图标,截图会一起拍进来,目标检测很容易把它们误检成字框,一旦被指派点击就把验证码切成了语音模式(真实踩坑)。缩放错位点不中:截图分辨率 ≠ 页面显示尺寸,坐标一缩放就偏。所以 drission 的示例监听c.dun.163.com/api/*:api/get取干净图 点击顺序,点完再读api/check的响应当作「点击是否被接收 / 是否通过」的铁证。usedrission::prelude::*;// 触发前就开监听,确保抓到第一题;只过滤 api/*,噪声最小tab.listen().start([dun.163.com/api]).await?;// 从 api/get 的 JSONP 里解析出 (干净底图 URL, front 点击顺序)// data.front 全验体 → 目标顺序 [全,验,体]提示文字也可以从 DOM 读(.yidun_tips)来交叉核对,但点击顺序以接口front为准——DOM 文案有时是「请点击下图中的 X、Y、Z」的包装,接口字段才是干净的有序答案。实在拿不到接口、只能截图时,drission 也留了后路:solve_excluding可以先丢弃中心落在「工具栏矩形」内的检测框再做识别,从源头杜绝「点到语音 / 刷新开关」。三、全链路一图流把整条链路摊开看,就是六步,前三步是「识别」,后三步是「执行 核验」:对应到 API,核心就这么几行——ClickWord::solve一步把「检测 → 逐框 OCR → 全局指派」做完,返回按提示顺序依次点击的命中点:usedrission::ocr::ClickWord;// 首次会自动下载两个模型(det ocr)到缓存,之后复用letcwClickWord::new().await?;// cap: 从 api/get 直拉的干净底图字节;targets: 接口 front 给的有序答案字lettargets[全.to_string(),验.to_string(),体.to_string()];lethitscw.solve(cap,targets)?;forhinhits{// target要点的字 · point图内点击点 · affinity置信度(可设阈值决定点/换图)println!(「{}」 aff{:.2} 图内点({},{}),h.target,h.affinity,h.point.0,h.point.1);}下面把最有意思的四环展开。四、亮点一:目标检测Det(YOLOX),纯 Rust tract 推理第一步是把图里每个字框出来。drission 的Det直接复用 ddddocr 的目标检测模型common_det.onnx(YOLOX),但推理引擎换成纯 Rust 的tract——这是后面「编译体积干净」的根:不链原生 onnxruntime,跨平台一份代码。流水线严格对齐 ddddocr,坐标才对得上:416×416 灰边(114)letterbox,等比缩放不拉伸;原始 0–255 RGB、不归一化(YOLOX 的输入约定),NCHW;输出按 stride 8/16/32 解码:(xygrid)*stride、whexp(...)*stride;分数阈值 0.1 → NMS 0.45 → 还原回原图像素坐标。usedrission::ocr::Det;letdetDet::new().await?;// 首次下载 common_det.onnx 到缓存letboxesdet.detect(img_bytes)?;// VecBBox:原图像素坐标 置信度,已 NMS、按分降序forbinboxes{println!(框 ({},{})-({},{}) score{:.3} 中心{:?},b.x1,b.y1,b.x2,b.y2,b.score,b.center());}单元测试里连锚点数都对齐了 YOLOX:52² 26² 13² 3549(stride 8/16/32 在 416 上的网格总数)。这种「对齐到能写进单测」的较真,正是它坐标能点中的原因。五、亮点二:受约束 OCR 字形模板「双信号」融合这是我觉得最漂亮的一环。检测出框之后,怎么知道每个框是什么字?朴素做法是对每个框做全字符集(8210 字)OCR、取argmax。问题是艺术体单字argmax极易误判——而且常常是「高置信地读错」(confident misread),这种错最坑。drission 用了两个互补的信号,融合起来再做指派:信号 ①:受约束 OCR(char_affinity)关键转念:点选的提示已经把标准答案字给你了。所以不必让模型在 8210 字里自由发挥,而是问它「这个框像不像『全』?像不像『验』?」——对每个候选目标字,取各时间步 softmax 概率的最大值作为亲和度(0–1)。这比全字符集argmax鲁棒得多。而且每个框识别前先做预处理 TTA:[原图, 自动对比度, Otsu 二值]各识别一次,按「置信锐度(top1−top2)」选最稳的那一版——相当于把「把杂乱背景抹掉让字浮出来」做成可证、只增不减的图像操作(没有更好就退回原图)。信号 ②:字形模板(渲染字体 / 真样本,NCC)既然知道目标字,干脆自己把它渲染出来当模板:用纯 Rust 的fontdue把目标字用系统 CJK 字体光栅化,再按几个角度旋转(易盾字常倾斜),与字框的Sobel 梯度幅值特征做归一化互相关(NCC,对亮度 / 对比免疫)。有标注真样本时优先用真样本(更贴目标字体,小固定字表几张就奏效,无需训练)。融合:combo OCR亲和度 1.5 × 模板相似度两个信号相加后再做指派。意义在于:当 OCR「高置信读错」时,形状对不上会把它压下去;反之模板拿不准时,OCR 顶上。一加一,误配明显减少。lethitscw.solve(cap,targets)?;forhinhits{// affinity 是 OCR 受约束识别置信度;template 是字形模板相似度(有 CJK 字体/真样本时才有)lettplh.template.map(|t|format!( tpl{t:.2})).unwrap_or_default();println!(「{}」 aff{:.2}{tpl},h.target,h.affinity);}真样本库零配置接入:DRISSION_GLYPH_SAMPLES目录,目录结构{字}/任意.png即可,ClickWord::new()自动加载。系统字体也自动探测(macOS PingFang / Windows 微软雅黑 / Linux Noto),都没有就优雅降级为纯 OCR。六、亮点三:全局最优指派,胜过「按目标序贪心」识别出「每个框对每个目标字的分数」后,还差最后一步:把目标字一一指派到互不相同的框。很多实现这里用贪心——按目标顺序,每个目标挑当前分最高的未用框。但这会犯经典错误:靠前的目标抢走了靠后目标更需要的框。drission 把它当指派问题精确求解:点选规模很小(一般 ≤ 6 字、十几个框),用DFS 分支定界求全局最优,目标是「先保证尽量多的目标都分到框(一个大 bonus),其次最大化总亲和度」。举个真实单测里的例子(分数矩阵aff[框][目标]):目标t0 目标t1 框b0 0.90 0.80 框b1 0.85 0.10 贪心:t0 抢 b0(0.90)→ t1 只能拿 b1(0.10)→ 合计 1.00 最优:t0→b1(0.85) t1→b0(0.80) → 合计 1.65 ✅贪心被「b0 对 t0 最高」带偏,逼得 t1 拿到几乎没用的 0.10;全局指派看的是总账,反而让两个目标都点对。框不足时,它也会让「哪些目标只能空缺」由总分最优决定,而不是僵硬地空缺靠后的。七、亮点四:拟人可信点击 check响应铁证识别全对 ≠ 点得中。验证码只认isTrusted的真实鼠标事件,而且有行为风控——直线瞬移、坐标完美反而像机器。drission 的示例用了几个真功夫:可信点击:走 CDP 原生Input派发真实事件(isTrustedtrue),JS.click()易盾不认;minimum-jerk 拟人轨迹:s 10t³ − 15t⁴ 6t⁵的最小急动度曲线做变速,叠加手抖,mouse_move_fast密集采样逼近真人;hover 保活:触发式面板是 hover 出现的,所以从验证条连续移入面板逐字点击,全程不离开控件——否则一mouseleave,面板收起,点击落到隐藏层不被接收;坐标映射:图内像素 → 页面坐标(按显示 rect 的 scale 映射)→ 钳制在显示区内,确保点在可点击层上。最后用监听api/check的响应作为铁证:点击若被易盾接收会发起 check,否则根本不发——这比「看页面提示」可靠得多。// 拟人轨迹逐字点击(minimum-jerk 变速 手抖,从验证条连续移入面板)hover_click(tab,bar_point,page_points).await?;// 铁证:点击被接收才会发 check;读它的响应判定是否通过ifletSome(body)wait_check(tab,Duration::from_secs(6)).await{letokbody.contains(\result\:true)||body.contains(验证成功);println!(check 响应 {} → {},body,ifok{通过 ✓}else{未通过});}跑通整条链路就一行命令(示例会把点击点叠加图、结果截图都落到target/yidun/,方便复盘):cargorun--exampleyidun_click--featurescdp,ocr实事求是:单字艺术体 OCR 非 100%(ddddocr 固有),且字点得准 ≠ 必过——易盾另有行为风控,是另一件持续对抗的事。本库负责把「监听取图 识别 全局指派 可信点击 结果核验」这条工程链路做扎实。八、重点:它编译出来到底有多干净讲完功能,说这次最想强调的一点——交付形态。点选这套东西用 Python 跑,通常意味着一坨环境;用 drission,就是一个文件。为什么能这么干净?核心就一条:推理走纯 Rust 的tract,不链原生 onnxruntime。于是整条点选链路(浏览器控制 目标检测 OCR 拟人点击)编出来是个自包含可执行文件,otool -L看依赖只有系统库:$ otool-Ltarget/release/examples/yidun_click /System/Library/Frameworks/Security.framework/.../Security /System/Library/Frameworks/SystemConfiguration.framework/.../SystemConfiguration /System/Library/Frameworks/CoreFoundation.framework/.../CoreFoundation /usr/lib/libiconv.2.dylib /usr/lib/libSystem.B.dylib全是 macOS 自带的系统库——没有 onnxruntime、没有 Python、没有任何第三方 ML 运行时。体积实测(完整点选自动化示例yidun_click,含 CDP 浏览器控制 det ocr):默认 release(opt-level3,未 strip) 33 MB(含符号表) release strip ~26 MB 体积优先 profile(实测) ~12 MB体积优先只需在Cargo.toml加一段 profile,重编一次即可:[profile.release] opt-level z # 体积优先 codegen-units 1 strip true panic abort那 ddddocr 的模型呢?不进二进制。common.onnx(~54MB)和common_det.onnx(~20MB)在首次使用时自动下载到缓存目录(~/Library/Caches/drission/ocr/),之后被你机器上所有用 drission 的程序复用——你分发的永远只是那个十几 MB 的可执行文件,而不是把 74MB 模型塞进每个二进制。需要离线 / 内网时,用DRISSION_OCR_MODEL/DRISSION_DET_MODEL指向本地模型即可,零改码。把这套和「ddddocr Python 栈」对比一下,差距很直观:ddddocr(Python)drission(Rust)运行时Python 解释器无,单可执行文件推理引擎onnxruntime(原生库,数十 MB)tract(纯 Rust,静态编入)还要装numpy / opencv / Pillow …pip install一串无交付物一个环境一个文件(~12–26MB)模型同样要带按需下载到缓存,不进二进制、可共用跨平台各平台对齐 onnxruntime同一套代码 → Win.exe/ Linux / macOS换句话说:你写一份 Rust,cargo build出一个文件,拷到目标机就能跑点选——不用在目标机装 Python、装 onnxruntime、对齐 CUDA / glibc。这对「做成工具分发」「塞进 CI / 容器」「上无 Python 的机器」都是实打实的省心。九、边界:别当万能钥匙老规矩,把话说清楚,免得你踩坑:识别非 100%:艺术体单字 OCR 是 ddddocr 的固有上限,双信号融合是把误配压低、不是消灭;solve返回的affinity就是给你设阈值用的——置信度低就换图重试,而不是乱点。风控是另一件事:字点得准也可能被行为风控判失败。可信点击 拟人轨迹是「尽力而为」,不是过盾保证。接口优先:能监听到干净图就别截图;非要截图就用solve_excluding排除工具栏。真样本 渲染字体:对固定小字表(如某些试用 demo 就那十来个字),攒几张真样本进DRISSION_GLYPH_SAMPLES,比渲染字体模板贴合得多。十、小结这篇把 drission 的文字点选验证码整条链路讲透了:监听接口取干净图与有序答案 → YOLOX 目标检测框字 → 受约束 OCR(char_affinity) 字形模板双信号融合纠正确信误读 → DFS分支定界全局最优指派 → minimum-jerk 拟人可信点击 → 读api/check当铁证;而且因为推理全程走纯 Rust 的 tract、不碰原生 onnxruntime,最终交付是一个无 Python、无 onnxruntime 的单文件(strip ~26MB,尺寸优先 ~12MB,模型按需缓存不进二进制),同一套代码跨平台出 Win/Linux/macOS。想试的话:crates.io:cargo add drission(点选需features [ocr],配合cdp驱动浏览器)文档:docs.rs/drission仓库:github.com/MageGojo/drission-rs(det_probe/yidun_click两个示例都在examples/)跑通点选:cargo run --example yidun_click --features cdp,ocr⚠️免责声明:本项目仅供学习与合法、非盈利的技术研究。文中以易盾「文字点选」公开试用页为例说明识别链路,禁止用于绕过任何站点授权、破坏验证机制或采集受保护数据。请遵守目标站点的robots协议与当地法律法规,使用本库产生的一切后果由使用者自行承担。如果这篇帮到你,欢迎到 GitHub 点个 star;评论区也聊聊你在点选 / 验证码对抗里踩过的坑。