一、介紹
現在出現了一種通過用戶鼠標移動滑塊來填補有缺口圖片的驗證碼,我們叫做滑動驗證碼。它的原理很簡單,首先生成一張圖片,然后隨機挖去一塊,在頁面展示被挖去部分的圖片,再通過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()