最近在研究Python爬虫,首先在学习了Python和selenium的基础后决定进行实践。在看完Jeeson_Z写的文章《python爬虫基础(9:验证识别之滑块验证)》后,决定使用文章中的代码进行试验,第一次运行的时候在输入完用户名密码后并没有任何反应,发现没有点击登录按钮的代码,于是添加后再次运行,提示需要获取的网页元素并不存在,于是F12发现哔哩哔哩的登录验证机制已经和以前不同了,验证控件使用了canvas,不再是之前的图片(在调试台的network里面发现依然会有案例中的图片加载),于是决定进行改进,采用截图的方式进行图片的对比以及缺口位置的处理。
所需工具和解决思路是一样的,在此不再赘述。
下面介绍新增或修改的几个步骤:
1.登录
# 登录 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) #获取登录按钮 login_btn=wait.until(EC.presence_of_element_located((By.CSS_SELECTOR,'a.btn.btn-login'))) #随机延时点击 time.sleep(random.random() * 3) login_btn.click()登录只是增加了点击登录按钮的动作,填写完用户名密码后给定一个随机时间去点击,更像是真人操作。
2.元素可见性
#设置元素可见 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;")这两个函数的功能就是对网页中的某个元素设置可见性,为什么要这么操作呢,因为我发现哔哩哔哩的滑动验证组件由3个canvas组成,分别是:滑块图,带缺口的背景图,完整的背景图。
F12但是默认情况下完整的背景图的display设置为none,这样我们在截图的时候就无法定位了,所以要对其设置为display:block还有一点就是在截取带有缺口的背景图时要设置滑块为不可见,截图不会分层截,不隐藏滑块会导致后续的识别出现问题。
3.截取元素并保存为图片
#对某元素截图 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)先对整个浏览器可见的页面范围进行截图,然后根据元素的绝对位置进行裁剪,获取分层图片
4.分别对滑动验证模块的不同层截图
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')我这里截图的顺序是:先隐藏掉滑块截取带缺口的背景图,显示回滑块,截取正常的滑动验证界面(可省去),最后设置完整背景图可见,截取完整背景,这样我们就获取了最关键的两张图片。
带滑块和缺口的图片 带缺口的图片 完整图片5.滑动
def slide(): distance=get_distance(Image.open('.\\bili_back.png'),Image.open('.\\bili_full.png')) print('计算偏移量为:%s Px' % distance) # 计算移动轨迹 trace = get_trace(distance-5) # 移动滑块 move_to_gap(trace) time.sleep(3)这里就是对刚刚获取的图片进行分析了,分析的部分使用的是原博主提供的代码
下面是演示:
用户名密码都是假的,哈哈
完整代码:
# -*- coding: utf-8 -*- """ Created on Tue May 21 14:19:32 2019 @author: xinyu """ 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 from PIL import Image web='http://literallycanvas.com/' # 初始化 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) #获取登录按钮 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 = 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'))) 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')) print('计算偏移量为:%s Px' % distance) # 计算移动轨迹 trace = get_trace(distance-5) # 移动滑块 move_to_gap(trace) time.sleep(3) init() login() cut() slide()目前的成功率还是蛮高的,不知为何有些时候明明对齐了还是让重试,故而此代码还需要完善。日后再说