1. 驗證碼識別
隨着爬蟲的發展,越來越多的網站開始采用各種各樣的措施來反爬蟲,其中一個措施便是使用驗證碼。隨着技術的發展,驗證碼也越來越花里胡哨的了。最開始就是幾個數字隨機組成的圖像驗證碼,后來加入了英文字母和混淆曲線,或者是人眼都很難識別的數字字母。很多國內網站還出現了中文字符的驗證碼,使得識別越發困難。
然后又出現了需要我們識別文字,點擊與文字相符合的圖片,驗證碼完全正確,驗證才能通過。下載的這種交互式驗證碼越來越多了,如滑動驗證碼需要滑動拼合滑塊才能完成驗證,點觸驗證碼需要完全點擊正確結果才可以完成驗證,另外還有滑動宮格驗證碼、計算題驗證碼等。
最讓我生氣的就是外國的一款郵箱的驗證碼,freemail郵箱的驗證碼,隨機生成一些圖片,讓你點擊符合標題的圖片,這種別說爬蟲了,對人為操作都不友好。(滿滿的怨念)
還有一種外國郵箱tutanota,是一個時鍾驗證碼,我們想要根據上面的時間指針來輸入正確的時間。但是被我們公司的大佬自己寫的OCR識別出來了,雖然錯誤率還很高,但是這是一個大的突破。
驗證碼變得越來越復雜,爬蟲的工作也變得愈發艱難,有時候我們必須通過驗證碼的驗證才可以訪問頁面,本章就專門針對簡單的驗證碼的識別做大概的講解(難的我也不會)。
1.1 使用百度OCR
tesserocr是很早的一款OCR文字識別技術了,算是過時的東西了。百度OCR中文字識別每天都有限制次數的免費額度,所以我們就用它了(別問,問就是白嫖)。
百度搜索百度ocr,進入官網。
往下翻,直到翻到下圖界面。
登錄即可,沒有賬號就注冊。
登錄成功后,創建應用。
中間內容填的合理就行。
這些內容不能給大家看了,下面的代碼中,我會將之用********替換,各位只要根據自己的百度平台的內容修改下即可。
1.2 圖形驗證碼的識別
我們首先識別最簡單的以種驗證碼,即圖形驗證碼。這種驗證碼最早出現,現在也很常見,一般由4位字母或者數字組成。例如,中國知網的注冊頁面有類似的驗證碼,鏈接為http://my.cnki.net/Register/CommonRegister.aspx。
表單的最后一項就是圖形驗證碼,我們必須完全正確輸入圖中的字符才可以完成注冊。
為了便於實驗,我們先將驗證碼的圖片保存到本地。
打開開發者工具,找到驗證碼元素。驗證碼元素是一張圖片,它的src屬性是heckCode.aspx。我們直接打開這個鏈接即:http://my.cnki.net/Register/CheckCode.aspx ,就可以看到個驗證碼,右鍵保存即可將其命名為code.jpg。
這樣我們就可以得到一張驗證碼圖片,以供測試識別使用。
接下來新建一個項目,將驗證碼圖片放到項目根目錄下,用百度ocr識別該驗證碼。
from aip import AipOcr import codecs# pip install baidu-aip #讀取圖片函數 def ocr(path): with open(path,'rb') as f: return f.read() def main(): filename = "code.jpg" print("已經收到,正在處理,請稍后....") app_id = '*********' api_key = '********************' secret_key = '*******************************' client = AipOcr(app_id,api_key,secret_key) #讀取圖片 image = ocr(filename) #進程OCR識別 dict1 = client.general(image) #print(dict1) with codecs.open(filename + ".txt","w","utf-8") as f: for i in dict1["words_result"]: f.write(str(i["words"] + "\r\n")) print ("處理完成") if __name__ == '__main__': main()
結果:
結果差強人意,可能是由於驗證碼內的多余線條干擾了圖片的識別。
對於這種情況,我們還需要做一下額外的處理,如轉灰度、二值化等操作。
這就需要我們使用到一個新的模塊PIL了,我們這里先用,以后我特意出一章關於這個模塊的使用。
pip install pillow -i http://pypi.douban.com/simple/ --trusted-host pypi.douban.com
Looking in indexes: http://pypi.douban.com/simple/
別問為什么安裝的是pillow模塊,而不是PIL模塊,到時候會說明的。
我們可以利用Image對象的convert()方法參數傳入L,即可將圖片轉化為灰度圖像。
image = image.convert('L') image.show()
傳入1即可將圖片進行二值化處理。
我們還可以指定二值化的闊值 上面的方法采用的是默認闊值 127 不過我們不能直接轉化原因, 要將原圖先轉為灰度圖像,然后再指定二值化闊值。
from aip import AipOcr import codecsfrom PIL import Image #讀取圖片函數 def ocr(path): with open(path,'rb') as f: return f.read() def main(): print("已經收到,正在處理,請稍后....") app_id = '**********' api_key = '************************' secret_key = '***************************' client = AipOcr(app_id,api_key,secret_key) #讀取圖片 image = Image.open('code.jpg') image = image.convert('L') threshold = 110 table = [] for i in range(256): if i < threshold: table.append(0) else: table.append(1) image = image.point(table,'1') image.save("code.png",'png') #讀取PIL處理后保存圖片函數 image = ocr('code.png') dict1 = client.general(image) with codecs.open('code1' + ".txt", "w", "utf-8") as f: for i in dict1["words_result"]: f.write(str(i["words"] + "\r\n")) print("處理完成") if __name__ == '__main__': main()
闊值110時:
闊值125時:
實在是把我弄自閉了,中間的值更加奇怪 ,技術不到家就是這樣吧。
然后我又換了幾次驗證碼,結果如下:
終於是成功了一次了,所以這個案例告訴我們,識別不出來,就刷新換下一張,瞎貓總會碰到死耗子。
而在真正的項目中,都會判斷是否驗證碼錯誤,驗證碼錯誤就刷新驗證碼,重新輸入。
遇到那種失敗就會跳轉界面的,那就只能重啟任務了。
1.3 滑動驗證碼的識別
滑動驗證碼需要拖動拼合滑塊才能完成驗證,相對於圖形驗證碼來說識別難度上升了幾個等級。制作滑動驗證碼的公司叫做GEETEST,官網是https://www.geetest.com/。主要驗證方式是拖動滑塊破解圖像。若圖像完全拼合,則驗證成功,即表單成功提交,否則需要重新驗證。
1.3.1 滑動驗證碼特點
滑動驗證碼相較於圖形驗證碼來說識別難度更大。現在極驗驗證碼已經更新到3.0版本,對於極驗驗證碼 3.0 版本,我們首先點擊按鈕進行智能驗證。如果驗證不通過,則會彈出滑動驗證的窗口,拖動滑塊拼合圖像進行驗證。之后三個加密參數會生成,通過表單提交到后台,后台還會進行一次驗證。
極驗驗證碼還增加了機器學習的方法來識別拖動軌跡。官方網站的安全防護有如下幾點說明:
三角防護之防模擬:惡意程序模仿人類行為軌跡對驗證碼進行識別。針對模擬,極驗驗證碼擁有超過4000萬人機行為樣本的海量數據。利用機器學習和神經網絡,構建線上線下的多重靜態、動態防御模型。識別模擬軌跡,界定人機邊界。
三角防護之防偽造:惡意程序通過偽造設備瀏覽器環境對驗證碼進行識別。針對偽造,極驗驗證碼利用設備基因技術。深度分析瀏覽器的實際性能來辨識偽造信息。同時根據偽造事件不斷更新黑名單,大幅提高防偽造能力。
三角防護之防暴力:惡意程序短時間內進行密集的攻擊,對驗證碼進行暴力識別。針對暴 力,極驗驗證碼擁有多種驗證形態,每一種驗證形態都有利用神經網絡生成的海藍圖庫儲 備,每一張圖片都是獨一無二的,且圖庫不斷更新,極大程度提高了暴力識別的成本。
另外,極驗驗證碼的驗證相對於普通驗證方式更方便,體驗更友好,其官方網站說明如下:
點擊一下,驗證只需要0.4秒。極驗驗證碼始終專注於去驗證化實踐,讓驗證環節不再打斷產品本身的交互流程,最終達到優化用戶體驗和提高用戶轉化率的效果。
全平台兼容 ,適用各種交互場景。極驗驗證碼兼容所有主流瀏覽器甚至於古老的IE6,也可以輕松應用在iOS和Android移動端平台,滿足各種業務需求,保護網站資源不被濫用和監取。
面向未來,懂科技,更懂人性。極驗驗證碼在保障安全同時不斷致力於提升用戶體驗、精雕細琢的驗證面板、流暢順滑的驗證動畫效果,讓驗證過程不再枯燥乏味。
相比一般驗證碼,極驗驗證碼的驗證安全性和易用性有了非常大的提高。
1.3.2 實現思路
對於應用了極驗驗證碼的網站,如果我們直接模擬表單提交,加密參數的構造是個問題,需要分析其加密和校驗邏輯,非常的復雜。但是我們如果采用模擬瀏覽器動作的方式來完成驗證,就會變得很簡單了。在python中,我們可以使用selenium來模擬人的行為來完成驗證、此驗證成本相對與直接去識別加密算法少得多。
首先找到一個帶有極驗驗證碼的網站,如B站,鏈接為:https://passport.bilibili.com/login。輸入賬號密碼點擊登錄,極驗驗證碼就會彈出來。
所以我們這個識別驗證案例 完成需要三步:
- 輸入賬號密碼,點擊登錄
- 識別滑動缺口的位置
- 模擬拖動滑塊
第一步操作最簡單,我們可以直接用selenium完成。
第二步操作識別缺口的位置比較關鍵,這里需要用到圖像的相關處理方法。首先觀察缺口的樣子。
缺口的四周邊緣又明顯的斷裂邊緣,邊緣和邊緣周圍又明顯的區別。我們可以實現一個邊緣檢測算法來找出缺口的位置。對於極驗驗證碼來說,我們可以利用和原圖對比檢測的方式來識別缺口的位置,因為在沒有滑動滑塊之前, 缺口並沒有呈現。
我們可以同時獲取兩張圖片。設定一個對比闊值,然后遍歷兩張圖片,找出相同位置像素RGB差距超過此闊值的像素點,那么此像素點的位置就是缺口的位置 。
第三步操作看似簡單,但其中的坑比較多。極驗驗證碼增加了機器軌跡識別,勻速移動、隨機速度移動等方法都不能通過驗證,只有完全模擬人的移動軌跡才可以通過驗證。人的移動軌跡一般是先加速后減速,我們需要模擬這個過程才能成功。
有了思路后,我們就用代碼來實現極驗驗證碼的識別過程吧。
1.3.3 初始化
我們先初始化一些配置,如selenium對象的初始化及一些參數的配置。
# -*- coding:utf-8 -*- from PIL import Image from time import sleep from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver import ActionChains from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.desired_capabilities import DesiredCapabilities headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36" } chrome_options = webdriver.ChromeOptions() chrome_options.add_experimental_option('w3c', False) caps = DesiredCapabilities.CHROME caps['loggingPrefs'] = {'performance': 'ALL'} class SliderVerificationCode(object): def __init__(self): # 初始化一些信息 self.left = 60 # 定義一個左邊的起點 缺口一般離圖片左側有一定的距離 有一個滑塊 self.url = 'https://passport.bilibili.com/login' self.driver = webdriver.Chrome(desired_capabilities=caps,options=chrome_options) self.wait = WebDriverWait(self.driver, 20) # 設置等待時間20秒 self.phone = "17369251763" #亂輸就行 self.passwd = "abcdefg" #亂輸就行
phone和passwd就是登錄B站的賬號和密碼。
1.3.4 模擬用戶登錄
輸入賬號密碼:
def input_name_password(self): # 輸入賬號密碼 self.driver.get(self.url) self.driver.maximize_window() # 窗口最大化 input_name = self.driver.find_element_by_xpath("//input[@id='login-username']") input_pwd = self.driver.find_element_by_xpath("//input[@id='login-passwd']") input_name.send_keys("username") input_pwd.send_keys("passport")
點擊登錄按鈕,等待驗證碼圖片加載
def click_login_button(self): # 點擊登錄按鈕,出現驗證碼圖片 login_btn = self.driver.find_element_by_class_name("btn-login") login_btn.click() sleep(3)
第一步的工作就完成了。
1.3.5 識別缺口
接下來識別缺口的位置,首先獲取前后兩張比對圖片,二者不一致的地方即為缺口。看到網上那些案例,接收到亂序的兩張圖片,然后用代碼拼接起來,麻煩的要死,而且我試了幾個,絕大部分是不能運行的,就一個能截圖出來的,截出來的圖如下:
我只想登錄一下,還要我干這么多事,我哭了。
所以我就換了種方法,這個版本的極驗驗證碼應該都可以這樣做,代碼如下:
def get_geetest_image(self): # 獲取驗證碼圖片 gapimg = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_bg'))) sleep(2) gapimg.screenshot(r'./captcha1.png') # 通過js代碼修改標簽樣式 顯示圖片2 js = 'var change = document.getElementsByClassName("geetest_canvas_fullbg");change[0].style = "display:block;"' self.driver.execute_script(js) sleep(2) fullimg = self.wait.until( EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_slice'))) fullimg.screenshot(r'./captcha2.png') def is_similar(self, image1, image2, x, y): '''判斷兩張圖片 各個位置的像素是否相同 #image1:帶缺口的圖片 :param image2: 不帶缺口的圖片 :param x: 位置x :param y: 位置y :return: (x,y)位置的像素是否相同 ''' # 獲取兩張圖片指定位置的像素點 pixel1 = image1.load()[x, y] pixel2 = image2.load()[x, y] # 設置一個閾值 允許有誤差 threshold = 60 # 彩色圖 每個位置的像素點有三個通道 if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs( pixel1[2] - pixel2[2]) < threshold: return True else: return False
截取到的圖如下:
比亂序亂序的圖舒服多了,而且代碼量也少。
1.3.6 模擬拖動滑塊
模擬拖動過程不復雜 ,但其中的坑比較多。現在我們只需要調用拖功的相關函數將滑塊拖動到對應位置。如果是勻速拖動,極驗必然會識別出它是程序的操作,因為人無法做到完全勻速拖動。極驗驗證碼利用機器學習模型,篩選此類數據為機器操作,驗證碼識別失敗。
我們嘗試分段模擬將拖動過程划分幾段,前段滑塊做勻加速運動,后段滑塊做勻減速運動, 利用物理學的加速度公式即可完成驗證。
滑塊滑動的加速度用a來表示,當前速度用表示,初速度用v0表示 ,位移用x表示 ,所需時間用t表示,它們滿足如下關系:
x = v0 * t +0.5 * a * t * t
v = v0 + a * t
利用這兩個公式可以構造軌跡移動算法,計算出先加速后減速的運動軌跡,代碼實現如下:
def get_diff_location(self): # 獲取缺口圖起點 captcha1 = Image.open('captcha1.png') captcha2 = Image.open('captcha2.png') for x in range(self.left, captcha1.size[0]): # 從左到右 x方向 for y in range(captcha1.size[1]): # 從上到下 y方向 if not self.is_similar(captcha1, captcha2, x, y): return x # 找到缺口的左側邊界 在x方向上的位置 def get_move_track(self, gap): track = [] # 移動軌跡 current = 0 # 當前位移 # 減速閾值 mid = gap * 4 / 5 # 前4/5段加速 后1/5段減速 t = 0.2 # 計算間隔 v = 0 # 初速度 while current < gap: if current < mid: a = 5 # 加速度為+5 else: a = -5 # 加速度為-5 v0 = v # 初速度v0 v = v0 + a * t # 當前速度 move = v0 * t + 1 / 2 * a * t * t # 移動距離 current += move # 當前位移 track.append(round(move)) # 加入軌跡 return track def move_slider(self, track): slider = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.geetest_slider_button'))) ActionChains(self.driver).click_and_hold(slider).perform() for x in track: # 只有水平方向有運動 按軌跡移動 ActionChains(self.driver).move_by_offset(xoffset=x, yoffset=0).perform() sleep(1) ActionChains(self.driver).release().perform() # 松開鼠標
1.3.7 完整代碼
# -*- coding:utf-8 -*- from PIL import Image from time import sleep from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver import ActionChains from selenium.webdriver.support.wait import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.desired_capabilities import DesiredCapabilities headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36" } chrome_options = webdriver.ChromeOptions() chrome_options.add_experimental_option('w3c', False) caps = DesiredCapabilities.CHROME caps['loggingPrefs'] = {'performance': 'ALL'} class SliderVerificationCode(object): def __init__(self): # 初始化一些信息 self.left = 60 # 定義一個左邊的起點 缺口一般離圖片左側有一定的距離 有一個滑塊 self.url = 'https://passport.bilibili.com/login' self.driver = webdriver.Chrome(desired_capabilities=caps,options=chrome_options) self.wait = WebDriverWait(self.driver, 20) # 設置等待時間20秒 self.phone = "17369251763" self.passwd = "abcdefg" def input_name_password(self): # 輸入賬號密碼 self.driver.get(self.url) self.driver.maximize_window() input_name = self.driver.find_element_by_xpath("//input[@id='login-username']") input_pwd = self.driver.find_element_by_xpath("//input[@id='login-passwd']") input_name.send_keys("username") input_pwd.send_keys("passport") sleep(3) def click_login_button(self): # 點擊登錄按鈕,出現驗證碼圖片 login_btn = self.driver.find_element_by_class_name("btn-login") login_btn.click() sleep(3) def get_geetest_image(self): # 獲取驗證碼圖片 gapimg = self.wait.until(EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_bg'))) sleep(2) gapimg.screenshot(r'./captcha1.png') # 通過js代碼修改標簽樣式 顯示圖片2 js = 'var change = document.getElementsByClassName("geetest_canvas_fullbg");change[0].style = "display:block;"' self.driver.execute_script(js) sleep(2) fullimg = self.wait.until( EC.presence_of_element_located((By.CLASS_NAME, 'geetest_canvas_slice'))) fullimg.screenshot(r'./captcha2.png') def is_similar(self, image1, image2, x, y): '''判斷兩張圖片 各個位置的像素是否相同 #image1:帶缺口的圖片 :param image2: 不帶缺口的圖片 :param x: 位置x :param y: 位置y :return: (x,y)位置的像素是否相同 ''' # 獲取兩張圖片指定位置的像素點 pixel1 = image1.load()[x, y] pixel2 = image2.load()[x, y] # 設置一個閾值 允許有誤差 threshold = 60 # 彩色圖 每個位置的像素點有三個通道 if abs(pixel1[0] - pixel2[0]) < threshold and abs(pixel1[1] - pixel2[1]) < threshold and abs( pixel1[2] - pixel2[2]) < threshold: return True else: return False def get_diff_location(self): # 獲取缺口圖起點 captcha1 = Image.open('captcha1.png') captcha2 = Image.open('captcha2.png') for x in range(self.left, captcha1.size[0]): # 從左到右 x方向 for y in range(captcha1.size[1]): # 從上到下 y方向 if not self.is_similar(captcha1, captcha2, x, y): return x # 找到缺口的左側邊界 在x方向上的位置 def get_move_track(self, gap): track = [] # 移動軌跡 current = 0 # 當前位移 # 減速閾值 mid = gap * 4 / 5 # 前4/5段加速 后1/5段減速 t = 0.2 # 計算間隔 v = 0 # 初速度 while current < gap: if current < mid: a = 5 # 加速度為+5 else: a = -5 # 加速度為-5 v0 = v # 初速度v0 v = v0 + a * t # 當前速度 move = v0 * t + 1 / 2 * a * t * t # 移動距離 current += move # 當前位移 track.append(round(move)) # 加入軌跡 return track def move_slider(self, track): slider = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.geetest_slider_button'))) ActionChains(self.driver).click_and_hold(slider).perform() for x in track: # 只有水平方向有運動 按軌跡移動 ActionChains(self.driver).move_by_offset(xoffset=x, yoffset=0).perform()
sleep(0.2) sleep(1) ActionChains(self.driver).release().perform() # 松開鼠標 def main(self): self.input_name_password() self.click_login_button() self.get_geetest_image() gap = self.get_diff_location() # 缺口左起點位置 gap = gap - 6 # 減去滑塊左側距離圖片左側在x方向上的距離 即為滑塊實際要移動的距離 track = self.get_move_track(gap) self.move_slider(track) if __name__ == "__main__": springAutumn = SliderVerificationCode() springAutumn.main()