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

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

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

  • 简介

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

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

(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属性来找
        '''
        <input type="text" value="" placeholder="你的手机号/邮箱" id="login-username" maxlength="50" autocomplete="off" class="">
        '''
        username = self.wait.until(EC.presence_of_element_located((By.ID,'login-username')))
        #找到密码输入框
        '''
        <input type="password" placeholder="密码" id="login-passwd" class="">
        '''
        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
        """
        '''
        <a class="btn btn-login">登录</a>
        '''
        button = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,'.btn btn-login')))
        return button
        

获取验证码图片:

    def get_geetest_image(self):
        """
        获取验证码图片
        :return: 图片对象
        """
        '''
        <canvas class="geetest_canvas_bg geetest_absolute" height="160" width="260"></canvas>
        '''
        # 带阴影的图片
        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: 滑块对象
        """
        '''
        <div class="geetest_slider_button" style="opacity: 1; transform: translate(0px, 0px);"></div>
        '''
        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:
            '''
           <a data-v-4d9bc88b="" href="//message.bilibili.com" target="_blank" title="消息" class="t"><div data-v-4d9bc88b="" class="num">3</div> <!---->
    消息
  </a>
            '''
            #登录成功后 界面上会有一个消息按钮
            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:
            '''
           <a data-v-4d9bc88b="" href="//message.bilibili.com" target="_blank" title="消息" class="t"><div data-v-4d9bc88b="" class="num">3</div> <!---->
    消息
  </a>
            '''
            #登录成功后 界面上会有一个消息按钮
            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: 滑块对象
        """
        '''
        <div class="geetest_slider_button" style="opacity: 1; transform: translate(0px, 0px);"></div>
        '''
        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: 图片对象
        """
        '''
        <canvas class="geetest_canvas_bg geetest_absolute" height="160" width="260"></canvas>
        '''
        # 带阴影的图片
        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
        """
        '''
        <a class="btn btn-login">登录</a>
        值有空格的 查找时写一半就好 要么前半段要么后半段
        '''
        #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属性来找
        '''
        <input type="text" value="" placeholder="你的手机号/邮箱" id="login-username" maxlength="50" autocomplete="off" class="">
        '''
        username = self.wait.until(EC.presence_of_element_located((By.ID,'login-username')))
        #找到密码输入框
        '''
        <input type="password" placeholder="密码" id="login-passwd" class="">
        '''
        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()

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 61
    点赞
  • 136
    收藏
    觉得还不错? 一键收藏
  • 18
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值