前文

近段時(shí)間 12306 訂票網(wǎng)站驗(yàn)證碼升級為用戶識別圖像內(nèi)容,然后選取符合條件的圖片為驗(yàn)證碼,比如這樣:

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

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

用這種方式作為驗(yàn)證碼最大的擔(dān)憂就是怕腳本或人工對其圖片進(jìn)行爬蟲遍歷,然后將所有的圖片保存后與關(guān)鍵字進(jìn)行對比并關(guān)聯(lián)入庫,當(dāng)然前提是這些圖片都是靜態(tài)的。

12306 驗(yàn)證碼究竟是靜態(tài)還是動(dòng)態(tài),昨晚對這個(gè)疑問進(jìn)行了實(shí)踐:http://linux.im/2015/03/17/12306-captcha-md5-go.html ,簡單的說測試后發(fā)現(xiàn)這整張圖片是在服務(wù)器后端動(dòng)態(tài)生成的,所以不難理解為什么生成驗(yàn)證碼頁面時(shí)會(huì)比較慢。

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

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

既然不是靜態(tài)的圖像(對比過近10w條圖像hash),那我們就不浪費(fèi)功夫爬取靜態(tài)圖片進(jìn)行數(shù)據(jù)關(guān)聯(lián)入庫了,但我們?nèi)匀恍枰?#8220;破”掉這個(gè)驗(yàn)證碼,沒有什么理由。

最后,下文出現(xiàn)的所有的片段代碼將會(huì)開源,無需擔(dān)心。

關(guān)鍵字識別

驗(yàn)證碼流程:

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

例如上面的驗(yàn)證碼圖,他是一整張圖片,識別其關(guān)鍵字首先要對關(guān)鍵字區(qū)域進(jìn)行圖像截取,隨后識別成文字。

這里使用 Python 的 PIL 圖像處理庫來進(jìn)行區(qū)域的選擇:

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函數(shù)首先會(huì)下載這張驗(yàn)證碼大圖(其中包括提示字、關(guān)鍵字、8張圖片等),然后保存至 ./12306_pic/ 目錄進(jìn)行存儲(chǔ),隨后使用 PIL 庫對圖像的 (120,0,290,25) 區(qū)域切割,也就是獲取關(guān)鍵字圖像區(qū)域。

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

現(xiàn)在我們已經(jīng)能夠?qū)Ⅱ?yàn)證碼下載并切割出想要的關(guān)鍵字區(qū)域了,下面我們要識別關(guān)鍵字,然后轉(zhuǎn)換為文本文字。

使用一些開源的光學(xué)字符識別模塊應(yīng)該就能進(jìn)行識別,但這不方便使用者運(yùn)行,所以我選擇了一款在線網(wǎng)站OCR識別,他能夠?qū)δ闵蟼鞯膱D像(我們剛剛切割好的圖像)進(jìn)行文字識別轉(zhuǎn)換,當(dāng)然準(zhǔn)確率并沒有那么高,一定得記住這一點(diǎn)!

這里貼出部分代碼,功能實(shí)現(xiàn)(傳入圖像返回關(guān)鍵字的文本內(nèi)容):

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

我們運(yùn)行試試效果:

[+] 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: 春聯(lián)

效果還不錯(cuò),足夠我們測試使用,還記得他的準(zhǔn)確率嗎?

巧妙的圖像識別

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

  1. 分割驗(yàn)證碼圖像
  2. 丟進(jìn)百度識圖API函數(shù)
  3. 返回百度識圖結(jié)果

橫向兩行,每行四個(gè),然后對其進(jìn)行圖像識別并返回:

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

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

(0, 0) 冰雕|建筑夜景
(0, 1) 炸暑條|快餐
(0, 2) 燈塔|高塔
(0, 3) 漢堡|麥當(dāng)勞薯?xiàng)l|開店
(1, 0) 運(yùn)動(dòng)外套|防護(hù)服|運(yùn)動(dòng)服
(1, 1) 銀灰色|手機(jī)|移動(dòng)版
(1, 2) 標(biāo)書制作|規(guī)劃
(1, 3) 手機(jī)

好,現(xiàn)在我們能夠識別出關(guān)鍵字,也能識別出驗(yàn)證碼8個(gè)圖像了,我們還需要機(jī)器幫助我們確認(rèn),究竟選擇哪幾個(gè)圖。

可能是它

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

首先將關(guān)鍵字進(jìn)行拆分,然后循環(huán)對比結(jié)果,這樣就能將未準(zhǔn)確識別的文字忽略并識別相應(yīng)識圖結(jié)果,這里我將8個(gè)圖像結(jié)果按照1-8區(qū)分,第一行從左到右(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

未結(jié)束

結(jié)束了嗎? 其實(shí)沒有。

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

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