尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

发票查验自动化实战:攻克中英文验证码识别,准确率95.2%

发布时间:2026/9/15 22:47:37

资讯中心
01
ARTICLE

发票查验自动化实战:攻克中英文验证码识别,准确率95.2%

发票查验自动化实战:攻克中英文验证码识别,准确率95.2%
做发票查验自动化这个需求最初根本不是冲着重构什么技术栈去的纯粹是想把每个月那几百张发票的查验时间省下来。但搞来搞去发现整个自动化链路里最难啃的骨头不是登录、不是发票号码校验而是那个五颜六色、中英混杂的验证码。我前前后后试过不少方案最后把识别率稳定在95.2%整个过程里有不少值得记下来的思路和坑这篇就围绕这个项目把从方案选型到数据准备、模型训练、工程部署的完整链路展开聊聊。1. 发票查验这个场景卡在了验证码上先说一下这次项目的真实背景。企业财务在处理进项发票的时候最烦的就是一张张手工登录发票查验平台录入发票代码、号码、开票日期、校验码后六位然后再把验证码填进去点击查验等审核结果。前面的四要素其实都能从发票票面上拿到完全可以自动化填充。唯一绕不过去的坎就是那个验证码。税务发票查验平台用的不是普通数字验证码而是中英文混合、带复杂背景干扰的图片验证码。这就意味着如果你想做自动化查验必须先把验证码这道关过了。我最初也想过用现成的打码平台但有两个问题一是频繁调用第三方打码平台速度和稳定性没有保障二是发票数据本身属于敏感信息把验证码截图传给第三方等于把校验数据也透露出去了合规风险很大。所以最稳妥的方案还是自研一个本地识别的验证码OCR模块。先说结论最后这个项目达到了整串验证码识别率95.2%也就是每20次提交大概有19次能一次通过。字级准确率能到96.5%。对于发票查验这种场景来说这个数字已经能满足自动化的基本诉求了剩下的失败样本靠重试机制消化掉就行。2. 先把验证码的“底细”摸清楚很多做验证码识别的人上来就找算法、上深度学习我反而建议先仔细收集一批验证码样本把验证码的生成规律摸清楚。这一步很关键因为不同的验证码生成策略直接决定了后续模型结构的选型。从税务发票查验平台公开页面上我抓了大约5000张验证码原始图做分析纯本地测试研究用不涉及任何攻击行为。归纳下来这套验证码有几个非常明显的特征字符长度为5位可能包含大写英文字母和中文汉字偶尔有数字字符之间有明显的位移和旋转但不是特别夸张的扭曲背景有干扰线、噪点以及半透明的背景纹理字符颜色不是纯黑而是带一定渐变中文汉字多来自常见发票用语相关字库如“汽”“增”“专”“售”“服”“运”等。这些特征意味着什么首先5位定长这个问题让模型结构可以简化不需要输出变长序列直接用定长分类就能搞定。其次中英文字符混合意味着字符类别数不是几十个而是几百个——中文常用字加上大小写字母和数字所以这是一个典型的开集分类问题。第三背景干扰虽然花哨但字符本身没有大面积粘连说明字符级检测是可行的。我第一版方案用的是网上随处可见的Tesseract直接整图识别识别率惨不忍睹只有20%多。原因很简单Tesseract是为文档扫描设计的对背景干扰的鲁棒性不够而且中文识别默认是针对整行文本的对这种带旋转、带背景噪声的短验证码完全不是它的强项。后来我用了OpenCV传统图像处理先二值化再找轮廓再做字符分割然后在分割后的单字符上用模板匹配或者KNN分类。这套老方法在纯数字验证码上效果还行但遇到中文字符时直接崩溃汉字本身笔画复杂分割后字符区域小模板匹配对字体、旋转、抗干扰的能力都很弱。走到这一步基本就确定了方向必须用深度学习而且不能走整图识别一键出的路线要拆成“目标检测单字符分类”两段式或者“目标检测序列识别”两段式。3. 数据是这场仗的胜负手严格来说验证码识别的模型结构本身并不难难的是数据。尤其税务发票验证码里的中文汉字不像MNIST那种公开数据集随手就能下到。你要想拿到高质量训练数据基本只有两条路一是靠大量标注真实验证码二是用程序合成模拟数据。先说真实数据这条线。我花了两个晚上人工标注了3000张真实验证码按字符框切出来得到约15000个单字符样本。这个过程有多痛苦做过的人都懂但真实数据的重要性无可替代——它直接反映了生产环境的字体、颜色、干扰方式。后来模型训练完我特意分析过真实数据训练的模型在真实验证码上的表现比纯合成数据训练的模型要高出大约11个百分点这个差距在后面调优时是致命的。不过3000张图还远远不够。深度神经网络做分类任务每个类别至少有几百个样本才比较稳。中文字符的类别数本身就大3000张图切出来的中文类别可能只覆盖了100多个汉字且很多汉字只出现了一两次模型根本学不到稳定的特征。所以必须上合成数据方案。合成数据不是随便贴几个字就行的必须严格模拟真实验证码的生成特征。我写了一个Python脚本核心思路是随机化一切能随机的参数from PIL import Image, ImageDraw, ImageFont, ImageFilter import random import numpy as np # 中文常用字 大写英文 数字 char_pool 零一二三四五六七八九甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥汽增专普票售服运劳务 char_pool ABCDEFGHJKLMNPQRSTUVWXYZ23456789 # 随机字体、大小、旋转角度、颜色 def random_char_img(char, size(32, 40)): font_list [/usr/share/fonts/msyh.ttc, /usr/share/fonts/simhei.ttf] font ImageFont.truetype(random.choice(font_list), random.randint(22, 30)) img Image.new(RGBA, size, (255, 255, 255, 255)) draw ImageDraw.Draw(img) # 字符颜色支持随机渐变 r, g, b random.randint(0, 120), random.randint(0, 120), random.randint(0, 120) draw.text((random.randint(0, 5), random.randint(-3, 3)), char, fontfont, fill(r, g, b, 255)) # 随机旋转 angle random.randint(-15, 15) img img.rotate(angle, expand1, fillcolor(255, 255, 255)) # 随机平移 canvas Image.new(RGBA, (34, 46), (255, 255, 255, 255)) canvas.paste(img, (random.randint(-3, 3), random.randint(-3, 3)), img) return canvas.convert(RGB) # 模拟背景干扰线、噪点、纹理 def random_noise_bg(size(170, 54)): bg Image.new(RGB, size, (255, 255, 255)) draw ImageDraw.Draw(bg) for _ in range(random.randint(8, 20)): x1 random.randint(0, size[0]) y1 random.randint(0, size[1]) x2 random.randint(0, size[0]) y2 random.randint(0, size[1]) draw.line([(x1, y1), (x2, y2)], fillrandom.randint(150, 255)) draw.arc([x1, y1, x2, y2], 0, 360, fillrandom.randint(150, 255)) for _ in range(random.randint(30, 80)): x random.randint(0, size[0]) y random.randint(0, size[1]) draw.point((x, y), fillrandom.randint(0, 255)) return bg合成数据也不是越多越好关键是覆盖度。我生成了10万张图片但严格控制了字符类别出现频率的分布保证每个汉字和字母都有足够样本。另外字体不能只用一个我收集了楷体、宋体、黑体、微软雅黑等6种字体确保模型不是只认识某一种特定字形。还有一个细节真实验证码的颜色不是纯白背景纯黑字符而是在某个主色调下随机抖动。我在合成阶段就加入了全局色调偏移、局部噪点、高斯模糊、轻微透视变换。这些增强手段的核心目的是逼模型去学字符本身的形态结构而不是颜色或者位置这些表面特征。4. 检测与识别两个模型怎么分工搞定数据之后就是模型结构选型。我最开始也试过一张图直接进CRNN做端到端识别——这也是当前验证码识别的主流做法之一。但实际操作下来发现中英文混合的5位验证码用CRNN整图识别的问题在于字符定位不稳定尤其当中文字符笔画复杂、和英文混排时经常出现漏字或者错位。所以我选择了更稳妥的两段式结构先用目标检测模型YOLOv8n给5个字符分别定位再把每个字符框从原图上切下来送进一个单字符分类模型。这个方案的好处是检测和分类各自优化哪一步出了问题都能精准定位。检测模型比较简单就是标准的目标检测。输入是整张验证码图输出是5个带坐标位置和置信度的字符框。这里没有太多讲究YOLOv8n本身就是为轻量实时场景设计的模型非常小CPU上单张推理只要30ms左右。训练时关键是要把所有字符框都标对因为控制点在于如果某个字符检测不到后面识别部分就算模型再强也没用。真正的技术含量在识别模型。字符分类我没有直接用CNNSoftmax这种最朴素的方案而是用了一个轻量级的ResNet18作为骨干网络输出层接一个全连接分类头。为什么不用更复杂的网络验证码字符是32x40左右的小图背景干扰再厉害单个字符的信息量其实是有限的。ResNet18在这个量级的数据上已经足够表达再深反而容易过拟合。分类的类别数这里有个坑。发票验证码虽然实际只出现一部分汉字但我在训练分类器时把类别字典扩展到了一级汉字表中常用的1500个汉字加上大小写字母和数字总共约1536类。这个设计是被逼出来的因为验证码平台会不定期更新汉字池类别太少的话一旦出现新字模型就直接报错。宁可多一些类别让模型学也不要做成封闭集合。训练时我还做了一个关键操作对检测模型切出来的字符框做额外的数据增强包括随机裁切、色彩抖动、平移缩放。这一步相当于把检测和识别之间的误差也模拟进了训练数据里防止识别模型对“完美切好的字符图”过拟合结果在实际切图上效果崩盘。至于为什么不直接用一个模型做端到端我还想说一点。发票查验这个场景对查错率要求极高——你识别错了提交到查验平台返回“验证码错误”整次请求就废了。两段式结构至少能告诉你“卡在检测还是卡在识别”方便做日志采样和定向优化。这一点在工程调优阶段非常重要。5. 训练与评估从87%到95.2%靠的不是玄学模型的整个训练过程我用的是PyTorch框架训练服务器只有一张RTX 3060数据集包含约5000张真实图片和8万张合成图片按7:2:1划分训练集、验证集和测试集。先说检测模型。YOLOv8n用官方预训练权重做人脸之外的通用物体检测能力做初始化然后冻结前几层在自己合成的验证码图上微调。大概训练了80个epoch因为数据相对简单很快就收敛了。最终mAP50能到99%以上——验证码字符框的检测对这个任务而言基本属于纯降维打击不是瓶颈。唯一需要注意的是检测框的坐标精度不要过拟合到合成数据的边界上否则真实图片上字符稍微歪一点切出来的图就会偏移。我做了一个松弛策略模型预测出的框在送入分类器之前向四周各扩展5%的像素范围。这样能带来大约0.8%的整体识别率提升。重头戏在分类模型。损失函数用的是标准的CrossEntropyLoss优化器是AdamW初始学习率1e-3配合CosineAnnealingLR余弦退火调度。整个训练大约60个epochBatch Size 128。简单跑下来的验证集准确率大概在92%左右——没错看起来挺高但离95%还有不小的距离。这个阶段的瓶颈主要是容易混淆的近似字比如“0”和“O”、“1”和“I”、“8”和“B”以及一些笔画结构非常接近的中文字。我做的第一个关键改动是给数据增强添加弹性畸变。验证码平台真实生成图片时字符往往有轻微的形变这种形变在普通训练数据里很难模拟。我加入了随机网格畸变和局部扭曲幅度控制在字符粗细的20%以内。这一步直接把字级准确率从92%干到了94.5%。第二个改动是类别加权损失。回归看一下混淆矩阵发现错误主要集中在40对高频混淆字上。我对这些特定的类别对在损失函数里增加了惩罚系数让模型对相似字符的区分更谨慎。这个改动效果显著字级准确率提升到了96%附近。第三个改动是投票策略。验证码识别不能只看模型一次输出我在推理阶段让模型对同一个字符做三次不同裁剪区域和变换的预测用投票决定最终结果。这个方法把字级准确率稳定推到了96.5%整串准确率从约92%提升到95.2%。关于整串识别率的计算这里有个数学问题值得展开说。假设每个字符的识别准确率是p5个字符的整串准确率在字符独立识别的情况下是p^5。所以96.5%的字符准确率理论上整串准确率只有0.965^50.838也就是83.8%。但你看到的95.2%远高于这个数字原因是验证码字符之间不独立——错误的样本往往是整张图都模糊、干扰极重的情况而清晰的图大概率5个字符全对。换句话说识别错误具有“图像级聚集性”一个验证码要么大概率全对要么大概率错一两个。这也是为什么投票机制能大幅提升整串识别率。从这个角度看95.2%的整串准确率换算成单字符独立准确率大概是0.952^(1/5)0.9888已经很接近99%了。这基本逼近了这个验证码体系下深度学习方法的实用天花板再往上抠收益非常有限。6. 工程化部署的几个暗坑模型训练完成只是第一步真正放进自动化查验流程里还有不少和“干净跑通”完全不沾边的细节。这些细节积累起来才是95.2%能不能稳定落地的关键。第一件要处理的事情是置信度过滤。阶段一检测模型为每个字符框输出一个置信度阶段二分类模型也会为每个字符类别输出一个概率。两个置信度相乘得到一个综合置信度。我在推理代码里设定阈值综合置信度低于0.85的样本直接判定为“不确定”不进入识别结果而是触发重试机制重新拉取验证码。这个策略很有效它让我们永远不会把低置信度的错误结果直接提交让整条链路的错误率更可控。第二件事是验证码的缩放和归一化。发票查验平台页面上的验证码显示尺寸和实际下载图片的尺寸不一定相同。这一点非常容易踩坑。我调试时发现直接用浏览器里截图的验证码图训练和预测效果还行但换到另一个环境页面上的验证码被CSS缩放过了识别率瞬间掉了8个百分点。后来我统一了流程验证码图片必须经过缩放至固定标准尺寸再做灰度化和标准化再送进模型。所有预处理逻辑必须和训练阶段保持完全一致。第三件事是异常网络处理。这里要特别提到搜索热词里说的“发票查验有一张发票一点就网络异常”这个现象。真正做自动化的时候你也会频繁遇到原因并不一定是网络差而是请求频率太高触发了平台的风控拦截。但还有一个容易被忽视的技术问题当验证码提交失败后如果页面没有正确刷新验证码下一次提交时会带着同一张验证码而同一张验证码一旦被标记为已使用后续无论你识别得多准都会失败。解决办法是做“验证码序号绑定”每次请求验证码图片时记录下图片的字节流哈希值提交查验时校验当前页面上的验证码图片哈希是否和识别时一致。如果哈希一致且识别置信度足够再提交如果不一致自动重新识别新图。这个逻辑虽然听着简单但在自动化流程里能省掉大量失败重试的时间。第四件事是请求频率控制。做自动化务必关注这一点既是为了合规也是为了保证系统稳定。设计上强制每次请求间隔至少2秒并且加一个随机抖动2到4秒之间避免形成机械化的请求节奏。遇到连续3次“验证码错误”或“请求失败”自动冷却30秒并降级为人工介入。第五件事是部署形式。这个OCR模块我最终打包成了一个独立的HTTP微服务输入是验证码图片的base64输出是识别文本置信度字符框坐标。发票查验主流程通过HTTP调用这个服务和查验逻辑彻底解耦。这样做的好处是模型升级的时候不需要停掉查验主服务也方便后续把这个OCR能力复用到其他场景里。7. 回头看好用的几个工具和选型这个项目里除了模型训练本身还有一些工程工具配合效率提升明显列出来供大家参考。LabelImg标注真实验证码检测框用的虽然是老工具但胜在轻量稳定PIL/Pillow合成数据生成的核心依赖功能实在太强了Ultralytics YOLOv8检测模型的框架API设计友好训练和导出都很方便PyTorch Lightning分类模型训练用的主要看重它能把训练流程规范化少写很多样板代码ONNXRuntime模型推理部署时转成ONNX格式CPU上的推理速度比PyTorch直接跑快约30%而且不依赖Python环境方便嵌到其他服务里。关于ONNX转换有一个小建议分类模型的动态维度不要开直接把输入固定为(1, 3, 32, 40)这样的小尺寸。验证码识别场景输入尺寸基本固定开启动态维度只会增加推理开销和转换复杂度没有任何实际好处。YOLOv8和分类模型两个模型都转ONNX之后在不启用GPU的普通4核CPU服务器上单张验证码从检测到识别的总耗时在80ms到120ms之间完全够用。还有一点对工程性能很重要模型预热。ONNX Runtime刚加载模型时第一次推理会有额外的初始化开销可能达到几百毫秒。所以服务启动后一定要做一次空张量的预热推理让显存或者内存中的算子都初始化好再对外提供服务。如果不做这一步线上第一个验证码请求大概率会慢得离谱导致超时。8. 故障排查与重试闭环一个自动化的发票查验流程不能把“验证码识别95.2%”当作唯一依靠必须配合完整的重试闭环否则99%和95%在实际体验上差别不大。我设计的完整闭环是出现验证码识别失败或查验提交失败时流程自动重新拉取新的验证码重置请求状态最多重试5次。如果5次都失败就把这张发票的所有参数和截图存档标记为“人工需处理”推向人工复核队列。实际跑了两周之后统计下来整个查验流程的最终成功率达到了99.5%以上剩下的0.5%基本是平台侧数据不一致或页面改版这类特殊异常。这里有个排查时非常实用的点给每一次验证码识别都做日志记录包括验证码图片的哈希值、模型输出、置信度、识别耗时、最终是否验证通过。等积累到几百条失败记录后再从失败样本里挑图片出来看到底卡在检测还是卡在识别针对性地补充训练数据或调参。这套“失败数据回收再训练”的闭环是识别率持续提升的真正发动机。在这个项目的后期我还用Grid Search小范围搜索过分类模型的学习率和权重衰减系数结论是学习率1e-3搭配权重衰减1e-4最优超出这个范围后识别率变化其实很小。这个阶段更应该把精力放在错误样本的分析迭代上而不是纠结超参数的微小调整。9. 中英文混合验证码的特殊难点最后单独聊聊中英文混合这个事。很多人做验证码识别时遇到英文数字的组合都做得不错但加上中文就全线崩溃。为什么有几个层面的原因。字形复杂度差异太大。英文字母和数字笔画少、结构简单模型用很少的卷积层就能提取有效特征。中文汉字笔画的复杂度可能是英文的十倍以上模型需要更深的网络和更多的数据来学习细微差别。同一个模型单独训练英文字符集时可能98%准确率一旦加入中文就掉到94%这类现象很正常。相似字问题被放大。英文里的“0/O”“1/I”已经是老生常谈中文里相似字更多“未”和“末”、“己”和“已”、“日”和“曰”、“干”和“千”。这些汉字本身在票面逻辑里也可能出现比如发票记载中的“服务”“运输”“货物”等字词一旦识别错了后续人工复核成本极高。字体多样性问题更突出。英文字体和数字的变体有限中文光宋体、仿宋、黑体、楷体在不同渲染环境下就差很多。我最终是给合成数据引入了6种主流字体并给每个字体随机加粗、斜体、反色等渲染方式才让模型在真实场景下保持泛化。还有一个容易被低估的点中文在验证码里的排列不是横平竖直的而是会围绕一个虚拟中心做随机旋转和偏移。如果训练数据里旋转角度太小模型对倾斜字符的鲁棒性就差如果旋转角度太大汉字笔画会出现明显形变连人眼都可能误判模型学起來也困难。经过实验正负15度以内的旋转范围是最平衡的。10. 这套方案还能迁移到哪些场景做完整套之后我发现这套“目标检测字符分类”两段式识别方案并不仅仅适用于税务发票查验。只要验证码是中英文混合、定长或短变长、背景干扰可控的场景基本都能复用这套技术架构。比如银行回单验证码、企业年报登录验证码、部分政务平台的短信/图片验证码以及物流平台的面单校验码等。但在迁移时也不要生搬硬套。如果目标验证码是纯数字、无粘连、背景干扰少的简单验证码直接用轻量OCR或传统图像处理就够用了没必要上两段式深度学习成本和复杂度都实在没必要。如果验证码是滑块、点选、拼图这类交互式验证码那这套方法完全无效得走另一套行为识别路线不要浪费时间。最后再分享一个个人经验心得验证码识别项目技术本身的难度只占三分之一数据质量与闭环迭代的难度占另外三分之二。像税务发票查验这种高合规要求的场景当识别率卡在90%的时候与其疯狂调模型结构不如回归数据认真收集几百条失败样本分析再针对性地增强、标注和重训练效果往往比换更大的模型快得多。这也是从这个95.2%的项目里我自己感受最深的一件事。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。