闲暇之时偶然翻出了一个几年前用Java写的预约课程工具。看这创建时间,尘土已相当厚了。

* @version $$Id: Main, v 0.1 2018/5/9 12:49 

在看到她的那一刻,瞬间勾起了有些遗憾的记忆。说起这位“老友”,当时还真帮了我的大忙。

话说是因为要在网站上预约课程。预约课程的流程与在电商平台购物有些相似,登录,根据日期查询当天是否有可预约的课程,如果有则从列表中找到合适的时间段,最后预订。经过几次这样的操作后便觉得实在麻烦。程序员最怕的就是这样的麻烦!如果别人也觉得上述情况麻烦,那Ta只能忍着继续在页面上点来点去,但作为一名讨厌繁琐的程序员可不会惯着它。随即,F12走起。综上,我这位“老友”便诞生了,而且之后还给我带来了意外惊喜。

这个预约课程工具本身很简单,只用了java.net包下与HTTP相关的类便很快搞定了登录和约课的功能,但这两个功能在操作时都需要将图片验证码中的字符一并提交给服务器。完成代码后,之前需要在页面上完成的输入,则改为在代码启动后的控制台进行输入。那图片验证码是怎么在控制台输出的?先来看看验证码图片的样子。

这是放大了一千多倍的验证码图片,就是这样的。说白了,上面的每个像素点也只是不同的数字而已,数值越小则颜色越深。那么上面这个验证码图片经过代码处理后,打印到控制台就是下面这样的。

                  0
0                                      0                0                      0  0                              0               0         0000 000 000000000    000                 0000000  000000000    000                 00           0       0000
0    00    00 00           0       00 00      0         0  00   0  00000 0      0       0  00
0     00  00  00 0000      0      00   00   0           0    0  00  00           00      000000               0  0000   000          00    00000000               0000  000           00    00     00              000   00           00   000     00              00    00          000   00      000             0             0                          

怎么样,经过处理后打印出来还是很好辨识的吧。将肉眼辨识出来的验证码字符从控制台输入给程序后便可直接预定指定时间的课程,而登录,查询,浏览,选择这些在页面上预约操作所必不可少的步骤则被全部省略掉了。如果在自己指定的时间没有可供预定的课程,则程序给出相应提示,如果预约成功,则返回预约成功课程的时间地点等信息。在测试代码的过程中,还给了我前面提到的意外惊喜————程序可以预约在网站上查询不到的课程。这个意外收获直接让我提前完成了所有课程,简直有点降维打击的感觉,如同菜还没上桌,你已经在厨房吃饱了。不过作为程序员要注意,这虽然算不上是一个bug,但肯定算是一个缺陷,即前台与后台的没有统一。

从这位“老友”第一次为我成功预定课程后,我就一直想着怎么再把她捯饬的漂亮些————把人肉识别图片验证码变成自动识别,这样就又可以省去敲四下键盘的巨大工作量了。为此,当时还特意买了一本《模式识别》。之所以买这本书而不是关于机器学习的书,是因为打算用数学模型将每个字符描述出来,而且第一感觉是描述字符的数学模型无法通过机器学习得到,得自己通过对字符的规律进行总结后给出(其实《模式识别》中的内容与机器学习的内容有很多相似之处)。不过后来随着课程的完毕,自动识别图片验证码的想法也就被搁置了,直到今天她又重见天日。如今某家要誓补当年之憾。

前段时间抽空用Java弥补了下这个当年的遗憾,总的来说识别效果还不错。具体的识别方法也是在以下将要介绍的实现过程中几经调整。

