selenium篇之滑动验证码

最近工作要爬取某音短视频网站,发现有个很常见的反扒策略就是滑动验证码,没有心思去钻研破解这个验证码,想着从第三方搞一个算了,第三方基本提供的接口就是给你返回缺口目标坐标,剩下的要你自己做,想着selenium搞嘛

发现用selenium如果滑动哪怕无限接近甚至重合了还是提示验证失败,好吧,如果是个正常人也无法忍受了,肯定验证码也有反扒策略。

怎么能模拟人呢?????

当当当当,来了

注意:get_track计算滑动轨迹是有误差的,如果你使用sum求和的话会发现跟远距离有级像素的误差,一两像素还是可以解锁通过的,查的太多就不行了,我这里备注下,以后可以优化

一、介绍

现在出现了一种通过用户鼠标移动滑块来填补有缺口图片的验证码,我们叫做滑动验证码。它的原理很简单,首先生成一张图片,然后随机挖去一块,在页面展示被挖去部分的图片,再通过js获取用户滑动距离,以及坐标等信息到后台进行校验。只要用户移动的距离符合,以及移动的轨迹行为检测通过即可视为验证通过。

解决思路

目前这种验证码的通用解决思路如下:

  1. 获取验证码图片,包含原图以及有缺口的图
  2. 算出缺口的位置,以及滑块要滑动的距离
  3. 通过算法模拟人工移动轨迹
  4. 通过selenium模拟操作

二、逻辑实现

我们以保温网为例<http://www.cnbaowen.net/api/geetest/>

1.获取验证码图片

注意我们需要获取两张图片,第一张是完整背景图,第二张是有缺口的背景图。

经过分析发现当鼠标位于按钮是上时显示完整背景图,当鼠标点击滑动按钮不松,显示有缺口的背景图。

根据之前学习的爬虫知识,图片一定是浏览器下载回来的,通过查看历史请求确实发现了图片

但是图片有点奇怪,仔细查看发现图片是被分块并有意随机拼接的。根据以页面的css可以利用背景将图片拼接出来。我们当然可以依葫芦画瓢的利用PIL进行图片拼接,但是太麻烦了。

selenium有个方法可以对元素进行截图,先找到图片所在的html元素,然后利用selenium分别进行截图即可获取图片。代码如下:

完整图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def get_full_image(driver):
"""
鼠标移动到滑块,显示完整图案
:param driver: webdriver
:return: 返回验证码背景图片Image对象
"""

webdriver.ActionChains(driver).move_to_element(slider).perform()
time.sleep(0.2)
img = driver.find_element_by_xpath('//*[@id="captcha"]/div/div[1]/div[2]/div[1]/a[2]')
if 'show' in img.get_attribute('class'):
res = img.screenshot_as_png
return Image.open(BytesIO(res))
else:
raise ValueError('获取验证码背景图片失败')

有缺口图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def get_cut_image(driver):
"""
点击滑动按钮获取有缺口图片
:param driver: webdriver
:return: 返回验证码有缺口图片的Image对象
"""

slider = driver.find_element_by_xpath('//*[@id="captcha"]/div/div[3]/div[2]')
webdriver.ActionChains(driver).click_and_hold(slider).perform()
time.sleep(0.1)
img = driver.find_element_by_xpath('//*[@id="captcha"]/div/div[1]/div[2]/div[1]/a[1]')
res = img.screenshot_as_png

cut_img = Image.open(BytesIO(res))
return Image.open(BytesIO(res))

2.找出缺口位置,计算移动距离

算法有很多,大家可以自由发挥。这里我们讲一种最简单的方法。我们要算出的距离是滑块要滑动的距离。

通过比较没有缺口的图片,和这张有缺口的图片,找出滑块的位置和缺口的位置即可。经过观察,发现滑块出现的位置固定在x轴的0-100像素范围内,所以循环比较两张图片的x轴0-100像素范围内的每一行像素点,直到找到第一行出现两个图片像素点颜色完全不同的点,即找到了滑块的最左边最上的第一个像素点。但是在实际操作中发现,虽然肉眼看起来两张图片公共部分一模一样,但是程序处理后的像素的具体rgb值也是不相同的,所以需要设置一个阈值来判断,具体需要进行测试。

