(Python)爬蟲解析特殊驗證碼?

昆明理工大學
我校的官網,不過這個驗證碼有點難搞。用pytesser庫不能識別,這個庫只能識別比較簡單的驗證碼。目前除了手動輸入驗證碼之外,沒有其他方法了嗎?


(python3.4)

本回答針對該驗證碼的識別,主要分以下五部分:

  1. 驗證碼分析
  2. 下載驗證碼樣本
  3. 生成初始字元圖片樣本
  4. 識別驗證碼
  5. 測試與總結

涉及的腳本及文件(腳本按照出現的先後順序排列)

.../captcha_char

.../captcha_example

.../download_captcha_0.py

.../download_captcha_1.py

.../pretreat_image.py

.../cut_all_char.py

.../new_char_example.py

.../load_char_example.py

.../distinguish_captcha.py

驗證碼分析

驗證碼直接地址:http://kmustjwcxk3.kmust.edu.cn/jwweb/sys/ValidateCode.aspx

驗證碼間接地址:昆明理工教務系統3

下載驗證碼觀察:

# download_captcha_0.py
# python3.4
from urllib import request
host_url = "http://kmustjwcxk3.kmust.edu.cn/jwweb/"
captcha_url = host_url+"sys/ValidateCode.aspx"
for k in range(10):
request.urlretrieve(captcha_url,"%d.jpg"%k)

0.jpg

1.jpg

2.jpg

觀察結論:

  1. 字元為數字+大寫英文字母.後面發現沒有數字0,1,字母I,L,O.應該是考慮到人也不易區分這幾個字元.字元有兩種字體,且都是斜體字.
  2. 有五條隨機的直線,上方有噪點,有一外框.干擾很小.
  3. 一般字體的變形有兩類:線性的,非線性的.線性的又可分為:平移,傾斜,旋轉,伸縮.很少有對稱的.這裡只有垂直方向的平移,較易處理.
  4. 總之,這是一個很弱的驗證碼.

(以下涉及兩個文件夾captcha_example,captcha_char.放置在python腳本所在路徑下.)

下載驗證碼樣本

# download_captcha_1.py
# 手動建立文件夾:captcha_example
from urllib import request
# 該站點速度更快
host_url = "http://jwmis.hhtc.edu.cn/"
captcha_url = host_url+"sys/ValidateCode.aspx"
for k in range(300):
request.urlretrieve(captcha_url,"captcha_example/%d.jpg"%k)

生成初始字元圖片樣本

分3步進行:

  1. 驗證碼預處理,主要是降噪
  2. 分割圖片,獲取單個字元的圖片
  3. 生成初始字元圖片樣本

驗證碼預處理,主要是降噪

預處理前(原始驗證碼圖片,jpg格式):

預處理後(紅色邊框內部的是結果,png格式):

原圖片的格式為JFIF(jpg).為了不失真,處理後的圖片一律保存為png格式

降噪方法參考自:完整的圖片去噪代碼(python2)

# pretreat_image.py
# 注意:二值圖像0/1統一轉化為0/255
from PIL import Image,ImageDraw,ImageChops
import os

# 驗證碼預處理,主要是降噪
# 預處理結束後返回0/255二值圖像
# 降噪,參考 http://blog.csdn.net/xinghun_4/article/details/47864949
def pretreat_image(image):
# 將圖片轉換成灰度圖片
image = image.convert("L")

# 二值化,得到0/255二值圖片
# 閥值threshold = 180
image = iamge2imbw(image,180)

# 對二值圖片進行降噪
# N = 4
clear_noise(image,4)

# 去除外邊框
# 原圖大小:122*54
# 左上右下,左 &<= x &< 右 box = ( 8, 10, 118, 50 ) image = image.crop(box) return image # 灰度圖像二值化,返回0/255二值圖像 def iamge2imbw(image,threshold): # 設置二值化閥值 table = [] for i in range(256): if i &< threshold: table.append(0) else: table.append(1) # 像素值變為0,1 image = image.point(table,"1") # 像素值變為0,255 image = image.convert("L") return image # 根據一個點A的灰度值(0/255值),與周圍的8個點的值比較 # 降噪率N: N=1,2,3,4,5,6,7 # 當A的值與周圍8個點的相等數小於N時,此點為噪點 # 如果確認是雜訊,用該點的上面一個點的值進行替換 def get_near_pixel(image,x,y,N): pix = image.getpixel((x,y)) near_dots = 0 if pix == image.getpixel((x - 1,y - 1)): near_dots += 1 if pix == image.getpixel((x - 1,y)): near_dots += 1 if pix == image.getpixel((x - 1,y + 1)): near_dots += 1 if pix == image.getpixel((x,y - 1)): near_dots += 1 if pix == image.getpixel((x,y + 1)): near_dots += 1 if pix == image.getpixel((x + 1,y - 1)): near_dots += 1 if pix == image.getpixel((x + 1,y)): near_dots += 1 if pix == image.getpixel((x + 1,y + 1)): near_dots += 1 if near_dots &< N: # 確定是雜訊,用上面一個點的值代替 return image.getpixel((x,y-1)) else: return None # 降噪處理 def clear_noise(image,N): draw = ImageDraw.Draw(image) # 外面一圈變白色 Width,Height=image.size for x in range(Width): draw.point((x,0),255) draw.point((x,Height-1),255) for y in range(Height): draw.point((0,y),255) draw.point((Width-1,y),255) # 內部降噪 for x in range(1,Width - 1): for y in range(1,Height - 1): color = get_near_pixel(image,x,y,N) if color != None: draw.point((x,y),color) if __name__ == "__main__": image = Image.open("captcha_example/0.jpg") image = pretreat_image(image) image.show()

