您的位置:首页 > 编程语言 > Python开发

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

2018-01-06 17:53 330 查看

图片字母验证码破解1——简易入门

Nuller

前言

搞爬虫有一段时间了,也和一些常见的反爬措施斗争了很久,验证码是反爬措施其中之一,diss它也是比较有趣有挑战性的。由于第一次搞验证码破解,虽然之前学过一些opencv和机器学习,但是这样实战实在有些酸爽,于是百度了一堆资料,在此做一下总结,并写下我踩到的坑,分享给大家,致力于写出更通俗易懂更面面俱到的破解验证码系列博客。

前段时间用了不到一天的时间,完成了对最简单的验证码的识别,最近才有空整理到博客上,分享给大家。

百度了一番大神对图像型验证码的破解经验,其中最基础最小白操作的就是Pytesser,Tesseract。这样做的缺点就是泛(bu)化(neng)能(geng)力(hao)比(de)较(zhuang)差(bi),没有让程序猿好好体会破解验证码的乐(nan)趣(du)。如果是真的想深入了解破解验证码的同学们不用紧张了,此系列不会接触到Pytesser,Tesseract,大可放心,我们自己来!

(嗯!这会我们不做代码的搬运工了!我们自己生产!)



面向人群

爬虫爱好者

人工智能爱好者

前面两个听起来有点高大上,可我只会python怎么办,没关系放心继续看 : )

项目目录说明

alpha

——data(用于存放图片的数据文件夹)

————source(爬取到的图片)

————cut(分割图片后的文件夹)

————test(测试集)

————train(分好类的训练集)

——create_feature.py(用于对图片特征进行创建的脚本)

——desision_tree.py(用于对图片进行训练分类的脚本)

——op_img.py(用于对图片进行降噪除杂的脚本)

——spider.py(爬取验证码的脚本)

步骤

搜集数据集

降噪去杂

切割字符

训练识别

废话不多说,干货开始:

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了,下面我们拿到了图片本例如下图所示(因为是入门教程,所以用个超级简单的验证码来讲解):



emmm…就那么简单,主要看思路,嗯对!思路!

2.降噪去杂

有些验证码会有杂点和线段之类的干扰,增加验证码识别的难度,例如下面的验证码:



因此我们要去除这些干扰,对其降噪去杂。一般验证码是彩色模式,我们为了减少干扰,降低去杂难度,于是先二值化图片,何为二值化呢?如果不懂的同学先在这里停留一下。

我们处理图片的时候可以把图片当成一个张量,其维度为三维,即:(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


种类点示意图如下:



其中:

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 0

if 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


运行后效果如下图所示:





咦?没啥效果啊!博主是不是在骗我!只是没颜色了而已啊!

不要慌不要慌,博士怎么会骗人,都说好了这是第一篇教程,我们用的是简单再简单的验证码,不信的话上另一组图片:





怎么样,博主向来是不会乱吹牛的。



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 image

def 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


结果如图所示:



于是我们就这样剪裁图片就可以了,这里用到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


这样我们的数据集就全部。。。哦不,来到了最苦逼的手工分码环节。。。。

这是我们切好的图片:



。。。然后。。。我们要一类一类的分进文件夹里。。。个人感觉这是这个项目中最困难最坑最苦的一个环节。。。不说了,赶紧分类!



4.训练识别

终于。。。分类完来到了全剧的高潮。。。按照剧情发展高潮后就该戛(疯)然(狂)而(跑)止(路)了。。。



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

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, ys

def 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_feature

def 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


效果如图所示:



OJ….OK达到了我们所期待的效果hahahaha…

撒花

嗯这就是我之前不到一天的研究成果,心中还是蛮高兴的,大神勿喷,博主心里还是有13数的知道自己很菜。不过有其他不对之处欢迎大家前来指正,一起学习一起进步!就这样我们《Diss验证码》的第一篇就这样完美撒花了,谢谢大家的耐心观看与支持!

转载请注明出处。Nuller

未完待续!

(等我整理完代码和数据都会放到github上)

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: