在本篇博客中,我们将使用selenium模拟登录bilibili网站,破解其登陆时的滑动验证码。
首先回顾一下,滑动验证码相关知识:
滑动图形验证码,主要由两个图片组成:抠块和带有抠块阴影的原图。
这里有两个重要特性保证被暴力破解的难度:
(1)抠块的形状随机
(2)抠块所在原图的位置随机
1)后端随机生成抠图和带有抠图阴影的背景图片,后台保存随机抠图位置坐标;
2)前端实现滑动交互,将抠图拼在抠图阴影之上,获取到用户滑动距离值;
3)前端将用户滑动距离值传入后端,后端校验误差是否在容许范围内。
1)获取没有缺口的图片
2)获取带缺口的图片
3)对比两张图片的所有RBG像素点,得到不一样像素点的x值(即要移动的距离)
4)模拟人的行为习惯(先匀加速拖动后匀减速拖动),把需要拖动的总距离分成一段一段小的轨迹
5)按照轨迹拖动,完成验证
程序主体框架:
#登陆Bilibili网站 破解滑动验证码
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 selenium.webdriver import ActionChains
from selenium.common.exceptions import TimeoutException
import time
import base64
from PIL import Image
#bilibili用户名、密码
#全局变量
USERNAME = ''
PASSWARD = ''
BORDER = 6
INIT_LEFT = 60
class loginBili():
def __init__(self):
self.url = 'https://passport.bilibili.com/login' #B站登录界面
self.browser = webdriver.Chrome()
#定义显示等待
self.wait = WebDriverWait(self.browser,20)
self.username = USERNAME
self.password = PASSWARD
def __del__(self):
#关闭浏览器
self.browser.close()
def login_successfully(self):
pass
def move_to_gap(self,slider,track):
def get_slider(self):
pass
def get_track(self,gap):
pass
def get_gap(self,image1,image2):
pass
def get_geetest_image(self):
pass
def get_login_btn(self):
pass
def open(self):
pass
def login(self):
#输入用户名和密码
self.open()
#点击登录按钮
button = self.get_login_btn() #找到登录按钮
button.click() #点击
#获取验证码图片
image1,image2 = self.get_geetest_image()
#找到缺口的左侧边界 在x方向上的位置
gap = self.get_gap(image1,image2)
print('缺口位置:',gap)
#减去滑块左侧距离图片左侧在x方向上的距离 即为滑块实际要移动的距离
gap -= BORDER
#获取移动轨迹
track = self.get_track(gap)
print('滑动轨迹:',track)
#点按滑块
slider = self.get_slider()
#按轨迹拖动滑块
self.move_to_gap(slider,track)
if self.login_successfully():
print("登录成功")
else: #可能不成功 再试一次
time.sleep(5)
self.login()
if __name__ == '__main__':
login = loginBili()
login.login()
打开登陆界面,输入用户名和密码:
def open(self):
"""
打开登陆界面,输入用户名和密码
:return: None
"""
self.browser.get(self.url) #打开网址
# 找到用户名输入框
# 在浏览器中定位它的HTML代码后 根据id属性来找
'''
'''
username = self.wait.until(EC.presence_of_element_located((By.ID,'login-username')))
#找到密码输入框
'''
'''
password = self.wait.until(EC.presence_of_element_located((By.ID,'login-passwd')))
# 输入用户名和密码
username.send_keys(self.username)
password.send_keys(self.password)
找到登录按钮:
def get_login_btn(self):
"""
登陆
:return: None
"""
'''
登录
'''
button = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,'.btn btn-login')))
return button
获取验证码图片:
def get_geetest_image(self):
"""
获取验证码图片
:return: 图片对象
"""
'''
'''
# 带阴影的图片
im = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR,'.geetest_canvas_bg')))
time.sleep(2)
im.screenshot('captcha.png')
# 执行 JS 代码并拿到图片 base64 数据
JS = 'return document.getElementsByClassName("geetest_canvas_fullbg")[0].toDataURL("image/png");' # 不带阴影的完整图片
im_info = self.browser.execute_script(JS) # 执行js文件得到带图片信息的图片数据
print(im_info)
# 拿到base64编码的图片信息
im_base64 = im_info.split(',')[1]
# 转为bytes类型
captcha1 = base64.b64decode(im_base64)
# 将图片保存在本地
with open('captcha1.png', 'wb') as f:
f.write(captcha1)
# 执行 JS 代码并拿到图片 base64 数据
JS = 'return document.getElementsByClassName("geetest_canvas_bg")[0].toDataURL("image/png");' # 带阴影的图片
im_info = self.browser.execute_script(JS) # 执行js文件得到带图片信息的图片数据
print(im_info)
# 拿到base64编码的图片信息
im_base64 = im_info.split(',')[1]
# 转为bytes类型
captcha2 = base64.b64decode(im_base64)
# 将图片保存在本地
with open('captcha2.png', 'wb') as f:
f.write(captcha2)
captcha1 = Image.open('captcha1.png')
captcha2 = Image.open('captcha2.png')
return captcha1, captcha2
找到缺口的左侧边界 在x方向上的位置:
def get_gap(self,image1,image2):
"""
获取缺口偏移量
:param image1:不带缺口的图片
:param image2: 带缺口的图片
:return:
"""
left = INIT_LEFT # 定义一个左边的起点 缺口一般离图片左侧有一定的距离 有一个滑块
for i in range(INIT_LEFT, image1.size[0]): # 从左到右 x方向
for j in range(image1.size[1]): # 从上到下 y方向
if not self.is_pixel_equal(image1, image2, i, j):
left = i # 找到缺口的左侧边界 在x方向上的位置
return left
return left
def is_pixel_equal(self, image1, image2, x, y):
"""
判断两张图片 各个位置的像素是否相同
:param image1:不带缺口的图片
:param image2: 带缺口的图片
:param x: 位置x
:param y: 位置y
:return: (x,y)位置的像素是否相同
"""
# 获取两张图片指定位置的像素点
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,gap):
"""
根据偏移量 获取移动轨迹
:param distance: 偏移量
:return: 移动轨迹
"""
# 移动轨迹
track = []
# 当前位移
current = 0
# 减速阈值
mid = gap * 4 / 5 # 前4/5段加速 后1/5段减速
# 计算间隔
t = 0.2
# 初速度
v = 0
while current < gap:
if current < mid:
a = 2 # 加速度为+2
else:
a = -3 # 加速度为-3
# 初速度v0
v0 = v
# 当前速度
v = v0 + a * t
# 移动距离
move = v0 * t + 1 / 2 * a * t * t
# 当前位移
current += move
# 加入轨迹
track.append(round(move))
return track
找到滑块对象(拖动滑块的按钮):
def get_slider(self):
"""
获取滑块
:return: 滑块对象
"""
'''
'''
slider = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR,'.geetest_slider_button')))
return slider
按照运动轨迹把滑块拖到缺口处:
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_successfully(self):
"""
判断是否登陆成功
:return:
"""
try:
'''
3
消息
'''
#登录成功后 界面上会有一个消息按钮
return bool(
WebDriverWait(self.browser,5).until(EC.presence_of_element_located((By.XPATH,'//a[@title="消息"]')))
)
except TimeoutException:
return False
完整代码:
#登陆Bilibili网站 破解滑动验证码
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 selenium.webdriver import ActionChains
from selenium.common.exceptions import TimeoutException
import time
import base64
from PIL import Image
#bilibili用户名、密码
#全局变量
USERNAME = ''
PASSWARD = ''
BORDER = 6
INIT_LEFT = 60
class loginBili():
def __init__(self):
self.url = 'https://passport.bilibili.com/login' #B站登录界面
self.browser = webdriver.Chrome()
#定义显示等待
self.wait = WebDriverWait(self.browser,20)
self.username = USERNAME
self.password = PASSWARD
def __del__(self):
#关闭浏览器
self.browser.close()
def login_successfully(self):
"""
判断是否登陆成功
:return:
"""
try:
'''
3
消息
'''
#登录成功后 界面上会有一个消息按钮
return bool(
WebDriverWait(self.browser,5).until(EC.presence_of_element_located((By.XPATH,'//a[@title="消息"]')))
)
except TimeoutException:
return False
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 get_slider(self):
"""
获取滑块
:return: 滑块对象
"""
'''
'''
slider = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR,'.geetest_slider_button')))
return slider
def get_track(self,gap):
"""
根据偏移量 获取移动轨迹
:param gap: 偏移量
:return: 移动轨迹
"""
# 移动轨迹
track = []
# 当前位移
current = 0
# 减速阈值
mid = gap * 4 / 5 # 前4/5段加速 后1/5段减速
# 计算间隔
t = 0.2
# 初速度
v = 0
while current < gap:
if current < mid:
a = 3 # 加速度为+3
else:
a = -3 # 加速度为-3
# 初速度v0
v0 = v
# 当前速度
v = v0 + a * t
# 移动距离
move = v0 * t + 1 / 2 * a * t * t
# 当前位移
current += move
# 加入轨迹
track.append(round(move))
return track
def is_pixel_equal(self, image1, image2, x, y):
"""
判断两张图片 各个位置的像素是否相同
:param image1:不带缺口的图片
:param image2: 带缺口的图片
:param x: 位置x
:param y: 位置y
:return: (x,y)位置的像素是否相同
"""
# 获取两张图片指定位置的像素点
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 = INIT_LEFT # 定义一个左边的起点 缺口一般离图片左侧有一定的距离 有一个滑块
for i in range(INIT_LEFT, image1.size[0]): # 从左到右 x方向
for j in range(image1.size[1]): # 从上到下 y方向
if not self.is_pixel_equal(image1, image2, i, j):
left = i # 找到缺口的左侧边界 在x方向上的位置
return left
return left
def get_geetest_image(self):
"""
获取验证码图片
:return: 图片对象
"""
'''
'''
# 带阴影的图片
im = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR,'.geetest_canvas_bg')))
time.sleep(2)
im.screenshot('captcha.png')
# 执行 JS 代码并拿到图片 base64 数据
JS = 'return document.getElementsByClassName("geetest_canvas_fullbg")[0].toDataURL("image/png");' # 不带阴影的完整图片
im_info = self.browser.execute_script(JS) # 执行js文件得到带图片信息的图片数据
print(im_info)
# 拿到base64编码的图片信息
im_base64 = im_info.split(',')[1]
# 转为bytes类型
captcha1 = base64.b64decode(im_base64)
# 将图片保存在本地
with open('captcha1.png', 'wb') as f:
f.write(captcha1)
# 执行 JS 代码并拿到图片 base64 数据
JS = 'return document.getElementsByClassName("geetest_canvas_bg")[0].toDataURL("image/png");' # 带阴影的图片
im_info = self.browser.execute_script(JS) # 执行js文件得到带图片信息的图片数据
print(im_info)
# 拿到base64编码的图片信息
im_base64 = im_info.split(',')[1]
# 转为bytes类型
captcha2 = base64.b64decode(im_base64)
# 将图片保存在本地
with open('captcha2.png', 'wb') as f:
f.write(captcha2)
captcha1 = Image.open('captcha1.png')
captcha2 = Image.open('captcha2.png')
return captcha1, captcha2
def get_login_btn(self):
"""
登陆
:return: None
"""
'''
登录
值有空格的 查找时写一半就好 要么前半段要么后半段
'''
#button = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,'.btn-login')))
button = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'btn')))
return button
def open(self):
"""
打开登陆界面,输入用户名和密码
:return: None
"""
self.browser.get(self.url) #打开网址
# 找到用户名输入框
# 在浏览器中定位它的HTML代码后 根据id属性来找
'''
'''
username = self.wait.until(EC.presence_of_element_located((By.ID,'login-username')))
#找到密码输入框
'''
'''
password = self.wait.until(EC.presence_of_element_located((By.ID,'login-passwd')))
# 输入用户名和密码
username.send_keys(self.username)
password.send_keys(self.password)
def login(self):
#输入用户名和密码
self.open()
#点击登录按钮
button = self.get_login_btn() #找到登录按钮
button.click() #点击
#获取验证码图片
image1,image2 = self.get_geetest_image()
#找到缺口的左侧边界 在x方向上的位置
gap = self.get_gap(image1,image2)
print('缺口位置:',gap)
#减去滑块左侧距离图片左侧在x方向上的距离 即为滑块实际要移动的距离
gap -= BORDER
#获取移动轨迹
track = self.get_track(gap)
print('滑动轨迹:',track)
#点按滑块
slider = self.get_slider()
#按轨迹拖动滑块
self.move_to_gap(slider,track)
if self.login_successfully():
print("登录成功")
else: #可能不成功 再试一次
time.sleep(5)
self.login()
if __name__ == '__main__':
login = loginBili()
login.login()