之所以人可以识别图片中的那些字符,是因为我们认识图片中的每一个字符,但如果让一个未识字的幼儿来指出这些字符是什么,那么他肯定是做不到的。如果想让他辨识字符,那就要先告诉他这些字符是什么。这时,计算机就是这个尚未识字的幼儿。首先要做的就是让计算机认识并记住它要识别的每个字符。这些字符包括0~9,a~z,A~Z。可以用数学模型或记录有效像素两种方法来让计算机认识它将要识别的字符。用数学模型给出每个字符的描述,也就是说要写出六十多个方程组,这工作量实在太可观了。虽然数学模型可以节省空间,并在识别效果不准时可对其进行参数调整,不过这种方法会出现与机器学习中的过拟合类似的问题,也就是对一个字符的某个样子识别准确了,但同一个字符的其他样子的识别能力就降低了,还可能把其他字符识别成这个字符,属于按下葫芦起了瓢,还是算了吧。那就只能用记录描述字符的有效像素的方法来让计算机认识每个字符了。这个方法的缺点是要记录每个字符的所有可能的样子,如果有一个样子没记录,那么就识别不出来或是识别成其他字符。不过也有优点,那就是简单,只要发现了未识别的字符,就将该字符的有效像素描述记录下来,并给与标记即可。总结就是,数学模型复杂但灵活,像素描述简单但死板。事儿就是这么个事儿,情况就是这么个情况。

用记录字符有效像素(文中提到的有效像素指的是表示出字符轮廓的像素)这种方法则需要在识别验证码前保存同一个字符的所有可能的样子,我把这称为有效像素描述。以下就是以文本形式保存的字符像素描述的内容。

   0    00    000    00    000   00    0    000   00         000    0   0000   00     00   0000   00    0   0000   00    0   0000   000  0000  000     000  0000   00    00  0000   00    00  0 00   000  0000  00       00  0000  00     00  0000  00     00  0000  0000  00 0  00       00  0  0  00     00  00 0  00     00  00 00 0000000  00 00       00000  00 0      00  0  00 00     00  0  00 000000  0000         0000  0000       0000  00 00      0000  00 000000  0000         0000   000       0000  0000       0000   000 0000   000         0000   000       0000   000       0000   000 000    000         000    000       000    000       000    000 00    000          00     00        00    000        00    000

这是大写的“W”在图片验证码中可能的样子,当然这四个有效像素描述分别位于文件名中包含大写W的四个文件中,像下图这样。

用这样的方法就需要在发现未能识别的字符时,将这个字符的有效像素描述保存为上图中的文件。缺点是,随着文件的增多,识别速度会变慢,而且会造成误识别为其他字符的情况增多。所以上述文件并不是越多越好。

有了上面这些文件,就距离让计算机识别出图片中的验证码进了一步。此时计算机已经知道了每个字符在验证码图片中的样子了。接下来就可以开始进行比较了。在比较有效像素的过程中,同时计算与字符集中每个字符的相似度,并返回相似度最高的那个字符对应的文件名,从而也就识别出了图片中的字符。

上面的文本文件中保存的是单个字符的有效像素描述,但验证码图片上的那些字符可是连接一起的。所以在与字符描述集中的字符进行比较前先得将图片中的字符切分成一个一个的,然后再进行比较。这也是图片验证码最常用的识别手段。现在,真正的重点来了————从图片中切分出可以识别的字符。

从前面的图片可以看出,验证码图片上的字符间的像素值显然是高于用于显示字符轮廓像素的值的。因此,这自然的就成为了字符的切分标准。仿佛又接近了目标一步。然而,现实总是残酷的,如果能仅凭这个标准就可将所有图片都准确的切分成四个可供识别的部分那可就太美好了。来看下这张图片。

K和W还好说,可是9和 j 的下方存在2个有效像素在同一X轴上,所以用有效像素个数为0的标准来拆分上面这个图时,只能切分出3个字符,即K,W,和9j,9和j是连接在一起的。如果这个切分标准放宽些,比如当X轴上有一个,两个或三个有效字符也允许切分,那确实可以将9和 j 切分开,但对于其他图片就有可能出现误切,比如下面这个两个图。

