登陆时间:2019-10-21
实现难度:★★★☆☆☆
请求链接:https://passport.bilibili.com/login
实现目标:模拟登陆哔哩哔哩,攻克滑动验证码
涉及知识:滑动验证码的攻克、自动化测试工具 Selenium 的使用
完整代码:https://github.com/TRHX/Python3-Spider-Practice/tree/master/bilibili-login
其他爬虫实战代码合集(持续更新):https://github.com/TRHX/Python3-Spider-Practice
爬虫实战专栏(持续更新):https://itrhx.blog.csdn.net/article/category/9351278


【1x00】思维导图


01
  • 利用自动化测试工具 Selenium 直接模拟人的行为方式来完成验证

  • 分析页面,想办法找到滑动验证码的完整图片、带有缺口的图片和需要滑动的图片

  • 对比原始的图片和带缺口的图片的像素,像素不同的地方就是缺口位置

  • 计算出滑块缺口的位置,得到所需要滑动的距离

  • 拖拽时要模仿人的行为,由于有个对准过程,所以要构造先快后慢的运动轨迹

  • 最后利用 Selenium 进行对滑块的拖拽


02

【2x00】登陆模块

【2x01】初始化函数

1
2
3
4
5
6
7
8
9
10
11
12
13
def init():
global url, browser, username, password, wait
url = 'https://passport.bilibili.com/login'
# path是谷歌浏览器驱动的目录,如果已经将目录添加到系统变量,则不用设置此路径
path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe'
chrome_options = Options()
chrome_options.add_argument('--start-maximized')
browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
# 你的哔哩哔哩用户名
username = '155********'
# 你的哔哩哔哩登陆密码
password = '***********'
wait = WebDriverWait(browser, 20)

global 关键字定义了发起请求的url、用户名、密码等全局变量,随后是登录页面url、谷歌浏览器驱动的目录path、实例化 Chrome 浏览器、设置浏览器分辨率最大化、用户名、密码、WebDriverWait() 方法设置等待超时


【2x02】登陆函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def login():
browser.get(url)
# 获取用户名输入框
user = wait.until(EC.presence_of_element_located((By.ID, 'login-username')))
# 获取密码输入框
passwd = wait.until(EC.presence_of_element_located((By.ID, 'login-passwd')))
# 输入用户名
user.send_keys(username)
# 输入密码
passwd.send_keys(password)
# 获取登录按钮
login_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'a.btn.btn-login')))
# 随机暂停几秒
time.sleep(random.random() * 3)
# 点击登陆按钮
login_btn.click()

等待用户名输入框和密码输入框对应的 ID 节点加载出来

获取这两个节点,用户名输入框 id="login-username",密码输入框 id="login-passwd"

调用 send_keys() 方法输入用户名和密码

获取登录按钮 class="btn btn-login"

随机产生一个数并将其扩大三倍作为暂停时间

最后调用 click() 方法实现登录按钮的点击


【3x00】验证码处理模块

【3x01】验证码元素查找函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def find_element():
# 获取带有缺口的图片
c_background = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_bg.geetest_absolute')))
# 获取需要滑动的图片
c_slice = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_slice.geetest_absolute')))
# 获取完整的图片
c_full_bg = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_fullbg.geetest_fade.geetest_absolute')))
# 隐藏需要滑动的图片
hide_element(c_slice)
# 保存带有缺口的图片
save_screenshot(c_background, 'back')
# 显示需要滑动的图片
show_element(c_slice)
# 保存需要滑动的图片
save_screenshot(c_slice, 'slice')
# 显示完整的图片
show_element(c_full_bg)
# 保存完整的图片
save_screenshot(c_full_bg, 'full')

获取验证码的三张图片,分别是完整的图片、带有缺口的图片和需要滑动的图片

分析页面代码,三张图片是由 3 个 canvas 组成,3 个 canvas 元素包含 CSS display 属性,display:block 为可见,display:none 为不可见,在分别获取三张图片时要将其他两张图片设置为 display:none,这样做才能单独提取到每张图片

定位三张图片的 class 分别为:带有缺口的图片(c_background):geetest_canvas_bg geetest_absolute、需要滑动的图片(c_slice):geetest_canvas_slice geetest_absolute、完整图片(c_full_bg):geetest_canvas_fullbg geetest_fade geetest_absolute

