那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

叄公子KCN發表於2018-12-08

寫在前面

本章將要介紹一下如何識別簡單的驗證碼。會涉及到一些影象的概念以及機器學習的知識。

我們本次識別的驗證碼來自csdn,長相如下:

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

在學習之前,我們先安裝本章需要的三個庫:影象庫Pillow、機器學習庫Scikit-Learn、科學計算庫Numpy。通過pip命令就可以進行安裝。

pip install pillow scikit-learn numpy
複製程式碼

原始碼介紹

本章節的案例稍微複雜,見:USTBCrawlers/lesson8

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

這裡主要有三個部分:下載器、分割器、與識別器。我們可以先把程式碼clone下來,然後進入到lesson8這個目錄下。

影象基本概念

下面我們先來對影象有個基本的介紹。影象是由一個一個畫素點構成的,其內部結構是一個二維的矩陣,或者理解成一個二維陣列。

例如,一個 M x N 的影象,可以表示成以下的格式:

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別
影象的座標和我們平時學的直角座標並不相同,直角座標的原點是在左下角;而影象的座標 起點是在左上角,如下圖所示。

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

在python中,我們可以使用PIL(Pillow)對影象進行操作。如下,我們開啟了我們的驗證碼,並呼叫convert("L")方法把圖片轉為灰度影象。

from PIL import Image

im = Image.open("csdn.png").convert("L")
im.show()  # 顯示影象
複製程式碼

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

然而我們真正操作並不是影象物件,而是一個矩陣,或者說是二維陣列,我們可以把影象轉成numpy陣列。

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

可以看到我們的驗證碼是20*48的。

影象直方圖

影象是由一個個的畫素構成的,畫素有灰度值,從0-255,一共256個灰度級。直方圖的作用是觀察每個灰度級所佔畫素的多少。

可以呼叫Image.histogram()獲取Image物件的直方圖。

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

比如說對於我們的驗證碼圖片,一共有20*48=960個畫素點,其中灰度級為94的畫素有754個,而灰度級為255的有129個。

再次觀察一下我們灰度化的驗證碼,可以看到驗證碼的字母是白色的,也就是灰度級為255。周圍的背景是灰色的,灰度級為94。

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

影象二值化

在處理驗證碼的時候,背景很多時候並不是同一個灰度級的,為了減少背景對資料的影響。一般都會講驗證碼進行二值化。

所謂二值化,其實就是把灰度影象變成只由純黑、或純白兩種畫素組成的影象。

方法很簡單,我們可以設定一個閾值,灰度大於100的畫素都變成純白(255),而灰度小於100的畫素都變成純黑(0)。

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

驗證碼字元分割

因為驗證碼包含四個數字,所以需要把每個字母分割開。筆者為讀者準備的驗證碼是很好分割的型別,只需要對指定區域進行篩選即可。

這裡的圖片數字的寬度都是8,起點分別位於5、14、23、32。分割程式碼如下:

def split_and_save(path):
    path = "../downloader/captchas/" + path
    pix = np.array(Image.open(path).convert("L"))
    # threshold image
    pix = (pix > 100) * 255

    col_ranges = [
        [5, 5 + 8],
        [14, 14 + 8],
        [23, 23 + 8],
        [32, 32 + 8]
    ]
    # split and save
    for col_range in col_ranges:
        letter = pix[:, col_range[0]: col_range[1]]
        im = Image.fromarray(np.uint8(letter))
        save_path = "./letters/" + str(uuid.uuid4()) + ".png"
        im.save(save_path)
複製程式碼

我們每個驗證碼字元的大小為:20*8。

建立資料集

對驗證碼分割後,我們就會得到一堆字母的圖片了。

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

但是這些圖片都沒有標註,下面我們使用的機器學習演算法是資料驅動的,所以需要一些已經標註好的驗證碼資料。我這裡標註的方法比較簡單,因為畢竟只有0-9十種字母,我就每種資料標註6個。直接通過檔名稱進行標識。

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

機器學習之KNN演算法

演算法描述

