Featured image of post AI 辅助编程实战:从零训练验证码识别模型,打造五笔查询 Workflow

AI 辅助编程实战:从零训练验证码识别模型,打造五笔查询 Workflow

背景

在 AI 时代,自然语言处理已经是我们获取信息的“第一工具”。我作为一个接触电脑较早的人,以及更重要的是拼音拼不准的南方人,自然五笔输入法是我的首选。如果没了五笔,我感觉自己几乎不会打字。虽然日常输入熟悉的汉字很快,但一旦遇到“提笔忘字”或不知道如何拆解的生僻字,思路往往会被打断。即便有些输入法可以反查五笔,但有时你仍然不知道它为何这么打。

为了解决这个痛点,周末闲着无事开发了个 Alfred Workflow,实现快速查看某个字是如何打的并且显示五笔拆字过程,方便真正理解你为何会打不出这个字。 当然作为技术文章,我更想分享的是:在 AI 的帮助下,我是如何突破开发过程中的重重困难的,而非 Workflow 本身。

效果展示

 肃字在alfred-workflow 中查看

你还可以修改一下 里面的命令使其打出其它编码是如何拆解的。只需要修改过滤展示(–only 参数)

alfred_wubi.py 支持通过 --only 选择要展示的字段,逗号分隔(不区分大小写):

  • summary:总体汇总行
  • 编码:num5, num6, num9, wb86, wb98, wbx (新世纪), strokes
  • 拆解图片:num6_parts, num9_parts, wb86_parts, wb98_parts, wbx_parts

分别是:王码 5/6/9 键、五笔 86/98/新世纪、笔画序列。

示例: 当前默认展示五笔 86 编码及其拆解:

1
python3 alfred_wubi.py "{query}" --max-retry 5 --cache-dir alfred_cache  --only wb86,wb86_parts

如果你是五笔输入法爱好者,只是想使用这个插件,那么跳到文章后面部分即可找到相关链接。接下来我会讲一下技术实践啦。

调研与选型