最后传值给 save_screenshot() 函数,进一步对验证码进行处理


03

【3x02】元素可见性设置函数

1
2
3
4
5
6
7
8
# 设置元素不可见
def hide_element(element):
browser.execute_script("arguments[0].style=arguments[1]", element, "display: none;")


# 设置元素可见
def show_element(element):
browser.execute_script("arguments[0].style=arguments[1]", element, "display: block;")

【3x03】验证码截图函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def save_screenshot(obj, name):
try:
# 首先对出现验证码后的整个页面进行截图保存
pic_url = browser.save_screenshot('.\\bilibili.png')
print("%s:截图成功!" % pic_url)
# 计算传入的obj,也就是三张图片的位置信息
left = obj.location['x']
top = obj.location['y']
right = left + obj.size['width']
bottom = top + obj.size['height']
# 打印输出一下每一张图的位置信息
print('图:' + name)
print('Left %s' % left)
print('Top %s' % top)
print('Right %s' % right)
print('Bottom %s' % bottom)
print('')
# 在整个页面截图的基础上,根据位置信息,分别剪裁出三张验证码图片并保存
im = Image.open('.\\bilibili.png')
im = im.crop((left, top, right, bottom))
file_name = 'bili_' + name + '.png'
im.save(file_name)
except BaseException as msg:
print("%s:截图失败!" % msg)

location 属性可以返回该图片对象在浏览器中的位置,坐标轴是以屏幕左上角为原点,x轴向右递增,y轴向下递增

size 属性可以返回该图片对象的高度和宽度,由此可以得到验证码的位置信息

首先调用 save_screenshot() 属性对整个页面截图并保存

然后向 crop() 方法传入验证码的位置信息,由位置信息再对验证码进行剪裁并保存


【4x00】验证码滑动模块

【4x01】滑动主函数

1
2
3
4
5
6
def slide():
distance = get_distance(Image.open('.\\bili_back.png'), Image.open('.\\bili_full.png'))
print('计算偏移量为:%s Px' % distance)
trace = get_trace(distance - 5)
move_to_gap(trace)
time.sleep(3)

get_distance() 函数传入完整的图片和缺口图片,计算滑块需要滑动的距离,再把距离信息传入 get_trace() 函数,构造滑块的移动轨迹,最后根据轨迹信息调用 move_to_gap() 函数移动滑块完成验证


【4x02】缺口位置寻找函数

1
2
3
4
5
6
7
8
9
10
11
12
def is_pixel_equal(bg_image, fullbg_image, x, y):
# 获取两张图片对应像素点的RGB数据
bg_pixel = bg_image.load()[x, y]
fullbg_pixel = fullbg_image.load()[x, y]
# 设定一个阈值
threshold = 60
# 比较两张图 RGB 的绝对值是否均小于定义的阈值
if (abs(bg_pixel[0] - fullbg_pixel[0] < threshold) and abs(bg_pixel[1] - fullbg_pixel[1] < threshold) and abs(
bg_pixel[2] - fullbg_pixel[2] < threshold)):
return True
else:
return False

将完整图片和缺口图片两个对象分别赋值给变量 bg_imagefullbg_image,接下来对比图片获取缺口。遍历图片的每个坐标点,获取两张图片对应像素点的 RGB 数据,判断像素的各个颜色之差,abs() 用于取绝对值,比较两张图 RGB 的绝对值是否均小于定义的阈值 threshold,如果绝对值均在阈值之内,则代表像素点相同,继续遍历,否则代表不相同的像素点,即缺口的位置


【4x03】计算滑块移动距离函数

1
2
3
4
5
6
7
8
9
def get_distance(bg_image, fullbg_image):
# 滑块的初始位置
distance = 60
# 遍历两张图片的每个像素
for i in range(distance, fullbg_image.size[0]):
for j in range(fullbg_image.size[1]):
# 调用缺口位置寻找函数
if not is_pixel_equal(fullbg_image, bg_image, i, j):
return i

get_distance() 方法即获取缺口位置的方法,此方法的参数是两张图片,一张为完整的图片,另一张为带缺口的图片,distance 为滑块的初始位置,遍历两张图片的每个像素,利用 is_pixel_equal() 缺口位置寻找函数判断两张图片同一位置的像素是否相同,若不相同则返回该点的值


