验证码程序识别小试
昨天上图书馆查了下我借了好像快一个月的书,发现竟然已经过期1天,于是今天赶紧还了去,最后还是被扣了钱。后来我想了一下,何不写一个程序,跑在我树莓派或者某台云主机上,每天零点自动登录图书馆系统给我查询有没有什么该还的书籍之类的,在到期前几天发送邮件通知我。
在大致了解了一下学校那个”我的图书馆”的功能和业务逻辑,发现那个网站用了个验证码,这下跪了,验证码识别向来就是爬虫和模拟登录的一大难题。验证码英文为CAPTCHA(Completely Automated Public Turing test to tell Computers and Humans Apart,全自动区分计算机和人类的图灵测试),就是用来屏蔽掉非人类的机器人登录。这也是12306那些访问量极大,而且有非常严重的刷票软件隐患的网站存在验证码的原因。在超过一定时间后,验证码可能失效,原因在于验证码的生成程序是固定的,不会自己改变算法产生其他格式的图片验证码。而经过一段时间的积累,势必会有部分软件开发人员通过结果反推出验证码的生成规律,从而找到与生成图片的算法逆向进行的算法,实现图片转回验证码生成时随机产生的数字。
不过还好,图书馆后台那个登录的验证码只是数字在变,颜色、位置、杂纹都没有变化,这样识别起来就很轻松了。
之前在网上看过一篇文章,是用js去破解类似形式的验证码,主要的算法思想就是将整幅图片灰度化,变成黑白图片,随后根据一个阈值,将整幅图片转换为一个与图片像素数量相同的数组,数组中0表示较深的颜色,1表示较浅的颜色。随后从对应位置中取出字符(此时是使用0/1矩阵表示字符的具体形态),随后将这个数组整体与事前已经穷举完毕的所有字符形状数据数组进行完全匹配,匹配上之后代表此数字即为对应的数字。
因为不必要任何图形界面,还要有网络,图像处理等功能的支持,我选择了Python。经过搜索一番,我发现需要的模块比较特殊的有两个,一个是PIL(Python Image Library)中的Image模块,另一个是numpy模块。前者负责图像解析和产生图像原始数据,后者负责将原始数据转换为数组以便操作。Python中本身不带这两个模块,这里我推荐使用Anaconda集成包,里边包含了一个python2.7和大量的第三方库,用起来也是很方便。
首先,来看一下需要识别的验证码:
随后我先写了一下将图片下载,存入Image,交给numpy处理的程序,随后依次打印出结果:
# -*- coding:utf-8 -*- from PIL import Image import httplib import StringIO import numpy Loginer=None try: Loginer=httplib.HTTPConnection('211.*.*.4',80,timeout=30) #创建HTTPClient对象,地址抹掉防止攻击 Loginer.request("GET","/reader/captcha.php") #请求图片地址 HTTPResponse=Loginer.getresponse(); #获得相应 IGif=Image.open(StringIO.StringIO(HTTPResponse.read())); IRaw=numpy.array(IGif) Line=' ' for x in range(0,60): Line=Line+str(x%10)+" " print Line; for x in range(0,36): Line=str(x%10)+' ' for y in range(0,60): Line=Line+str(IRaw[x][y])+" " print Line except Exception,e: print e finally: if Loginer: Loginer.close();
运行结果如下:
图中最左侧是行号的末位,第一行为列号的末位,用末位是因为防止字符过长顶歪矩阵,不方便观察。发现文字的轮廓在远看整个数组的情况下已经依稀可见。随后开始针对整个二维数组进行分析:
从上面这个图片可以看出,整个验证码由两部分组成,一部分是外围的空白区域,这部分对于识别没有任何帮助;另一部分是位于中央的字符区,是识别操作主要的区域,所以随后添加代码将重心区域枚举:
# -*- coding:utf-8 -*- from PIL import Image import httplib import StringIO import numpy Loginer=None try: Loginer=httplib.HTTPConnection('211.*.*.4',80,timeout=30) #创建HTTPClient对象,地址抹掉防止攻击 Loginer.request("GET","/reader/captcha.php") #请求图片地址 HTTPResponse=Loginer.getresponse(); #获得相应 IGif=Image.open(StringIO.StringIO(HTTPResponse.read())); IRaw=numpy.array(IGif) Line=' ' for x in range(6,50): Line=Line+str(x%10)+" " print Line; for x in range(16,26): Line=str(x%10)+' ' for y in range(6,50): Line=Line+str(IRaw[x][y])+" " print Line except Exception,e: print e finally: if Loginer: Loginer.close();
运行后发现整个图片幅面减小至正好和字符边缘相切:
随后我将每个字符区域继续划分,按行连接,通过数次枚举得出每个字符对应的识别字符串:
Char 0:11100111110000111001100100111100001111000011110000111100100110011100001111100111 Char 1:11100111110001111000011111100111111001111110011111100111111001111110011110000001 Char 2:11000011100110010011110011111100111110011111001111100111110011111001111100000000 Char 3:10000011001110011111110011111001111000111111100111111100111111000011100110000011 Char 4:11111001111100011110000111001001100110010011100100000000111110011111100111111001 Char 5:00000001001111110011111100100011000110011111110011111100001111001001100111000011 Char 6:11000011100110010011110100111111001000110001100100111100001111001001100111000011 Char 7:00000000111111001111110011111001111100111110011111001111100111110011111100111111 Char 8:11000011100110010011110010011001110000111001100100111100001111001001100111000011 Char 9:11000011100110010011110000111100100110001100010011111100101111001001100111000011
剩下的事情就简单了,按照相同规则取出数据,随后逐行进行字符串的拼接,将最终的结果传给一个函数进行匹配处理得到最终结果,匹配函数先写一下:
def getChar(str): sample=[ '11100111110000111001100100111100001111000011110000111100100110011100001111100111', '11100111110001111000011111100111111001111110011111100111111001111110011110000001', '11000011100110010011110011111100111110011111001111100111110011111001111100000000', '10000011001110011111110011111001111000111111100111111100111111000011100110000011', '11111001111100011110000111001001100110010011100100000000111110011111100111111001', '00000001001111110011111100100011000110011111110011111100001111001001100111000011', '11000011100110010011110100111111001000110001100100111100001111001001100111000011', '00000000111111001111110011111001111100111110011111001111100111110011111100111111', '11000011100110010011110010011001110000111001100100111100001111001001100111000011', '11000011100110010011110000111100100110001100010011111100101111001001100111000011' ] for i in range(0,10): if str==sample[i]: return i return -1;
随后与生产sample相同算法处理每次接受的图像数据数组.
for shift in range(0,4): Line='' for y in range(16,26): for x in range(6+shift*12,14+shift*12): Line=Line+str(IRaw[y][x]) print "Character "+str(shift)+":"+str(getChar(Line));
最终获取得到验证码的内容。
最终程序我目前只是写了验证码识别部分,还没有做模拟登录。
# -*- coding:utf-8 -*- from PIL import Image import httplib import StringIO import numpy Loginer=None Crawer=None def getChar(str): sample=[ '11100111110000111001100100111100001111000011110000111100100110011100001111100111', '11100111110001111000011111100111111001111110011111100111111001111110011110000001', '11000011100110010011110011111100111110011111001111100111110011111001111100000000', '10000011001110011111110011111001111000111111100111111100111111000011100110000011', '11111001111100011110000111001001100110010011100100000000111110011111100111111001', '00000001001111110011111100100011000110011111110011111100001111001001100111000011', '11000011100110010011110100111111001000110001100100111100001111001001100111000011', '00000000111111001111110011111001111100111110011111001111100111110011111100111111', '11000011100110010011110010011001110000111001100100111100001111001001100111000011', '11000011100110010011110000111100100110001100010011111100101111001001100111000011' ] for i in range(0,10): if str==sample[i]: return i return -1; try: Loginer=httplib.HTTPConnection('211.*.*.4',80,timeout=30) #创建对象 Loginer.request("GET","/reader/captcha.php") #请求地址 HTTPResponse=Loginer.getresponse(); #获得相应 IGif=Image.open(StringIO.StringIO(HTTPResponse.read())); IRaw=numpy.array(IGif) Line=' ' for x in range(6,50): Line=Line+str(x%10)+" " print Line; for x in range(16,26): Line=str(x%10)+' ' for y in range(6,50): Line=Line+str(IRaw[x][y])+" " print Line for shift in range(0,4): Line='' for y in range(16,26): for x in range(6+shift*12,14+shift*12): Line=Line+str(IRaw[y][x]) print "Character "+str(shift)+":"+str(getChar(Line)); except Exception,e: print e finally: if Loginer: Loginer.close();
我进行了若干次执行,发现结果还是比较准确的。
注意:本文仅适用于一些形同虚设的验证码,不适用于制作贴吧水帖机之类的东西,不能解决复杂的验证码.本文只是针对简单的图像识别.
博主感觉能从你这里学到很多东西呢