一、介绍
现在出现了一种通过用户鼠标移动滑块来填补有缺口图片的验证码,我们叫做滑动验证码。它的原理很简单,首先生成一张图片,然后随机挖去一块,在页面展示被挖去部分的图片,再通过js获取用户滑动距离,以及坐标等信息到后台进行校验。只要用户移动的距离符合,以及移动的轨迹行为检测通过即可视为验证通过。
解决思路
目前这种验证码的通用解决思路如下:
-
获取验证码图片,包含原图以及有缺口的图
-
算出缺口的位置,以及滑块要滑动的距离
-
通过算法模拟人工移动轨迹
-
通过selenium模拟操作
二、逻辑实现
我们以保温网为例http://www.cnbaowen.net/api/geetest/
1.获取验证码图片
注意我们需要获取两张图片,第一张是完整背景图,第二张是有缺口的背景图。
经过分析发现当鼠标位于按钮是上时显示完整背景图,当鼠标点击滑动按钮不松,显示有缺口的背景图。
根据之前学习的爬虫知识,图片一定是浏览器下载回来的,通过查看历史请求确实发现了图片
def get_full_image(driver): """ 鼠标移动到滑块,显示完整图案 :param driver: webdriver :return: 返回验证码背景图片Image对象 """ webdriver.ActionChains(driver).move_to_element(slider).perform() time.sleep(0.2) img = driver.find_element_by_xpath('//*[@id="captcha"]/div/div[1]/div[2]/div[1]/a[2]') if 'show' in img.get_attribute('class'): res = img.screenshot_as_png return Image.open(BytesIO(res)) else: raise ValueError('获取验证码背景图片失败')
def get_cut_image(driver): """ 点击滑动按钮获取有缺口图片 :param driver: webdriver :return: 返回验证码有缺口图片的Image对象 """ slider = driver.find_element_by_xpath('//*[@id="captcha"]/div/div[3]/div[2]') webdriver.ActionChains(driver).click_and_hold(slider).perform() time.sleep(0.1) img = driver.find_element_by_xpath('//*[@id="captcha"]/div/div[1]/div[2]/div[1]/a[1]') res = img.screenshot_as_png cut_img = Image.open(BytesIO(res)) return Image.open(BytesIO(res))
2.找出缺口位置,计算移动距离
算法有很多,大家可以自由发挥。这里我们讲一种最简单的方法。我们要算出的距离是滑块要滑动的距离。
按照相同的思路,比较两张图片x轴100-end像素的部分,找到缺口的最左最上那个点。
用找到的缺口像素点的x坐标减去找到的滑块的点的x坐标得到近似移动距离。这种算法,经过测试准确率还不错,大家如果在实际工作过程中发现有问题,需要根据具体情况去设计不同算法。
def get_distance(full_image, cut_image): full_pixies = full_image.load() cut_pixies = cut_image.load() w, h = full_image.size full_image.save('full.png') cut_image.save('cut.png') # 先找最左边不同的点 left = [] for j in range(h): for i in range(100): if abs(full_pixies[i, j][0] - cut_pixies[i, j][0]) + abs(full_pixies[i, j][1] - cut_pixies[i, j][1]) + abs( full_pixies[i, j][2] - cut_pixies[i, j][2]) > 150: left.append((i, j)) if left: break # 再找最右边不同的点 right = [] for j in range(h): for i in range(100, w): if abs(full_pixies[i, j][0] - cut_pixies[i, j][0]) + abs(full_pixies[i, j][1] - cut_pixies[i, j][1]) + abs( full_pixies[i, j][2] - cut_pixies[i, j][2]) > 150: right.append((i, j)) if right: break length = right[0][0] - left[0][0] return length
滑动验证码早期刚面世的时候没有做行为校验,很快被破解。随着人工智能的发展,目前所有商用滑动验证码后台都有做行为校验,根据前端传递的移动轨迹,后台会进行特征校验,如果判定非人工则返回校验失败。模拟人的滑动行为,最常见的以中方法是通过加速度公式。目前这个方法已经被识别,但相对较简单,我们首先学习其思路。大家根据自己的能力可以自行扩展。
def get_track(self, distance): ''' 拿到移动轨迹,模仿人的滑动行为,先匀加速后匀减速 匀变速运动基本公式: ①v=v0+at ②s=v0t+(1/2)at² ③v²-v0²=2as :param distance: 需要移动的距离 :return: 存放每0.2秒移动的距离 ''' # 初速度 v=0 # 单位时间为0.2s来统计轨迹,轨迹即0.2内的位移 t=0.3 # 位移/轨迹列表,列表内的一个元素代表0.2s的位移 tracks=[] # 当前的位移 current=0 # 到达mid值开始减速 mid=distance * 5/8 distance += 10 # 先滑过一点,最后再反着滑动回来 # a = random.randint(1,3) while current < distance: if current < mid: # 加速度越小,单位时间的位移越小,模拟的轨迹就越多越详细 a = random.randint(1,3) # 加速运动 else: a = -random.randint(2,4) # 减速运动 # 初速度 v0 = v # 0.2秒时间内的位移 s = v0*t+0.5*a*(t**2) # 当前的位置 current += s # 添加到轨迹列表 tracks.append(round(s)) # 速度已经达到v,该速度作为下次的初速度 v= v0+a*t # 反着滑动到大概准确位置 for i in range(4): tracks.append(-random.randint(1,3)) # for i in range(4): # tracks.append(-random.randint(1,3)) random.shuffle(tracks) return tracks
4.滑动滑块
利用selenium,根据算出的轨迹,进行模拟滑动,代码如下:
def slide(self, tracks): # slider = self.driver.find_element_by_xpath('//*[@id="captcha"]/div/div[3]/div[2]') # 鼠标点击并按住不松 # webdriver.ActionChains(self.driver).click_and_hold(self.slider).perform() # 让鼠标随机往下移动一段距离 webdriver.ActionChains(self.driver).move_by_offset(xoffset=0, yoffset=100).perform() time.sleep(0.15) for item in tracks: webdriver.ActionChains(self.driver).move_by_offset(xoffset=item, yoffset=random.randint(-2,2)).perform() # 稳定一秒再松开 time.sleep(1) webdriver.ActionChains(self.driver).release(self.slider).perform() time.sleep(1) # 随机拿开鼠标 webdriver.ActionChains(self.driver).move_by_offset(xoffset=random.randint(200, 300), yoffset=random.randint(200, 300)).perform() time.sleep(0.2) info = self.driver.find_element_by_xpath('//*[@id="login-modal"]/div/div/div/div[2]/div[1]/div[2]/div[1]/div/div[1]/div[2]/div[2]/div/div[2]/span[1]') if '验证通过' in info.text: return 1 if '验证失败' in info.text: return 2 if '再来一次' in info.text: return 3 if '出现错误' in info.text: return 4
#!/usr/bin/env python # encoding: utf-8 #@author: jack #@contact: 935650354@qq.com #@site: https://www.cnblogs.com/jackzz import re import time import random import requests from PIL import Image 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 io import BytesIO from selenium.webdriver.common.action_chains import ActionChains def get_merge_img(img_content,location_list,num): ''' 拼接图片 :param img_content: :param location_list: :param num: :return: ''' im = Image.open(img_content) im_list_upper = [] im_list_done = [] for location in location_list: # print(location) if int(location['y']) == -58: im_list_upper.append(im.crop((abs(int(location['x'])),58,abs(int(location['x']))+10,116))) if int(location['y']) == 0: im_list_done.append(im.crop((abs(int(location['x'])),0,abs(int(location['x']))+10,58))) #create new image new_im = Image.new('RGB',(260,116)) x_offset=0 for im in im_list_upper: new_im.paste(im,(x_offset,0)) x_offset +=10 x_offset = 0 for im in im_list_done: new_im.paste(im, (x_offset, 58)) x_offset += 10 return new_im def get_img(driver,div_class,num): ''' 获取图片 :param driver: :param div_class: :param num: :return: ''' background_imgs = driver.find_elements_by_class_name(div_class) location_list = [] imge_url = '' for img in background_imgs: location = {} imge_url = re.findall(r'background-image: url\(\"(.*?)\"\); background-position: (.*?)px (.*?)px;',img.get_attribute('style'))[0][0] location['x'] = re.findall(r'background-image: url\(\"(.*?)\"\); background-position: (.*?)px (.*?)px;',img.get_attribute('style'))[0][1] location['y'] = re.findall(r'background-image: url\(\"(.*?)\"\); background-position: (.*?)px (.*?)px;',img.get_attribute('style'))[0][2] location_list.append(location) response = requests.get(imge_url).content img_content = BytesIO(response) image = get_merge_img(img_content,location_list,num) image.save('{}.jpg'.format(num)) return image def get_diff_location(image1,image2): ''' 通过像素对比 找到缺口位置 :param image1: :param image2: :return: ''' for x in range(1,259): for y in range(1, 115): if is_similar(image1,image2,x,y) == False: #判断成立 表示xy这个点 两张图不一样 return x def is_similar(image1,image2,x,y): pixel1 = image1.getpixel((x,y)) pixel2 = image2.getpixel((x,y)) for i in range(0,3): if abs(pixel1[i]) - pixel2[i] >=50: return False return True def get_track(x): ''' 滑块移动轨迹 初速度 v =0 单位时间 t = 0.2 位移轨迹 tracks = [] 当前位移 ccurrent = 0 :param x: :return: ''' v = 0 t = 0.2 tracks = [] current = 0 # mid = x*5/8#到达mid值开始减速 # x = x+10 while current < x: # if current < mid: # a = random.randint(1,3) # else: # a = -random.randint(2,4) a = 2 v0 = v #单位时间内位移公式 s =v0*t+0.5*a*(t**2) #当前位移 current = current+s tracks.append(round(s)) v = v0+a*t for i in range(3): tracks.append(-1) for i in range(3): tracks.append(-2) return tracks def main(driver,element): #1为完整图、2为有缺口图 image1 = get_img(driver,'gt_cut_fullbg_slice',1) image2 = get_img(driver,'gt_cut_bg_slice',2) x = get_diff_location(image1,image2) tracks = get_track(x) ActionChains(driver).click_and_hold(element).perform() for x in tracks: ActionChains(driver).move_by_offset(xoffset=x,yoffset=0).perform() ActionChains(driver).release(element).perform() time.sleep(3) if __name__ == '__main__': driver = webdriver.Firefox() driver.maximize_window() driver.get('http://www.cnbaowen.net/api/geetest/') try: count = 5 # waiting slidingVC loading wait = WebDriverWait(driver, 10) element = wait.until(EC.element_to_be_clickable((By.CLASS_NAME, 'gt_slider_knob'))) while count >0: main(driver,element) try: succes = wait.until(EC.presence_of_all_elements_located((By.XPATH,'//div[@class="gt_ajax_tip gt_success"]'))) if succes: print('恭喜你!识别成功...') break except Exception as e: print('识别错误,继续') count -=1 finally: driver.quit()