【4x04】构造移动轨迹函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def get_trace(distance):
trace = []
# 设置加速距离为总距离的4/5
faster_distance = distance * (4 / 5)
# 设置初始位置、初始速度、时间间隔
start, v0, t = 0, 0, 0.1
while start < distance:
if start < faster_distance:
a = 10
else:
a = -10
# 位移
move = v0 * t + 1 / 2 * a * t * t
# 当前时刻的速度
v = v0 + a * t
v0 = v
start += move
trace.append(round(move))
# trace 记录了每个时间间隔移动了多少位移
return trace

get_trace() 方法传入的参数为移动的总距离,返回的是运动轨迹,运动轨迹用 trace 表示,它是一个列表,列表的每个元素代表每次移动多少距离,利用 Selenium 进行对滑块的拖拽时要模仿人的行为,由于有个对准过程,所以是先快后慢,匀速移动、随机速度移动都不会成功,因此要设置一个加速和减速的距离,这里设置加速距离 faster_distance 是总距离 distance 的4/5倍,滑块滑动的加速度用 a 来表示,当前速度用 v 表示,初速度用 v0 表示,位移用 move 表示,所需时间用 t 表示,它们之间满足以下关系:

1
2
move = v0 * t + 0.5 * a * t * t 
v = v0 + a * t

设置初始位置、初始速度、时间间隔分别为0, 0, 0.1,加速阶段和减速阶段的加速度分别设置为10和-10,直到运动轨迹达到总距离时,循环终止,最后得到的 trace 记录了每个时间间隔移动了多少位移,这样滑块的运动轨迹就得到了


【4x05】模拟拖动函数

1
2
3
4
5
6
7
8
9
10
11
12
def move_to_gap(trace):
# 获取滑动按钮
slider = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div.geetest_slider_button')))
# 点击并拖动滑块
ActionChains(browser).click_and_hold(slider).perform()
# 遍历运动轨迹获取每小段位移距离
for x in trace:
# 移动此位移
ActionChains(browser).move_by_offset(xoffset=x, yoffset=0).perform()
time.sleep(0.5)
# 释放鼠标
ActionChains(browser).release().perform()

传入的参数为运动轨迹,首先查找到滑动按钮,然后调用 ActionChains 的 click_and_hold() 方法按住拖动底部滑块,perform() 方法用于执行,遍历运动轨迹获取每小段位移距离,调用 move_by_offset() 方法移动此位移,最后调用 release() 方法松开鼠标即可


【5x00】完整代码

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
# =============================================
# --*-- coding: utf-8 --*--
# @Time : 2019-10-21
# @Author : TRHX
# @Blog : www.itrhx.com
# @CSDN : https://blog.csdn.net/qq_36759224
# @FileName: bilibili.py
# @Software: PyCharm
# =============================================

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver import ActionChains
import time
import random
from PIL import Image


# 初始化函数
def init():
global url, browser, username, password, wait
url = 'https://passport.bilibili.com/login'
# path是谷歌浏览器驱动的目录,如果已经将目录添加到系统变量,则不用设置此路径
path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe'
chrome_options = Options()
chrome_options.add_argument('--start-maximized')
browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options)
# 你的哔哩哔哩用户名
username = '155********'
# 你的哔哩哔哩登录密码
password = '***********'
wait = WebDriverWait(browser, 20)


# 登录函数
def login():
browser.get(url)
# 获取用户名输入框
user = wait.until(EC.presence_of_element_located((By.ID, 'login-username')))
# 获取密码输入框
passwd = wait.until(EC.presence_of_element_located((By.ID, 'login-passwd')))
# 输入用户名
user.send_keys(username)
# 输入密码
passwd.send_keys(password)
# 获取登录按钮
login_btn = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'a.btn.btn-login')))
# 随机暂停几秒
time.sleep(random.random() * 3)
# 点击登陆按钮
login_btn.click()


