资讯详情

验证码识别核心环节:字符分割工具caveman的原理与实践

📅 2026/10/15 8:09:57 | 华诺云谱 👁 阅读
验证码识别核心环节:字符分割工具caveman的原理与实践
去年我维护一个对内数据核对脚本跑批时候频繁被登录验证码卡住。人工识别虽然准确但一多起来就想吐。后来我试了一堆OCR方案tesseract、ddddocr都上了粘连字符一多照样翻车。直到有人甩给我一个叫caveman的开源小工具我才意识到问题根本不在于“认不出来”而在于“没切对”。这个工具的名字听起来像原始人处理方式也确实有点像——不讲什么高深模型就用长度分割法硬切但效果意外地扎实。如果你也在搞验证码识别、OCR预处理、爬虫自动化或者只是想验证自己生成的验证码是否合理这篇文章值得花几分钟看完。我会从caveman的定位、原理讲起再带一个完整可复现的识别流水线最后把我在实际调试中踩过的坑和参数调优经验一并交代清楚。1. caveman是谁它解决的是验证码识别里最容易被忽略的一环1.1 先说清楚这个工具的定位caveman是一个开源字符分割工具核心功能就是从图片验证码里把一个个字符分离开。它不做“识别”这件事只做“切分”。一个完整的OCR流程通常分两步先分割后识别。分割质量直接决定识别上限因为再强的识别模型吃进去一张“W”和“a”粘在一起或出格的图也很难输出正确结果。为什么说它容易被忽略因为现在大部分人遇到验证码第一反应是“上深度学习模型”或者是“调API”。模型确实能端到端识别但它是个黑盒不好干预中间过程API有调用成本和隐私顾虑也不适合所有场景。caveman提供的是一条传统的、可解释的、完全本地化的路径先把字符切干净再用任何一个OCR引擎或轻量分类器去做单字识别。好处是你随时能查看每一步切出来的图问题出在哪一目了然。我见过很多人在tesseract上折腾半天调psm、换whitelist识别率死活上不去最后发现是图片里字符连成了一片。这时候如果先用caveman把粘连字符拆开准确率能立刻上一个台阶。所以caveman适合这些场景图片型验证码识别前置处理、OCR流水线里的分割模块、字符分割算法的学习和对比实验。1.2 为什么粘连字符是OCR的“使绊子”角色验证码生成方为了对抗自动识别最常用的手段就是把字符压扁、拉近、交错排列甚至让两个字符的笔画叠在一起。对OCR引擎来说字符分割一旦出错后面所有环节都白搭。比如下面这类情况“m”和“n”连成一条波浪线OCR直接当成一个字符读输出乱码字符太宽单个连通域里包含了两个字符识别引擎又把它们当成一个整体字符出格或倾斜垂直投影无法形成干净的间隔传统按列切分直接失效。这些问题在真实数据里非常常见。我统计过一批带干扰线的验证码样本“肉眼能看清但不粘连”的大概只有四成剩下的不是粘连就是倾斜或带噪声。所以只靠tesseract的默认分割逻辑基本是碰运气。caveman解决的就是这个“碰运气”的部分——它用一套基于字符宽度估算的规则把粘连域强制拆开并且尽量拆在正确的边界上。1.3 长度分割法的核心思想等宽字符假设caveman的底层逻辑说穿了不复杂就是一个假设验证码里的字符基本都是等宽或近似等宽的。在这个前提下只要估算出“平均字符宽度”再结合连通域分析就能推算出某个连通区域里大概藏着几个字符然后在合适的位置下刀。具体拆解下来它的判断依据有三个通过统计整张图里所有文字像素的分布估算出平均字符宽度通过连通域分析找出一个个独立的笔画块当一个连通域的宽度明显大于平均字符宽度或者相邻连通域的合并宽度异常时判定为“这里有多个字符粘在一起”需要切分。这个思路和人类辨认粘连字符的心理过程很像你看到一个超宽的笔画块自动会想“这肯定不止一个字母”然后在视觉上找“最窄处”或“凹陷位置”切进去。caveman干的也是这事只是用像素和宽度数据代替了人眼。值得一提的是长度分割法并不是唯一的分割思路。业界还有垂直投影分割法、滴水算法、基于深度学习的分割模型等。垂直投影法对干净字符有效但一遇到轻微倾斜或字符搭边就崩滴水算法能模拟水滴沿字符轮廓下滑找路径抗粘连能力强但实现复杂、计算慢深度学习方法效果好但需要大量标注数据。caveman选择的长度分割法优势在于速度极快、不依赖训练数据、可解释性强——这点在做批量任务或快速验证时很实用。2. 动手前先弄懂caveman的核心处理流水线2.1 图片预处理从彩色验证码到干净的二值图caveman处理的第一步是拿到一张“干净”的图片这一步往往被人忽视但直接决定后续分割成功率。理想输入是白底黑字、边缘清晰的二值图。如果喂给它的是一张带彩色噪点、抗干扰线或渐变背景的验证码最好先做预处理。我常用的预处理流程是这样灰度化把RGB三通道图转成单通道灰度图二值化用固定阈值如127或自适应阈值算法把灰度图变成0/1二值图降噪去掉孤立噪点可以借用中值滤波或形态学开运算校正如果字符有倾斜做一下旋转校正避免投影统计失真。实际操作中二值化阈值的选择很讲究。阈值太高字符笔画会被切断变成一堆碎点阈值太低背景噪点全留下连通域爆炸。我通常会写一个快速循环脚本把同一张图按不同阈值处理跑一遍caveman看哪组参数切出来最稳定。这种“跑批试参”的方法在后文的调参环节还会用到。2.2 字符分割阶段连通域分析和粘连判断的逻辑预处理完成后caveman会进入核心分割阶段。这个阶段的执行流程大致如下第一步扫描二值图找出所有文字像素连通区域记录每个连通域的外接矩形坐标第二步统计所有连通域的宽度分布估算出“平均字符宽度”。这里要注意验证码里可能混入干扰线形成的连通域所以它通常取的是宽度分布的中位数或众数更抗噪第三步遍历每个连通域如果宽度落在合理字符宽度范围内就标记为“单字符”如果宽度明显超标就根据超标倍数推断里面包含几个字符第四步对标记为“多字符”的连通域执行切分。切分时优先找凹陷点即在字符轮廓的凹进去的位置下刀找不到明显凹陷点时按等分宽度切第五步输出每个字符子图的边界坐标供后续OCR引擎逐块识别。这里有一个容易被误解的点caveman并不总是“每一个宽度超标的连通域都能切对”。当两个字符的笔画大面积交叠时按等分宽度切出的位置可能刚好砍在某个字符的中间导致识别时出现“半个字母”。所以后面我们还要靠识别器的置信度做二次修正这个我在实战环节会具体演示。2.3 caveman能输出什么从坐标框到字符候选集如果你的理解还停留在“输入一张图输出一段文本”那说明你还没真正用好caveman。这个工具定位是一个分割器所以它最有价值的输出是“每个字符子图的坐标和切分后的图片”而不是最终识别结果。拿到这些坐标框之后你可以做很多事情把切好的字符子图逐个保存为文件用tesseract按时识别把坐标框画回原图可视化检查切分位置是否合理把坐标框和另一个方向——比如字符上下溢出——结合起来做清洗把这些切分结果作为训练数据去训练一个小型单字符分类器。我个人的经验是永远先可视化你的切分结果再谈识别率。caveman切得好不好一眼就能看出来。如果你发现切分框歪七扭八那后面接什么识别引擎都救不回来不如回到预处理环节去调参数。3. 实战用caveman tesseract跑通一个完整的识别流程3.1 环境准备和最小可运行代码我的环境是Python 3.10caveman通过pip直接安装即可。注意它依赖Pillow和numpy这两个库做图像处理是标配。tesseract则需要单独安装OCR引擎本体和语言包我用的是tesseract 5.0版本的英文训练数据。最小识别代码其实很短核心就是“先切分、后识别”from PIL import Image import caveman import pytesseract # 1. 读取图片并做预处理 img Image.open(captcha.png).convert(L) img img.point(lambda x: 255 if x 160 else 0) img.save(captcha_bin.png) # 2. 用caveman找出所有字符子图 boxes caveman.find_boxes(captcha_bin.png) # 3. 逐个识别对每个子图单独送tesseract result_text for i, box in enumerate(boxes): piece img.crop((box[x], box[y], box[x] box[width], box[y] box[height])) piece piece.resize((piece.width * 4, piece.height * 4)) # 放大后再识别效果更好 char pytesseract.image_to_string(piece, config--psm 10 -c tessedit_char_whitelistabcdefghijklmnopqrstuvwxyz0123456789) result_text char.strip() piece.save(fchar_{i}.png) print(识别结果:, result_text)这里有两个容易踩的细节。第一--psm 10表示“识别单个字符”这是关键配置因为我们已经切分了不需要tesseract再自动分段。第二子图放大4倍再识别能显著提高单字符识别准确率但不要用最近邻插值放大锯齿会干扰识别建议用PIL默认的Lanczos重采样。3.2 一顿调试当默认参数碰上“J”我踩的坑第一次跑这个流程我的识别器几乎把验证码里的“J”全识别成了“]”把“l”识别成了“1”。这个问题不是caveman切错了而是切出来的子图里字符本身带有轻微的倾斜和笔画残缺。单独看字符“J”的底钩如果因为二值化掉了几个像素看起来就真的像“]”。我当时的排查思路是先保存所有字符子图人工查看一遍。结果发现caveman切分坐标基本是对的但有一小部分字符笔画出界正好在子图边缘被裁掉了一截。这种情况多半出现在首尾字符上因为验证码生成时字符不自带边距边缘像素贴得太紧。解决办法有两个在传给caveman之前先给整张图加一圈白边比如用ImageOps.expand(img, border20, fill255)这样所有字符都离开图片边缘二值化时把阈值放宽到170~180之间宁可多留一点浅灰也不要过早把笔画切掉。加上白边之后“J”识别成“]”的问题立刻消失了。这个小改动也让我意识到caveman分割本身没问题但它的输出质量高度依赖输入图的边界条件。做任何OCR预处理都要先想清楚“字符是不是被裁到了”。3.3 换个验证码风格参数要跟着改一个批量调参脚本我手里的验证码有两类一类是字符间有明显间距的简单验证码一类是字符挤在一起甚至部分重叠的复杂验证码。第一类用默认参数几乎百分之百切对第二类就需要调节caveman的字符宽度估算参数。caveman允许你传入最大字符宽、最小字符宽和平均字符宽等参数。简单验证码用默认值没问题复杂验证码则需要根据样本重新估算。我写了一个简单的批量调参脚本import caveman import statistics widths [] for sample in samples: result caveman.find_boxes(sample) for box in result: if box[width] 5: # 过滤掉孤点噪声 widths.append(box[width]) avg_width statistics.median(widths) min_width int(avg_width * 0.5) max_width int(avg_width * 1.5) print(f中位数宽度: {avg_width}, 最小宽: {min_width}, 最大宽: {max_width})把统计出来的宽度参数回传给caveman重新跑分割切分准确率提升非常明显。核心逻辑是当验证码风格变化时平均字符宽度也会变只有用样本自己统计出来的宽度才能适配这一套样式。这个思路和训练模型时的数据集校准是同一道理只不过它发生在规则层。4. 识别率之外的事稳定性、边界条件和工程整合4.1 用样本量说话什么情况才叫“参数调好了”很多人在调参时凭感觉觉得“这张切对了就行”。但在真实工程里验证码样式是会变的偶尔还会出现你从没见过的字体变体。所以我会专门留一批标注过的小样本集每次调完参数都跑一遍整体统计。具体做法是准备50~100张已人工标注过答案的验证码图片跑完整流水线统计整图识别准确率、单字符准确率、分割失败率三个指标。如果整图准确率稳定在90%以上单字符准确率在97%以上再叠加合理的重试策略整体方案就基本可用了。这里有个容易被忽略的原则不要追求单张图的完美而是追求整批图的高可用。哪怕偶尔有一张识别错了只要重试机制能兜底整体体验依然是好的。相反如果为了把那张刁钻样本切对而牺牲掉大量普通样本就得不偿失了。4.2 容易翻车的字符形态i、l、1、0、O的前世今生即便caveman切得很好识别阶段仍会有一批“天生难分”的字符组合。我踩得最多的就是三组l和1、0和O、i和l。尤其是切分出来的子图分辨率不高时这些组合几乎无法靠字符形态区分只能靠上下文或白名单约束。处理思路有三个层次第一层在验证码识别场景里如果能确认来源验证码只包含数字那就直接限定字符集为数字O和0的歧义自动消失第二层如果你不能确定字符集可以做后处理纠错比如用字典校验对于连串字符0旁边的字母通常是O第三层在字符图片本身上下工夫比如把“i”的上点去掉之后再识别减少它和“l”的混淆概率。我个人最推荐的是第一层方案在可控场景下把字符集白名单写死。这不仅解决了歧义问题还顺手提升了单字符识别的速度。这也是为什么我在上面的最小示例代码里就加了-c tessedit_char_whitelist的原因。4.3 放到真实工程里异步化、重试和日志留痕当你把caveman识别流水线从脚本搬到正式服务时有几个工程化问题会被立刻放大。首先是并发与并发安全。caveman本身是CPU密集型的纯Python计算受GIL影响多线程并不会带来线性加速建议用进程池实现并行。我这里用的是concurrent.futures.ProcessPoolExecutor每个进程独立初始化模型和caveman实例这样既规避了GIL又能共享稳定的内存池。其次是识别失败的重试策略。OCR识别天然不是100%准确在工程上要默认它可能失败。我习惯做两到三次识别尝试每次随机微调预处理参数比如二值化阈值上下浮动10放大倍数浮动0.5倍取置信度最高的那次结果。如果仍然失败记录日志并抛给人工流程处理不要硬撑。最后是留痕。每条识别记录我都会保存原始图片、切分中间图、识别结果和置信度时间戳。这样无论用户反馈什么都能回溯到具体哪一步出了问题。这套日志机制曾经帮我从一堆“识别异常”里精准定位到“某个时间段网站验证码换了字体”堪称救命。4.4 一条更隐蔽的坑caveman对非等宽字符的误判前面讲了caveman依赖等宽字符假设但在真实验证码中有的字体不是等宽字体比如手写体风格。字符“W”和“i”的宽度差异极大平均字符宽度的估算就会失真。“W”可能被判定为“包含两个字符”然后被错误切分反过来“i”这种窄字符可能被当成噪点过滤掉。解决思路有两个方向。一个是绕过换用滴水算法或基于轮廓分析的分割器它们不依赖等宽假设适合手写体另一个是改造在调用caveman之前对字符区域做宽度归一化的预处理比如把每个连通域单独提取并缩放到统一宽度再拼接成一行输入。第二个方案我试过效果不错但要注意缩放会破坏字符比例导致“O”变成“0”之类的变形。5. 必须说清楚的合法使用边界5.1 这套技术能做和不能做的事任何OCR技术都有双面性。caveman能做的是自动化识别你自己拥有、或被明确授权测试的系统中的验证码比如你自己开发的网站或应用里的验证码可用性测试你在做爬虫数据采集时目标网站明确允许自动化访问的场景内部系统的自动化回归测试。它不能也不应该用来做这件事未经授权绕过他人网站的验证码批量注册账号、抢票、刷接口、发送垃圾请求。这类行为在国内外法律框架下都有明确的违法或违约风险我也强烈不建议任何人去碰。验证码存在的目的是保护系统资源绕过它去薅羊毛轻则封IP重则吃官司完全不值得。我说这些不是站在道德高地上说教而是作为从业者基于风险给你一个实在的建议。技术能力应该用在提升自身效率上而不是破坏别人的防线。如果你不确定当前应用场景是否合规我的建议是先停下来问一下你自己“如果我做的这件事被公开报道了我能坦然面对吗”只要有一丝犹豫就别做。5.2 一个更稳妥的用法用caveman检验你自家验证码的质量我在实际工作中发现caveman最好的应用场景其实是“验证码质量评估”。很多开发者在生成验证码后不知道自己的验证码到底够不够“抗自动识别”。人工看觉得还行但机器识别的风险心里没底。这时候你可以直接用cavemantesseract搭一个自动识别器拿它来测量自家验证码的自动识别通过率。如果识别率太高说明验证码太弱需要增加字符粘连程度、干扰线或旋转角度如果识别率低到连正常用户都看不清那说明验证码设计过头了容易流失用户。理想状态是自动识别通过率控制在可接受范围同时人类用户能轻松通过。这个“攻防平衡测试”的玩法比单纯拿caveman去攻破别人网站有价值得多也更符合行业伦理。我在给自家业务做验证码优化时就专门建了一套自动化评估链路自动生成几百个验证码样本 → 用caveman切分 → 用tesseract识别 → 统计自动通过率 → 调整生成参数。这套流程帮我迅速定位到“背景噪声密度”和“字符间距离”两个关键参数的最优区间效果立竿见影。6. 最后聊点实在的我把caveman用顺手之后的一些体会用caveman跑了半年多从最初在脚本里玩一玩到现在沉淀成内部OCR流水线的固定前置模块我的感受有两个。第一不要迷信端到端黑盒。深度学习模型确实能解决很多传统算法搞不定的问题但它不擅长给你反馈。caveman这种传统可解释工具的价值在于每个环节都能看、能调、能量化。当你发现识别率下降时你能立刻判断是预处理的问题、切分的问题还是识别的问题而不是对着一个模型发呆。这种“可干预性”在做工程时比什么都重要。第二参数调优的本质是理解数据。很多人在调参时盯着代码看却忘了把头抬起来看看样本。caveman默认参数好不好用完全取决于你的验证码长什么样。先用一批有代表性的样本做统计再根据统计结果设置参数往往比盲目试参数更快见效。工具永远是次要的理解和掌控数据才是核心能力。如果你现在也被OCR识别、验证码处理这类问题卡着不妨像我一样搭建这样一套用caveman做分割、可视化验证一下切分结果、再搭配轻量识别器的流水线。切分对了识别就是水到渠成的事。把这个环节做扎实之后你会发现后续所有跟字符提取相关的任务都变得异常顺滑。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