按照相同的思路,比较两张图片x轴100-end像素的部分,找到缺口的最左最上那个点。

用找到的缺口像素点的x坐标减去找到的滑块的点的x坐标得到近似移动距离。这种算法,经过测试准确率还不错,大家如果在实际工作过程中发现有问题,需要根据具体情况去设计不同算法。

代码如下:

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
def get_distance(full_image, cut_image):
full_pixies = full_image.load()
cut_pixies = cut_image.load()

w, h = full_image.size

full_image.save('full.png')
cut_image.save('cut.png')

# 先找最左边不同的点
left = []

for j in range(h):

for i in range(100):

if abs(full_pixies[i, j][0] - cut_pixies[i, j][0]) + abs(full_pixies[i, j][1] - cut_pixies[i, j][1]) + abs(
full_pixies[i, j][2] - cut_pixies[i, j][2]) > 150:
left.append((i, j))

if left:
break
# 再找最右边不同的点
right = []

for j in range(h):

for i in range(100, w):

if abs(full_pixies[i, j][0] - cut_pixies[i, j][0]) + abs(full_pixies[i, j][1] - cut_pixies[i, j][1]) + abs(
full_pixies[i, j][2] - cut_pixies[i, j][2]) > 150:
right.append((i, j))

if right:
break

length = right[0][0] - left[0][0]


return length

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
def get_track(self, distance):
'''
拿到移动轨迹,模仿人的滑动行为,先匀加速后匀减速
匀变速运动基本公式:
①v=v0+at
②s=v0t+(1/2)at²
③v²-v0²=2as

:param distance: 需要移动的距离
:return: 存放每0.2秒移动的距离
'''
# 初速度
v=0
# 单位时间为0.2s来统计轨迹,轨迹即0.2内的位移
t=0.3
# 位移/轨迹列表,列表内的一个元素代表0.2s的位移
tracks=[]
# 当前的位移
current=0
# 到达mid值开始减速
mid=distance * 5/8

distance += 10 # 先滑过一点,最后再反着滑动回来
# a = random.randint(1,3)
while current < distance:
if current < mid:
# 加速度越小,单位时间的位移越小,模拟的轨迹就越多越详细
a = random.randint(1,3) # 加速运动
else:
a = -random.randint(2,4) # 减速运动

# 初速度
v0 = v
# 0.2秒时间内的位移
s = v0*t+0.5*a*(t**2)
# 当前的位置
current += s
# 添加到轨迹列表
tracks.append(round(s))

# 速度已经达到v,该速度作为下次的初速度
v= v0+a*t

# 反着滑动到大概准确位置
for i in range(4):
tracks.append(-random.randint(1,3))
# for i in range(4):
# tracks.append(-random.randint(1,3))
random.shuffle(tracks)
return tracks

4.滑动滑块

利用selenium,根据算出的轨迹,进行模拟滑动,代码如下:

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
def slide(self, tracks):

# slider = self.driver.find_element_by_xpath('//*[@id="captcha"]/div/div[3]/div[2]')
# 鼠标点击并按住不松
# webdriver.ActionChains(self.driver).click_and_hold(self.slider).perform()
# 让鼠标随机往下移动一段距离
webdriver.ActionChains(self.driver).move_by_offset(xoffset=0, yoffset=100).perform()
time.sleep(0.15)
for item in tracks:
webdriver.ActionChains(self.driver).move_by_offset(xoffset=item, yoffset=random.randint(-2,2)).perform()
# 稳定一秒再松开
time.sleep(1)
webdriver.ActionChains(self.driver).release(self.slider).perform()
time.sleep(1)
# 随机拿开鼠标
webdriver.ActionChains(self.driver).move_by_offset(xoffset=random.randint(200, 300), yoffset=random.randint(200, 300)).perform()
time.sleep(0.2)
info = self.driver.find_element_by_xpath('//*[@id="login-modal"]/div/div/div/div[2]/div[1]/div[2]/div[1]/div/div[1]/div[2]/div[2]/div/div[2]/span[1]')
if '验证通过' in info.text:
return 1

if '验证失败' in info.text:
return 2

if '再来一次' in info.text:
return 3

if '出现错误' in info.text:
return 4

