前文

近段時間 12306 訂票網站驗證碼升級為用戶識別圖像內容,然后選取符合條件的圖片為驗證碼,比如這樣:

http://ww1.sinaimg.cn/large/c334041bgw1eq8ijbaf75j209806xaaj.jpg

不少媒體新聞大呼搶票工具集體失效、12306終極驗證碼等新聞,這種驗證碼的推出有好同樣也有壞處:機器識別困難,同樣人眼識別也輕松不到哪里去。

用這種方式作為驗證碼最大的擔憂就是怕腳本或人工對其圖片進行爬蟲遍歷,然后將所有的圖片保存后與關鍵字進行對比并關聯入庫,當然前提是這些圖片都是靜態的。

12306 驗證碼究竟是靜態還是動態,昨晚對這個疑問進行了實踐:http://linux.im/2015/03/17/12306-captcha-md5-go.html ,簡單的說測試后發現這整張圖片是在服務器后端動態生成的,所以不難理解為什么生成驗證碼頁面時會比較慢。

同樣上午我們又進行了第二個實踐,將整張驗證碼中的八張圖像拆分為8張小圖然后進行感知hash處理,獲得樣本總數72225張,不重復的圖庫為15478張,重復最高為869次,繪制成圖如下:

http://ww1.sinaimg.cn/large/c334041bgw1eq8v4upbm6j217o0l0q5m.jpg

既然不是靜態的圖像(對比過近10w條圖像hash),那我們就不浪費功夫爬取靜態圖片進行數據關聯入庫了,但我們仍然需要“破”掉這個驗證碼,沒有什么理由。

最后,下文出現的所有的片段代碼將會開源,無需擔心。

關鍵字識別

驗證碼流程:

  • 驗證碼提問
  • 選擇答案(多選)

例如上面的驗證碼圖,他是一整張圖片,識別其關鍵字首先要對關鍵字區域進行圖像截取,隨后識別成文字。

這里使用 Python 的 PIL 圖像處理庫來進行區域的選擇:

def imgCut():
    pic_file = downloadImg()
    pic_path = "./12306_pic/%s.jpg" % pic_file
    pic_text_path = './12306_pic/%s_text.jpg' % pic_file
    pic_obj = Image.open(pic_path)
    box = (120,0,290,25)
    region = pic_obj.crop(box)
    region.save(pic_text_path)
    print '[*] Picture Text Picture: {}'.format(pic_text_path)
    return pic_path, pic_text_path

imgGut函數首先會下載這張驗證碼大圖(其中包括提示字、關鍵字、8張圖片等),然后保存至 ./12306_pic/ 目錄進行存儲,隨后使用 PIL 庫對圖像的 (120,0,290,25) 區域切割,也就是獲取關鍵字圖像區域。

http://ww3.sinaimg.cn/large/c334041bgw1eq8uap1ub5j20hp0cjq43.jpg

現在我們已經能夠將驗證碼下載并切割出想要的關鍵字區域了,下面我們要識別關鍵字,然后轉換為文本文字。

使用一些開源的光學字符識別模塊應該就能進行識別,但這不方便使用者運行,所以我選擇了一款在線網站OCR識別,他能夠對你上傳的圖像(我們剛剛切割好的圖像)進行文字識別轉換,當然準確率并沒有那么高,一定得記住這一點!

這里貼出部分代碼,功能實現(傳入圖像返回關鍵字的文本內容):

upload_pic_url = "http://cn.docs88.com/pdftowordupload2.php"
filename_tmp = filename.split('/')[-1]
pic_text_content = open(filename).read()
para = {'Filename': filename_tmp,
       'sourcename': filename_tmp,
       'sourcelanguage': 'cn',
       'desttype': 'txt',
       'Upload': 'Submit Query',}
upload_pic = requests.post(upload_pic_url, data=para, files={"Filedata" : open(filename, 'rb')})
text_result_url = 'http://cn.docs88.com/' + upload_pic.content[3:]
text_result = requests.get(text_result_url)
return text_result.content

我們運行試試效果:

[+] Download Picture: https://kyfw.12306.cn/otn/passcode...
[*] Picture Text Picture: ./12306_pic/1426580454_text.jpg
[*] Text: 襯 衫

[+] Download Picture: https://kyfw.12306.cn/otn/passcod...
[*] Picture Text Picture: ./12306_pic/1426580454_text.jpg
[*] Text: )帽子

[+] Download Picture: https://kyfw.12306.cn/otn/passcod...
[*] Picture Text Picture: ./12306_pic/1426580454_text.jpg
[*] Text: 春聯

效果還不錯,足夠我們測試使用,還記得他的準確率嗎?

巧妙的圖像識別

之前關于圖像識別我在 Buzz 發表過相關文章:使用CloudSight API進行圖像識別的Python腳本,這次我們不使用這個腳本,原因是雖然識別準確度較高但速度略慢,所以我并不是很鐘愛這一套,恰巧知乎上有位朋友寫了一篇利用百度識圖來進行圖像識別的文章及代碼,Google識圖當然也不錯,但剛好在這我們會用到,所以不必糾結。

  1. 分割驗證碼圖像
  2. 丟進百度識圖API函數
  3. 返回百度識圖結果

橫向兩行,每行四個,然后對其進行圖像識別并返回:

dict_list = {}
count = 0
for y in range(2):
    for x in range(4):
        count += 1
        im2 = get_sub_img(pic_path, x, y)
        result = baidu_stu_lookup(im2)
        dict_list[count] = result
        print (y,x), result

其中函數因文章長度原因暫不在這貼出,識別效果如下:

(0, 0) 冰雕|建筑夜景
(0, 1) 炸暑條|快餐
(0, 2) 燈塔|高塔
(0, 3) 漢堡|麥當勞薯條|開店
(1, 0) 運動外套|防護服|運動服
(1, 1) 銀灰色|手機|移動版
(1, 2) 標書制作|規劃
(1, 3) 手機

好,現在我們能夠識別出關鍵字,也能識別出驗證碼8個圖像了,我們還需要機器幫助我們確認,究竟選擇哪幾個圖。

可能是它

前面兩次提到使用的 OCR 在線識別準確度并沒有那么高,所以為了方便程序能夠聰明的幫我們思考這道選擇題,我們進行結果偽分詞對比。

首先將關鍵字進行拆分,然后循環對比結果,這樣就能將未準確識別的文字忽略并識別相應識圖結果,這里我將8個圖像結果按照1-8區分,第一行從左到右(1-4),第二行(5-8):

if captcha_text.strip() > 2:
    print '\n[*] Maybe the result of the:'
    maybe_result = []
    for v in dict_list:
        for c in range(len(unicode(captcha_text.strip(), 'utf8'))):
            text = unicode(captcha_text, 'utf8')[c]
            if text in dict_list[v]:
                _str_res = '%s --- %s' % (v, dict_list[v])
                maybe_result.append(_str_res)
    for r in list(set(maybe_result)):
        print r
else:
    print '[-] False'

好了,這樣一來就算識別率沒有那么高我們也能盡可能的將答案尋找出來了,看下效果:

http://ww1.sinaimg.cn/large/c334041bgw1eq8ux0jpkoj20yz0qrn3f.jpg

http://ww1.sinaimg.cn/large/c334041bgw1eq8uxfmz78j20p90igdj7.jpg

未結束

結束了嗎? 其實沒有。

我們使用腳本進行了大量的測試,成功率可喜的足夠令一些邪惡的人做些事兒了,但驗證碼對抗一直在進行,當然也越來越有趣:)

文中完整代碼鏈接:https://gist.github.com/Evi1m0/fbbdb1ba7c66cc4e1bb2