介紹
在Python的實戰中爬蟲承擔相當重要的角色,而驗證碼識別則是爬蟲中一個重點。驗證碼是一個網站項目的守衛,如果不能通過驗證碼識別,那後期的爬蟲則無法進行。本文詳細介紹Python驗證碼識別的具體細節。鄭重聲明:僅討論技術,不能用於違法手段,如若不然則受法律嚴懲且與作者無關。
準備工作——驗證碼解析環境搭建
安裝Tesseract
Tesserocr 是 Python 的一個 OCR 識別庫,但其實是對 Tesseract 做的一層 Python API 封裝,它的核心是 Tesseract,所以在安裝 Tesserocr 之前我們需要先安裝 Tesseract
官方網址:https://digi.bib.uni-mannheim.de/tesseract/
選擇版本:
此處選擇4.0.0版本,因爲截至目前(2020-2-28)對應的python庫的支持最新只到這個版本。
具體看https://github.com/simonflueckiger/tesserocr-windows_build/releases的顯示版本,括號裏是支持Tesserocr的版本。
安裝時可以勾選多語言支持(但會導致整個過程很慢):
安裝完成後,需要設置環境變量。在Path中設置C:\Program Files\Tesseract-OCR(路徑以自己爲準)
確認是否設置正確:
安裝Tesserocr(Tesseract-OCR)
使用pip直接安裝:
pip install tesserocr pillow
如果安裝失敗,嘗試使用以下方法:
- 1.下載安裝tesserocr的whl格式文件。
whl格式本質上是一個壓縮包,裏面包含了py文件,以及經過編譯的pyd文件
網址:https://github.com/simonflueckiger/tesserocr-windows_build/releases
- 2.查看本機python對應的版本:
新建test2.py文件並執行:
import pip import pip._internal
print(pip._internal.pep425tags.get_supported())
輸出:
[('cp37', 'cp37m', 'win_amd64'), ('cp37', 'none', 'win_amd64'), ('py3', 'none', 'win_amd64'), ('cp37', 'none', 'any'), ('cp3', 'none', 'any'), ('py37', 'none', 'any'), ('py3', 'none', 'any'), ('py36', 'none', 'any'), ('py35', 'none', 'any'), ('py34', 'none', 'any'), ('py33', 'none', 'any'), ('py32', 'none', 'any'), ('py31', 'none', 'any'), ('py30', 'none', 'any')]
意思是對應版本是'cp37', 'cp37m', 'win_amd64'。
- 3.找到對應的版本:
- 4.下載後使用pip安裝.whl文件(路徑以自己實際路徑爲準):
pip install C:\tesserocr-2.4.0-cp37-cp37m-win_amd64.whl
牛刀小試——簡單驗證碼識別
首先安裝依賴:
pip install pillow
如果安裝失敗。使用:
python -m pip install --upgrade pip
完成後執行install命令。
使用tesseract識別驗證碼
找一張較簡單的驗證碼(test.jpg):
解析驗證碼(test3.py):
import tesserocr
from PIL import Image
image=Image.open('test.jpg')
image.show() #可以打印出圖片,供預覽
print(tesserocr.image_to_text(image))
如果執行過程中報錯:
Failed to init API, possibly an invalid tessdata path: C:\Users\XXXXX\AppData\Local\Programs\Python\Python37\/tessdata/
則將Tesseract安裝目錄下的tessdata文件夾複製到python的根目錄,即報錯顯示的目錄。
使用pytesseract識別驗證碼
以上範例使用的是tesserocr.image_to_text(),但是識別效率很低,推薦使用pytesseract。pytesseract是在Tesseract-OCR基礎上封裝的,識別效果更好的類庫。
官方介紹:Python-tesseract is a wrapper for Google’s Tesseract-OCR Engine. It is also useful as a stand-alone invocation script to tesseract, as it can read all image types supported by the Pillow and Leptonica imaging libraries, including jpeg, png, gif, bmp, tiff, and others.
首先安裝pytesseract:
pip install pytesseract
使用pytesseract的image_to_string()方法:
from PIL import Image
from pytesseract import *
result = image_to_string(Image.open("test.jpg"), lang='eng', config='--psm 10 --oem 3 -c tessedit_char_whitelist=0123456789')
lang表示識別的語言。
psm是一個設置驗證碼識別的重要參數,可以用它來精確提升驗證通過率(下方是官網給出的值範圍)。
oem沒有找到專門的解釋,官網給的範例使用的值是3。
tessedit_char_whitelist表示白名單,將識別的結果控制在白名單範圍(經測試,效果有限)
psm值:
Page segmentation modes:
0 Orientation and script detection (OSD) only.
1 Automatic page segmentation with OSD.
2 Automatic page segmentation, but no OSD, or OCR.
3 Fully automatic page segmentation, but no OSD. (Default)
4 Assume a single column of text of variable sizes.
5 Assume a single uniform block of vertically aligned text.
6 Assume a single uniform block of text.
7 Treat the image as a single text line.
8 Treat the image as a single word.
9 Treat the image as a single word in a circle.
10 Treat the image as a single character.
11 Sparse text. Find as much text as possible in no particular order.
12 Sparse text with OSD.
13 Raw line. Treat the image as a single text line,bypassing hacks that are Tesseract-specific.
頗費功夫——複雜驗證碼識別
上文的驗證碼已經算是非常簡單的一種,幾乎使用原生的驗證碼識別庫就可以識別。但是大部分時候我們面對的是下面這種驗證碼:
或者這種:
亦或者這種:
這些驗證碼時間使用庫來識別通過率會非常低,幾乎無法識別。這時候就得用到我們的新手段——圖片處理。
不同的驗證碼圖片需要做的處理是不一樣的,需要對症下藥,比如第一種,它的特點是有一條很細的邊框以及極多的背景干擾線。這樣我們需要作出兩點操作:
1.點性降噪
2.去除邊框
圖片是由像素點構成的,我們放大圖像就可以一目瞭然。這些像素點中,有些是組成驗證碼的重要像素點,而大部分則是造成識別干擾的像素。
圖片當中的像素點不是獨立存在的, 一個像素點周圍有8個像素點(邊框除外)。如下圖,若中心點與8個像素中絕大部分的像素點RBG值不一樣,就像臉上的粉刺一樣,這個孤零零的點破壞了整體的RBG統一性,成爲了我們必須去除的點——噪點。
上圖中組成MABC四個字母的像素點是連貫的,但是噪點卻是隨機分佈的。利用這個特點我們就可以判斷是否是噪點。
當然,中心點與周圍RBG值完全不同是特殊情況。實際中我們看到的往往是這樣:
上圖裏中心點與周圍像素有RBG相同的也有不同的,面對這種情況,我們就需要設定一個值(N),N表示在判定噪點的時候,中心像素點與周圍像素點相同的個數的臨界值。
當中心點與周圍像素的RBG值相同的數量小於N時,該點爲噪點。
上圖中,因爲與中心點相同像素數是2個。當我們將N設爲3,中心點將會被認爲是噪點。若設爲1,則中心點不是噪點。N值的設定需要我們根據情況判斷調整。
按照這個邏輯,對每一個像素點進行判斷,若是噪點則將其顏色置爲白色即可。
但是實際中有可能因爲圖片的噪點太過密集而出現漏網之魚。這樣我們再引入一個新的想法——多次降噪。
意思是,在對每個像素點降噪判斷後,多次重新掃描保證儘可能多的噪點被去除。
但是多次降噪可能會導致驗證碼像素受影響,需根據情況斟酌。
依照這個思路,我們寫出降噪代碼如下。(image是圖片二值閾值,N是噪點判斷的臨界值,K是多次降噪的次數)
def clearNoise(image, N, K):
for i in range(0, K):
t2val[(0, 0)] = 1
t2val[(image.size[0] - 1, image.size[1] - 1)] = 1
for x in range(1, image.size[0] - 1):
for y in range(1, image.size[1] - 1):
nearDots = 0
L = t2val[(x, y)]
if L == t2val[(x - 1, y - 1)]:
nearDots += 1
if L == t2val[(x - 1, y)]:
nearDots += 1
if L == t2val[(x - 1, y + 1)]:
nearDots += 1
if L == t2val[(x, y - 1)]:
nearDots += 1
if L == t2val[(x, y + 1)]:
nearDots += 1
if L == t2val[(x + 1, y - 1)]:
nearDots += 1
if L == t2val[(x + 1, y)]:
nearDots += 1
if L == t2val[(x + 1, y + 1)]:
nearDots += 1
if nearDots < N:
t2val[(x, y)] = 1
處理完成後得到圖片:
可以看出,降噪完成後的圖片背景已經變得非常“乾淨”。除了邊框外,這個驗證碼已經比較容易識別。
由於邊框像素本身也是一串連續的點,與驗證碼相似,且位置在邊界處,降噪不能對其處理。
第二步進行邊框去除。這個就比較簡單了。將邊框處的像素剪裁變色。
def clear_border(img_name):
img = cv_imread(path_extends.get_absolute_path()+"\\images\\"+img_name)
filename = path_extends.get_absolute_path()+"\\images\\" + \
img_name.split('-')[0] + '-clearBorder.jpg'
h, w = img.shape[:2]
for y in range(0, w):
for x in range(0, h):
if y < 2 or y > w - 2:
img[x, y] = 255
if x < 2 or x > h - 2:
img[x, y] = 255
cv_imwrite(filename, img)
return img
經過一系列的處理,得到結果:
完整的代碼(調用image_to_text函數即可識別,驗證碼原始圖片需放置在images文件夾內並命名爲test.png):
# coding:utf-8
import sys, os
from PIL import Image, ImageDraw
from pytesseract import *
import cv2
from tools import path_extends
import numpy as np
# 二值數組
t2val = {}
def twoValue(image, G):
for y in range(0, image.size[1]):
for x in range(0, image.size[0]):
g = image.getpixel((x, y))
if g > G:
t2val[(x, y)] = 1
else:
t2val[(x, y)] = 0
def clear_border(img_name):
img = cv_imread(path_extends.get_absolute_path()+"\\images\\"+img_name)
filename = path_extends.get_absolute_path()+"\\images\\" + \
img_name.split('-')[0] + '-clearBorder.jpg'
h, w = img.shape[:2]
for y in range(0, w):
for x in range(0, h):
if y < 2 or y > w - 2:
img[x, y] = 255
if x < 2 or x > h - 2:
img[x, y] = 255
cv_imwrite(filename, img)
return img
def clearNoise(image, N, K):
for i in range(0, K):
t2val[(0, 0)] = 1
t2val[(image.size[0] - 1, image.size[1] - 1)] = 1
for x in range(1, image.size[0] - 1):
for y in range(1, image.size[1] - 1):
nearDots = 0
L = t2val[(x, y)]
if L == t2val[(x - 1, y - 1)]:
nearDots += 1
if L == t2val[(x - 1, y)]:
nearDots += 1
if L == t2val[(x - 1, y + 1)]:
nearDots += 1
if L == t2val[(x, y - 1)]:
nearDots += 1
if L == t2val[(x, y + 1)]:
nearDots += 1
if L == t2val[(x + 1, y - 1)]:
nearDots += 1
if L == t2val[(x + 1, y)]:
nearDots += 1
if L == t2val[(x + 1, y + 1)]:
nearDots += 1
if nearDots < N:
t2val[(x, y)] = 1
def cv_imread(filePath):
cv_img = cv2.imdecode(np.fromfile(filePath, dtype=np.uint8), -1)
return cv_img
def cv_imwrite(filePath, features):
cv2.imencode('.jpg', features)[1].tofile(filePath)
def saveImage(filename, size):
image = Image.new("1", size)
draw = ImageDraw.Draw(image)
for x in range(0, size[0]):
for y in range(0, size[1]):
draw.point((x, y), t2val[(x, y)])
image.save(filename)
def image_to_text():
image = Image.open(path_extends.get_absolute_path()+"\\images\\test.png").convert("L")
twoValue(image, 100)
clearNoise(image, 2, 1)
path1 = path_extends.get_absolute_path()+"\\images\\test-clearNoise.jpg"
saveImage(path1, image.size)
clear_border("my-clearNoise.jpg")
result = image_to_string(Image.open(
path_extends.get_absolute_path()+"\\images\\test-clearBorder.jpg"), lang='eng', config='--psm 10 --oem 3 -c tessedit_char_whitelist=QWERTYUIOPLKHJHGFDSAZXCVBNM')
return result
究極難度——開始樣本訓練吧
以上的驗證碼還不算是最難識別的,我們一定見過這種的(圖片來自百度):
文字扭曲、傾斜、擠靠。這些驗證碼即便是人來看都得多看一眼,更何況程序識別。這時候我們上文的辦法已經力不從心,需要一個新的思路。
計算機有比人快而準的優點,但是一個字母或者符號稍加變形程序便無法識別,這種過於較真的特點反倒成了缺點。假如我們能告訴程序m等於m,也等於m,問題就得以解決。
這就需要引入一個概念——樣本訓練。
我們在做訓練之前先需要收集樣本,這些樣本可以通過手動截圖,也可以通過程序分割。舉個簡單的例子,我們需要訓練0~9的數字,就需要先收集這10個數字的樣本圖片,之後進行下一步。
下載jTessBoxEditor:
官方下載(較慢):https://sourceforge.net/projects/vietocr/files/jTessBoxEditor/
國內下載:https://www.jb51.net/softs/676483.html#downintro2
下載庫:
訓練庫下載: https://sourceforge.net/projects/tess4j/files/tess4j/
製作樣本:
png轉化爲tif
轉化網址:https://cloudconvert.com/png-to-tiff
導入訓練樣本
選擇訓練圖片:
選擇後會繼續彈框讓你選擇目錄,用來保存合併後的tiff。
文件名命名爲xl.normal.exp0.tif
執行命令行(開始訓練):
tesseract xl.normal.exp0.tif xl.normal.exp0 -l eng batch.nochop makebox
樣本訓練完畢,接下來是關鍵的一步——分割驗證碼,以方便程序對照樣本進行識別。
分割的邏輯都大抵相似,這裏直接引用shaomine的博文:
#coding:utf8
import os
from PIL import Image,ImageDraw,ImageFile
import numpy
import pytesseract
import cv2
import imagehash
import collections
class pictureIdenti:
#rownum:切割行數;colnum:切割列數;dstpath:圖片文件路徑;img_name:要切割的圖片文件
def splitimage(self, rownum=1, colnum=4, dstpath="D:\work\python36_crawl\Veriycode",
img_name="D:\work\python36_crawl\Veriycode\mode_5246.png",):
img = Image.open(img_name)
w, h = img.size
if rownum <= h and colnum <= w:
print('Original image info: %sx%s, %s, %s' % (w, h, img.format, img.mode))
print('開始處理圖片切割, 請稍候...')
s = os.path.split(img_name)
if dstpath == '':
dstpath = s[0]
fn = s[1].split('.')
basename = fn[0]
ext = fn[-1]
num = 1
rowheight = h // rownum
colwidth = w // colnum
file_list = []
for r in range(rownum):
index = 0
for c in range(colnum):
# (left, upper, right, lower)
# box = (c * colwidth, r * rowheight, (c + 1) * colwidth, (r + 1) * rowheight)
if index<1:
colwid = colwidth+6
elif index<2:
colwid = colwidth + 1
elif index < 3:
colwid = colwidth
box = (c * colwid, r * rowheight, (c + 1) * colwid, (r + 1) * rowheight)
newfile = os.path.join(dstpath, basename + '_' + str(num) + '.' + ext)
file_list.append(newfile)
img.crop(box).save(os.path.join(dstpath, basename + '_' + str(num) + '.' + ext), ext)
num = num + 1
index+=1
for f in file_list:
print(f)
print('圖片切割完畢,共生成 %s 張小圖片。' % num)
宿命之敵——邏輯驗證碼
事實上,邏輯驗證碼已經不再是“碼”,而是一種邏輯判斷。舉個例子(圖片來自百度):
以及我們最熟悉的:
這已經不是上文的1=1,而是需要觀察者識別內容後進行邏輯判斷再輸入結果。依照上文的方式已經很難再識別。具體的解決方法也已經不是本文的討論範圍。
結束語
驗證碼是網站和應用程序的守衛,它的作用也越來越重要。如果你不是一個Python爬蟲研究者,而是一個網站管理員,也需要深入瞭解驗證碼的識別,因爲這對你的網站安全尤爲重要。
我們研究驗證碼識別是爲了更好的加固網絡安全性。對使用爬蟲技術的人來說,安全、非破壞式的使用該技術是底線也是自我要求。在爬取數據的時候應當先了解這些內容是否允許被爬,遵守robots.txt守則,且在爬取過程中應該儘可能的多等待,而不是無節制刷取數據而對服務器造成影響。
部分引用: