现在的反爬措施越来越复杂,一些常见的反爬措施有Headers and referer反爬机制、语音动作识别、各种变态二维码(12306)、字符js加密、Ajax动态加载、模拟登录和cookie限制。。。

目标网站:极验官网 哔哩哔哩
反爬机制:极验滑动3.0版本
相关技术:selenium
技术难度:★★☆☆☆☆
爬取目标:模拟登录官网,破解滑动二维码
源代码:本页面
说明:找到相关节点DOM,其他网站则可复用此源代码

1.识别思路

目标网站:哔哩哔哩动画
   但是对于应用了极验验证码的网站,识别并不是没有办法的。如果我们直接模拟表单提交的话,加密参数的构造是个问题,参数构造有问题服务端就会校验失败,所以在这里我们采用直接模拟浏览器动作的方式来完成验证,在 Python 中我们就可以使用 Selenium 来通过完全模拟人的行为的方式来完成验证,此验证成本相对于直接去识别加密算法容易不少。

  • 模拟登录,找到入口url
  • 模拟点击验证按钮
  • 识别滑动缺口的位置
  • 模拟拖动滑块
        可以看到缺口的四周边缘有明显的断裂边缘,而且边缘和边缘周围有明显的区别,我们可以实现一个边缘检测算法来找出缺口的位置。对于极验来说,我们可以利用和原图对比检测的方式来识别缺口的位置,因为在没有滑动滑块之前,缺口其实是没有呈现的.
        所以我们可以同时获取两张图片,设定一个对比阈值,然后遍历两张图片找出相同位置像素 RGB 差距超过此阈值的像素点位置,那么此位置就是缺口的位置。
        第四步操作看似简单,但是其中的坑比较多,极验验证码增加了机器轨迹识别,匀速移动、随机速度移动等方法都是不行的,只有完全模拟人的移动轨迹才可以通过验证,而人的移动轨迹一般是先加速后减速的,这又涉及到物理学中加速度的相关问题,我们需要模拟这个过程才能成功。

    2.初始化

        首先这次我们选定的链接为:https://account.geetest.com/login,也就是极验的管理后台登录页面,在这里我们首先初始化一些配置,如 Selenium 对象的初始化及一些参数的配置:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # 极验账号
    EMAIL = 'test@test.com'
    # 极验密码
    PASSWORD = '123456'

    class CrackGeetest():
    def __init__(self):
    self.url = 'https://account.geetest.com/login'
    self.browser = webdriver.Chrome()
    self.wait = WebDriverWait(self.browser, 20)
    self.email = EMAIL
    self.password = PASSWORD

3.模拟点击

随后我们需要实现第一步的操作,也就是模拟点击初始的验证按钮,弹出滑动二维码,所以我们定义一个方法来获取这个按钮,利用显式等待的方法来实现:
斗鱼登录页面

1
2
3
4
5
6
7
8
def get_geetest_button(self):
"""
获取初始验证按钮(点击就会弹出验证码滑块,这一步按实际情况出发,
有些验证码滑块是直接存在,不用再点击一次来触发生成滑块)
:return: 按钮对象
"""
button = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_radar_tip')))
return button

这里返回一个网页节点对象,直接调用click()方法即可模拟点击

1
2
button = self.get_geetest_button()
button.click()

到这里就完成了第一步,点击一下就会弹出极验滑动二维码界面

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
def get_position(self):
"""
获取验证码位置
:return: 验证码位置元组
"""
img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_img')))
time.sleep(2)
location = img.location
size = img.size
top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size[
'width']
return (top, bottom, left, right)

def get_geetest_image(self, name='captcha.png'):
"""
获取验证码图片
:return: 图片对象
"""
top, bottom, left, right = self.get_position()
print('验证码位置', top, bottom, left, right)
screenshot = self.get_screenshot()
captcha = screenshot.crop((left, top, right, bottom))
return captcha

    在这里 get_position() 函数首先获取了图片对象,然后获取了它的位置和宽高,随后返回了其左上角和右下角的坐标。而 get_geetest_image() 方法则是获取了网页截图,然后调用了 crop() 方法将图片再裁切出来,返回的是 Image 对象。
    随后我们需要获取第二张图片,也就是带缺口的图片,要使得图片出现缺口,我们只需要点击一下下方的滑块即可,触发这个动作之后,图片中的缺口就会显现,实现如下:

