隨着爬蟲的發展,愈來愈多的網站開始採用各類各樣的措施來反爬蟲,其中一個措施即是使用驗證碼。隨着技術的發展,驗證碼也愈來愈花裏胡哨的了。最開始就是幾個數字隨機組成的圖像驗證碼,後來加入了英文字母和混淆曲線,或者是人眼都很難識別的數字字母。不少國內網站還出現了中文字符的驗證碼,使得識別愈加困難。python
而後又出現了須要咱們識別文字,點擊與文字相符合的圖片,驗證碼徹底正確,驗證才能經過。下載的這種交互式驗證碼愈來愈多了,如滑動驗證碼須要滑動拼合滑塊才能完成驗證,點觸驗證碼須要徹底點擊正確結果才能夠完成驗證,另外還有滑動宮格驗證碼、計算題驗證碼等。web
最讓我生氣的就是外國的一款郵箱的驗證碼,freemail郵箱的驗證碼,隨機生成一些圖片,讓你點擊符合標題的圖片,這種別說爬蟲了,對人爲操做都不友好。(滿滿的怨念)算法
還有一種外國郵箱tutanota,是一個時鐘驗證碼,咱們想要根據上面的時間指針來輸入正確的時間。可是被咱們公司的大佬本身寫的OCR識別出來了,雖然錯誤率還很高,可是這是一個大的突破。spring
驗證碼變得愈來愈複雜,爬蟲的工做也變得愈發艱難,有時候咱們必須經過驗證碼的驗證才能夠訪問頁面,本章就專門針對簡單的驗證碼的識別作大概的講解(難的我也不會)。chrome
tesserocr是很早的一款OCR文字識別技術了,算是過期的東西了。百度OCR中文字識別天天都有限制次數的免費額度,因此咱們就用它了(別問,問就是白嫖)。canvas
百度搜索百度ocr,進入官網。api
往下翻,直到翻到下圖界面。瀏覽器
登陸便可,沒有帳號就註冊。安全
登陸成功後,建立應用。網絡
中間內容填的合理就行。
這些內容不能給你們看了,下面的代碼中,我會將之用********替換,各位只要根據本身的百度平臺的內容修改下便可。
咱們首先識別最簡單的以種驗證碼,即圖形驗證碼。這種驗證碼最先出現,如今也很常見,通常由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時:
實在是把我弄自閉了,中間的值更加奇怪 ,技術不到家就是這樣吧。
而後我又換了幾回驗證碼,結果以下:
終因而成功了一次了,因此這個案例告訴咱們,識別不出來,就刷新換下一張,瞎貓總會碰到死耗子。
而在真正的項目中,都會判斷是否驗證碼錯誤,驗證碼錯誤就刷新驗證碼,從新輸入。
遇到那種失敗就會跳轉界面的,那就只能重啓任務了。
滑動驗證碼須要拖動拼合滑塊才能完成驗證,相對於圖形驗證碼來講識別難度上升了幾個等級。製做滑動驗證碼的公司叫作GEETEST,官網是https://www.geetest.com/。主要驗證方式是拖動滑塊破解圖像。若圖像徹底拼合,則驗證成功,即表單成功提交,不然須要從新驗證。
滑動驗證碼相較於圖形驗證碼來講識別難度更大。如今極驗驗證碼已經更新到3.0版本,對於極驗驗證碼 3.0 版本,咱們首先點擊按鈕進行智能驗證。若是驗證不經過,則會彈出滑動驗證的窗口,拖動滑塊拼合圖像進行驗證。以後三個加密參數會生成,經過表單提交到後臺,後臺還會進行一次驗證。
極驗驗證碼還增長了機器學習的方法來識別拖動軌跡。官方網站的安全防禦有以下幾點說明:
三角防禦之防模擬:惡意程序模仿人類行爲軌跡對驗證碼進行識別。針對模擬,極驗驗證碼擁有超過4000萬人機行爲樣本的海量數據。利用機器學習和神經網絡,構建線上線下的多重靜態、動態防護模型。識別模擬軌跡,界定人機邊界。
三角防禦之防僞造:惡意程序經過僞造設備瀏覽器環境對驗證碼進行識別。針對僞造,極驗驗證碼利用設備基因技術。深度分析瀏覽器的實際性能來辨識僞造信息。同時根據僞造事件不斷更新黑名單,大幅提升防僞造能力。
三角防禦之防暴力:惡意程序短期內進行密集的攻擊,對驗證碼進行暴力識別。針對暴 力,極驗驗證碼擁有多種驗證形態,每一種驗證形態都有利用神經網絡生成的海藍圖庫儲 備,每一張圖片都是獨一無二的,且圖庫不斷更新,極大程度提升了暴力識別的成本。
另外,極驗驗證碼的驗證相對於普通驗證方式更方便,體驗更友好,其官方網站說明以下:
點擊一下,驗證只須要0.4秒。極驗驗證碼始終專一於去驗證化實踐,讓驗證環節再也不打斷產品自己的交互流程,最終達到優化用戶體驗和提升用戶轉化率的效果。
全平臺兼容 ,適用各類交互場景。極驗驗證碼兼容全部主流瀏覽器甚至於古老的IE6,也能夠輕鬆應用在iOS和Android移動端平臺,知足各類業務需求,保護網站資源不被濫用和監取。
面向將來,懂科技,更懂人性。極驗驗證碼在保障安全同時不斷致力於提高用戶體驗、精雕細琢的驗證面板、流暢順滑的驗證動畫效果,讓驗證過程再也不枯燥乏味。
相比通常驗證碼,極驗驗證碼的驗證安全性和易用性有了很是大的提升。
對於應用了極驗驗證碼的網站,若是咱們直接模擬表單提交,加密參數的構造是個問題,須要分析其加密和校驗邏輯,很是的複雜。可是咱們若是採用模擬瀏覽器動做的方式來完成驗證,就會變得很簡單了。在python中,咱們可使用selenium來模擬人的行爲來完成驗證、此驗證成本相對與直接去識別加密算法少得多。
首先找到一個帶有極驗驗證碼的網站,如B站,連接爲:https://passport.bilibili.com/login。輸入帳號密碼點擊登陸,極驗驗證碼就會彈出來。
因此咱們這個識別驗證案例 完成須要三步:
第一步操做最簡單,咱們能夠直接用selenium完成。
第二步操做識別缺口的位置比較關鍵,這裏須要用到圖像的相關處理方法。首先觀察缺口的樣子。
缺口的四周邊緣又明顯的斷裂邊緣,邊緣和邊緣周圍又明顯的區別。咱們能夠實現一個邊緣檢測算法來找出缺口的位置。對於極驗驗證碼來講,咱們能夠利用和原圖對比檢測的方式來識別缺口的位置,由於在沒有滑動滑塊以前, 缺口並無呈現。
咱們能夠同時獲取兩張圖片。設定一個對比闊值,而後遍歷兩張圖片,找出相同位置像素RGB差距超過此闊值的像素點,那麼此像素點的位置就是缺口的位置 。
第三步操做看似簡單,但其中的坑比較多。極驗驗證碼增長了機器軌跡識別,勻速移動、隨機速度移動等方法都不能經過驗證,只有徹底模擬人的移動軌跡才能夠經過驗證。人的移動軌跡通常是先加速後減速,咱們須要模擬這個過程才能成功。
有了思路後,咱們就用代碼來實現極驗驗證碼的識別過程吧。
咱們先初始化一些配置,如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站的帳號和密碼。
輸入帳號密碼:
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)
第一步的工做就完成了。
接下來識別缺口的位置,首先獲取先後兩張比對圖片,兩者不一致的地方即爲缺口。看到網上那些案例,接收到亂序的兩張圖片,而後用代碼拼接起來,麻煩的要死,並且我試了幾個,絕大部分是不能運行的,就一個能截圖出來的,截出來的圖以下:
我只想登陸一下,還要我幹這麼多事,我哭了。
因此我就換了種方法,這個版本的極驗驗證碼應該均可以這樣作,代碼以下:
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
截取到的圖以下:
比亂序亂序的圖舒服多了,並且代碼量也少。
模擬拖動過程不復雜 ,但其中的坑比較多。如今咱們只須要調用拖功的相關函數將滑塊拖動到對應位置。若是是勻速拖動,極驗必然會識別出它是程序的操做,由於人沒法作到徹底勻速拖動。極驗驗證碼利用機器學習模型,篩選此類數據爲機器操做,驗證碼識別失敗。
咱們嘗試分段模擬將拖動過程劃分幾段,前段滑塊作勻加速運動,後段滑塊作勻減速運動, 利用物理學的加速度公式便可完成驗證。
滑塊滑動的加速度用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() # 鬆開鼠標
# -*- 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(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()