两种方式识别“传统”图片验证码

2020 年 8 月 25 日

两种方式识别“传统”图片验证码

目前,很多网站为了反爬都会采取各种各样的策略,比较简单粗暴的一种做法就是图片验证码,随着爬虫技术与反爬技术的演变,目前验证码也越来越复杂,比较高端的如Google的I‘m not a robot,极验等等。这些新的反爬方式大多都基于用户行为分析用户点击前的鼠标轨迹来判断是访问者是程序还是人。


基于图像处理的图片验证码识别


这篇文章介绍的是破解一般“传统”的图片验证码的步骤。上面提到的极验(目前应用比较广)也已经可以被破解,知乎上有相关的专栏,这里就不重复了。


即便是传统的图片验证码,也是有难度区分的(图一是我母校研究生院官网上的验证码,基本形同虚设;图二则是某网站的会员登录时的验证码增加了一些干扰信息,字符也有所粘连),但是破解的流程大致是一样的。



图 1



图 2


识别步骤


获取样本


从目标网站获取了 5000 个验证码图片到本地,作为样本。因为后期需要进行监督学习样本量要足够大。


样本去噪


✎ 先二值化图片


这一步是为了增强图片的对比度,利于后期图片图像处理,代码如下:


# 二值化图片    @staticmethod    def two_value_img(img_path, threshold):        img = Image.open(img_path).convert('L')        #  setup a converting table with constant threshold        tables = []        for i in range(256):            if i < threshold:                tables.append(0)            else:                tables.append(1)
# convert to binary image by the table bim = img.point(tables, '1') return bim
复制代码


效果如下:



✎ 图片去噪


该案例中就是去除两条干扰线,常规的去噪算法有很多(洪水法等等),这里根据图片的特点采用了两种去噪算法,一种是自己根据图片的特征实现的算法,另一种是“八值法”。去噪后的效果如下,可以看到去除了大部分的干扰线(剩下的根据字宽可以直接过滤掉),但是部分字符也变细了,所以这一步的去噪阀值需要不断调整,在去噪的基础上要尽量保持原图的完整和可读性。


代码如下:


# 根据图片特点,自己写的降噪算法def clean_img(img, threshold):    width, height = img.size    for j in range(height):        for i in range(width):            point = img.getpixel((i, j))            if point == 0:                for x in range(threshold):                    if j + x >= height:                        break                    else:                        if point != img.getpixel((i, j + x)):                            img.putpixel((i, j), 1)                            break    return img

# 八值法降噪def clean_img_eight(img, threshold): width, height = img.size arr = [[0 for col in range(width)] for row in range(height)] arr = array(arr) for j in range(height): for i in range(width): point = img.getpixel((i, j)) if point == 0: sum = 0 for x in range(-1, 2): for y in range(-1, 2): if i + x > width - 1 or j + y > height - 1 or \ i + x < 0 or j + y < 0: sum += 1 else: sum += img.getpixel((i + x, j + y)) if sum >= threshold: arr[j, i] = 1
for i in range(len(arr)): for j in range(len(arr[i])): if arr[i, j] == 1: img.putpixel((j, i), 1) return img
复制代码


效果如下:



图片切割


图片切割有很多算法如投影法、CFS 以及滴水法等。投影法适用于字符垂直方向上没有粘连和重合的情况,CFS 能够很好的切割垂直方向有粘连但是没有粘连的字符,水滴法可以分割粘连字符。目前采用的 CFS 切割法。切割效果如下图,对于非粘连字符,效果很不错。CFS 联通域切割的实现算法主要用的是图的广度/深度遍历,代码如下:


# CFS图像切割    @staticmethod    def cut_img(img, threshold, cut_width, cut_height, width_min, width_max, height_min):        charters_imgs = []        width, height = img.size        charters_pixels = []        visited_pixels = []        pixel_arr = ImgTools.get_pixel_arr(img)        for i in range(width):            for j in range(height):                pixel = img.getpixel((i, j))                if pixel == 0 and [i, j] not in visited_pixels:                    charter_pixels = Node(i, j, pixel_arr, []).traversal()                    visited_pixels.extend(charter_pixels)                    if len(charter_pixels) > threshold:                        charters_pixels.append(charter_pixels)        for i in range(len(charters_pixels)):            x_min = 0            y_min = 0            x_max = 0            y_max = 0            # 这里是为了处理没有粘连但是垂直方向有重合的字符            width, height = img.size            tmp_img = Image.new('1', (width * 2, height * 2), 255)            for j in range(len(charters_pixels[i])):                x, y = charters_pixels[i][j]                tmp_img.putpixel((x, y), 0)                if x > x_max:                    x_max = x                else:                    if x < x_min or x_min == 0:                        x_min = x                if y > y_max:                    y_max = y                else:                    if y < y_min or y_min == 0:                        y_min = y            if width_min < x_max - x_min < width_max and y_max - y_min > height_min:                # charters_imgs.append(tmp_img.crop((x_min, y_min, x_max, y_max)))                # 这里是为了将所有的图片切成一样大,便于后期的特征提取                charters_imgs.append(tmp_img.crop((x_min, y_min, x_min + cut_width, y_min + cut_height)))        return charters_imgs
复制代码


class Node:    x = 0    y = 0    graph_arr = []    visited_neighbors = []
def __init__(self, x, y, graph_arr, visited_neighbors): self.x = x self.y = y self.graph_arr = graph_arr self.visited_neighbors = visited_neighbors
def traversal(self): for i in range(-1, 2): for j in range(-1, 2): p = self.x + i q = self.y + j if (0 <= p < len(self.graph_arr)) and (0 <= q < len(self.graph_arr[0])): if array(self.graph_arr)[p, q] == 0 and [p, q] not in self.visited_neighbors: self.visited_neighbors.append([p, q]) next_node = Node(p, q, self.graph_arr, self.visited_neighbors) next_node.traversal() return self.visited_neighbors
复制代码


效果如下:



提取 feature 并训练特征模型


✎ 提取 feature


每个字符用了 40 个样本(每个字符都切成了 60×60)进行打标签,如果效果不好后续可以增加样本量(由于 M 大多数粘连严重,所以切出来的 M 很少,没有达到 40 个,直接导致后面 M 的识别结果也很不好)。



✎ 训练模型


这里采用了 libsvm 来训练模型,从个样本中预留了 1/10 个作为检验集,accuracy 达到 95%。


识别效果


先手动挑选了“乍一看”粘连不是很严重的 30 个样本,进行训练,结果如下,在 80%左右。



总结和优化方向


1、目前整个识别流程已经走通,验证码识别服务也初具对外服务的能力;


2、虽然目前对于整体验证码的识别效果不是很好,但是,验证码服务拼的是识别率,比如说一个验证码需要识别,我在对其进行预处理和切割之后发现字符粘连效果不好,则完全可以抛弃,这并不影响识别率。换句话讲我只是别切出来是四个字符的验证码即可(如果遇到一个网站每个图片的粘连都比较严重,这条路就走不通了);


3、优化方向有两个:


(1)优化切割字符的算法,目前的机器学习算法在图片切割比较好的情况下识别率是非常高的,因此目前这类验证码的切割是整个过程中的难点,对于该案例可以采用波斯平滑后通过垂直投影图找到极值点作为水滴法的起点是一个思路;


(2)增加样本量,目前是 40 个识别率已经可以接受,如果增加训练集的 Size 识别率应该会有所提升。


基于神经网络的图片验证码识别


上文提到的识别方式效果严重依赖于图像切割的效果。对于一些粘连严重的验证码,需要花很大的精力来进行去噪和分割,即使这样,效果也不一定会达到令人满意的程度。


基于 CNN 来进行验证码识别的优势在与整体识别,字符的粘连对于识别效果的影响不是特别大。



图一


识别过程


以图一验证码为例,在本次识别中,构建了一个 4 层(一开始是 3 层,效果并不好)隐层的深度神经网络,网络结构如下(示意图中每一层的图片看起来虽然没有区别但是每一层的 size 是有区别的,重点看下标):



参数如下:


卷积核大小为 5×5(zero padding),Pooling 采用 max pooling(block 为 2×2),激活函数为 ReLU,学习率(LR)为 0.001,在训练集识别率达到 99%的时候输出模型。


训练过程优化的过程包括:


  • 一开始使用三层卷积网络,效果并不理想,随后调整成了四层卷积层,效果有所提升;

  • 训练集样本量的提高;

  • 学习率的降低;


使用 Google 的 Tensorflow 机器学习框架进行训练。涉及到公司政策,代码就不上传了,网上可以搜到一些类似的。


