python爬虫进阶-滑块验证码破解(bilibili)

目标

如下图,利用selenium模拟拖动滑块完成验证
python爬虫进阶-滑块验证码破解(bilibili)_第1张图片

关键问题

我们知道selenium可以定位到用户名和密码,用send_keys可以实现输入账号密码。同样我们可以用selenium定位到滑块,用click_and_hold方法可以实现拖动滑块。

关键问题是:

1)如何计算滑块拖动的距离?
2)知道了滑块拖动的距离后,如何设计路径,使得浏览器操作更像人而不被识别出来。

思路

1)滑块拖动距离的计算
python爬虫进阶-滑块验证码破解(bilibili)_第2张图片

通过隐藏滑块,截取只含缺口图片bg_image,再截取既不含滑块又不含缺口的图片fullbg_image
对比两张图的差异,可以得到缺口的位置x1

截取含有滑块和缺口的图片slice_image,对比fullbg_image,可以得到滑块的初始位置x2;

滑块拖动的距离x=x2-x1

2)路径的设计

由牛顿第二定律衍生出的位移计算公式为
l=vt+1/2at*t
把滑块拖动的距离x拆分成l1,l2,l3,l4…每一段设置不同的a和t(后一段的初始速度为前一段的结束速度),a可以设置先增后减,t可以设置随机数

爬虫实施

Step1:初始化

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.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

Step2:用Selenium打开目标网站

url = 'https://passport.bilibili.com/login'
driver = webdriver.Chrome(r"C:\Users\ThinkPad\AppData\Local\Google\Chrome\Application\chromedriver.exe")
wait = WebDriverWait(driver, 20)
phone_number = "18519146290"
passwd="xc080218"
driver.get(url)
driver.maximize_window()

Step3:定位账号密码输入框,并输入参数,点击登录

send_phone=driver.find_element_by_xpath('//div[@class="item username status-box"]/input')
send_phone.send_keys(phone_number)
send_passwd=driver.find_element_by_xpath('//div[@class="item password status-box"]/input')
send_passwd.send_keys(passwd)
button = wait.until(EC.element_to_be_clickable((By.XPATH, '//div[@class="btn-box"]/a[@class="btn btn-login"]')))
button.click()

这里使用了显式等待

Step4:依次定位到缺口图片、滑块位置、完整图片

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

Step5:隐藏滑块,截取全图,根据c_background裁剪只含缺口的图片

driver.execute_script("arguments[0].style=arguments[1]",c_slice,"display: none;")
obj=c_background
name='back'

try:
    pic_url=driver.save_screenshot('.\\bilibili.png')
    print("%s:截图成功!" % pic_url)

    #获取元素位置信息
    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)

Step6:显示滑块,截图,根据c_slice裁剪既含滑块又含缺口的图片

driver.execute_script("arguments[0].style=arguments[1]",c_slice,"display: block;")
obj=c_slice
name='slice'

try:
    pic_url=driver.save_screenshot('.\\bilibili.png')
    print("%s:截图成功!" % pic_url)

    #获取元素位置信息
    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)

Step7:

driver.execute_script("arguments[0].style=arguments[1]",c_full_bg,"display: block;")
obj=c_full_bg
name='full'

try:
    pic_url=driver.save_screenshot('.\\bilibili.png')
    print("%s:截图成功!" % pic_url)

    #获取元素位置信息
    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)

Step8:通过对比像素点,得到两张图像素点第一次不一致时的横坐标

fullbg_image=Image.open('.\\bili_full.png')
bg_image=Image.open('.\\bili_back.png')
# 遍历像素点横坐标
for i in range(fullbg_image.size[0]):
    # 遍历像素点纵坐标
    for j in range(fullbg_image.size[1]):
     
        bg_pixel = bg_image.load()[i, j]
        # 获取完整图片的像素点(按照RGB格式)
        fullbg_pixel = fullbg_image.load()[i, j]
        # 设置一个判定值,像素值之差超过判定值则认为该像素不相同
        threshold = 10
        # 判断像素的各个颜色之差,abs()用于取绝对值
        if not (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)):
           # 如果差值在判断值之内,返回是相同像素
            distance=i
            break  
        else:
            continue
    else:
        continue
    break

Step9:设计滑块路径,输出轨迹字典

# 创建存放轨迹信息的列表
trace = []
# 设置加速的距离
distance=distance*1.68
faster_distance = distance*(4/5)
# 设置初始位置、初始速度、时间间隔
start, v0, t = 0, 0, 0.1
# 当尚未移动到终点时
while start < distance:
    # 如果处于加速阶段
    if start < faster_distance:
        # 设置加速度为2
        a = 1.5
    # 如果处于减速阶段
    else:
        # 设置加速度为-3
        a = -3
    # 移动的距离公式
    move = v0 * t + 1 / 2 * a * t * t
    # 此刻速度
    v = v0 + a * t
    # 重置初速度
    v0 = v
    # 重置起点
    start += move
    # 将移动的距离加入轨迹列表
    trace.append(round(move,2))

看一下这一段代码的输出:
python爬虫进阶-滑块验证码破解(bilibili)_第3张图片
127.68的距离被拆分成了134小段,步长先快递增加后缓慢减少

Step10: 模拟滑块移动,每移动一小段,停歇一小段时间

slider=wait.until(EC.presence_of_element_located((By.CSS_SELECTOR,'div.geetest_slider_button')))
# 使用click_and_hold()方法悬停在滑块上,perform()方法用于执行
ActionChains(driver).click_and_hold(slider).perform()
for x in trace:
    # 使用move_by_offset()方法拖动滑块,perform()方法用于执行
    ActionChains(driver).move_by_offset(xoffset=x, yoffset=0).perform()
