作者|餘俊(連舟)
編輯|橙子君
出品|阿里巴巴新零售淘系技術
目前,很多網站為了反爬都會採取各種各樣的策略,比較簡單粗暴的一種做法就是圖片驗證碼,隨著爬蟲技術與反爬技術的演變,目前驗證碼也越來越複雜,比較高端的如Google的I‘m not a robot,極驗等等。這些新的反爬方式大多都基於用戶行為分析用戶點擊前的鼠標軌跡來判斷是訪問者是程序還是人。
基於圖像處理的圖片驗證碼識別
這篇文章介紹的是破解一般“傳統”的圖片驗證碼的步驟。上面提到的極驗(目前應用比較廣)也已經可以被破解,知乎上有相關的專欄,這裡就不重複了。
即便是傳統的圖片驗證碼,也是有難度區分的(圖一是我母校研究生院官網上的驗證碼,基本形同虛設;圖二則是某網站的會員登錄時的驗證碼增加了一些干擾信息,字符也有所粘連),但是破解的流程大致是一樣的。
圖1
圖2
▐ 識別步驟
獲取樣本
從目標網站獲取了5000個驗證碼圖片到本地,作為樣本。因為後期需要進行監督學習樣本量要足夠大。
樣本去噪
✎ 先二值化圖片
這一步是為了增強圖片的對比度,利於後期圖片圖像處理,代碼如下:
# 二值化圖片
@staticmethod
def two_value_img(img_path, threshold):
img = Image.open(img_path).convert('L')
# setup a converting table with constant threshold
tables = []
for i in range(256):
if i < threshold:
tables.append(0)
else:
tables.append(1)
# convert to binary image by the table
bim = img.point(tables, '1')
return bim
效果如下:
✎ 圖片去噪
該案例中就是去除兩條幹擾線,常規的去噪算法有很多(洪水法等等),這裡根據圖片的特點採用了兩種去噪算法,一種是自己根據圖片的特徵實現的算法,另一種是“八值法”。去噪後的效果如下,可以看到去除了大部分的干擾線(剩下的根據字寬可以直接過濾掉),但是部分字符也變細了,所以這一步的去噪閥值需要不斷調整,在去噪的基礎上要儘量保持原圖的完整和可讀性。
代碼如下:
# 根據圖片特點,自己寫的降噪算法
def clean_img(img, threshold):
width, height = img.size
for j in range(height):
for i in range(width):
point = img.getpixel((i, j))
if point == 0:
for x in range(threshold):
if j + x >= height:
break
else:
if point != img.getpixel((i, j + x)):
img.putpixel((i, j), 1)
break
return img
# 八值法降噪
def clean_img_eight(img, threshold):
width, height = img.size
arr = [[0 for col in range(width)] for row in range(height)]
arr = array(arr)
for j in range(height):
for i in range(width):
point = img.getpixel((i, j))
if point == 0:
sum = 0
for x in range(-1, 2):
for y in range(-1, 2):
if i + x > width - 1 or j + y > height - 1 or \
i + x < 0 or j + y < 0:
sum += 1
else:
sum += img.getpixel((i + x, j + y))
if sum >= threshold:
arr[j, i] = 1
for i in range(len(arr)):
for j in range(len(arr[i])):
if arr[i, j] == 1:
img.putpixel((j, i), 1)
return img
效果如下:
圖片切割
圖片切割有很多算法如投影法、CFS以及滴水法等。投影法適用於字符垂直方向上沒有粘連和重合的情況,CFS能夠很好的切割垂直方向有粘連但是沒有粘連的字符,水滴法可以分割粘連字符。目前採用的CFS切割法。切割效果如下圖,對於非粘連字符,效果很不錯。CFS聯通域切割的實現算法主要用的是圖的廣度/深度遍歷,代碼如下:
# CFS圖像切割
@staticmethod
def cut_img(img, threshold, cut_width, cut_height, width_min, width_max, height_min):
charters_imgs = []
width, height = img.size
charters_pixels = []
visited_pixels = []
pixel_arr = ImgTools.get_pixel_arr(img)
for i in range(width):
for j in range(height):
pixel = img.getpixel((i, j))
if pixel == 0 and [i, j] not in visited_pixels:
charter_pixels = Node(i, j, pixel_arr, []).traversal()
visited_pixels.extend(charter_pixels)
if len(charter_pixels) > threshold:
charters_pixels.append(charter_pixels)
for i in range(len(charters_pixels)):
x_min = 0
y_min = 0
x_max = 0
y_max = 0
# 這裡是為了處理沒有粘連但是垂直方向有重合的字符
width, height = img.size
tmp_img = Image.new('1', (width * 2, height * 2), 255)
for j in range(len(charters_pixels[i])):
x, y = charters_pixels[i][j]
tmp_img.putpixel((x, y), 0)
if x > x_max:
x_max = x
else:
if x < x_min or x_min == 0:
x_min = x
if y > y_max:
y_max = y
else:
if y < y_min or y_min == 0:
y_min = y
if width_min < x_max - x_min < width_max and y_max - y_min > height_min:
# charters_imgs.append(tmp_img.crop((x_min, y_min, x_max, y_max)))
# 這裡是為了將所有的圖片切成一樣大,便於後期的特徵提取
charters_imgs.append(tmp_img.crop((x_min, y_min, x_min + cut_width, y_min + cut_height)))
return charters_imgs
class Node:
x = 0
y = 0
graph_arr = []
visited_neighbors = []
def __init__(self, x, y, graph_arr, visited_neighbors):
self.x = x
self.y = y
self.graph_arr = graph_arr
self.visited_neighbors = visited_neighbors
def traversal(self):
for i in range(-1, 2):
for j in range(-1, 2):
p = self.x + i
q = self.y + j
if (0 <= p < len(self.graph_arr)) and (0 <= q < len(self.graph_arr[0])):
if array(self.graph_arr)[p, q] == 0 and [p, q] not in self.visited_neighbors:
self.visited_neighbors.append([p, q])
next_node = Node(p, q, self.graph_arr, self.visited_neighbors)
next_node.traversal()
return self.visited_neighbors
效果如下:
提取 feature 並訓練特徵模型
✎ 提取 feature
每個字符用了40個樣本(每個字符都切成了60×60)進行打標籤,如果效果不好後續可以增加樣本量(由於M大多數粘連嚴重,所以切出來的M很少,沒有達到40個,直接導致後面M的識別結果也很不好)。
✎ 訓練模型
這裡採用了libsvm來訓練模型,從個樣本中預留了1/10個作為檢驗集,accuracy達到95%。
識別效果
先手動挑選了“乍一看”粘連不是很嚴重的30個樣本,進行訓練,結果如下,在80%左右。
總結和優化方向
1、目前整個識別流程已經走通,驗證碼識別服務也初具對外服務的能力;
2、雖然目前對於整體驗證碼的識別效果不是很好,但是,驗證碼服務拼的是識別率,比如說一個驗證碼需要識別,我在對其進行預處理和切割之後發現字符粘連效果不好,則完全可以拋棄,這並不影響識別率。換句話講我只是別切出來是四個字符的驗證碼即可(如果遇到一個網站每個圖片的粘連都比較嚴重,這條路就走不通了);
3、優化方向有兩個:
(1)優化切割字符的算法,目前的機器學習算法在圖片切割比較好的情況下識別率是非常高的,因此目前這類驗證碼的切割是整個過程中的難點,對於該案例可以採用波斯平滑後通過垂直投影圖找到極值點作為水滴法的起點是一個思路;
(2)增加樣本量,目前是40個識別率已經可以接受,如果增加訓練集的Size識別率應該會有所提升。
基於神經網絡的圖片驗證碼識別
上文提到的識別方式效果嚴重依賴於圖像切割的效果。對於一些粘連嚴重的驗證碼,需要花很大的精力來進行去噪和分割,即使這樣,效果也不一定會達到令人滿意的程度。
基於CNN來進行驗證碼識別的優勢在與整體識別,字符的粘連對於識別效果的影響不是特別大。
圖一
▐ 識別過程
以圖一驗證碼為例,在本次識別中,構建了一個4層(一開始是3層,效果並不好)隱層的深度神經網絡,網絡結構如下(示意圖中每一層的圖片看起來雖然沒有區別但是每一層的size是有區別的,重點看下標):
參數如下:
卷積核大小為5×5(zero padding),Pooling採用max pooling(block為2×2),激活函數為ReLU,學習率(LR)為0.001,在訓練集識別率達到99%的時候輸出模型。
訓練過程優化的過程包括:
- 一開始使用三層卷積網絡,效果並不理想,隨後調整成了四層卷積層,效果有所提升;
- 訓練集樣本量的提高;
- 學習率的降低;
使用Google的Tensorflow機器學習框架進行訓練。涉及到公司政策,代碼就不上傳了,網上可以搜到一些類似的。
▐ 識別效果
識別效果如下,左圖是學習樣本量為3500的時候識別率,右圖是樣本量為4500(人工打碼約8小時)的時候識別率:
CNN:訓練集樣本量為3500時
CNN:訓練集樣本量為4500時
對比一下用libsvm來識別的情況,左圖是單個字符樣本量為40張的時候識別情況,右圖為單個字符樣本量為100的時候的樣本量(識別結果為xxxx的表示切割字符失敗):
切割+SVM:每個字符樣本為40時
切割+SVM:每個字符樣本為100時
▐ 存在的問題
整個過程遇到的問題主要包括兩個方面:
- 中四層網絡的訓練明顯要比三層收斂的更慢,MBP只能用CPU跑,正常4層網絡要輸出一個可用的模型(訓練集識別率達到99%)需要1-2天;
- 樣本集對於學習至關重要,目前沒有較好的對樣本進行打標記的方式,只能人工打碼(人工智能之人工),4500的樣本量我打了4個晚上。並且人工打碼會有很多不確定因素,在本例中,後期發現很多7和T都打錯了,勢必會對最後的識別效果有所影響;
將來的優化方向包括:
- 增加訓練集;
- 調低LR學習率;
- 調低keep_prob的值;
- 增加捲積層;
▐ 總結
驗證碼識別哪怕模型的識別率只有20%也是可用的,識別錯了換一個就可以,但是在整個爬蟲和反爬的過程中,主動權往往掌握在反爬的這一邊,切換驗證碼的成本比破解一個類型的驗證碼的成本要低太多,更何況驗證碼只是眾多反爬手段之一,也正是如此爬蟲和反爬才會顯得格外的有意思。
和其他互聯網攻防技術一樣,這篇文章只是驗證碼識別技術探討,旨在為設計驗證碼防爬提供思路,並不鼓勵讀者帶著炫技或其他目的去破解驗證碼,瘋狂爬取別人的網站。任何技術本身都是中立的,be a reasonable crawler。
淘系技術天貓奢品團隊
我們是一支支撐天貓奢侈品、品牌客戶、淘寶心選等大店數據化經營解決方案的技術團隊,依託於阿里大中臺推動品牌經營解決方案升級,不斷提升客戶經營的效率,持續提升業務價值賦能業務。
如果您有興趣可講簡歷發至:[email protected],期待您的加入!
關注「淘系技術」微信公眾號,一個有溫度有內容的技術社區~