1
2
3
4
5
6
7
def get_slider(self):
"""
获取滑块
:return: 滑块对象
"""
slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_slider_button')))
return slider

利用 get_slider() 方法获取滑块对象,接下来调用其 click() 方法即可触发点击,缺口图片即可呈现:

1
2
3
# 点击呼出缺口
slider = self.get_slider()
slider.click()

    随后还是调用 get_geetest_image() 方法将第二张图片获取下来即可。

    到现在我们就已经得到了两张图片对象了,分别赋值给变量 image1 和 image2,接下来对比图片获取缺口即可。要对比图片的不同之处,我们在这里遍历图片的每个坐标点,获取两张图片对应像素点的 RGB 数据,然后判断二者的 RGB 数据差异,如果差距超过在一定范围内,那就代表两个像素相同,继续比对下一个像素点,如果差距超过一定范围,则判断像素点不同,当前位置即为缺口位置,代码实现如下:

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
def is_pixel_equal(self, image1, image2, x, y):
"""
判断两个像素是否相同
:param image1: 图片1
:param image2: 图片2
:param x: 位置x
:param y: 位置y
:return: 像素是否相同
"""
# 取两个图片的像素点
pixel1 = image1.load()[x, y]
pixel2 = image2.load()[x, y]
threshold = 60
if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(
pixel1[2] - pixel2[2]) < threshold:
return True
else:
return False

def get_gap(self, image1, image2):
"""
获取缺口偏移量
:param image1: 不带缺口图片
:param image2: 带缺口图片
:return:
"""
left = 60
for i in range(left, image1.size[0]):
for j in range(image1.size[1]):
if not self.is_pixel_equal(image1, image2, i, j):
left = i
return left
return left

    get_gap() 方法即为获取缺口位置的方法,此方法的参数为两张图片,一张为带缺口图片,另一张为不带缺口图片,在这里遍历两张图片的每个像素,然后利用 is_pixel_equal() 方法判断两张图片同一位置的像素是否相同,比对的时候比较了两张图 RGB 的绝对值是否均小于定义的阈值 threshold,如果均在阈值之内,则像素点相同,继续遍历,否则遇到不相同的像素点就是缺口的位置。

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
def get_track(self, distance):
"""
根据偏移量获取移动轨迹
:param distance: 偏移量
:return: 移动轨迹
"""
# 移动轨迹
track = []
# 当前位移
current = 0
# 减速阈值
mid = distance * 4 / 5
# 计算间隔
t = 0.2
# 初速度
v = 0

while current < distance:
if current < mid:
# 加速度为正2
a = 2
else:
# 加速度为负3
a = -3
# 初速度v0
v0 = v
# 当前速度v = v0 + at
v = v0 + a * t
# 移动距离x = v0t + 1/2 * a * t^2
move = v0 * t + 1 / 2 * a * t * t
# 当前位移
current += move
# 加入轨迹
track.append(round(move))
return track


def move_to_gap(self, slider, tracks):
"""
拖动滑块到缺口处
:param slider: 滑块
:param tracks: 轨迹
:return:
"""
ActionChains(self.browser).click_and_hold(slider).perform()
for x in tracks:
ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform()
time.sleep(0.5)
ActionChains(self.browser).release().perform()

在这里传入的参数为滑块对象和运动轨迹,首先调用ActionChains 的 click_and_hold() 方法按住拖动底部滑块,随后遍历运动轨迹获取每小段位移距离,调用 move_by_offset() 方法移动此位移,最后移动完成之后调用 release() 方法松开鼠标即可。

6.完整源代码

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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import time
from io import BytesIO
from PIL import Image
from selenium import webdriver
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

EMAIL = 'example@example.com'
PASSWORD = ''
BORDER = 6
INIT_LEFT = 60


class CrackGeetest():
def __init__(self):
self.url = 'https://account.geetest.com/login'
self.browser = webdriver.Chrome()
self.wait = WebDriverWait(self.browser, 20)
self.email = EMAIL
self.password = PASSWORD

def __del__(self):
self.browser.close()

def get_geetest_button(self):
"""
获取初始验证按钮
:return:
"""
button = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_radar_tip')))
return button

def get_position(self):
"""
获取验证码位置
:return: 验证码位置元组
"""
img = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_img')))
time.sleep(2)
location = img.location
size = img.size
top, bottom, left, right = location['y'], location['y'] + size['height'], location['x'], location['x'] + size[
'width']
return (top, bottom, left, right)

def get_screenshot(self):
"""
获取网页截图
:return: 截图对象
"""
screenshot = self.browser.get_screenshot_as_png()
screenshot = Image.open(BytesIO(screenshot))
return screenshot

def get_slider(self):
"""
获取滑块
:return: 滑块对象
"""
slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'geetest_slider_button')))
return slider

def get_geetest_image(self, name='captcha.png'):
"""
获取验证码图片
:return: 图片对象
"""
top, bottom, left, right = self.get_position()
print('验证码位置', top, bottom, left, right)
screenshot = self.get_screenshot()
captcha = screenshot.crop((left, top, right, bottom))
captcha.save(name)
return captcha

def open(self):
"""
打开网页输入用户名密码
:return: None
"""
self.browser.get(self.url)
email = self.wait.until(EC.presence_of_element_located((By.ID, 'email')))
password = self.wait.until(EC.presence_of_element_located((By.ID, 'password')))
email.send_keys(self.email)
password.send_keys(self.password)

def get_gap(self, image1, image2):
"""
获取缺口偏移量
:param image1: 不带缺口图片
:param image2: 带缺口图片
:return:
"""
left = 60
for i in range(left, image1.size[0]):
for j in range(image1.size[1]):
if not self.is_pixel_equal(image1, image2, i, j):
left = i
return left
return left

def is_pixel_equal(self, image1, image2, x, y):
"""
判断两个像素是否相同
:param image1: 图片1
:param image2: 图片2
:param x: 位置x
:param y: 位置y
:return: 像素是否相同
"""
# 取两个图片的像素点
pixel1 = image1.load()[x, y]
pixel2 = image2.load()[x, y]
threshold = 60
if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs(
pixel1[2] - pixel2[2]) < threshold:
return True
else:
return False

def get_track(self, distance):
"""
根据偏移量获取移动轨迹
:param distance: 偏移量
:return: 移动轨迹
"""
# 移动轨迹
track = []
# 当前位移
current = 0
# 减速阈值
mid = distance * 4 / 5
# 计算间隔
t = 0.2
# 初速度
v = 0

while current < distance:
if current < mid:
# 加速度为正2
a = 2
else:
# 加速度为负3
a = -3
# 初速度v0
v0 = v
# 当前速度v = v0 + at
v = v0 + a * t
# 移动距离x = v0t + 1/2 * a * t^2
move = v0 * t + 1 / 2 * a * t * t
# 当前位移
current += move
# 加入轨迹
track.append(round(move))
return track

def move_to_gap(self, slider, track):
"""
拖动滑块到缺口处
:param slider: 滑块
:param track: 轨迹
:return:
"""
ActionChains(self.browser).click_and_hold(slider).perform()
for x in track:
ActionChains(self.browser).move_by_offset(xoffset=x, yoffset=0).perform()
time.sleep(0.5)
ActionChains(self.browser).release().perform()

def login(self):
"""
登录
:return: None
"""
submit = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'login-btn')))
submit.click()
time.sleep(10)
print('登录成功')

def crack(self):
# 输入用户名密码
self.open()
# 点击验证按钮
button = self.get_geetest_button()
button.click()
# 获取验证码图片
image1 = self.get_geetest_image('captcha1.png')
# 点按呼出缺口
slider = self.get_slider()
slider.click()
# 获取带缺口的验证码图片
image2 = self.get_geetest_image('captcha2.png')
# 获取缺口位置
gap = self.get_gap(image1, image2)
print('缺口位置', gap)
# 减去缺口位移
gap -= BORDER
# 获取移动轨迹
track = self.get_track(gap)
print('滑动轨迹', track)
# 拖动滑块
self.move_to_gap(slider, track)

success = self.wait.until(
EC.text_to_be_present_in_element((By.CLASS_NAME, 'geetest_success_radar_tip_content'), '验证成功'))
print(success)

# 失败后重试
if not success:
self.crack()
else:
self.login()


if __name__ == '__main__':
crack = CrackGeetest()
crack.crack()

 评论

联系我 | Contact with me

Copyright © 2019-2020 谁知你知我,我知你知深。此恨经年深,比情度日久

博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议