diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))

图片字母验证码破解1——简易入门 Nuller
前言 搞爬虫有一段时间了,也和一些常见的反爬措施斗争了很久,验证码是反爬措施其中之一,diss它也是比较有趣有挑战性的。由于第一次搞验证码破解,虽然之前学过一些opencv和机器学习,但是这样实战实在有些酸爽,于是百度了一堆资料,在此做一下总结,并写下我踩到的坑,分享给大家,致力于写出更通俗易懂更面面俱到的破解验证码系列博客。
前段时间用了不到一天的时间,完成了对最简单的验证码的识别,最近才有空整理到博客上,分享给大家。
百度了一番大神对图像型验证码的破解经验,其中最基础最小白操作的就是Pytesser,Tesseract。这样做的缺点就是泛(bu)化(neng)能(geng)力(hao)比(de)较(zhuang)差(bi),没有让程序猿好好体会破解验证码的乐(nan)趣(du)。如果是真的想深入了解破解验证码的同学们不用紧张了,此系列不会接触到Pytesser,Tesseract,大可放心,我们自己来!
(嗯!这会我们不做代码的搬运工了!我们自己生产!)
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

面向人群

  1. 爬虫爱好者
  2. 人工智能爱好者
  3. 前面两个听起来有点高大上,可我只会python怎么办,没关系放心继续看 : )
项目目录说明 alpha
——data(用于存放图片的数据文件夹)
————source(爬取到的图片)
————cut(分割图片后的文件夹)
————test(测试集)
————train(分好类的训练集)
——create_feature.py(用于对图片特征进行创建的脚本)
——desision_tree.py(用于对图片进行训练分类的脚本)
——op_img.py(用于对图片进行降噪除杂的脚本)
——spider.py(爬取验证码的脚本)
步骤
  1. 搜集数据集
  2. 降噪去杂
  3. 切割字符
  4. 训练识别
废话不多说,干货开始:
1. 搜集数据集 相信这对搞爬虫的同学来说肯定很轻松,几行代码就搞定(Java另谈),找一个你要diss的验证码网站,写个爬虫爬下来就OK,为了让第一次可以稳稳当当的实(zhuang)验(bi)成功,我们来个简单的,代码如下(spider.py脚本内容):
import requests import time # nuller # 2017.12.23 url = "" for _ in range(100): img = requests.get(url).content with open(".//data//%s.jpg" % time.time(), 'wb') as f: f.write(img)

什么?你不懂爬虫?只会派森(python)?不要着急,这里我来讲解一下代码吧(看懂可跳过),为了和原有代码有所差别,讲解注释用##来表明:
##首先导入两个包,requests可以下载网上数据,time用来生成文件名(我要爬的验证码需要时间戳,所以引了time包,别告诉我你不知道什么是时间戳。。。百度就学会了) import requests import time ##下面。。。下面是注释 # nuller # 2017.12.22 ##要下载验证码的地址 url = "" ##用了for循环,下载了100个验证码到本地 for _ in range(100):##用get请求去下载验证码图片,并把content(内容)传给img img = requests.get(url).content ##把img保存到代码所在文件夹里的data文件夹里,文件名为当前时间戳,格式为jpg with open(".//data//%s.jpg" % time.time(), 'wb') as f: f.write(img)

这样,我们的第一步就OJ。。就OK了,下面我们拿到了图片本例如下图所示(因为是入门教程,所以用个超级简单的验证码来讲解):
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

emmm…就那么简单,主要看思路,嗯对!思路!
2.降噪去杂 有些验证码会有杂点和线段之类的干扰,增加验证码识别的难度,例如下面的验证码:
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

因此我们要去除这些干扰,对其降噪去杂。一般验证码是彩色模式,我们为了减少干扰,降低去杂难度,于是先二值化图片,何为二值化呢?如果不懂的同学先在这里停留一下。
我们处理图片的时候可以把图片当成一个张量,其维度为三维,即:(width, height, colors)宽度高度和颜色通道,一般彩色图片为三通道(RGB),即一个像素可以表现为(x,y,(255,0,0)),其中(255,0,0)就是colors三通道的值了。我们利用二值化,把colors由三元组变成一个数组(即灰度处理)。代码写进op_img.py脚本里:
以下为要进入的包
from PIL import Image import numpy as np import time import os

以下为函数代码
def convert(img):二值化图像 :param img:需要二值化的图像 :return:二值化后的图像##由RGB转为灰度 img_grey = img.convert('L') ##二值化阈值,若大于threshold置为1,小于为0 threshold = 200 table = [] for i in range(256): if i < threshold: table.append(0) else: table.append(1) ##把table矩阵转为图片 out_img = img_grey.point(table, '1') return out_img

以上就是二值化的函数了,什么?我看不到任何效果啊?那我们就写一个输出图像的函数来观察图像处理后的状态吧,方便学习更易于理解:
def print_mat_img(img): ##这里用到了numpy包,需要引入 ##把图片转为矩阵,传给mat_img,类型为int8 mat_img = np.asarray(img, np.int8) ##两层遍历每一点 for h in range(img.height): for w in range(img.width): print(mat_img[h][w], end='') print('')

运行后可看出二值化的输出一下所示,请把网页全屏,近视眼的摘下眼镜就可以看到隐隐约约有之前验证码s8vn的字样了:
11111111111111111111111111111111111111111111111111111111111111111
11111111100000011111110000011111001111111100111000111110011111111
11111111001111011111000111000111100111111100111000111110011111111
11111110011111111111001111100111100111111001111000011110011111111
11111110011111111111001111100111100111111001111001011110011111111
11111110001111111111100111001111110011111011111001001110011111111
11111110000001111111111000111111110011110011111001101110011111111
11111111000000011111100111001111111011110011111001100110011111111
11111111111100001111001111100111111001100111111001110110011111111
11111111111111001111001111100111111001100111111001110010011111111
11111111111111001111001111100111111100100111111001111010011111111
11111111111111001111001111100111111100001111111001111000011111111
11111110011110011111100111001111111110001111111001111100011111111
11111110000000111111110000011111111110011111111001111100011111111
11111111111111111111111111111111111111111111111111111111111111111
11111111111111111111111111111111111111111111111111111111111111111
这就是我们二值化后的结果,接下来要去杂了。去杂的思路就是排除那些被孤立的点,然后把孤立的点变成背景色。所以我们先写一个查找孤立点的函数(此函数是受其他博客大神总结出来的)下面给出思路,此思路也叫【洪水填充法 Flood Fill】下面引用一下其他博客的说明:
对某个 黑点 周边的九宫格里面的黑色点计数
如果黑色点少于2个则证明此点为孤立点,然后得到所有的孤立点
对所有孤立点一次批量移除。
下面将详细介绍关于具体的算法原理。
将所有的像素点如下图分成三大类
顶点A 非顶点的边界点B 内部点C

种类点示意图如下:
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

其中:
A类点计算周边相邻的3个点(如上图红框所示) B类点计算周边相邻的5个点(如上图红框所示) C类点计算周边相邻的8个点(如上图红框所示)

当然,由于基准点在计算区域的方向不同,A类点和B类点还会有细分:
A类点继续细分为:左上,左下,右上,右下 B类点继续细分为:上,下,左,右 C类点不用细分