# 模拟人类对准时间
    time.sleep(0.02)
# 释放滑块
ActionChains(driver).release().perform()

Step11:distance修正

运行Step10中的代码,会发现滑块按照计算轨迹滑动后无法与缺口吻合,笔者尝试手动修改distance促成滑块吻合,得到多组数据,用Sklearn跑一次线性回归

import pandas as pd
data=pd.read_excel(r"E:\崔庆才爬虫\七月在线\滑块测试.xlsx")
data=data.sort_values("x")
print(data)

import matplotlib.pyplot as plt
plt.scatter(data["x"].values,data["y"].values)
plt.show()

from sklearn import linear_model        #表示,可以调用sklearn中的linear_model模块进行线性回归。
import numpy as np
model = linear_model.LinearRegression()
model.fit(data["x"].values.reshape(-1,1),data["y"].values)
display(model.intercept_)  #截距
display(model.coef_) 

python爬虫进阶-滑块验证码破解(bilibili)_第4张图片
得出线性回归方程:y=1.29*x+34

But,带入代码中,发现时灵时不灵。。。
很急,不知道咋搞了,以后再说!

完整代码:

# -*- coding: utf-8 -*-

 
from selenium import webdriver
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
import os
from PIL import Image
 
# 初始化

# 登录界面的url
url = 'https://passport.bilibili.com/login'
# 实例化一个chrome浏览器
browser = webdriver.Chrome(r"C:\Users\ThinkPad\AppData\Local\Google\Chrome\Application\chromedriver.exe")
# 用户名
username = '18519146290'
# 密码
password = 'xc080218'
# 设置等待超时
wait = WebDriverWait(browser, 20)
 
# 登录
def login():
    # 打开登录页面
    browser.get(url)
    browser.maximize_window()
    # 获取用户名输入框
    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 show_element(element):
    browser.execute_script("arguments[0].style=arguments[1]",element,"display: block;")
#设置元素不可见
def hide_element(element):
    browser.execute_script("arguments[0].style=arguments[1]",element,"display: none;")

#对某元素截图   
def save_pic(obj,name):
    try:
        pic_url=browser.save_screenshot('.\\bilibili.png')
        print("%s:截图成功!" % pic_url)
        
        #获取元素位置信息
        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 cut():
    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_pic(c_background,'back')
    show_element(c_slice)
    save_pic(c_slice,'slice')
    show_element(c_full_bg)
    save_pic(c_full_bg,'full')
    
# 判断像素是否相同
def is_pixel_equal(bg_image, fullbg_image, x, y):
    """
    :param bg_image: (Image)缺口图片
    :param fullbg_image: (Image)完整图片
    :param x: (Int)位置x
    :param y: (Int)位置y
    :return: (Boolean)像素是否相同
    """
 
    # 获取缺口图片的像素点(按照RGB格式)
    bg_pixel = bg_image.load()[x, y]
    # 获取完整图片的像素点(按照RGB格式)
    fullbg_pixel = fullbg_image.load()[x, y]
    # 设置一个判定值,像素值之差超过判定值则认为该像素不相同
    threshold = 20
    # 判断像素的各个颜色之差,abs()用于取绝对值
    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_distance(bg_image, fullbg_image):
    '''
    :param bg_image: (Image)缺口图片
    :param fullbg_image: (Image)完整图片
    :return: (Int)缺口离滑块的距离
    '''
 
    # 滑块的初始位置
    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 get_trace(distance):
    '''
    :param distance: (Int)缺口离滑块的距离
    :return: (List)移动轨迹
    '''
 
    # 创建存放轨迹信息的列表
    trace = []
    # 设置加速的距离
    faster_distance = distance*(4/5)
    # 设置初始位置、初始速度、时间间隔
    start, v0, t = 0, 0, 0.2
    # 当尚未移动到终点时
    while start < distance:
        # 如果处于加速阶段
        if start < faster_distance:
            # 设置加速度为2
            a = 1.5
        # 如果处于减速阶段
        else:
            # 设置加速度为-3
            a = -3
        # 移动的距离公式
        move = v0 * t + 1 / 2 * a * t * t
        # 此刻速度
        v = v0 + a * t
        # 重置初速度
        v0 = v
        # 重置起点
        start += move
        # 将移动的距离加入轨迹列表
        trace.append(round(move))
    # 返回轨迹信息
    return trace
 
 # 模拟拖动
def move_to_gap(trace):
    # 得到滑块标签
    #slider = wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'gt_slider_knob')))
    slider=wait.until(EC.presence_of_element_located((By.CSS_SELECTOR,'div.geetest_slider_button')))
    # 使用click_and_hold()方法悬停在滑块上,perform()方法用于执行
    ActionChains(browser).click_and_hold(slider).perform()
    for x in trace:
        # 使用move_by_offset()方法拖动滑块,perform()方法用于执行
        ActionChains(browser).move_by_offset(xoffset=x, yoffset=0).perform()
    # 模拟人类对准时间
    time.sleep(0.5)
    # 释放滑块
    ActionChains(browser).release().perform()
 
            
def slide():
    distance=get_distance(Image.open('.\\bili_back.png'),Image.open('.\\bili_full.png'))*1.29+34
    print('计算偏移量为:%s Px' % distance)
    # 计算移动轨迹
    trace = get_trace(distance)
    # 移动滑块
    move_to_gap(trace)
    time.sleep(3)

os.chdir(r"E:\崔庆才爬虫\七月在线")
login()
cut()
slide()

你可能感兴趣的:(python爬虫专栏)