如果以小于等于两个或三个有效像素的标准来切分上面两个图的话,则前一个图中的L和后一个图中的 m 将被拆分成两个以上且无法识别或是被识别成其他字符的部分。因此,要是切分后得到的字符少于或多余4个的话,就不宜再用有效像素法进行处理了,必须换个思路。

最初所采用的切分方法是:当切分出的字符少于4个时,就找出其中最宽的那个,然后放宽切分条件再次进行切分。这种方法有一定的效果,但问题是最宽的那部分并非一定是由多个字符组成的。比如,三个小写 j 也没有一个大写的W宽,还是会出现误切分,那就换一种办法。从所有切分出的部分中找出在X轴上有效像素最少的那段,然后以这个X轴为界再次进行切分。只能说也有一定效果,但当识别出了一个图片后,对其他图片的识别能力便减弱了,这显然不行。随后,试着用这两种方法分别再次处理无法切分出4个字符的图片。因为这里要得到两种方法怎么交替结合运行才能给出最高相似度,看到“最”这个字,一下想到了用动态规划算法试试。之后结合动态规划,将上述两种方法结合了起来,效果还是不理想,而且代码也越来越复杂。不行,还得换思路。

事实证明,当没有好的思路的时候,先暂时放下,干点别的事,也许灵感大爷自己就找上门来了。这不,骑车出去浪一圈,打会儿篮球,思路就有了。不是有个KMP算法吗,可以借鉴一下。既然更换切分标准会误伤友军,那干脆就不切分了,反正现在已经有了一堆字符描述文件,直接比对不就可以了。

