上篇我们借助 tesserocr 库解决了图片码的识别验证,但在实际生活中,用得更多的是滑动验证,这篇就来解决滑动验证的问题
大部分网站都采用的是滑动验证,就是拖动滑块拼接图片,其中又大都采用极验(http://www.geetest.com/)所提供的技术,官方网页如下
本篇案例选用哔哩哔哩动画验证登录(https://passport.bilibili.com/login)
chromedriver:浏览器驱动,可以理解为一个没有界面的chrome浏览器
selenium:用于模拟人对浏览器进行点击、输出、拖拽等操作,就相当于是个人在使用浏览器,也常常用来应付反爬虫措施,配合chromedriver使用,使用方法直接粘大神写的:https://cuiqingcai.com/5630.html
Image模块:提供很多对图片进行处理的方法的库,用法请查看https://www.cnblogs.com/kongzhagen/p/6295925.html
1. 获取验证图片
通过访问登录页面,分析源码找到完整图片和带滑块缺口的图片 ,通过 selenium 键入登录信息
2. 获取缺口位置
通过对比原始的图片和带滑块缺口的图片的像素,计算出滑块缺口的位置,得到所需要滑动的距离
3.模拟拖动
利用selenium进行对滑块的拖拽,注意模仿人的行为:先快后慢,有个对准过程
1.初始化一些需要用到的参数
from selenium import webdriver from selenium.webdriver.support.wait import WebDriverWait # 初始化 def init(): # 定义为全局变量,方便其他模块使用 global url, browser, username, password, wait # 登录界面的url url = 'https://passport.bilibili.com/login' # 实例化一个chrome浏览器 browser = webdriver.Chrome() # 用户名 username = '***********' # 密码 password = '***********' # 设置等待超时 wait = WebDriverWait(browser, 20)
2.通过 selenium 键入登录信息
(by the way:之前用 post 提交表单做过模拟登录,其实用selenium模拟人键入登录信息和点击提交也可以成功登录,但问题是selenium相率太低,所以一般能不用selenium就不用)
from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 登录 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)
3.获取验证图片
通过分析源码找到原始完整图片和带滑块缺口的图片
from urllib.request import urlretrieve from bs4 import BeautifulSoup import re from PIL import Image # 获取图片信息 def get_image_info(img): ''' :param img: (Str)想要获取的图片类型:带缺口、原始 :return: 该图片(Image)、位置信息(List) ''' # 将网页源码转化为能被解析的lxml格式 soup = BeautifulSoup(browser.page_source, 'lxml') # 获取验证图片的所有组成片标签 imgs = soup.find_all('div', {'class': 'gt_cut_'+img+'_slice'}) # 用正则提取缺口的小图片的url,并替换后缀 img_url = re.findall('url\(\"(.*)\"\);', imgs[0].get('style'))[0].replace('webp', 'jpg') # 使用urlretrieve()方法根据url下载缺口图片对象 urlretrieve(url=img_url, filename=img+'.jpg') # 生成缺口图片对象 image = Image.open(img+'.jpg') # 获取组成他们的小图片的位置信息 position = get_position(imgs) # 返回图片对象及其位置信息 return image, position
但是这里有一个问题,验证图片是由两行许多的小图片组成的,而这些小图片不是按顺序排列的,这样就无法计算滑块缺口的位置了
因此我们上面的代码在有一个获取每个小图片正确位置信息的方法 get_position()
其代码如下:
# 获取小图片位置 def get_position(img): ''' :param img: (List)存放多个小图片的标签 :return: (List)每个小图片的位置信息 ''' img_position = [] for small_img in img: position = {} # 获取每个小图片的横坐标 position['x'] = int(re.findall('background-position: (.*)px (.*)px;', small_img.get('style'))[0][0]) # 获取每个小图片的纵坐标 position['y'] = int(re.findall('background-position: (.*)px (.*)px;', small_img.get('style'))[0][1]) img_position.append(position) return img_position
4.把这些小图片裁剪下来以方便重新拼成顺序正确的图片
from PIL import Image # 裁剪图片 def Corp(image, position): ''' :param image:(Image)被裁剪的图片 :param position: (List)该图片的位置信息 :return: (List)存放裁剪后的每个图片信息 ''' # 第一行图片信息 first_line_img = [] # 第二行图片信息 second_line_img = [] for pos in position: if pos['y'] == -58: first_line_img.append(image.crop((abs(pos['x']), 58, abs(pos['x']) + 10, 116))) if pos['y'] == 0: second_line_img.append(image.crop((abs(pos['x']), 0, abs(pos['x']) + 10, 58))) return first_line_img, second_line_img
5.拼接处正确图片
按两行逐次拼接,使用 paste() 方法按照位置信息得到正确图片
# 拼接大图 def put_imgs_together(first_line_img, second_line_img, img_name): ''' :param first_line_img: (List)第一行图片位置信息 :param second_line_img: (List)第二行图片信息 :return: (Image)拼接后的正确顺序的图片 ''' # 新建一个图片,new()第一个参数是颜色模式,第二个是图片尺寸 image = Image.new('RGB', (260,116)) # 初始化偏移量为0 offset = 0 # 拼接第一行 for img in first_line_img: # past()方法进行粘贴,第一个参数是被粘对象,第二个是粘贴位置 image.paste(img, (offset, 0)) # 偏移量对应增加移动到下一个图片位置,size[0]表示图片宽度 offset += img.size[0] # 偏移量重置为0 x_offset = 0 # 拼接第二行 for img in second_line_img: # past()方法进行粘贴,第一个参数是被粘对象,第二个是粘贴位置 image.paste(img, (x_offset, 58)) # 偏移量对应增加移动到下一个图片位置,size[0]表示图片宽度 x_offset += img.size[0] # 保存图片 image.save(img_name) # 返回图片对象 return image
此时,我们得到的就是位置正确的图片了
以上,第一大步 获取验证图片 就算是完成了
1.计算缺口位置(即滑块需要滑动的距离)
# 计算滑块移动距离 def get_distance(bg_image, fullbg_image): ''' :param bg_image: (Image)缺口图片 :param fullbg_image: (Image)完整图片 :return: (Int)缺口离滑块的距离 ''' # 滑块的初始位置 distance = 57 # 遍历像素点横坐标 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
这其中有个is_pixel_equal()方法用于判断是否为相同像素,从而判断是不是缺口位置
其代码如下:
# 判断像素是否相同 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 = 60 # 判断像素的各个颜色之差,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
1.构造模拟人类的滑块移动轨迹:先快后慢,有对准时间
# 构造滑动轨迹 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
2.利用selenium拖拽滑块
# 模拟拖动 def move_to_gap(trace): # 得到滑块标签 slider = wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'gt_slider_knob'))) # 使用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() # 模拟人类对准时间 sleep(0.5) # 释放滑块 ActionChains(browser).release().perform()
程序结构图
完整代码
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 from urllib.request import urlretrieve from bs4 import BeautifulSoup import re from PIL import Image from time import sleep # 初始化 def init(): # 定义为全局变量,方便其他模块使用 global url, browser, username, password, wait # 登录界面的url url = 'https://passport.bilibili.com/login' # 实例化一个chrome浏览器 browser = webdriver.Chrome() # 用户名 username = '***********' # 密码 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) # 获取图片信息 def get_image_info(img): ''' :param img: (Str)想要获取的图片类型:带缺口、原始 :return: 该图片(Image)、位置信息(List) ''' # 将网页源码转化为能被解析的lxml格式 soup = BeautifulSoup(browser.page_source, 'lxml') # 获取验证图片的所有组成片标签 imgs = soup.find_all('div', {'class': 'gt_cut_'+img+'_slice'}) # 用正则提取缺口的小图片的url,并替换后缀 img_url = re.findall('url\(\"(.*)\"\);', imgs[0].get('style'))[0].replace('webp', 'jpg') # 使用urlretrieve()方法根据url下载缺口图片对象 urlretrieve(url=img_url, filename=img+'.jpg') # 生成缺口图片对象 image = Image.open(img+'.jpg') # 获取组成他们的小图片的位置信息 position = get_position(imgs) # 返回图片对象及其位置信息 return image, position # 获取小图片位置 def get_position(img): ''' :param img: (List)存放多个小图片的标签 :return: (List)每个小图片的位置信息 ''' img_position = [] for small_img in img: position = {} # 获取每个小图片的横坐标 position['x'] = int(re.findall('background-position: (.*)px (.*)px;', small_img.get('style'))[0][0]) # 获取每个小图片的纵坐标 position['y'] = int(re.findall('background-position: (.*)px (.*)px;', small_img.get('style'))[0][1]) img_position.append(position) return img_position # 裁剪图片 def Corp(image, position): ''' :param image:(Image)被裁剪的图片 :param position: (List)该图片的位置信息 :return: (List)存放裁剪后的每个图片信息 ''' # 第一行图片信息 first_line_img = [] # 第二行图片信息 second_line_img = [] for pos in position: if pos['y'] == -58: first_line_img.append(image.crop((abs(pos['x']), 58, abs(pos['x']) + 10, 116))) if pos['y'] == 0: second_line_img.append(image.crop((abs(pos['x']), 0, abs(pos['x']) + 10, 58))) return first_line_img, second_line_img # 拼接大图 def put_imgs_together(first_line_img, second_line_img, img_name): ''' :param first_line_img: (List)第一行图片位置信息 :param second_line_img: (List)第二行图片信息 :return: (Image)拼接后的正确顺序的图片 ''' # 新建一个图片,new()第一个参数是颜色模式,第二个是图片尺寸 image = Image.new('RGB', (260,116)) # 初始化偏移量为0 offset = 0 # 拼接第一行 for img in first_line_img: # past()方法进行粘贴,第一个参数是被粘对象,第二个是粘贴位置 image.paste(img, (offset, 0)) # 偏移量对应增加移动到下一个图片位置,size[0]表示图片宽度 offset += img.size[0] # 偏移量重置为0 x_offset = 0 # 拼接第二行 for img in second_line_img: # past()方法进行粘贴,第一个参数是被粘对象,第二个是粘贴位置 image.paste(img, (x_offset, 58)) # 偏移量对应增加移动到下一个图片位置,size[0]表示图片宽度 x_offset += img.size[0] # 保存图片 image.save(img_name) # 返回图片对象 return image # 判断像素是否相同 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 = 60 # 判断像素的各个颜色之差,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 = 57 # 遍历像素点横坐标 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'))) # 使用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() # 模拟人类对准时间 sleep(0.5) # 释放滑块 ActionChains(browser).release().perform() # 主程序 def main(): # 初始化 init() # 登录 login() # 获取缺口图片及其位置信息 bg, bg_position = get_image_info('bg') # 获取完整图片及其位置信息 fullbg, fullbg_position = get_image_info('fullbg') # 将混乱的缺口图片裁剪成小图,获取两行的位置信息 bg_first_line_img, bg_second_line_img = Corp(bg, bg_position) # 将混乱的完整图片裁剪成小图,获取两行的位置信息 fullbg_first_line_img, fullbg_second_line_img = Corp(fullbg, fullbg_position) # 根据两行图片信息拼接出缺口图片正确排列的图片 bg_image = put_imgs_together(bg_first_line_img, bg_second_line_img, 'bg.jpg') # 根据两行图片信息拼接出完整图片正确排列的图片 fullbg_image = put_imgs_together(fullbg_first_line_img, fullbg_second_line_img, 'fullbg.jpg') # 计算滑块移动距离 distance = get_distance(bg_image, fullbg_image) # 计算移动轨迹 trace = get_trace(distance-10) # 移动滑块 move_to_gap(trace) sleep(5) # 程序入口 if __name__ == '__main__': main()
github: https://github.com/JeesonZhang/pythonspider/blob/master/bilibili_crack