Python识别登录验证码(附完整代码)
【项目介绍】
网络迅速在人类生活中扎根,我们每天都会不可避免地接触大量网站和碎片化的信息,为了保护用户的账号安全和防止信息泄露,很多网站通行的方式是设置登录验证码。生活中,我们在登录微博,邮箱的时候,常常会碰到验证码。在工作时,如果想要爬取一些数据,也会碰到验证码的阻碍。所以,在经过一学期的学习之后,打算体验利用Python工具集实现基本向量空间搜索引擎理论下的简单验证码图片内数字的读取。认识验证码的一些特性,并利用Python中的pillow库完成对验证码的破解。
验证码可以有效防止机器恶意注册,对某个用户帐号使用特定程序暴力破解的方式进行不断地登陆尝试。不少网站为了防止用户利用机器人自动注册、登录、灌水,都采用了验证码技术。基于一串随机产生的数字或符号生成一幅图片,图片中加入一些干扰像素(防止OCP)就生成了简单的验证码。由用户识别其中的信息,输入表单提交网站验证,验证成功后才能使用某项功能。验证码具有以下特性:
- 一般用于防止批量注册,还有各大论坛用来避免大规模匿名回帖现象的发生。
- 简单易操作,人机交互性好
- 安全系数低,容易被破解
【环境搭建】
- 使用Pycharm作为开发环境
- Python版本:3.9.6
- 相关工具包:PIL.Image, math, os, string, hashlib, time(Pilow是一个Python图像处理库)
【项目原理】
首先针对一个验证码图片进行像素颜色统计检测,并人工验证哪些颜色组成了验证码的数字。 利用上一步得到的颜色,通过判断该颜色所在的横坐标,将验证码中每个数字所在的横坐标范围确定下来。 利用单数字图片与Pillow的Image.getdata()方法,得到每种数字或小写字母的有效像素的集合,作为“训练集”。 最后,利用上两步得到的“验证码的数字坐标集”与“训练集”,在VectorCompare.relation()算法下,比较验证码每一位与训练集中每一个字符的数据,选最相似(相同坐标值相同的次数最多)的那个返回,这样就得到了验证码图片中数字及字母的内容。
【框架图】
【程序流程处理图】
【模块详细实现步骤】
准备工作如下:
在windowsx10下安装pillow(PIL)库
win+r,打开“运行”,再输入cmd,打开命令提示符;
输入pip install pillow,回车,等待约2分钟,安装成功;
安装的版本为Pillow-8.4.0-cp310-cp310-win_amd64.whl (3.2 MB);
输入pip list可查看pyhton已安装库的最新版本。
下载实验中用的验证码文件:
http://labfile.oss.aliyuncs.com/courses/364/python_captcha.zip
将实验过程中使用的验证码文件下载到/password/Code目录下,解压后,在python_captcha目录下新建crack.py文件,进行编辑:
启动命令行输入如下指令:
start powershell
在powershell中我们输入一下命令
$client = new-object System.Net.WebClient
$client.DownloadFile(' http://labfile.oss.aliyuncs.com/courses/364/python_captcha.zip', 'D:\python\password\Code\python_captcha.zip')
下载完成后解压缩:(需要下载unzip;地址:http://gnuwin32.sourceforge.net/packages/unzip.htm)
键入命令:unzip python_captcha.zip
新建crack.py文件
type nul>crack.py
至此完成所有准备工作,下面开始正式编写代码
分析图片组成像素
将captcha.gif复制到开发目录下,用Pillow打开这个图片(Image.open()),并转换为8-bit彩色(Image.convert()),并观察哪些像素构成了数字的绝大部分(Image.histogram())。
#-*- coding:utf8 -*-
from PIL import Image
im = Image.open("captcha.gif")
#(将图片转换为8位像素模式)
im.convert("P")
# 打印颜色直方图
print(im.histogram())
输出:
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 , 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0 , 1, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 1, 2, 0, 1, 0, 0, 1, 0, 2, 0, 0, 1, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 3, 1, 3, 3, 0, 0, 0, 0, 0, 0, 1, 0, 3, 2, 132, 1, 1, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 15, 0 , 1, 0, 1, 0, 0, 8, 1, 0, 0, 0, 0, 1, 6, 0, 2, 0, 0, 0, 0, 18, 1, 1, 1, 1, 1, 2, 365, 115, 0, 1, 0, 0, 0, 135, 186, 0, 0, 1, 0, 0, 0, 116, 3, 0, 0, 0, 0, 0, 21, 1, 1, 0, 0, 0, 2, 10, 2, 0, 0, 0, 0, 2, 10, 0, 0, 0, 0, 1, 0, 625]
若输出颜色直方图,说明上述操作成功。
获取颜色直方图
颜色直方图的每一位数字都代表了在图片中含有对应位的颜色的像素的数量。每个像素点可表现 256种颜色,会发现白点是最多(白色序号255的位置,也就是最后一位,可以看到,有625个白色像素)。红像素在序号200左右,于是可以通过排序得到有用的颜色。
```python
def getHis(image):
#颜色直方图
his = im.histogram()
values = {}
for i in range(256):
values[i] = his[i]
#默认reverse=False升序排列,reverse=True降序排列
#按像素数量降序排列
for j,k in sorted(values.items(),key=lambda x:x[1],reverse = True)[:10]:
print j, k
```
此处打印出图片,表明成功实现
构造一张黑白二值图片:
新建一个图片,同样采用8bit彩色模式,默认全白色,将有数字的部分填涂为黑色。(Image库的new(),getpixel(),putpixel())
```python
#-*- coding:utf8 -*-
from PIL import Image
image = Image.open("captcha.gif")
def blackWhite(image):
# 通过size属性可以获取图片的尺寸。这是一个二元组,包含水平和垂直方向上的像素数
im2 = Image.new("P", image.size, 255)
# 将图片转换为8位像素模式
image.convert("P")
temp = {}
for x in range(image.size[1]):
# 这里y指的是纵向,y = a是点(a,...)构成的直线
for y in range(image.size[0]):
''' getpixel函数是用来获取图像中某一点的像素的RGB颜色值,getpixel的参数是一个坐标点。
对于图像的不同的模式,getpixel函数返回的值有所不同。'''
pix = image.getpixel((y, x))
temp[pix] = pix
# 这些是需要得到的数字
if pix == 220 or pix == 227:
im2.putpixel((y, x), 0)
im2.save('new.gif')
return im2
im2 = blackWhite(image)
im2.show()
```
提取单个字符图片,接下来获取单个字符的像素集合,对其进行纵向切割
```python
# 提取单个字符像素合集
def pixelCollection(blackWhiteCaptcha):
inletter = False
foundletter = False
start = 0
end = 0
im2 = blackWhiteCaptcha
letters = []
# 获取每个字符的始末位置
# 横向切割
for y in range(im2.size[0]):
# 纵向切割
for x in range(im2.size[1]):
pix = im2.getpixel((y, x))
if pix != 255:
inletter = True
if foundletter == False and inletter == True:
foundletter = True
start = y
if foundletter == True and inletter == False:
foundletter = False
end = y
letters.append((start, end))
inletter = False
# print(letters)
return letters
# 打开一张验证码图
image = Image.open("captcha.gif")
blackWhiteCaptcha = blackWhite(image)
print(pixelCollection(blackWhiteCaptcha))
```
输出
[(6, 14), (15, 25), (27, 35), (37, 46), (48, 56), (57, 67)]
此处得到得到每个字符开始和结束的列序号。
接下来对图片进行切割,得到每个字符所在的那部分图片:
利用黑色像素所在的横坐标的位置,设计算法得到每一位字符所在的横坐标范围。该范围可以利用起来对图片进行分割并保存(使用Image.crop())。
```python
import hashlib
import time
# 识别字符个数
count = 0
# 对验证码图片进行切割
for letter in letters:
# md5加密生成每个字符图片的名称
m = hashlib.md5()
# 前两个值为左上角坐标,后两个值为右下角坐标
im3 = blackWhiteCaptcha.crop((letter[0], 0, letter[1], blackWhiteCaptcha.size[1]))
guess = []
im3.save("./%s.gif"%(m.hexdigest()))
count += 1
```
用 Python 类实现向量空间
参考利用基本向量空间搜索引擎论文(http://ondoc.logand.com/d/2697/pdf),写出向量比较算法。这里涉及到较复杂的知识,需要理解。
在这里使用向量空间搜索引擎来做字符识别,它具有很多优点:
- 不需要大量的训练迭代;
- 不会训练过度;
- 可以随时加入/移除错误的数据查看效果;
- 很容易理解和编写成代码;
- 提供分级结果,并且可以查看最接近的多个匹配;
- 对于无法识别的东西只要加入到搜索引擎中,马上就能识别。
当然它也有缺点,例如分类的速度比神经网络慢很多,也不能找到自己的方法解决问题等等。
```python
import math
class VectorCompare:
# 计算矢量大小
def magnitude(self,concordance):
total = 0
for word,count in concordance.items():
#返回count的2次幂
total += count ** 2
return math.sqrt(total)
# 计算矢量之间的 cos 值
def relation(self,concordance1, concordance2):
relevance = 0
topvalue = 0
for word, count in concordance1.items():
if word in concordance2:
topvalue += count * concordance2[word]
return topvalue / (self.magnitude(concordance1) * self.magnitude(concordance2))
```
它会比较两个 python 字典类型并输出它们的相似度(用 0~1 的数字表示)
将之前的内容放在一起,接下来是取大量验证码提取单个字符图片作为训练集合。也可以使用iconset 目录下存放的已有的训练集,将“训练集”中的字符图片的像素数据读入。
识别验证码图片
利用向量比较算法,比较训练集与验证码每一位的相符程度,并保存结果。打印该结果,观察效果。
```python
# 将图片转换为矢量
def buildvector(im):
d1 = {}
count = 0
for i in im.getdata():
d1[count] = i
count += 1
return d1
# 字符图标集合
def letterIconset():
#需要训练的字符
iconset = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']
# 加载训练集
imageset = []
for letter in iconset:
for img in os.listdir('./iconset/%s/'%(letter)):
temp = []
if img != "Thumbs.db" and img != ".DS_Store":
temp.append(buildvector(Image.open("./iconset/%s/%s"%(letter, img))))
imageset.append({letter:temp})
return imageset
# 识别验证码
def identifyCaptcha(image):
# 黑白二值图片中字符的像素信息
blackWhiteCaptcha = blackWhite(image)
# 单个字符的像素集合
letters = pixelCollection(blackWhiteCaptcha)
# 字符图标集合
imageset = letterIconset()
# 向量空间
v = VectorCompare()
# 识别字符个数
count = 0
# 对验证码图片进行切割
for letter in letters:
m = hashlib.md5()
im3 = blackWhiteCaptcha.crop((letter[0], 0, letter[1], blackWhiteCaptcha.size[1]))
guess = []
# 将切割得到的验证码小片段与每个训练片段进行比较
for image in imageset:
for x,y in image.items():
if len(y) != 0:
guess.append( ( v.relation(y[0],buildvector(im3)),x) )
# 默认reverse=False升序排列,reverse=True降序排列
guess.sort(reverse=True)
print(guess[0])
count += 1
#打开一张验证码图
image = Image.open(“captcha.gif”)
#识别验证码并返回结果
identifyCaptcha(image)
```
接下来运行代码查看结果:
python crack.py
输出:
(0.9637681159420289, '7') (0.96234028545977, 's') (0.9286884286888929, '9') (0.9835037060984447, 't') (0.9675116507250627, '9') (0.96989711688772623, 'j')
与验证码中的字符一一对应,成功识别。
实现功能执行代码行:im = Image.open("captcha.gif") ,更改此处即可对其他验证码进行识别。
将以上各模块分装成函数,整理思路并运行代码,检验能否成功识别验证码。完整代码在crack1.py文件中。最终实验结果如图
选取多张图片进行验证,得到的结果均与预期结果均相同。
【项目总结】
该课程项目通过理论联系算法,得到了一种较为容易理解的机器学习算法,可以用来体验较原始的机器学习算法的实质,并为Python读取无干扰无扭曲的简易验证码提供了一套方案。
【心得体会】
Python是一种跨平台的、库相当丰富的、计算机程序设计语言,语法简单,可读性强,应用广泛。
在该课程项目中使用分割图片+向量识别的方式,实现了简易的验证码破解。然而现在的验证码越来越复杂,此方法明显处理不了更加复杂的验证码,这时候就需要在现有的基础上增添新的判别方式,提升系统的适应性。项目仍然有需要改进之处,对python语言的学习将不会停止,能够熟练运用python语言写脚本,提升编程能力和水平。
附全部代码如下:
# -*- coding:utf-8 -*-
from PIL import Image
import hashlib
import time
import os
import math
import string
# 向量空间类
class VectorCompare:
# 计算矢量大小
# magnitude()函数中的self代表类的实例,而非类,类似于this。
def magnitude(self, concordance):
total = 0
for word, count in concordance.items():
# 返回count的2次幂
total += count ** 2
return math.sqrt(total)
# 计算矢量之间的cos值
def relation(self, concordance1, concordance2):
relevance = 0
topvalue = 0
# 字典的items方法:可以将字典中的所有项,以列表方式返回
# 因为字典是无序的,所以用items方法返回字典的所有项,也是没有顺序的。
for word, count in concordance1.items():
if word in concordance2:
topvalue += count * concordance2[word]
return topvalue / (self.magnitude(concordance1) * self.magnitude(concordance2))
# 将图片转换为矢量
def buildvector(image):
d1 = {}
count = 0
for i in image.getdata():
d1[count] = i
count += 1
return d1
# 字符图标集合
def letterIconset():
# 需要训练的字符
iconset = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k',
'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']
# 字符训练集目录
letterPath = 'iconset'
# 加载训练集
imageset = []
for letter in iconset:
for img in os.listdir(letterPath + '/%s/' % (letter)):
temp = []
# windows check...
# if img != "Thumbs.db" and img != ".DS_Store":
# temp.append(buildvector(Image.open("./iconset/%s/%s" % (letter, img))))
# imageset.append({letter: temp})
# 过滤非gif格式的文件
if img.endswith(".gif"):
temp.append(buildvector(Image.open(letterPath + '/%s/%s' % (letter, img))))
imageset.append({letter: temp})
return imageset
# 提取单个字符像素合集
def pixelCollection(blackWhiteCaptcha):
inletter = False
foundletter = False
start = 0
end = 0
im2 = blackWhiteCaptcha
letters = []
# 获取每个字符的始末位置
# 横向切割
for y in range(im2.size[0]):
# 纵向切割
for x in range(im2.size[1]):
pix = im2.getpixel((y, x))
if pix != 255:
inletter = True
if foundletter == False and inletter == True:
foundletter = True
start = y
if foundletter == True and inletter == False:
foundletter = False
end = y
letters.append((start, end))
inletter = False
# print(letters)
return letters
# 通过size属性可以获取图片的尺寸。这是一个二元组,包含水平和垂直方向上的像素数
# 构造一张黑白二值图片
def blackWhite(image):
im2 = Image.new("P", image.size, 255)
# 将图片转换为8位像素模式
image.convert("P")
temp = {}
for x in range(image.size[1]):
# 这里y指的是纵向,y = a是点(a,...)构成的直线
for y in range(image.size[0]):
''' getpixel函数是用来获取图像中某一点的像素的RGB颜色值,getpixel的参数是一个坐标点。
对于图像的不同的模式,getpixel函数返回的值有所不同。'''
pix = image.getpixel((y, x))
temp[pix] = pix
# 这些是需要得到的数字
if pix == 220 or pix == 227:
im2.putpixel((y, x), 0)
im2.save('new.gif')
return im2
# 识别验证码
def identifyCaptcha(image):
# 黑白二值图片中字符的像素信息
blackWhiteCaptcha = blackWhite(image)
# 单个字符的像素集合
letters = pixelCollection(blackWhiteCaptcha)
# 字符图标集合
imageset = letterIconset()
# 向量空间
v = VectorCompare()
# 识别字符个数
count = 0
# 识别验证码
guessLetter = ''
# 对验证码图片进行切割
for letter in letters:
# md5加密生成每个字符图片的名称
m = hashlib.md5()
'''Image.crop(box=None):Returns a rectangular region from this image.
The box is a 4-tuple defining the left, upper, right, and lower pixel coordinate.'''
# 前两个值为左上角坐标
# 后两个值为右下角坐标
im3 = blackWhiteCaptcha.crop((letter[0], 0, letter[1], blackWhiteCaptcha.size[1]))
guess = []
# 将切割得到的验证码小片段与每个训练片段进行比较
for image in imageset:
for x, y in image.items():
if len(y) != 0:
guess.append((v.relation(y[0], buildvector(im3)), x))
# 默认reverse=False升序排列,reverse=True降序排列
guess.sort(reverse=True)
print(guess[0])
guessLetter += guess[0][1]
count += 1
return count,guessLetter
def getHis(image):
# 颜色直方图
his = image.histogram()
values = {}
for i in range(256):
values[i] = his[i]
# 按像素数量降序排列
valueSeq = {}
# 默认reverse=False升序排列,reverse=True降序排列
for j, k in sorted(values.items(), key=lambda x: x[1], reverse=True)[:11]:
valueSeq[j] = k
# print(j, k)
# print(valueSeq)
return valueSeq
# 主函数
if __name__ == '__main__':
# 打开一张验证码图
image = Image.open("r2lvkd.gif")
# 对图像进行处理,获取相关信息
# 打印颜色直方图
getHis(image)
# 识别验证码返回结果
result = identifyCaptcha(image)
print("识别出%d位验证码:%s" % (result))