K近鄰演算法的定義十分簡單,在百度百科上有這樣的解釋:如果一個樣本在特徵空間中的k個最相似(即特徵空間中最鄰近)的樣本中的大多數屬於某一個類別,則該樣本也屬於這個類別。

也就是說,需要找到要識別的字母在訓練樣本中K個最近的字母,然後找出這K個字母中最多的是某個類的?要識別的圖片也就是該類的。

演算法舉例

上面那麼描述可能稍微有點兒晦澀,那我們舉個例子。

這裡以電影分類作為例子,電影題材可分為愛情片,動作片等。這裡假定將電影分為愛情片和動作片兩類,直觀感受的話,如果一部電影中接吻鏡頭很多,打鬥鏡頭較少,顯然是屬於愛情片,反之為動作片。

這裡我們的資料有兩個特徵:一個是接吻鏡頭的數目,一個是打鬥鏡頭的數目。下面我們有一組已知的資料。

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

我們這裡的目標是利用已知的四個電影資料,預測未知電影的型別。

我們把愛情電影定義為紅色的叉子,動作電影定義為綠色的圓圈,未知電影為問號。這裡可以畫個圖直觀感受一下。

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

假設我們K取3的話,那麼從圖中可以很清晰的看到,離未知電影最近的三個電影分別是:愛情電影、愛情電影、動作電影。愛情電影占比大,所以我們未知的電影是愛情片。

scikit-learn中使用KNN

我們將使用scikit-learn來實現KNN,所以不需要關注演算法的實現(雖然實現也很簡單),只要有資料和標籤就好了。我們來看看怎麼實現上面的預測電影型別的功能。

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

這裡X是我們已知型別的四部電影,y是四部電影的標籤。0代表愛情電影,1代表動作電影。然後呼叫scikit-learn中的KNeighborsClassifier先通過fit擬合資料,再呼叫predict預測就好了。

可以看到我們最後預測出未知電影的標籤為0,也就是愛情電影,和想法一致。

驗證碼識別

瞭解了以上知識後,我們可以編寫驗證碼識別指令碼了。

我們這裡首先編寫載入資料的函式,載入之前標註好的驗證碼字母資料。我們驗證碼字母資料是20*8的,也就是相當於有160個特徵。

def load_dataset():
    X = []
    y = []

    for i in range(60):
        path = "./dataset/%d%d.png" % (i / 6, i % 6 + 1)
        pix = np.array(Image.open(path).convert("L"))
        X.append(pix.reshape(8*20))
        y.append(i/6)
    return np.array(X), np.array(y)
複製程式碼

然後對資料進行擬合:

X, y = load_dataset()
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X, y.astype('uint8'))
複製程式碼

最後先分割圖片,再使用擬合好資料的knn進行預測。

def split_letters(path):
    pix = np.array(Image.open(path).convert("L"))
    # threshold image
    pix = (pix > 100) * 255

    col_ranges = [
        [5, 5 + 8],
        [14, 14 + 8],
        [23, 23 + 8],
        [32, 32 + 8]
    ]
    letters = []
    for col_range in col_ranges:
        letter = pix[:, col_range[0]: col_range[1]]
        letters.append(letter.reshape(8*20))

    return letters

if __name__ == "__main__":
    if len(sys.argv) != 2:
        print("Usage: python recognizer.py <image_filename>")

    letters = split_letters(sys.argv[1])
    print(knn.predict(letters))
複製程式碼

我們執行一下,可以看到以下識別的結果,都識別出來了。

那些年,我爬過的北科(八)——反反爬蟲之驗證碼識別

內容補充

以上的驗證碼識別只是一個基本的操作流程。現在只要有足夠多的資料,利用深度學習基本上所有的驗證碼都能識別出來。

深度學習由於需要讀者有數學基礎以及相關的背景知識,這裡筆者就提供一些我自己寫過的驗證碼相關的資料,如果感興趣可以自己去學習。

如果讀者感興趣可以進行深入學習。

筆者的驗證碼相關的一個專案:

筆者的驗證碼相關的部落格:

相關文章