分割圖片,獲取單個字元的圖片

預處理前:

預處理後(紅色邊框內部的是結果):

分割後(紅色邊框內部的是結果):

# cut_all_char.py
# 注意:二值圖像0/1統一轉化為0/255
from PIL import Image,ImageDraw,ImageChops
import os
from pretreat_image import *

# 分割圖片,獲取單個字元的圖片
# 二值圖片的分割
def cut_one_char(image):
# 再次降噪
# N = 4
clear_noise(image,4)

CharWidth=25
CharHeight=26
Width,Height=image.size
# 找出image上出現黑點的第一列
x = find_first_column(image)

# 第一行
# 左上右下,左 &<= x &< 右 box = (x,0,x+CharWidth,Height) image2 = crop_white(image,box) y = find_first_row(image2) # 切割出一個字元 box = (x,y,x+CharWidth,y+CharHeight) image_char = crop_white(image,box) # 剩下的圖片 if x+CharWidth &> Width:
image_residue = None
else:
box = (x+CharWidth,0,Width,Height)
image_residue = crop_white(image,box)
return [image_char,image_residue]

# 沒有字元W的情況下,切割的都比較好.
# 出現W的概率為1-(1-1/36)^4≈10.66%
# 這樣一來準確率無法超過90%
# 這裡處理4個字元的情況
def cut_all_char(image):
image_char1,image = cut_one_char(image)
image_char2,image = cut_one_char(image)
image_char3,image = cut_one_char(image)
image_char4,image = cut_one_char(image)
return [image_char1,image_char2,image_char3,image_char4]

# 如果box超出原圖範圍,默認會以黑色填充
# 因此為了讓圖片超出部分以白色填充,進行反色處理,最後再反色回來
def crop_white(image,box):
# 255 - old
image = ImageChops.invert(image)
image = image.crop(box)
return ImageChops.invert(image)

# 找出image上出現黑點的第一列
def find_first_column(image):
Width,Height=image.size
for x in range(Width):
for y in range(Height):
if image.getpixel( (x,y) ) == 0:
return x
# 如果沒有黑點,返回第一列
return 0

# 找出image上出現黑點的第一行
def find_first_row(image):
Width,Height=image.size
for y in range(Height):
for x in range(Width):
if image.getpixel( (x,y) ) == 0:
return y
# 如果沒有黑點,返回第一行
return 0

if __name__ == "__main__":
image = Image.open("captcha_example/0.jpg")
image = pretreat_image(image)
image_char_list = cut_all_char(image)
image_char_list[0].show()

生成初始字元圖片樣本

過程示意圖1:

在此發現字元0,1,I,L,O並不出現.

過程示意圖2:

# new_char_example.py
from PIL import Image,ImageDraw,ImageChops
import os
from pretreat_image import *
from cut_all_char import *

# 生成存放樣本的文件夾
def new_char_folder():
# 0-9
for k in range(48,58):
try:
os.mkdir( "captcha_char/%c" % k )
except:
pass

# A-Z
for k in range(65,91):
try:
os.mkdir( "captcha_char/%c" % k )
except:
pass

# 生成樣本字元,然後手動將對應字元移動到上面生成的文件夾中
# 每個文件夾手動移動10個以上
def new_char_example():
new_char_folder()
for s in range(300):
image = pretreat_image( "captcha_example/%d.jpg" % s )
image.save( "captcha_char/%d.png" % s )
image_char_list = cut_all_char(image)
for k in range(4):
image_char_list[k].save( "captcha_char/%d_%d.png" % (s,k) )

if __name__ == "__main__":
pass

識別驗證碼

分4步進行(其中前兩步與前面生成初始字元圖片樣本的步驟一樣):

  1. 驗證碼預處理,主要是降噪
  2. 分割圖片,獲取單個字元的圖片
  3. 載入字元圖片樣本
  4. 分割的字元圖片與字元圖片樣本進行比對

載入字元圖片樣本

# load_char_example.py
# 注意:二值圖像0/1統一轉化為0/255
from PIL import Image,ImageDraw,ImageChops
import os

