Python爬虫实战 | (15) 破解bilibili登陆滑动验证码

在本篇博客中,我们将使用selenium模拟登录bilibili网站,破解其登陆时的滑动验证码。

首先回顾一下,滑动验证码相关知识:

Python爬虫实战 | (15) 破解bilibili登陆滑动验证码_第1张图片

  • 简介

滑动图形验证码,主要由两个图片组成:抠块和带有抠块阴影的原图。

这里有两个重要特性保证被暴力破解的难度:

(1)抠块的形状随机

(2)抠块所在原图的位置随机

  • 生成滑动验证码

1)后端随机生成抠图和带有抠图阴影的背景图片,后台保存随机抠图位置坐标;

2)前端实现滑动交互,将抠图拼在抠图阴影之上,获取到用户滑动距离值;

3)前端将用户滑动距离值传入后端,后端校验误差是否在容许范围内。

  • 利用selenium解决滑动验证码

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() #松开鼠标

判断登录是否成功:

Python爬虫实战 | (15) 破解bilibili登陆滑动验证码_第2张图片

    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()

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(Python爬虫实战 | (15) 破解bilibili登陆滑动验证码)