(Python)爬蟲解析特殊驗證碼?
昆明理工大學
我校的官網,不過這個驗證碼有點難搞。用pytesser庫不能識別,這個庫只能識別比較簡單的驗證碼。目前除了手動輸入驗證碼之外,沒有其他方法了嗎?
(python3.4)
本回答針對該驗證碼的識別,主要分以下五部分:
- 驗證碼分析
- 下載驗證碼樣本
- 生成初始字元圖片樣本
- 識別驗證碼
- 測試與總結
涉及的腳本及文件(腳本按照出現的先後順序排列)
.../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
觀察結論:
- 字元為數字+大寫英文字母.後面發現沒有數字0,1,字母I,L,O.應該是考慮到人也不易區分這幾個字元.字元有兩種字體,且都是斜體字.
- 有五條隨機的直線,上方有噪點,有一外框.干擾很小.
- 一般字體的變形有兩類:線性的,非線性的.線性的又可分為:平移,傾斜,旋轉,伸縮.很少有對稱的.這裡只有垂直方向的平移,較易處理.
- 總之,這是一個很弱的驗證碼.
(以下涉及兩個文件夾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步進行:
- 驗證碼預處理,主要是降噪
- 分割圖片,獲取單個字元的圖片
- 生成初始字元圖片樣本
驗證碼預處理,主要是降噪
預處理前(原始驗證碼圖片,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步進行(其中前兩步與前面生成初始字元圖片樣本的步驟一樣):
- 驗證碼預處理,主要是降噪
- 分割圖片,獲取單個字元的圖片
- 載入字元圖片樣本
- 分割的字元圖片與字元圖片樣本進行比對
載入字元圖片樣本
# 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)
測試與總結
- 凡是 W 後面一個字元的識別基本上出錯,這是由於 W 較寬,導致 W 與其右邊一個字元出現粘合,從而切割時出錯.如果 W 只出現在最後一個位置,錯誤率不高.因為切割從左向右進行, W 在最右邊時,不會影響到其他字元.
- 計算理想的準確率,即 W 不出現在前面三個位置的概率.沒有 W 的概率 , W 只在最後一個位置出現的概率 .理想的準確率為上述兩者相加: .
- 如有必要,需改進分割字元.
- 進行3次網路測試,每次為100個驗證碼.正確率為:87%,87%,88%.由此,正確率穩定在87%左右,與90.63%相差的部分應該是由其他誤差導致的.
第一次在知乎長文回答,發現代碼高亮和 tex 都挺好用的.
20170211
我也是手動輸入,咱們的教育網系統差不多
推薦閱讀:
※文科女,已在職,零基礎想學編程,求解?
※自己寫的爬蟲程序運行停止,下次運行如何不重複爬取?
※如何用scrapy提取不在標籤內的文字?
※tcp 編程中,connect 連接成功的標準是什麼?
※tcp連接的問題?