识别效果


识别效果如下,左图是学习样本量为 3500 的时候识别率,右图是样本量为 4500(人工打码约 8 小时)的时候识别率:



CNN:训练集样本量为 3500 时



CNN:训练集样本量为 4500 时


对比一下用 libsvm 来识别的情况,左图是单个字符样本量为 40 张的时候识别情况,右图为单个字符样本量为 100 的时候的样本量(识别结果为 xxxx 的表示切割字符失败):



切割+SVM:每个字符样本为 40 时



切割+SVM:每个字符样本为 100 时


存在的问题


整个过程遇到的问题主要包括两个方面:


  1. 中四层网络的训练明显要比三层收敛的更慢,MBP只能用CPU跑,正常4层网络要输出一个可用的模型(训练集识别率达到99%)需要1-2天;

  2. 样本集对于学习至关重要,目前没有较好的对样本进行打标记的方式,只能人工打码(人工智能之人工),4500的样本量我打了4个晚上。并且人工打码会有很多不确定因素,在本例中,后期发现很多7和T都打错了,势必会对最后的识别效果有所影响;


将来的优化方向包括:


  • 增加训练集;

  • 调低LR学习率;

  • 调低keep_prob的值;

  • 增加卷积层;


总结


验证码识别哪怕模型的识别率只有 20%也是可用的,识别错了换一个就可以,但是在整个爬虫和反爬的过程中,主动权往往掌握在反爬的这一边,切换验证码的成本比破解一个类型的验证码的成本要低太多,更何况验证码只是众多反爬手段之一,也正是如此爬虫和反爬才会显得格外的有意思。


和其他互联网攻防技术一样,这篇文章只是验证码识别技术探讨,旨在为设计验证码防爬提供思路,并不鼓励读者带着炫技或其他目的去破解验证码,疯狂爬取别人的网站。任何技术本身都是中立的,be a reasonable crawler。


本文转载自公众号淘系技术(ID:AlibabaMTT)。


原文链接


两种方式识别“传统”图片验证码


2020 年 8 月 25 日 10:10906

评论 1 条评论

发布
用户头像
讲解得很详细,点赞。如果有源码分享就更好了
2020 年 08 月 25 日 10:55
回复
没有更多评论了
发现更多内容

socket通信,你还会实现么?

小隐乐乐

关键绩效指标KPI

JackWangGeek

链表应用之设计高性能访客记录系统

架构师修行之路

数据结构 链表 架构师

Dubbo源码分析--dubbo-config配置层的套路

jason

我期待,这是个多彩的世界

瓜藤老祖

大三儿 乐队的夏天 九连真人

架构师训练营--第10周作业

Just顾

数据仓库

JackWangGeek

架构师训练营第十章总结

叮叮董董

40张图入门Linux——(前端够用,运维入门)

执鸢者

Linux 前端

Code Review 失败后总结的几个实践技巧

Phoenix

团队管理 团队协作 技术人 代码质量

炸裂!40+图万字长文拿下HTTP

我是程序员小贱

计算机网络

微服务与DDD学习总结

qihuajun

Week 10

一叶知秋

热乎的宇宙条总部面经,已拿offer,速来围观

我是程序员小贱

招银网络问了啥?这么尬?妥妥的安排

我是程序员小贱

架构师训练营第十周作业

qihuajun

数据挖掘

JackWangGeek

SSAS查询性能最佳实践

JackWangGeek

SSIS主要功能

JackWangGeek

常见的BI项目问题和解决方案

JackWangGeek

架构师训练营第十章作业

叮叮董董

幂律分布 - 世界是不公平的

石云升

幂律分布 正态分布 二八法则

芯片破壁者(十一):回看日本半导体的倾塌

脑极体

Go make 和 new 的区别

曲镇

go golang make

关于微服务架构(中台架构、领域驱动设计、组件设计原则)的一点思考

jason

腾讯一面面试官让我关闭连接

我是程序员小贱

第十周学习总结

赵龙

第十周命题作业

赵龙

六张图从HTTP/0.9进化到HTTP3.0

执鸢者

前端 网络 HTTP

远程办公暴露过程管理的不足

翟志军

管理 软件工程 远程办公

什么是商业智能?

JackWangGeek

两种方式识别“传统”图片验证码-InfoQ