5.完整代码

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
import re
import time
import random
import requests
from PIL import Image
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from io import BytesIO
from selenium.webdriver.common.action_chains import ActionChains



def get_merge_img(img_content,location_list,num):
'''
拼接图片
:param img_content:
:param location_list:
:param num:
:return:
'''
im = Image.open(img_content)
im_list_upper = []
im_list_done = []
for location in location_list:
# print(location)
if int(location['y']) == -58:
im_list_upper.append(im.crop((abs(int(location['x'])),58,abs(int(location['x']))+10,116)))
if int(location['y']) == 0:
im_list_done.append(im.crop((abs(int(location['x'])),0,abs(int(location['x']))+10,58)))

#create new image
new_im = Image.new('RGB',(260,116))
x_offset=0
for im in im_list_upper:
new_im.paste(im,(x_offset,0))
x_offset +=10

x_offset = 0
for im in im_list_done:
new_im.paste(im, (x_offset, 58))
x_offset += 10

return new_im


def get_img(driver,div_class,num):
'''
获取图片
:param driver:
:param div_class:
:param num:
:return:
'''
background_imgs = driver.find_elements_by_class_name(div_class)
location_list = []
imge_url = ''
for img in background_imgs:

location = {}
imge_url = re.findall(r'background-image: url\(\"(.*?)\"\); background-position: (.*?)px (.*?)px;',img.get_attribute('style'))[0][0]
location['x'] = re.findall(r'background-image: url\(\"(.*?)\"\); background-position: (.*?)px (.*?)px;',img.get_attribute('style'))[0][1]
location['y'] = re.findall(r'background-image: url\(\"(.*?)\"\); background-position: (.*?)px (.*?)px;',img.get_attribute('style'))[0][2]

location_list.append(location)

response = requests.get(imge_url).content
img_content = BytesIO(response)

image = get_merge_img(img_content,location_list,num)
image.save('{}.jpg'.format(num))
return image


def get_diff_location(image1,image2):
'''
通过像素对比 找到缺口位置
:param image1:
:param image2:
:return:
'''
for x in range(1,259):
for y in range(1, 115):
if is_similar(image1,image2,x,y) == False:
#判断成立 表示xy这个点 两张图不一样
return x


def is_similar(image1,image2,x,y):
pixel1 = image1.getpixel((x,y))
pixel2 = image2.getpixel((x,y))

for i in range(0,3):
if abs(pixel1[i]) - pixel2[i] >=50:
return False
return True

def get_track(x):
'''
滑块移动轨迹
初速度 v =0
单位时间 t = 0.2
位移轨迹 tracks = []
当前位移 ccurrent = 0
:param x:
:return:
'''
v = 0
t = 0.2
tracks = []
current = 0
# mid = x*5/8#到达mid值开始减速
# x = x+10
while current < x:
# if current < mid:
# a = random.randint(1,3)
# else:
# a = -random.randint(2,4)
a = 2
v0 = v
#单位时间内位移公式
s =v0*t+0.5*a*(t**2)
#当前位移
current = current+s
tracks.append(round(s))
v = v0+a*t

for i in range(3):
tracks.append(-1)
for i in range(3):
tracks.append(-2)
return tracks

def main(driver,element):

#1为完整图、2为有缺口图
image1 = get_img(driver,'gt_cut_fullbg_slice',1)
image2 = get_img(driver,'gt_cut_bg_slice',2)

x = get_diff_location(image1,image2)
tracks = get_track(x)
ActionChains(driver).click_and_hold(element).perform()
for x in tracks:
ActionChains(driver).move_by_offset(xoffset=x,yoffset=0).perform()
ActionChains(driver).release(element).perform()
time.sleep(3)


if __name__ == '__main__':
driver = webdriver.Firefox()
driver.maximize_window()
driver.get('http://www.cnbaowen.net/api/geetest/')
try:
count = 5
# waiting slidingVC loading
wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'gt_slider_knob')))
while count >0:
main(driver,element)
try:
succes = wait.until(EC.presence_of_all_elements_located((By.XPATH,'//div[@class="gt_ajax_tip gt_success"]')))
if succes:
print('恭喜你!识别成功...')
break
except Exception as e:
print('识别错误,继续')
count -=1
finally:
driver.quit()