# 遍歷指定目錄,顯示目錄下的所有文件名
def eachfile(filepath):
dir_list = os.listdir(filepath)
all_dir = []
for dir in dir_list:
child = "%s%s" % (filepath, dir)
all_dir.append(child)
return all_dir

# 共31個字元
char_set = [
"2","3","4","5","6","7","8","9",
"A","B","C","D","E","F","G",
"H", "J","K", "M","N",
"P","Q","R","S","T",
"U","V","W","X","Y","Z"
]

# 載入字元圖片樣本
# 將對應字元的樣本按照上述 char_set 的順序載入
def load_char_example():
global char_set

char_example = []
for char in char_set:
char_example.append([])
folder_path = "captcha_char/%c/"%char
image_name_list = eachfile(folder_path)
for image_name in image_name_list:
image = Image.open(image_name)
# 注意這裡讀取的數據為灰度圖像,像素值為0,255
# 為此將其他地方出現的0/1二值圖像統一處理成0/255二值圖像
char_example[-1].append(image)
return char_example

if __name__ == "__main__":
load_char_example()

分割的字元圖片與字元圖片樣本進行比對

# distinguish_captcha.py
# 注意:二值圖像0/1統一轉化為0/255
# 涉及文件夾:captcha_example,captcha_char
from PIL import Image,ImageDraw,ImageChops
import os
from pretreat_image import *
from cut_all_char import *
from load_char_example import *

# 比較兩個0/255二值圖像
# 計算相似度,公式:相似度 = 相等的像素點數 / 總像素點數
def compare2imbw(imbw1,imbw2):
# out = abs(img1, img2),相同的點變為0,不同的變為255
image = ImageChops.difference(imbw1,imbw2)

# 統計相同的點的個數
# 直方圖統計,返回長度為256的list
a = image.histogram()
same_pixel = a[0]

Width,Height=imbw1.size
all_pixel = Width*Height
return same_pixel/all_pixel

# 與樣本比較,設置一個評分體系
# 分別取最大的五個相似度相加,再取最大的對應的字元
def distinguish_one_char(char_example,image_char):
global char_set
score_set=[]
for image_list in char_example:
score_set.append([])
for image in image_list:
score_set[-1].append( compare2imbw(image,image_char) )

# 對於score_set[k],取分值最高的5個相加
char_num=len(char_set)
for k in range(char_num):
# 從小到大排序
score_set[k].sort()
# 逆序
score_set[k].reverse()

# 調試列印節點1
# print(char_set[k],score_set[k][0:1])

# 取前5
score_set[k] = score_set[k][0:5]
# 前5相加,保存在score_set[k]中
score_set[k] = sum(score_set[k])

# 獲得相似度最大的字元,並返回
# index 返回找到的第一個值的位置
a = score_set.index( max(score_set) )

# 調試列印節點2
# print(char_set[a])
return char_set[a]

def distinguish_all_char(char_example,image_char_list):
s = ""
for image_char in image_char_list:
s += distinguish_one_char(char_example,image_char)
return s

def distinguish_captcha(image):
# 預處理圖片,返回0/255二值圖像
image = pretreat_image(image)

# 切割二值圖像
image_char_list = cut_all_char(image)

# 載入樣本數據
char_example = load_char_example()

# 比對識別各個字元
result = distinguish_all_char(char_example,image_char_list)
return result

if __name__ == "__main__":
image = Image.open("captcha_example/0.jpg")
s = distinguish_captcha(image)
print(s)

測試與總結

  1. 凡是 W 後面一個字元的識別基本上出錯,這是由於 W 較寬,導致 W 與其右邊一個字元出現粘合,從而切割時出錯.如果 W 只出現在最後一個位置,錯誤率不高.因為切割從左向右進行, W 在最右邊時,不會影響到其他字元.
  2. 計算理想的準確率,即 W 不出現在前面三個位置的概率.沒有 W 的概率 (frac{30}{31})^4 , W 只在最後一個位置出現的概率(frac{30}{31})^3	imes(frac{1}{31}) .理想的準確率為上述兩者相加:(frac{30}{31})^3approx90.63\% .
  3. 如有必要,需改進分割字元.
  4. 進行3次網路測試,每次為100個驗證碼.正確率為:87%,87%,88%.由此,正確率穩定在87%左右,與90.63%相差的部分應該是由其他誤差導致的.

第一次在知乎長文回答,發現代碼高亮和 tex 都挺好用的.

20170211


我也是手動輸入,咱們的教育網系統差不多


推薦閱讀:

文科女,已在職,零基礎想學編程,求解?
自己寫的爬蟲程序運行停止,下次運行如何不重複爬取?
如何用scrapy提取不在標籤內的文字?
tcp 編程中,connect 連接成功的標準是什麼?
tcp連接的問題?

TAG:Python | 編程 | 爬蟲計算機網路 |