爬蟲(十二):圖形驗證碼的識別、滑動驗證碼的識別(B站滑動驗證碼)

1. 驗證碼識別

隨着爬蟲的發展,愈來愈多的網站開始採用各類各樣的措施來反爬蟲,其中一個措施即是使用驗證碼。隨着技術的發展,驗證碼也愈來愈花裏胡哨的了。最開始就是幾個數字隨機組成的圖像驗證碼,後來加入了英文字母和混淆曲線,或者是人眼都很難識別的數字字母。不少國內網站還出現了中文字符的驗證碼,使得識別愈加困難。python

而後又出現了須要咱們識別文字,點擊與文字相符合的圖片,驗證碼徹底正確,驗證才能經過。下載的這種交互式驗證碼愈來愈多了,如滑動驗證碼須要滑動拼合滑塊才能完成驗證,點觸驗證碼須要徹底點擊正確結果才能夠完成驗證,另外還有滑動宮格驗證碼、計算題驗證碼等。web

最讓我生氣的就是外國的一款郵箱的驗證碼,freemail郵箱的驗證碼,隨機生成一些圖片,讓你點擊符合標題的圖片,這種別說爬蟲了,對人爲操做都不友好。(滿滿的怨念)算法

還有一種外國郵箱tutanota,是一個時鐘驗證碼,咱們想要根據上面的時間指針來輸入正確的時間。可是被咱們公司的大佬本身寫的OCR識別出來了,雖然錯誤率還很高,可是這是一個大的突破。spring

驗證碼變得愈來愈複雜,爬蟲的工做也變得愈發艱難,有時候咱們必須經過驗證碼的驗證才能夠訪問頁面,本章就專門針對簡單的驗證碼的識別作大概的講解(難的我也不會)。chrome

1.1 使用百度OCR

tesserocr是很早的一款OCR文字識別技術了,算是過期的東西了。百度OCR中文字識別天天都有限制次數的免費額度,因此咱們就用它了(別問,問就是白嫖)。canvas

百度搜索百度ocr,進入官網。api

 

往下翻,直到翻到下圖界面。瀏覽器

 

 

登陸便可,沒有帳號就註冊。安全

 

登陸成功後,建立應用。網絡

 

中間內容填的合理就行。

 

這些內容不能給你們看了,下面的代碼中,我會將之用********替換,各位只要根據本身的百度平臺的內容修改下便可。 

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。輸入帳號密碼點擊登陸,極驗驗證碼就會彈出來。

因此咱們這個識別驗證案例 完成須要三步:

  1. 輸入帳號密碼,點擊登陸
  2. 識別滑動缺口的位置
  3. 模擬拖動滑塊

第一步操做最簡單,咱們能夠直接用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(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()
相關文章
相關標籤/搜索