然后这些细分点将成为后续坐标获取的准则。
话不多说,直接上代码,此函数类似于给某个点的孤立性来打分:
def flood_fill(img, x, y): ''' 降噪 :param img: :param x: 当前x坐标 :param y: 当前y坐标 :return: ''' cur_pixel = img.getpixel((x, y)) width = img.width height = img.height# 若当前点为白色,则不统计邻域值 if cur_pixel == 1: return 0if y == 0: if x == 0: sum = cur_pixel \ + img.getpixel((x, y + 1)) \ + img.getpixel((x + 1, y)) \ + img.getpixel((x + 1, y + 1)) return 4 - sum elif x == width - 1: sum = cur_pixel \ + img.getpixel((x, y + 1)) \ + img.getpixel((x - 1, y)) \ + img.getpixel((x - 1, y + 1)) + img.getpixel((x + 1, y + 1)) return 4 - sum else: sum = img.getpixel((x - 1, y)) \ + img.getpixel((x - 1, y + 1)) \ + cur_pixel \ + img.getpixel((x, y + 1)) \ + img.getpixel((x + 1, y)) \ + img.getpixel((x + 1, y + 1)) return 6 - sum elif y == height - 1:# 最下面一行 if x == 0:# 左下顶点 # 中心点旁边3个点 sum = cur_pixel \ + img.getpixel((x + 1, y)) \ + img.getpixel((x + 1, y - 1)) \ + img.getpixel((x, y - 1)) return 4 - sum elif x == width - 1:# 右下顶点 sum = cur_pixel \ + img.getpixel((x, y - 1)) \ + img.getpixel((x - 1, y)) \ + img.getpixel((x - 1, y - 1))return 4 - sum else:# 最下非顶点,6邻域 sum = cur_pixel \ + img.getpixel((x - 1, y)) \ + img.getpixel((x + 1, y)) \ + img.getpixel((x, y - 1)) \ + img.getpixel((x - 1, y - 1)) \ + img.getpixel((x + 1, y - 1)) return 6 - sum else:# y不在边界 if x == 0:# 左边非顶点 sum = img.getpixel((x, y - 1)) \ + cur_pixel \ + img.getpixel((x, y + 1)) \ + img.getpixel((x + 1, y - 1)) \ + img.getpixel((x + 1, y)) \ + img.getpixel((x + 1, y + 1))return 6 - sum elif x == width - 1:# 右边非顶点 # print('%s,%s' % (x, y)) sum = img.getpixel((x, y - 1)) \ + cur_pixel \ + img.getpixel((x, y + 1)) \ + img.getpixel((x - 1, y - 1)) \ + img.getpixel((x - 1, y)) \ + img.getpixel((x - 1, y + 1))return 6 - sum else:# 具备9领域条件的 sum = img.getpixel((x - 1, y - 1)) \ + img.getpixel((x - 1, y)) \ + img.getpixel((x - 1, y + 1)) \ + img.getpixel((x, y - 1)) \ + cur_pixel \ + img.getpixel((x, y + 1)) \ + img.getpixel((x + 1, y - 1)) \ + img.getpixel((x + 1, y)) \ + img.getpixel((x + 1, y + 1)) return 9 - sum

然后利用再写一个删掉(抹去)噪点的函数:
def remove_noise_point(img, start, end): ''' 去杂点 :param img:要去除噪点的图片 :param start:噪点评分下限 :param end:噪点评分上限 :return:去除噪点后的Img ''' ##新建noise_point_list来储存噪点 noise_point_list = [] ##两层for遍历每个像素,再调用之前写好的flood_fill寻找噪点 for w in range(img.width): for h in range(img.height): around_num = flood_fill(img, w, h) ##若当前像素的噪点评分在start和end之间,则判断为噪点 if(start < around_num < end) and img.getpixel((w, h)) == 0: pos = (w, h) noise_point_list.append(pos)##把噪点置为背景色 for pos in noise_point_list: img.putpixel((pos[0], pos[1]), 1) return img

运行后效果如下图所示:
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

咦?没啥效果啊!博主是不是在骗我!只是没颜色了而已啊!
不要慌不要慌,博士怎么会骗人,都说好了这是第一篇教程,我们用的是简单再简单的验证码,不信的话上另一组图片:
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

怎么样,博主向来是不会乱吹牛的。
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

3.切割字符 第二个步骤是为了给这一步来打基础,为何我们要分割字符?是不是傻!一个验证码是由4个数字+字母组成,其组合数为(10+26)^4种情况,由于我们用的是机器学习算法不是端到端的深度学习,我们只能老老实实的切割字符,去搜集10+26种情况即可!
怎么切割字符呢,我们要找到字与字之间的间距,我们这种验证码简单特殊,所以不要写什么算法,直接暴力的找到分界线就OK了,因为字符位置都是固定的,于是我很多余的用代码找了起来。。。
def add_line_by_x(x_list, img): mat_img = np.asarray(img, np.int8) for x in x_list: for h in range(img.height): mat_img[h, x] = 0 image = Image.fromarray(np.uint8(mat_img)) return imagedef add_line_by_y(y_list, img): mat_img = np.asarray(img, np.int8) for y in y_list: for w in range(img.width): mat_img[y, w] = 0 image = Image.fromarray(np.uint8(mat_img)) return image

结果如图所示:
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

于是我们就这样剪裁图片就可以了,这里用到PIL包的Image.crop(left,top,right,bottom)函数来对图像进行切割,并保存到“.//data//cut//“中。
我们利用之前的程序找到了分割字符的x线和y线,所以我们要找出x,y线相交的点,来填入crop中的left,top,right,bottom(我是真的懒不想算,所以写个函数来找点了):
def make_crop_points(x_list, y_list): ##x_list为找到的可以切的x轴的线,y_list相同 width = x_list[1] - x_list[0] height = y_list[1] - y_list[0] points = [] for x in x_list: for y in y_list: pos = (x, y) points.append(pos) return points, width, height

然后切割:
def crop_img(img, points, width, height): ''' 剪裁图像 :param img: Image图像 :param points: 要剪裁的点的组合 :return: ''' imgs = [] index = 0 for point in points[:-1]: print(point[0], point[1]) chirld_img = img.crop((point[0], point[1], point[0] + width, point[1] + height)) imgs.append(chirld_img) chirld_img.save(".//data//cut//%s.jpg" % (str(time.time())+str(index)), "jpeg") index += 1

这样我们的数据集就全部。。。哦不,来到了最苦逼的手工分码环节。。。。
这是我们切好的图片:
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

。。。然后。。。我们要一类一类的分进文件夹里。。。个人感觉这是这个项目中最困难最坑最苦的一个环节。。。不说了,赶紧分类!
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

4.训练识别 终于。。。分类完来到了全剧的高潮。。。按照剧情发展高潮后就该戛(疯)然(狂)而(跑)止(路)了。。。
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

识别验证码的目的就是要让计算机认识我们切出的图片,怎么认识?我们这次要用机器学习,莫慌莫慌,这次我们不讲机器学习算法,不写机器学习算法,我们来调包:
from sklearn import tree

我们这次用决策树来进行分类,之前是想用libsvm来做,可是莫名报错,还是怪博主学术太浅。
何为决策树呢,我来给大家。。。建议一下去看看百度吧。
机器学习讲究的是训练和测试,训练我们要用到具有label的数据集来告诉决策树,这个图片就是a,这个是c,其中的a,c就是label(标签)了,而测试就是给决策树这些图片,让他告诉你label是什么。显然易得我们这是多分类任务。而决策树是如何认识这些图片的呢,我们要告诉他这些图片的特征,传入矩阵告诉他label,而一个被我们cut后的图片也是很大的:16*13维大小,因此我们要缩小特征维度,即降维。由于我们不是来专门讲机器学习的,所以降维也不会用PCA之类的算法,直接开出一个新思路就行:计算每一行中像素为黑色的个数,这样我们把特征维度从16*13就降到了16(create_feature.py):
from img_to_string.alpha.op_img import * from PIL import Image ''' nuller 2017.12.23 ''' def create_train_np_array(data_file_paths): xs = [] ys = [] labels = os.listdir(data_file_paths) for label in labels: img_paths = [data_file_paths + label + "//" + path for path in os.listdir(data_file_paths + label + "//")] for path in img_paths: img = Image.open(path) img = convert(img) feature_values = get_feature(img) xs.append(feature_values) ys.append(ord(label)) xs = np.asarray(xs) ys = np.asarray(ys).reshape((-1,1)) return xs, ysdef create_test_np_array(data_file_paths): xs = [] img_paths = [data_file_paths + path for path in os.listdir(data_file_paths)] for path in img_paths: img = Image.open(path) img = convert(img) feature_values = get_feature(img) xs.append(feature_values) return xs

下面就可以用决策树来训练和测试了(desision_tree.py):
from sklearn import tree from alpha import create_featuredef train_and_test(train_xs, train_ys, test_xs): clf = tree.DecisionTreeClassifier() clf.fit(train_xs, train_ys) test_y = clf.predict(test_xs)return test_y

效果如图所示:
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

OJ….OK达到了我们所期待的效果hahahaha…
撒花 【diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))】嗯这就是我之前不到一天的研究成果,心中还是蛮高兴的,大神勿喷,博主心里还是有13数的知道自己很菜。不过有其他不对之处欢迎大家前来指正,一起学习一起进步!就这样我们《Diss验证码》的第一篇就这样完美撒花了,谢谢大家的耐心观看与支持!
转载请注明出处。Nuller
未完待续!
github:https://github.com/LambdaNuller/uncaptcha/tree/master/img_to_string/alpha
diss验证码系列|《Diss验证码》——Python验证码破解(图像字符验证码识别(1-入门))
文章图片

    推荐阅读