前兩天首頁上有篇文章,講用C#生成驗證碼.今天又看到一篇文章,講用python識別驗證碼.於是我就寫了這篇文章,講用Canvas識別驗證碼
我們今天要識別的是那種最最簡單的驗證碼,只有隨機顏色和隨機背景,而沒有隨機變形,隨機噪點.
為了方便試驗,我從谷歌中隨便搜了一個使用了這種驗證碼的網站,這種驗證碼的確很常見 (點擊可更換).
分析
首先,我們需要分析驗證碼的生成規律,多次刷新頁面就能看出,該驗證碼由四個數字組成,且每個數字除了顏色隨機改變,形狀和位置是固定不變的.
打開Photoshop,把網頁中的驗證碼圖片拖進來,然后再拖出幾根參考線,讓每兩根參考線包圍一個數字,一開始肯定有偏差,再次刷新頁面,按住shift鍵把驗證碼圖片拖到photoshop中,微調參考線的位置,經過多次操作,最終會成為下面這樣
上圖中,從橫向上來看,驗證碼圖片中的第一個數字是從7px開始的,然后每個數字占了9px,中間間隔是4px.於是我們可以得出一個公式,x=13*i+7,i表示數字的索引[0,1,2,3],x表示橫坐標
縱向上比較簡單,y坐標的范圍是恆定的,3px到16px.
圖像處理
首先,我並沒有專業的圖像處理知識,所以下面說的專業詞匯肯定是有問題的,以理解為主.由於數字和背景都是隨機顏色的,那么我們生成的模板字符串豈不是每次都會變嗎.的確是這樣的,由於canvas在獲取某個像素點的像素值時,返回的是rgba值.也就是一共有四個值.我們需要使用一個公式,把rgba顏色轉換成灰度值:gray = r*0.3 + g*0.59 + b*0.11,灰度值的范圍是0~255,我這里把128看成臨界點,也就是把0~128看成是暗,用0表示,128~255看成是明,用1表示,我把明暗簡寫為ld(Light and Dark).也就是公式,ld = gray>128?1:0.為什么通過明暗值能把數字和背景色區分開來呢,因為這種驗證碼在進行灰度化以后,背景明顯是屬於亮的,偏白色,而數字是屬於暗的,偏黑色.所以能夠區分.
下面是在photoshop中演示的過程.
原圖,RGB顏色:
灰度化:
二值化,在photoshop里叫位圖顏色模式.有多種轉換的算法.我們用的應該是50%閾值
轉換后就是
生成模板
既然每個數字的形狀和位置都是一定的,那我們就能把0-9這10個數字的像素信息存儲下來作為模板,在識別驗證碼時,取出驗證碼圖片中的數字依次對比.如果相等說明就是這個數字.下面是我寫的生成模板的代碼.
var image = document.querySelector(".greyfont2 img"); //獲取到驗證碼圖片 var canvas = document.createElement('canvas'); //新建一個canvas var ctx = canvas.getContext("2d"); //獲取2D上下文 var numbers = []; //存儲數字模板的數組 canvas.width = image.width; //設置canvas的寬度 canvas.height = image.height; //設置canvas的高度 document.body.appendChild(canvas); //將canvas添加進文檔 ctx.drawImage(image, 0, 0); //將驗證碼繪制到canvas上 for (var i = 0; i < 4; i++) { //循環四次,識別四個數字 var pixels = ctx.getImageData(13 * i + 7, 3, 9, 16).data; //按照公式獲取到每個數字上的像素點 var ldString = ""; //用來存儲明暗值的字符串 for (var j = 0, length = pixels.length; j < length; j += 4) { //每次循環取四個值,分別是一個像素點的r,g,b,a值 ldString = ldString + (+(pixels[j] * 0.3 + pixels[j + 1] * 0.59 + pixels[j + 2] * 0.11 >= 128)); //灰度化+二值化,但我們並沒有真正的處理圖像 } console.log(ldString); //輸出存儲着明暗值的字符串 }
在控制台中執行上面的腳本.
我們就知道了,5,9,0的模板字符串各是什么,然后刷新頁面,繼續執行腳本,一直到到收集齊0-9的模板字符串.把這些模版字符串組成數組賦值給numbers變量.
然后接着寫識別程序.識別程序和輸出模板程序差別不大,就多了一個對比的過程.
識別驗證碼
控制台中執行下面的代碼,就會自動填寫好驗證碼.
var image = document.querySelector(".greyfont2 img"); //如果要用在greasemonkey腳本里,可以把下面的代碼放在image的onload事件里 var canvas = document.createElement('canvas'); var ctx = canvas.getContext("2d"); var numbers = [ //模板,依次是0-9十個數字對應的明暗值字符串 "111000111100000001100111001001111100001111100001111100001111100001111100001111100001111100100111001100000001111000111111111111111111111111111111", "111000111100000111100000111111100111111100111111100111111100111111100111111100111111100111111100111100000000100000000111111111111111111111111111", "100000111000000011011111001111111001111111001111110011111100111111001111110011111100111111001111111000000001000000001111111111111111111111111111", "100000111000000001011111001111111001111110011100000111100000011111110001111111001111111001011110001000000011100000111111111111111111111111111111", "111110011111100011111100011111000011110010011110010011100110011100110011000000000000000000111110011111110011111110011111111111111111111111111111", "000000001000000001001111111001111111001111111000001111000000011111110001111111001111111001011110001000000011100000111111111111111111111111111111", "111000011110000001100111101100111111001111111001000011000000001000111000001111100001111100100111000100000001111000011111111111111111111111111111", "100000000100000000111111100111111101111111001111110011111110111111100111111101111111001111111001111110011111110011111111111111111111111111111111", "110000011100000001100111001100111001100011011110000011110000011100110001001111100001111100000111000100000001110000011111111111111111111111111111", "110000111100000001000111001001111100001111100000111000100000000110000100111111100111111001101111001100000011110000111111111111111111111111111111"]; var captcha = ""; //存放識別后的驗證碼 canvas.width = image.width; canvas.height = image.height; document.body.appendChild(canvas); ctx.drawImage(image, 0, 0); for (var i = 0; i < 4; i++) { var pixels = ctx.getImageData(13 * i + 7, 3, 9, 16).data; var ldString = ""; for (var j = 0,length = pixels.length; j < length; j += 4) { ldString = ldString + (+(pixels[j] * 0.3 + pixels[j + 1] * 0.59 + pixels[j + 2] * 0.11 >= 140)); } var comms = numbers.map(function (value) { //為了100%識別率,這里不能直接判斷是否和模板字符串相等,因為可能有個別0被計算成1,或者相反 return ldString.split("").filter(function (v, index) { return value[index] === v }).length }); captcha += comms.indexOf(Math.max.apply(null, comms)); //添加到識別好的驗證碼中 } document.querySelector("input[name=validateCode]").value = captcha; //寫入目標文本框
總結
你可以看到,這種級別的驗證碼,用js識別才用了幾行代碼.如果是更強勁的perl,python寫的話,更沒什么困難.所以驗證碼除了在邏輯上要防止繞過以外,本身還要有一定的復雜程度.否則一點意義也么有,只有浪費用戶的時間.