# 验证码元素查找函数
def find_element():
# 获取带有缺口的图片
c_background = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_bg.geetest_absolute')))
# 获取需要滑动的图片
c_slice = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_slice.geetest_absolute')))
# 获取完整的图片
c_full_bg = wait.until(
EC.presence_of_element_located((By.CSS_SELECTOR, 'canvas.geetest_canvas_fullbg.geetest_fade.geetest_absolute')))
# 隐藏需要滑动的图片
hide_element(c_slice)
# 保存带有缺口的图片
save_screenshot(c_background, 'back')
# 显示需要滑动的图片
show_element(c_slice)
# 保存需要滑动的图片
save_screenshot(c_slice, 'slice')
# 显示完整的图片
show_element(c_full_bg)
# 保存完整的图片
save_screenshot(c_full_bg, 'full')


# 设置元素不可见
def hide_element(element):
browser.execute_script("arguments[0].style=arguments[1]", element, "display: none;")


# 设置元素可见
def show_element(element):
browser.execute_script("arguments[0].style=arguments[1]", element, "display: block;")


# 验证码截图函数
def save_screenshot(obj, name):
try:
# 首先对出现验证码后的整个页面进行截图保存
pic_url = browser.save_screenshot('.\\bilibili.png')
print("%s:截图成功!" % pic_url)
# 计算传入的obj,也就是三张图片的位置信息
left = obj.location['x']
top = obj.location['y']
right = left + obj.size['width']
bottom = top + obj.size['height']
# 打印输出一下每一张图的位置信息
print('图:' + name)
print('Left %s' % left)
print('Top %s' % top)
print('Right %s' % right)
print('Bottom %s' % bottom)
print('')
# 在整个页面截图的基础上,根据位置信息,分别剪裁出三张验证码图片并保存
im = Image.open('.\\bilibili.png')
im = im.crop((left, top, right, bottom))
file_name = 'bili_' + name + '.png'
im.save(file_name)
except BaseException as msg:
print("%s:截图失败!" % msg)


# 滑动模块的主函数
def slide():
distance = get_distance(Image.open('.\\bili_back.png'), Image.open('.\\bili_full.png'))
print('计算偏移量为:%s Px' % distance)
trace = get_trace(distance - 5)
move_to_gap(trace)
time.sleep(3)


# 计算滑块移动距离函数
def get_distance(bg_image, fullbg_image):
# 滑块的初始位置
distance = 60
# 遍历两张图片的每个像素
for i in range(distance, fullbg_image.size[0]):
for j in range(fullbg_image.size[1]):
# 调用缺口位置寻找函数
if not is_pixel_equal(fullbg_image, bg_image, i, j):
return i


# 缺口位置寻找函数
def is_pixel_equal(bg_image, fullbg_image, x, y):
# 获取两张图片对应像素点的RGB数据
bg_pixel = bg_image.load()[x, y]
fullbg_pixel = fullbg_image.load()[x, y]
# 设定一个阈值
threshold = 60
# 比较两张图 RGB 的绝对值是否均小于定义的阈值
if (abs(bg_pixel[0] - fullbg_pixel[0] < threshold) and abs(bg_pixel[1] - fullbg_pixel[1] < threshold) and abs(
bg_pixel[2] - fullbg_pixel[2] < threshold)):
return True
else:
return False


# 构造移动轨迹函数
def get_trace(distance):
trace = []
# 设置加速距离为总距离的4/5
faster_distance = distance * (4 / 5)
# 设置初始位置、初始速度、时间间隔
start, v0, t = 0, 0, 0.1
while start < distance:
if start < faster_distance:
a = 10
else:
a = -10
# 位移
move = v0 * t + 1 / 2 * a * t * t
# 当前时刻的速度
v = v0 + a * t
v0 = v
start += move
trace.append(round(move))
# trace 记录了每个时间间隔移动了多少位移
return trace


# 模拟拖动函数
def move_to_gap(trace):
# 获取滑动按钮
slider = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div.geetest_slider_button')))
# 点击并拖动滑块
ActionChains(browser).click_and_hold(slider).perform()
# 遍历运动轨迹获取每小段位移距离
for x in trace:
# 移动此位移
ActionChains(browser).move_by_offset(xoffset=x, yoffset=0).perform()
time.sleep(0.5)
# 释放鼠标
ActionChains(browser).release().perform()


if __name__ == '__main__':
init()
login()
find_element()
slide()

【6x00】效果实现动图

最终实现效果图:(关键信息已经过打码处理)


04

 评论


Copyright 2018-2019 TRHX'BLOG   |   鄂ICP备19003281号-4  |   正在载入...   |   站点地图  |   站长统计

UV
PV
WordCount125.4k