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

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

所以我們這個識別驗證案例 完成需要三步:

  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(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()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章