在开始开发前,我调研了现有的开源方案,发现 alfred-wubi-workflow 已经是 6 年前更新的仓库,其依赖的 chaiwubi.com 也已无法访问。经过搜索网站,我找到了两个可用的五笔反查源:

  1. 打字吧 (http://www.daziba.cn)
  2. 王码官方 (http://www.wangma.com.cn)

王码官方的数据显然更权威,不仅有 86/98 版,还有新世纪版和数字王码。考虑到刚才那个仓库依赖的网站已不可用,我就打算傍个大款,就选择王码了。但它有一个巨大的拦路虎:查询时必须输入 4 位数字验证码。

这意味着,想要集成在工具中自动化查询,必须先破解它的验证码。

梳理整体逻辑

我将需求以及关键截图告诉 AI:

1
› 我在学习五笔输入,有这样一个网站,用于一些汉字的输入拆解,你帮我查看和分析一下它的结构:http://www.wangma.com.cn/query/wmhz1.asp。 我想做一个alfred 的workflow,方便我可以直接在本机操作并返回某些字是如何拆解的。比如我输入后提交有第二图,这里面的王码五笔 86/98 版的输出是我关注的,我应该如何实现。我注意到这个网页在查询时会要求输入验证码,可能是个图片,你需要解决这个限制

AI 基于网页进行自动分析,并给出了它的分析结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
• 这里把页面和请求流梳理好,并给出在 Alfred 里可复用的流程(含验证码处理思路)。

  页面结构与提交参数

  - wmhz1.asp:表单 method="post",action="wmhz2.asp",charset=GB2312;字段 query_hz(要查的汉字,最多4)、yanzhengma(验证码)、ok(提交
  按钮文字,可随便填)。
  - 验证码图片:<img src="/include/v.asp">,刷新按钮只是重新请求此 URL。返回的是 40x11 的 4-bit BMP(调色板颜色很少,噪声极轻),验证码长
  度 4。
  - 会话:首次 GET 会下发 ASPSESSIONID…,同一 cookie 必须贯穿“取验证码”与“提交表单”。
  - 错误提示:验证码错 -> “验证码错误。”;未填/汉字非法 -> “指定汉字错误。”。

  查询时的网络顺序

  1. GET http://www.wangma.com.cn/query/wmhz1.asp(存下 cookie)。
  2. GET http://www.wangma.com.cn/include/v.asp(同 cookie)拿到 BMP。
  3. OCR 出 4 位验证码。
  4. POST http://www.wangma.com.cn/query/wmhz2.asp,Content-Type: application/x-www-form-urlencoded,编码用 GB2312,带上 cookie(最好带
  Referer: .../wmhz1.asp)。
  5. 返回页是 HTML,使用 GB2312 解码后解析出 86/98/新世纪等编码段即可。

这个处理流程细节满满,感觉它对这个网站的分析已经很透彻了,将这个过程保存为文档,后续交给 AI 作为实现的上下文。

核心挑战:攻克验证码

这是整个项目最核心,也是最有意思的部分。为了解决这个 4 位数字验证码,我尝试了四种方案,见证了从“传统算法”到“深度学习”的降维打击。

方案一:模板匹配

最直觉的想法,当然也是 AI 提议下,我下载了一批验证码(20 个),切分出 0-9 的数字图片作为“模板”。识别时,拿目标图片去和模板逐一比对。

相关代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
    def solve(self, im):
        # 1. 二值化 & 降噪
        mask = binarize(im)
        # 2. 连通域切割 (Segmentation)
        comps = connected_components(mask)
        
        result = ""
        for bbox, _ in comps:
            # 提取字符图像并归一化
            norm_comp = normalize_component(comp_mask)
            
            # 3. 暴力比对所有模板 (Hamming Distance)
            # 基于 Hamming 距离的网格匹配
            best_digit, min_dist = None, float('inf')
            for digit, templates in self.templates.items():
                for tpl in templates:
                    dist = hamming(norm_comp, tpl)
                    if dist < min_dist:
                        min_dist = dist
                        best_digit = digit
            result += best_digit
        return result

刚开始用这个方法跑了一下,我在默认重试 5 次后,发现还是会出现有些验证码过失败了。仔细查看发现这个方案的一些问题:

  1. 对于旋转/位移敏感:只要数字稍微歪一点,模板匹配就会匹配错误。例如 把 3 认成了 0(正确=9301, Template=9001)。
  2. 相似度陷阱:38 极其容易混淆,68 也是高频混淆对。

方案二:使用 EasyOCR

为了验证方案一的准确率,我想引入另一个 OCR 来校验一下。于是我接入了流行的开源库 EasyOCR

1
2
3
    import easyocr
    reader = easyocr.Reader(['en'])
    result = reader.readtext('captcha.bmp', detail=0)

使用虽然简单,它需要下载一个比较大的模型,比我们上面的模板匹配要好一些,但整体感受还是不理想。比如EasyOCR 经常把背景噪点强行识别为数字,导致多读位数。例如真实是 1889,EasyOCR 读成了 18889,多读了一位,可能是噪点被当成了8。再比如0 经常被误读为 86

方案三:Tesseract

看起来这个 EasyOCR 可能对我们这个专属场景并不太合适,AI 又建议我使用老牌 OCR 引擎 Tesseract。

1
2
3
4
5
    import pytesseract
    from PIL import Image

    # 简单的直接调用
    text = pytesseract.image_to_string(Image.open('captcha.bmp'), config='--psm 7 digits')

这个结果更差,有不少图片无法识别,更别说准确率了。即使识别出来,准确度也很低,偶尔能出几个正确的。说来也神奇,它倒是有些能把 EasyOCR 认错的给整对,我也是奇了个怪。

方案四:训练自己的 CNN 模型

到这里我有点生气了,为何看似这么简单的验证码,这几个方法的效果都不尽人意呢?!

在 LLM 的建议下,我决定“杀鸡用牛刀”:训练一个专门识别这 4 位数字的卷积神经网络(CNN)。 有多个强大的 LLM 作为编程伙伴,以前想都不敢想的训练自己的模型,现在 闭眼冲 (自信又莽撞)了 :P

1. 训练数据从何而来

数据当然是直接从网页拉取,这里主要说数据的标注过程。 前面的模型或方法虽然弱, 但好歹能够识别出一些信息,我不想手动去标注几百张图,那就让两个弱模型(Template Matching 和 EasyOCR)打打工,由它们初步标注出数据,我在对有异议的内容进行人工纠错。

后续随着我们自己的 CNN 模型也具备一定能力,让它也参与标注,我们实际手工要修改的数据越来越少。看 100 张图实际也就 1-2 分钟搞定。

2. 模型设计

LLM 帮我生成了一个轻量级的 CNN 结构 (PyTorch),核心还是经典的“卷积提特征 + 全连接分类”:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
class SimpleCNN(nn.Module):
    def __init__(self):
        super(SimpleCNN, self).__init__()
        # 3层卷积提取特征
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.conv3 = nn.Conv2d(64, 128, kernel_size=3, padding=1)
        
        # 4个独立的输出头,分别预测4个位置的数字
        self.fc_digit1 = nn.Linear(256, 10)
        self.fc_digit2 = nn.Linear(256, 10)
        self.fc_digit3 = nn.Linear(256, 10)
        self.fc_digit4 = nn.Linear(256, 10)

    def forward(self, x):
        # ... 卷积 + 池化 ...
        x = x.view(x.size(0), -1) # Flatten
        # ... 全连接 ...
        # 输出4个结果
        return self.fc_digit1(x), self.fc_digit2(x), self.fc_digit3(x), self.fc_digit4(x)

训练时的 Loss Design:因为有 4 个输出,Loss 也是 4 部分的总和:

1
2
3
criterion = nn.CrossEntropyLoss()
# 计算每一位的 Loss 并相加
loss = criterion(out1, label1) + criterion(out2, label2) + \\\n       criterion(out3, label3) + criterion(out4, label4)

同时准备好训练脚本,我们只要指定数据集路径,就能开始训练:

1
python3 train_model.py <train_data_path1> <train_data_path2> ... <train_data_pathN>

3. 训练过程

本着边跑边看,我分了多轮进行训练,这样我们的标注也会越来越准确。

  • 第一轮:先抓取 20 张图,让 Template Matching 和 EasyOCR 同时跑,人工校对后,得到了这三个方案的准确率:
    ModelAccuracyCorrectTotal
    Template Matching100.0%2020
    EasyOCR65.0%1320
    Tesseract20.0%420

然后基于这个训练了第一版 CNN 模型。

  • 第二轮:基于第一轮的模型,再抓取 20 张图, 这样加前面共 40 张图,让 CNN 模型和其它方案一起跑,得到四个方案的对比数据:
ModelAccuracyCorrectTotal
EasyOCR75.0%1520
Template Matching50.0%1020
Tesseract25.0%520
CNN (Round 1)5.0%120

可以看到只经过第一轮训练的模型( 20 张图片),识别效果很差,只有 5.0%。 我们 将这20 张图加入训练集,继续训练第二版CNN 模型。

  • 第三轮:再抓取 60 张图(为了凑个 100),再次让 CNN 模型和 其它方案同时跑。
ModelAccuracyCorrectTotal
EasyOCR60.0%3660
Template Matching51.7%3160
CNN (Round 2)33.3%2060
Tesseract21.7%1360

已经看过 40 张图的 CNN 模型正确率已经从 5% 上升到 33.3%。然后让它继续学这 60 张图:

1
python3 train_model.py captchas test_verification new_batch_60

现在看了 100 张图,CNN 模型会怎么样呢?

  • 第四轮:基于第三轮的模型,再抓取 100 张图,再次对比:
ModelAccuracyCorrectTotal
CNN (Round 3)83.0%83100
EasyOCR60.0%60100
Template Matching45.0%45100
Tesseract16.0%16100

惊喜,居然从 33.3% 上升到 83.0%。这才看了 100张图呢,我这不又准备了 100 张素材,我有预感,似乎宝剑就要锻造出炉了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
python3 train_model.py captchas test_verification new_batch_60 batch_100_test

Found 20 images in captchas
Found 20 images in test_verification
Found 60 images in new_batch_60
Found 100 images in batch_100_test
Found 200 valid images for training.
Debug: First 5 labels in dataset:
  captchas/vc0015_5279.bmp -> [5, 2, 7, 9]
  captchas/vc0009_9085.bmp -> [9, 0, 8, 5]
  captchas/vc0013_6504.bmp -> [6, 5, 0, 4]
  captchas/vc0014_6842.bmp -> [6, 8, 4, 2]
  captchas/vc0006_1883.bmp -> [1, 8, 8, 3]
Training on device: mps

Starting Training...
Epoch [20/300], Loss: 0.0400, Acc: 100.00%
Epoch [40/300], Loss: 0.0077, Acc: 100.00%
Epoch [60/300], Loss: 0.0038, Acc: 100.00%
Epoch [80/300], Loss: 0.0019, Acc: 100.00%
Epoch [100/300], Loss: 0.0012, Acc: 100.00%
Epoch [120/300], Loss: 0.0009, Acc: 100.00%
Epoch [140/300], Loss: 0.0006, Acc: 100.00%
Epoch [160/300], Loss: 0.0005, Acc: 100.00%
Epoch [180/300], Loss: 0.0003, Acc: 100.00%
Epoch [200/300], Loss: 0.0003, Acc: 100.00%
Epoch [220/300], Loss: 0.0002, Acc: 100.00%
Epoch [240/300], Loss: 0.0002, Acc: 100.00%
Epoch [260/300], Loss: 0.0001, Acc: 100.00%
Epoch [280/300], Loss: 0.0001, Acc: 100.00%
Epoch [300/300], Loss: 0.0001, Acc: 100.00%

Model saved to captcha_cnn.pth

现在这个模型已经经过了 200 张图的训练,让我们拭目以待!

  • 第五轮:我拉了全新的 100 张测试集,让模型再跑一次,这一次我没有标注,我将 CNN 和其它不同模型的识别结果对比,看下结果。Status 列中的状态是以 CNN 的角度来看和其它模型的差别:
  • ✅ Trusted:模型和至少 2 个其它模型一致,可信。
  • ❌ Unique:模型和所有模型都不一致,需要确认。
  • ❓ Possible:模型只和其中一个一致,需要人工判断。

以下节选一部分:

FileOriginalTemplateEasyOCRTesseractCustom CNNStatus
vc0000.bmp00008681868186848681✅ Trusted
vc0001.bmp00016560656065686560✅ Trusted
vc0002.bmp000290019304193049301❌ Unique
vc0003.bmp00034710471074184710✅ Trusted
vc0004.bmp00045325582252255225❓ Possible
vc0005.bmp0005048904890489✅ Trusted
vc0006.bmp00066918699869986998✅ Trusted
vc0007.bmp0007364386488648❓ Possible
vc0008.bmp00083626662666266626✅ Trusted
vc0009.bmp00090602060286820602✅ Trusted
vc0010.bmp00102458274724532453❓ Possible
vc0011.bmp00115001538153845381❓ Possible
vc0012.bmp00128467847678467❓ Possible
vc0013.bmp00138531054135343531❌ Unique
vc0014.bmp0014715791979197❓ Possible
vc0015.bmp00154360436043664360✅ Trusted
vc0016.bmp0016976997497099769❓ Possible
vc0017.bmp0017921502750275❓ Possible
vc0018.bmp00182025202528252025✅ Trusted
vc0019.bmp0019350485248524❓ Possible
vc0020.bmp002086903698326983698❓ Possible

不看不知道,一检查吓一跳,这些CNN 和其它模型不一致的结果中,惊人的发现————CNN 识别的正确率高达 100%,为了确认不是花眼,我再次检查了这 100 张图,完全正确!

ModelAccuracyCorrectTotal
CNN (Round 4)100.0%100100
EasyOCR63.0%63100
Template Matching38.0%38100
Tesseract17.0%17100

这意味着,在识别这个网页的验证码这个特定任务上,我们的小模型已经“超神”了。它不仅完爆了 Template Matching,也大幅超越了 EasyOCR,而整个模型文件只有 7.9MB 大小。 这或许就是我们自己的领域专用视觉模型了。 有了它我很有信心可以将验证码的识别重试次数从 5 改到 1,咱不太可能失败了,哈哈,满意的笑了。

组装 Workflow

攻克了验证码,剩下的就是组装一下 Python 胶水代码给Alfred Workflow调用了。 AI 火速编写了 alfred_wubi.py,流程如下:

  1. 请求页面1:获取 Session 和验证码图片。
  2. 模型推理:加载我的 captcha_cnn.pth,瞬间识别出验证码。
  3. 构造 Payload:带上验证码 POST 请求页面 2。
  4. 解析 HTML:用 BeautifulSoup 提取五笔编码和拆字图 URL。
  5. 输出:JSON 格式给 Alfred 显示。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 核心代码片段
from cnn_inference import CNNInference

# 加载模型 (仅需一次)
predictor = CNNInference(model_path="captcha_cnn.pth")

# 预测
image_path = Path("temp_captcha.bmp")
code, conf = predictor.predict(image_path)
print(f"识别结果: {code}, 置信度: {conf}")

# ... POST request with code ...

这里对于 Python 库的依赖,我们可以使用.venv 来管理,避免和系统环境冲突。 你可以这样使用这个项目:

1
2
3
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt

然后使用 .venv/bin/python 来运行 python alfred_wubi.py

后记

本文所提到的 Workflow 以及训练整个过程及数据已经开源在GitHub,你可以在这里查看:https://github.com/kevin1sMe/alfred-wubi-workflow。

文章到这里就是尾声啦,这其实是周末折腾其它过程中的小插曲,感觉挺有意思给大家一起分享下。现在我们打造的这个五笔反查工具,能够让我们告别某些生僻不会打的字。就像我前面截图中,“肃”字你会打吗?更关键的是通过这次折腾,让我更深刻感受到,在 AI 辅助编程的加持下,个人开发者解决问题的边界被极大地拓宽了

以前遇到“需要训练一个模型来识别验证码”这种需求,可能因为觉得门槛太高就放弃了。但现在:

  1. 代码:LLM 帮我写好了 CNN 结构、帮我准备各种脚本,配合我一步步训练。
  2. 数据:LLM 帮我设计了自动化的标注脚本。

我只需要专注于聊清需求、定义问题和决策方案。从零开始到模型落地,只用了短短1-2个小时,这或许就是 AI 时代的编程范式吧。但是要整理出本文,为了将过程数据完整重现,我又重新一步步跑了一次,严谨的记录和对比了各个轮次的效果,再加上代码开源及整理,这里居然花了周末的一整个晚上。看在这么认真的份上,小手点个关注、点个赞不过分吧:)

持续折腾,不断进步,我们一起加油。期待下次有机会继续分享更好玩的内容,再会!

我是个爱折腾技术的工程师,也乐于分享。欢迎点赞、关注、分享,更欢迎一起探讨技术问题,共同学习,共同进步。为了获得更及时的文章推送,欢迎关注我的公众号:爱折腾的风

扫码关注公众号