Python3 爬虫学习笔记第十三章 —— 【验证码对抗系列 — 滑动验证码】
【13.1】关于滑动验证码 滑动验证码属于行为式验证码,需要通过用户的操作行为来完成验证,一般是根据提示用鼠标将滑块拖动到指定的位置完成验证,此类验证码背景图片采用多种图像加密技术,且添加了很多随机效果,能有效防止OCR文字识别,另外,验证码上的文字采用了随机印刷技术,能够随机采用多种字体、多种变形的实时随机印刷,防止暴力破解;斗鱼、哔哩哔哩、淘宝等平台都使用了滑动验证码
【13.2】滑动验证码攻克思路 利用自动化测试工具 Selenium 直接模拟人的行为方式来完成验证,首先要分析页面,想办法找到滑动验证码的完整图片、带有缺口的图片和需要滑动的图片,通过对比原始的图片和带滑块缺口的图片的像素,像素不同的地方就是缺口位置,计算出滑块缺口的位置,得到所需要滑动的距离,最后利用 Selenium 进行对滑块的拖拽,拖拽时要模仿人的行为,由于有个对准过程,所以是先快后慢,匀速移动、随机速度移动都不会成功
以下以哔哩哔哩为例来做模拟登录练习
【13.3】模拟登录 bilibili — 总体思路 首先使用 Selenium 模拟登陆 bilibili,自动输入账号密码,查找到登陆按钮并点击,使其出现滑动验证码,此时分析页面,滑动验证组件是由3个 canvas 组成,分别代表完整图片、带有缺口的图片和需要滑动的图片,3个 canvas 元素包含 CSS display
属性,display:block
为可见,display:none
为不可见,分别获取三张图片时要将其他两张图片设置为 display:none
,获取元素位置后即可对图片截图并保存,通过图片像素对比,找到缺口位置即为滑块要移动的距离,随后构造滑动轨迹,按照先加速后减速的方式移动滑块完成验证。
整个程序包含的函数:
1 2 3 4 5 6 7 8 9 10 11 def init () : 初始化函数,定义全局变量def login () : 登录函数,输入账号密码并点击登录def find_element () : 验证码元素查找函数,查找三张图的元素def hide_element () : 设置元素不可见函数def show_element () : 设置元素可见函数def save_screenshot () : 验证码截图函数,截取三张图并保存def slide () : 滑动函数def is_pixel_equal () : 像素判断函数,寻找缺口位置def get_distance () : 计算滑块移动距离函数def get_track () : 构造移动轨迹函数def move_to_gap () : 模拟拖动函数
整个程序用到的库:
1 2 3 4 5 6 7 8 from selenium import webdriverfrom selenium.webdriver.chrome.options import Optionsfrom selenium.webdriver.support.wait import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.common.by import Byfrom selenium.webdriver import ActionChainsimport timeimport random
【13.4】主函数 1 2 3 4 5 if __name__ == '__main__' : init() login() find_element() slide()
【13.5】初始化函数 1 2 3 4 5 6 7 8 9 10 def init () : global url, browser, username, password, wait url = 'https://passport.bilibili.com/login' path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe' chrome_options = Options() chrome_options.add_argument('--start-maximized' ) browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options) username = '155********' password = '***********' wait = WebDriverWait(browser, 20 )
global 关键字定义了全局变量,随后是登录页面url、谷歌浏览器驱动的目录path、实例化 Chrome 浏览器、设置浏览器分辨率最大化、用户名、密码、WebDriverWait() 方法设置等待超时
【13.6】登录函数 1 2 3 4 5 6 7 8 9 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()
等待用户名输入框和密码输入框对应的 ID 节点加载出来,分析页面可知,用户名输入框 id="login-username"
,密码输入框 id="login-passwd"
,获取这两个节点,调用 send_keys()
方法输入用户名和密码,随后获取登录按钮,分析页面可知登录按钮 class="btn btn-login"
,随机产生一个数并将其扩大三倍作为暂停时间,最后调用 click()
方法实现登录按钮的点击
【13.7】验证码元素查找函数 1 2 3 4 5 6 7 8 9 10 11 12 13 def find_element () : 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_screenshot(c_background, 'back' ) show_element(c_slice) save_screenshot(c_slice, 'slice' ) show_element(c_full_bg) save_screenshot(c_full_bg, 'full' )
我们要获取验证码的三张图片,分别是完整的图片、带有缺口的图片和需要滑动的图片,分析页面代码,这三张图片是由 3 个 canvas 组成,3 个 canvas 元素包含 CSS display
属性,display:block
为可见,display:none
为不可见,在分别获取三张图片时要将其他两张图片设置为 display:none
,这样做才能单独提取到每张图片,定位三张图片的 class 分别为:带有缺口的图片(c_background):geetest_canvas_bg geetest_absolute
、需要滑动的图片(c_slice):geetest_canvas_slice geetest_absolute
、完整图片(c_full_bg):geetest_canvas_fullbg geetest_fade geetest_absolute
,随后传值给 save_screenshot()
函数,进一步对验证码进行处理
【13.8】元素可见性设置函数 1 2 3 4 5 6 7 8 # 设置元素不可见 def hide_element(element): browser.execute_script("arguments[0].style=arguments[1]", element, "display: none;") # 设置元素可见 def show_element(element): browser.execute_script("arguments[0].style=arguments[1]", element, "display: block;")
【13.9】验证码截图函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 def save_screenshot (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)
location
属性可以返回该图片对象在浏览器中的位置,坐标轴是以屏幕左上角为原点,x轴向右递增,y轴向下递增,size
属性可以返回该图片对象的高度和宽度,由此可以得到验证码的位置信息,首先调用 save_screenshot()
属性对整个页面截图并保存,然后向 crop()
方法传入验证码的位置信息,由位置信息再对验证码进行剪裁并保存
【13.10】滑动函数 1 2 3 4 5 6 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 )
向 get_distance()
函数传入完整的图片和缺口图片,计算滑块需要滑动的距离,再把距离信息传入 get_trace()
函数,构造滑块的移动轨迹,最后根据轨迹信息调用 move_to_gap()
函数移动滑块完成验证
【13.11】计算滑块移动距离函数 1 2 3 4 5 6 def get_distance (bg_image, fullbg_image) : 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
get_distance()
方法即获取缺口位置的方法,此方法的参数是两张图片,一张为完整的图片,另一张为带缺口的图片,distance 为滑块的初始位置,遍历两张图片的每个像素,利用 is_pixel_equal()
像素判断函数判断两张图片同一位置的像素是否相同,比较两张图 RGB 的绝对值是否均小于定义的阈值 threshold,如果绝对值均在阈值之内,则代表像素点相同,继续遍历,否则代表不相同的像素点,即缺口的位置
【13.12】像素判断函数 1 2 3 4 5 6 7 8 9 def is_pixel_equal (bg_image, fullbg_image, x, y) : bg_pixel = bg_image.load()[x, y] fullbg_pixel = fullbg_image.load()[x, y] threshold = 60 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
将完整图片和缺口图片两个对象分别赋值给变量 bg_image和 fullbg_image,接下来对比图片获取缺口。我们在这里遍历图片的每个坐标点,获取两张图片对应像素点的 RGB 数据,判断像素的各个颜色之差,abs()
用于取绝对值,如果二者的 RGB 数据差距在一定范围内,那就代表两个像素相同,继续比对下一个像素点,如果差距超过一定范围,则代表像素点不同,当前位置即为缺口位置
【13.13】构造移动轨迹函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 def get_trace (distance) : trace = [] faster_distance = distance * (4 / 5 ) start, v0, t = 0 , 0 , 0.1 while start < distance: if start < faster_distance: a = 20 else : a = -20 move = v0 * t + 1 / 2 * a * t * t v = v0 + a * t v0 = v start += move trace.append(round(move)) return trace
get_trace()
方法传入的参数为移动的总距离,返回的是运动轨迹,运动轨迹用 trace 表示,它是一个列表,列表的每个元素代表每次移动多少距离,利用 Selenium 进行对滑块的拖拽时要模仿人的行为,由于有个对准过程,所以是先快后慢,匀速移动、随机速度移动都不会成功,因此要设置一个加速和减速的距离,这里设置加速距离 faster_distance
是总距离 distance
的4/5倍,滑块滑动的加速度用 a 来表示,当前速度用 v 表示,初速度用 v0 表示,位移用 move 表示,所需时间用 t 表示,它们之间满足以下关系:
1 2 move = v0 * t + 0.5 * a * t * t v = v0 + a * t
设置初始位置、初始速度、时间间隔分别为0, 0, 0.1,加速阶段和减速阶段的加速度分别设置为20和-20,直到运动轨迹达到总距离时,循环终止,最后得到的 trace 记录了每个时间间隔移动了多少位移,这样滑块的运动轨迹就得到了
【13.14】模拟拖动函数 1 2 3 4 5 6 7 def move_to_gap (trace) : slider = wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, 'div.geetest_slider_button' ))) ActionChains(browser).click_and_hold(slider).perform() for x in trace: ActionChains(browser).move_by_offset(xoffset=x, yoffset=0 ).perform() time.sleep(0.5 ) ActionChains(browser).release().perform()
传入的参数为运动轨迹,首先查找到滑动按钮,然后调用 ActionChains 的 click_and_hold()
方法按住拖动底部滑块,perform()
方法用于执行,遍历运动轨迹获取每小段位移距离,调用 move_by_offset()
方法移动此位移,最后调用 release()
方法松开鼠标即可
【13.15】效果实现动图 最终实现效果图:(关键信息已经过打码处理)
【13.16】完整代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 from selenium import webdriverfrom selenium.webdriver.chrome.options import Optionsfrom selenium.webdriver.support.wait import WebDriverWaitfrom selenium.webdriver.support import expected_conditions as ECfrom selenium.webdriver.common.by import Byfrom selenium.webdriver import ActionChainsimport timeimport randomfrom PIL import Imagedef init () : global url, browser, username, password, wait url = 'https://passport.bilibili.com/login' path = r'F:\PycharmProjects\Python3爬虫\chromedriver.exe' chrome_options = Options() chrome_options.add_argument('--start-maximized' ) browser = webdriver.Chrome(executable_path=path, chrome_options=chrome_options) username = '155********' 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 find_element () : 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_screenshot(c_background, 'back' ) show_element(c_slice) save_screenshot(c_slice, 'slice' ) show_element(c_full_bg) save_screenshot(c_full_bg, 'full' ) def hide_element (element) : browser.execute_script("arguments[0].style=arguments[1]" , element, "display: none;" ) def show_element (element) : browser.execute_script("arguments[0].style=arguments[1]" , element, "display: block;" ) def save_screenshot (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 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 ) def get_distance (bg_image, fullbg_image) : 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 is_pixel_equal (bg_image, fullbg_image, x, y) : bg_pixel = bg_image.load()[x, y] fullbg_pixel = fullbg_image.load()[x, y] threshold = 60 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_trace (distance) : trace = [] faster_distance = distance * (4 / 5 ) start, v0, t = 0 , 0 , 0.1 while start < distance: if start < faster_distance: a = 20 else : a = -20 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.CSS_SELECTOR, 'div.geetest_slider_button' ))) ActionChains(browser).click_and_hold(slider).perform() for x in trace: ActionChains(browser).move_by_offset(xoffset=x, yoffset=0 ).perform() time.sleep(0.5 ) ActionChains(browser).release().perform() if __name__ == '__main__' : init() login() find_element() slide()