各种验证码的处理

本文主要介绍如何利用 Python 实现对图形验证码和滑动验证码的识别

图形验证码的识别

图形验证码是出现最早,也是最容易识别的一类验证码,这类验证码很常见,学校教务系统登录界面就有类似的验证码5c5fbb683bb2d

通过审查元素可以发现该验证码的链接为 http://219.231.36.52/CheckCode.aspx ,访问这个链接就能得到一个验证码(前提是先连上学校的 vpn ),先将验证码下载并保存起来以供测试识别之用

接下来新建一个项目,将验证码图片放到项目文件夹下,首先用 PIL 库对验证码图片进行二值化处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from PIL import Image

image = Image.open('code.jpg','r')

image = image.convert('L')
threshold = 60
table = []
for i in range(256):
if i<threshold:
table.append(0)
else:
table.append(1)
image = image.point(table,'1')
image.show()

处理后的验证码图片如下:5c65844d53970

换一张背景复杂一点的图片处理效果可以看得更明显一点5c65865f58ccf

5c65863cb27d7

这种做法的原理是首先利用 convert('L') 将图像转换成灰度图像,然后生成一个256位的列表( table )来对应灰色的256个色阶。列表在生成的时候对应着一个阈值 (threshold) ,列表内序号小于阈值的将被赋为0,大于等于阈值的赋为1。之后利用 point( table,'1') 对灰度图片进行映射,后面的 1 表示的是黑白模式。这样图片中灰度色阶小于阈值的像素点将被填充成黑色,其他的像素点将被填充成白色。通过这种操作可以将验证码的文字部分和背景区分开。

然后就可以直接调用 tesserocr 库对处理后的验证码图片进行识别。这种方法可以应对一般的图形验证码,如果要提升识别准确度还需要对验证码图片进行切割对比,最好的方法是运用深度学习自己训练一个数据集,这个以后慢慢学。

滑动验证码

快一个星期过去了,终于可以来补完剩下的滑动验证码部分了,来自菜鸡的悲愤 X999

先来看看效果

回到话题

滑动验证码是一种新式验证码,因为其操作简单人机辨识度高,在很短的时间内就流行起来,其中比较出名的就是极验

GeeTest

由于滑动验证码的加密参数太复杂,所以通过构造加密参数进行破解的难度太大,性价比太低,直接忽略。这时候就应该祭出神器 selenium 了。具体解决思路是点击验证按钮弹出滑动验证窗口然后通过验证码图片找出滑块和其对应的缺口位置,最通过 selenium 拖动滑块到达指定位置即可

在这里,选取魅族官网注册界面的滑动验证码进行尝试,其页面内容如下:

初始化webdriver

1
2
3
4
5
from selenium import webdriver

browser = webdriver.Firefox()
#url 为魅族官网注册页面
browser.get(url)

模拟点击验证按钮

初期方案是利用审查元素得到验证按钮的 css 选择器,然后显式等待直到按钮可以点击后使用 webdriver 的 click() 方法,但在点击之后只会出现点触验证码而不是期望的滑动验证码,重新分析了一下页面发现,验证按钮上面有一个圆圈会一直追寻鼠标的的方向旋转,据此推测验证按钮内附加了监视鼠标移动轨迹的方法,于是引入 ActionChains ,先将鼠标移动到按钮的上方再进行点击

1
2
3
4
5
6
7
8
9
10
11
12
13
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.action_chains import ActionChains

browser = webdriver.Firefox()
browser.get(url)
wait = WebDriverWait(self.browser,20)
#显式等待验证按钮
button=self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,'.geetest_radar_tip')))
ActionChains(browser).move_to_element(button).perform()
button.click()

使用此方法后点击验证按钮后一般就会直接显示验证成功,后面啥都没了,既没有点触验证码也没有我想要的滑动验证码,这验证也太草率了点吧。没办法,只好重复运行程序,总会有那么几次会出现滑动验证码的(我感觉我写这个程序的绝大部分时间都是在重启程序然后等待响应…)

当出现滑动验证码时就要开始主要操作也就是验证码图片滑块缺口识别以及滑块的操作了

滑块缺口识别

首先来一张滑动验证码的图片

这是一张 325 * 200 的图片,图片上的缺口是一个半透明的阴影蒙版,现在要做的工作就是将这个缺口的位置识别出来。看了看网上的方法,都是将原图片与出现缺口的图片进行比对,由于缺口位置的像素相较于原图片会有较大差异,从而可以确定出缺口位置,他们获取原图片的方法有两种,但都是老版验证码的。一种是将点击开始之前的正常验证码图片保存起来,另一种是将请求到的乱码图片根据网页元素的数据重新组合成正常图片。乱码图片就是这样的

但现在两种方法都没法用了,点击开始操作连带着原图片一起被取消了,拼合乱码图片网页数据也被放到了 js 文件里,我也找不到请求链接。网上前辈用过的方法全被封堵了,还真是前人砍树,后人中暑啊。没有办法只好不用原图片对着验证码图片硬杠了。

以下是我的解决方法:

首先将验证码图片转化为灰度模式然后保存,然后使用 numpy 打开,之所以使用灰度模式是因为普通 RGB 模式的图片使用 numpy 打开形成的是一个三维向量组分析起来很麻烦,而灰度图片打开后只是一个二维向量组,里面的每一个数字都对应着图片上响应像素点的灰阶,这样看起来更直观。

然后按列遍历图片上的像素,我是想通过这个找到滑块缺口最左边的那条直线。

通过观察可以发现,这条直线由于阴影的存在,其灰阶肯定比左侧部位高,又由于他是一条视觉上的直线所有其上下相邻的像素灰度值必然在一定范围之内。然后把图扔进 ps 里可以看到点的灰度,至于灰度和灰阶的换算这就要靠自己摸索了。

由于缺口不会出现在滑块内,所以遍历时直接从第 滑块的尾部大概60列遍历就行了,图片上的黑字会对查找边缘造成影响,最好在确定遍历范围的时候将上面那行大字去掉,小字也有干扰但不能去,因为滑块有时会出现在那里。当有连续的像素点符合这上述条件的时候用列表存下其横坐标,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

def find_shadow_edge(self):
#不赋初值后面判断列表尾部元素会报错

edge = [1]
img = np.array(Image.open('img.png'))
for x in range(60,300):
m = 0
for y in range(35,199):
#筛掉重复以及相邻位置
#把连续像素的值设大一点可以有效避免干扰项
if m==18 and edge[-1]!=x and abs(edge[-1]-x)>1:

edge.append(x)
#筛查条件

if int(img[y][x - 1]) - int(img[y][x]) > 20 and abs(int(img[y + 1][x] - int(img[y][x])) < 30):
m += 1
else:
m=0
del edge[0]
print('可能的边缘坐标:',edge)
return edge

在上述条件下,普通图片一般会直接找到缺口左端边缘,干扰大的可能会有两到三个错误位置,不过问题不大。之后利用相似的手段可以得到滑块左端边缘的位置,二者相减后就能得到拖动距离。需要注意的是,由于有些缺口左侧并不是一条完整的直线还可能会有半圆之类的附加物,例如 ps 中的那张图,所以连续像素点的要求不能设得太高,否则可能会找不到边缘。

滑块拖动

由于极验对滑块的拖动轨迹进行了人工智能识别,所以拖动时不能直接拖而是要尽可能切合人类操作的特征。所以在此使用了匀变速运动的拖动轨迹,进行先加速后减速运动。利用方程 s=v0*t+vt^2/2 计算出每个单位时间 t 内应该运动的距离然后添加到列表内,通过遍历列表拖动距离来实现变速运动,这种操作也是很奇妙。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def get_track(self,distance):
track = []
current = 0
distance /= 1.26
mid = distance*3/4
#时间间隔过大会显得像匀速运动
t = 0.3
v = 2
while current<distance:
if current<mid:
a = 9
else:
a = -7
v0 = v
v = v0 + a*t
step = v * t + a * t * t / 2
current += step
track.append(round(step))
#防止拖过头了
if current - distance>2:
track[-1] -= int(current-distance)
return track

关于上面代码中的参数 1.26 ,这是因为拖动按钮的位移和拼图滑块的位移并不是完全一样的,我也是被这个给坑了好久,最后是通过每拖动 1 像素截一次图然后放到 ps 内查看滑块移动距离的方法才找到了两者之间的关系,每次略有差异但大致都在 1.26 左右。巨坑!

保存页面内元素的操作如下:

1
2
img = self.browser.find_element_by_css_selector('body > div.geetest_fullpage_click.geetest_float.geetest_wind.geetest_slide3 > div.geetest_fullpage_click_wrap > div.geetest_fullpage_click_box > div > div.geetest_wrap > div.geetest_widget > div > a > div.geetest_canvas_img.geetest_absolute > div > canvas.geetest_canvas_slice.geetest_absolute')  
img.screenshot('img.png')

截图会被保存在项目文件夹下

点触验证码目前没有很好的解决方法,主流做法就是将验证码图片发给打码平台,然后根据打码平台发回的坐标进行点击即可。

记录一下自己挖的坑

1.不要将 ActionChains 类初始化成 self.action = ActionChains(self.browser) 的形式然后其他地方直接拿去用,这样会造成一些稀奇古怪的 bug ,此外,ActionChains 类之后要加上 perform() 才能执行。

2.Firefox 拖动速度较慢,如果需要进行变速拖动需要给一个较大的加速度,否则它其实就是个匀速运动

3.在执行对列表内元素的操作时,请确保它不是个空列表

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.common.action_chains import ActionChains
from PIL import Image
from time import sleep
import numpy as np



class Verification():
def __init__(self):
self.browser = webdriver.Firefox()
self.wait = WebDriverWait(self.browser,20)
#这么用会出错,我也不知道为什么。。。
self.action = ActionChains(self.browser)
self.ratio = 1.26


def click_verification(self):
'''
点击验证按钮
'''
self.browser.get('https://i.flyme.cn/register?useruri=http%3A%2F%2Fstore.meizu.com%2Fmember%2Flogin.htm%3Fuseruri%3Dhttps%253A%252F%252Fwww.meizu.com%252F&service=store&sid=unionlogin')
button =self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,'.geetest_radar_tip')))
# sleep(0.5)
self.action.move_to_element(button).perform()
# sleep(0.5)
button.click()

def judge(self):
'''
判断点击之后出现验证码种类
'''
sleep(5)
try:
self.browser.find_element_by_css_selector('.geetest_item_img')
print('点触验证码')
self.browser.quit()

except:
try:
self.browser.find_element_by_css_selector('.geetest_canvas_slice')
print('滑动验证码')
self.slide_action()

except:
if self.pass_verification():
print('验证通过')
self.browser.quit()
else:
print('验证失败')

def pass_verification(self):
'''
判断是否验证成功
'''
success = self.browser.find_element_by_css_selector('.geetest_success_radar_tip')
if success.text == '验证成功':
return 1
else:
return 0


def slide_action(self):
'''
滑动验证码识别流程
'''
self.crop_and_save_img()
shadow_edge = self.find_shadow_edge()
slider_edge = self.find_slider_edge()

for x in shadow_edge:
print('两条边界距离:',x-slider_edge)
track = self.get_track(x-slider_edge)
self.move(track)

def crop_and_save_img(self):
'''
将验证码图片截取并转化为灰度图后存储
'''
img = self.browser.find_element_by_css_selector('body > div.geetest_fullpage_click.geetest_float.geetest_wind.geetest_slide3 > div.geetest_fullpage_click_wrap > div.geetest_fullpage_click_box > div > div.geetest_wrap > div.geetest_widget > div > a > div.geetest_canvas_img.geetest_absolute > div > canvas.geetest_canvas_slice.geetest_absolute')
img.screenshot('img.png')
img = Image.open('img.png')
img.convert('L').save("img.png")

def find_slider_edge(self):
'''
找到滑块的左侧边缘
'''
img = np.array(Image.open('img.png'))
for x in range(3, 20):
m = 0
for y in range(40, 199):
if m >= 10:
return x
if int(img[y][x]) - int(img[y][x - 1]) > 20 and abs(int(img[y + 1][x] - int(img[y][x])) < 30):
m += 1
else:
m = 0


def find_shadow_edge(self):
'''
找到缺口的左侧边缘
'''
edge = [1]
img = np.array(Image.open('img.png'))
for x in range(60,300):
m = 0
for y in range(35,199):
#防止重复存储
if m==18 and edge[-1]!=x and abs(edge[-1]-x)>1:
edge.append(x)
if int(img[y][x - 1]) - int(img[y][x]) > 20 and abs(int(img[y + 1][x] - int(img[y][x])) < 30):
m += 1
else:
m=0
del edge[0]
print('可能的边缘坐标:',edge)
return edge

def get_track(self,distance):
'''
生成拖动轨迹
:param distance: 需要拖动的距离
'''
track = []
current = 0
distance /= 1.26
mid = distance*3/4
#时间间隔过大会显得像匀速运动
t = 0.3
v = 2
while current<distance:
if current<mid:
a = 9
else:
a = -7
v0 = v
v = v0 + a*t
step = v * t + a * t * t / 2
current += step
track.append(round(step))
if current - distance>2:
track[-1] -= int(current-distance)
return track

def move(self,track):
'''
根据轨迹拖动按钮
:param track: 轨迹列表
'''
button = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,'html body div.geetest_fullpage_click.geetest_float.geetest_wind.geetest_slide3 div.geetest_fullpage_click_wrap div.geetest_fullpage_click_box div.geetest_holder.geetest_mobile.geetest_ant.geetest_embed div.geetest_wrap div.geetest_slider.geetest_ready div.geetest_slider_button')))
ActionChains(self.browser).click_and_hold(button).perform()
for x in track:
print('偏移量:',x)
ActionChains(self.browser).move_by_offset(xoffset=x,yoffset=2).perform()
sleep(0.5)
ActionChains(self.browser).release().perform()
sleep(2)
if self.pass_verification():
print('验证成功')
else:
pass


meizu = Verification()
meizu.click_verification()
meizu.judge()
-----------本文结束感谢您的阅读-----------
0%