具体做法就是从候选字符集中取一个字符描述,然后根据这个字符描述的宽度,从图片右侧开始比对与该宽度相等的范围内所有像素,比对完之后计算出一个相似度,然后再取下一个字符描述重复这个过程,直到比对完全部字符集,随后便得到了最高相似度的那个字符描述,接着缩小图片范围,重复上述全部过程,直到图片范围缩小到0。核心代码如下。

    private static final float SIMILARITY_THRESHOLD = 0.9f;private void selectMaxSimilarityChar(CharRange charRange) {//从图片右侧开始依次比对字符集所有字符,取相似度最高者if (!charRange.alreadyMatched) {MatchedChar matchedChar = matchEqualsCharWidth(charRange);if (matchedChar != null && matchedChars.size() < CHAR_COUNT) {matchedChars.add(matchedChar);} else {charRange.alreadyMatched = true;stack.add(0, charRange);}} else {Map<CharRange, MatchedChar> recognizeResult = matchFromEndToStart(charRange, new CharRange(charRange.startX, charRange.endX));MatchedChar bestMatchedChar = new MatchedChar(null, 0, null);for (MatchedChar matchedChar : recognizeResult.values()) {if (matchedChar.similarity > bestMatchedChar.similarity && matchedChar.similarity > SIMILARITY_THRESHOLD) {bestMatchedChar = matchedChar;}}if (bestMatchedChar.aChar != null) {matchedChars.add(bestMatchedChar);charRange.endX = bestMatchedChar.charRange.startX - 1;charRange.alreadyMatched = false;stack.add(0, charRange);} else {if (matchedChars.size() == CHAR_COUNT) {return;}recognizeResult = matchFromStartToEnd(charRange);bestMatchedChar = new MatchedChar(null, 0, null);for (MatchedChar matchedChar : recognizeResult.values()) {if (matchedChar.similarity > bestMatchedChar.similarity && matchedChar.similarity > SIMILARITY_THRESHOLD) {bestMatchedChar = matchedChar;}}if (bestMatchedChar.aChar != null) {matchedChars.add(bestMatchedChar);charRange.startX = bestMatchedChar.charRange.endX + 1;charRange.alreadyMatched = false;stack.add(0, charRange);} else {log.out("未能给" + charRange + "匹配到任何字符");}}}}
    private MatchedChar calculateSimilarity(CharRange charRange) {MatchedChar bestMatchedChar = new MatchedChar(null, 0f, charRange);for (Map.Entry<boolean[][], Character> candidateChar : CharSetLoader.getCharDict().entrySet()) {int candidateCharWidth = candidateChar.getKey()[0].length, recognizeCharWidth = charRange.endX - charRange.startX + 1;//过滤掉宽度不一致的字符if (recognizeCharWidth != candidateCharWidth) {continue;}//以候选字符的矩阵进行比较,可以得到很高的相似度,但对于有重叠情况的字符有误判,如 q 和 oMatchedChar mc = calculateSimilarity(charRange, candidateChar);if (mc.similarity > bestMatchedChar.similarity) {bestMatchedChar = mc;}}return bestMatchedChar;}private MatchedChar calculateSimilarity(CharRange charRange, Map.Entry<boolean[][], Character> candidateChar) {float similarity = getBingoByCandidateChar(charRange, candidateChar) / (candidateChar.getKey().length * candidateChar.getKey()[0].length);return new MatchedChar(candidateChar.getValue(), similarity, charRange);}

这个方法的效果要优于之前的两个方法,不过也存在误判,这取决于字符描述集中不同字符的相似程度,这也就是之前所说的,字符描述集合中的文件不易太多的原因,就像布隆过滤器一样,随着数据的增多,误判率也会随之增长。

未识别文件:[{文件名:DBYc | 识别结果:DEYc} {文件名:ewfr | 识别结果:Efr} {文件名:jwwk | 识别结果:JWk} {文件名:l4am | 识别结果:AM} {文件名:phcw | 识别结果:HCw} {文件名:rmlf | 识别结果:rmf} ]
共计 83.0 文件,识别正确数量:76.0 正确率:0.91566265

程序识别的正确率还是比较邻人满意的。

因为上述识别代码只是针对那一种图片而写的,为了验证识别程序的通用性,于是在网上找了一些与之前进行识别的不一样的验证码图片,比如下面这些。

 

当然,一开始是没能成功识别的,问题出在对字符像素和背景像素的区分上,之后便重写了字符像素和背景像素的识别代码。终于,经过不断的调整,得到了一个更为通用的图片验证码识别程序。

      0000000000   000 00 0          000     0000         0000     0     00   00       000         000      000 00     0      0    0      00           000       00 00   0       00    0     00            0 00    0 00 00   0       00    00    0 000         0 00    0 00 000000       00   00    000  00        0 00   0  00 00   0        0000      00    0        0  00  0  00 00   0       00  0      00    00       0  00  0  00 00           00  00     00    00       0   000   00 00      0     0   0      0    00       0   000   00 00     0     00    0     00  00        0    0    00 0000000000    00000   0 0   0000       00000  0  000000000            0000000000     0000                        000000           0000000000    0000000                      000   00                 000   000   00                      00    00                 00    00     00                     00          00 000       00     0      00              000000 00 000      00000       00             00         00000000000 00000000    000        00             00    00000000000       000    00    00         0             0000000000000            00      0   00         00         00000000000                  00      0   00        00     00000000000                       0      00   0        000000000000000                           00     00   0     00000000000    00                            000   000   00000000000         00                             0000000000000000000000000000   00000000                       000000000000     00000000000  000000000                       00000000000                                                        00000                                                              0000    00000     0000      000       0  00   00            0    0  00      0   00 00        0    00   0      0    00  00      00     00 00      0          0        0       00      00       0  00        0    0  00      00000     000     0000      000   
                               00000000000                                      000000              0000000000000                                    00000000             0000000000     0000000000000                                    0000000000            000000000000   000000000000                                   000000000000            000000000000  000000000000            000000000             00000    000             000000000000  0000000 0000           0000000000             0000      00             000000  00000  0000    0000         0000000000000           00000                     00000   00000  0000   00000        0000000  000000         00000                      00000  000000         0000          0000000 000000         0000                       0000000000000         0000          0000000 000000         0000   000                 00000000000000          0000           00000  000000         0000   00000000            000000000000           00000           000000000000          0000   00000000            000000000000          000000           000000000000          0000   0000000             00000 00000           000000           0000000000            0000     0000              00000000000          000000            00000000000           000000   0000              0000000 00000          000000            000000000000          000000   0000              0000000  000000         00000             000000 000000          000000000000              000000000 0000000       000               000000 0000000000       000000000                0000000 0000000                         000000  000000000          000                   00000  0000000                          000000 0000000              0                   00000                            0000000 000000                                  00000000  00                                     0000000                                          000000                                    
                       000                                                  00000                                                 0000000                                                 000  000                                                0       000   00                                                 00000    000  0000                           000                   000000   00   00         0000000            00000
0000     00 000   00  000        000000000           00000
00000    00  00   00  00         000   000  0000    000 000      00000
0  0000  000 00  000 000 0       00  00000 000000    00 000   0000000000
0   0000 000 00  00  0000000     00 00000  00 000    00  0000000000   000
0     000000 000 00  000000000  000 000    00  00    000 0000000       000
00 00  00000 000 00  00    0000 00  00     000 00    000 0000   000000  00
00 000   000  00000         000 00  00     000 000    00 00000 00000000 000
00 00000  000 00000    0000  00000 0000000  00 000    00  0000 000   00  00
00 000000     00000   000000 00000 00000000 00  00    000 0000 000   00  00
00  00 0000   00000   00  00 0000  000   000000 00    000 00000 0000000 000
000 00  0000  000000 000 000  0000        00000 000    00 00000 000000  00
000 000   000  00000  00000  000000000000  0000 000    00  0000 0000   000
0000000    000 00000   0000  0000000000000 0000  00    000 00000     0000   0000000000000 000000       000  000   000  0000 00    000 00000  000000    00  0000000000000 0000   0000    000   00  0000 000   000 00000 00000      000 00   0000 00000000000000    000000000 00000 000   00  00 00  00        000 000    0000000000000000     00 00000  00 00  0000000  00 000 00        00 000    000000  000000000000 00  00   000 000  00000  000  00 000       00  00     00000       0000000000000000000   000       000   00  00       000000      000               0000000000000000000   000000   000 00       000000                           000000000000000000000000000000000000000000000                                        0000000000000000000000000000000000       000        

该说不说的,那个空心的验证码实在是太损了!

经过这么一通折腾,积攒了下面这些图片,当然不能让它们闲着,等有时间整理下Spark ML和TensorFlow对这些图片处理的资料,然后再来码一篇。

后记

语音识别其实跟图片验证码识别十分相似,声音在经过处理后直观的展示到屏幕上就是波形,而波形跟像素一样,在计算机中同样是数字,有了数字就可以跟语音库中的数据进行比对,然后进行识别,语音库也是将每个字拆分为独立文件,识别出来后再组合起来,这也就是Siri们说话有顿挫感的原因了。

不用OCR,如何实现图片验证码的自动识别相关推荐

  1. Python OpenCV 图片滑块验证码 滑块图片验证码 快速自动识别方案 代码简单 模板匹配识别 识别成功率达90%+

    前言 通过上一篇的文章大家已经对图片滑块验证码已经有了初步的了解,图片滑块验证码的核心关键在于图片识别接下来接入讲解.因为初版滑块图片识别虽然能识别验证码,通过一些策略调整也相对提高了一些图片识别率, ...

  2. 自动识别图片验证码登录

    自动识别图片验证码登录 目标:从需要会有登录的网站抓取数据. 场景:A网站需要会员登录才能查阅信息,A网站采用了AntiForgery防止XSRF攻击. 创建windows应用,采用webBrowse ...

  3. 百度云 OCR 识别图片验证码

    操作系统:Mac OS Python版本:3.7.2 OCR:百度云 遇到的问题: API测试过程中,遇到API Resopnse 为图片验证码的情况,需要对图片进行识别得到text code,进行断 ...

  4. java自动识别图片验证码插件_JMeter开发插件——图片验证码识别

    我们在性能测试中总会时不时地遭遇到来自于应用系统的各种阻碍,图片验证码就是一类最常见的束缚,登录或交易时需要按照图片中的内容输入正确的验证信息后,数据才可以提交成功,这使得许多性能测试工具只能望而却步 ...

  5. python—简单数据抓取四(利用超级鹰的ocr识别图片验证码模拟登录超级鹰网站、利用百度云的ocr识别自如租房网价格图片获取到自如网的价格)

    学习目标: python学习二十四 -简单数据抓取四 学习内容: 1.利用超级鹰的ocr识别图片验证码模拟登录超级鹰网站 2.利用百度云的ocr识别自如租房网的价格图片,获取到自如网的价格数据 1.利 ...

  6. 【成长笔记】图片验证码识别

    记得很早以前,我对如携程飞猪等第三方平台购买火车票不用输入验证码感到很--牛!百度后发现其可能是实现了自动打码,或者说机器自动识别验证码,我很好奇. 后来,当我觉得我必须要给自己找些有趣的知识来学习的 ...

  7. C#-调用OCR组件识别图片文字

    C#调用OCR组件识别图片文字 图片识别的技术到几天已经很成熟了,只是相关的资料很少,为了方便在此汇总一下(C#实现),方便需要的朋友查阅,也给自己做个记号. 图片识别的用途:很多人用它去破解网站的验 ...

  8. 字符识别Python实现 图片验证码识别

    字符型图片验证码识别完整过程及Python实现 1   摘要 验证码是目前互联网上非常常见也是非常重要的一个事物,充当着很多系统的 防火墙 功能,但是随时OCR技术的发展,验证码暴露出来的安全问题也越 ...

  9. python 验证码图片 模拟登录_【python】带图片验证码的登录自动化实战

    近期在跟进新项目的时候,整体的业务线非常之长,会一直重复登录退出不同账号的这个流程,所以想从登录开始实现部分的自动化.因为是B/S的架构,所以采用的是selenium的框架来实现.大致实现步骤如下: ...

最新文章

  1. TensorRT和PyTorch模型的故事
  2. 用完U盘忘记安全退出了,会造成数据丢失吗
  3. framebuffer
  4. Oracle 数据库直接执行本地sql文件、sql脚本实例演示
  5. 聊聊网易技术如何帮教育行业开出花
  6. php+compose+使用,docker使用 docker-compose配置PHP环境(php+nginx+mysql)及启动
  7. SpringMVC与Ajax交互
  8. 9.27模拟:至暗时刻
  9. 软考信息安全工程师学习笔记四(1.4 信息安全标准化知识)
  10. 【X264系列】之编码YUV的内存流程
  11. 程序员:我们都在为错误买单!
  12. 【note】Swift之闭包表达式的几种简化缩写形式
  13. 主席树-----动态开点,不hash
  14. 高速PCB设计系列基础知识
  15. 小米11青春版 MIUI12安装谷歌条件GMS点击登录没反应的解决办法
  16. Java Web 前端到后台常用框架介绍
  17. 强学习器------随机森林
  18. 35岁后,互联网数据分析人的出路在哪里?
  19. 20190527-陈静初-文件操作-笔记
  20. 大数据学习计划【2019经典不断更新】

热门文章

  1. java设计最简单记账本_Springboot简单练手的记账本
  2. 【matlab 激活】2017.11.11日后matlab统一过期需激活解决办法
  3. MySQL数据库综合练习题
  4. 互联网反欺诈体系之二
  5. CASS道路横断面线,如何折线变直线?
  6. 自动驾驶 | 传感器融合–自动驾驶的关键技术
  7. 如何成就一个App 游戏界的百万富翁
  8. 解决集成 editor.md编辑器时,报 editormd is not defined的解决办法
  9. python素数生成器_在python中用滤波器和生成器生成无穷素数
  10. 树莓派循迹+图片识别(模板匹配)小车 C++