由於項目須要,須要屢次登陸某網站抓取信息。因此學習了驗證碼的一些小知識。文章參考http://blog.csdn.net/problc/article/details/5794460的部份內容。php
須要程序識別的驗證碼格式如圖所示:,這個圖片符合固定大小,固定位置,固定字體,固定顏色的範圍,實現起來相對簡單。java
驗證碼識別基本分四步,圖片預處理,分割,訓練,識別。爲便於演示,我這裏分更多的步驟。git
BTW:apache
若是是形如:的驗證碼,請參考:http://blog.csdn.net/problc/article/details/5797507app
若是是形如:的驗證碼,請參考:http://blog.csdn.net/problc/article/details/5800093ide
若是是形如:的驗證碼,請參考:http://blog.csdn.net/problc/article/details/5846614工具
更多驗證碼相關內容,請參考:http://blog.csdn.net/problc/article/details/5983276post
目錄結構:download目錄用於存放下載的驗證碼;train用於存放供比對的標準圖片;result用於存放比對結果。學習
包:HttpClient4.2(用於抓取圖片)測試
// 1.下載驗證碼:將多個驗證碼圖片下載到指定目錄,要求各類可能的驗證碼(單個數字)都應該有,好比:0-9。 private void downloadImage() throws Exception { HttpClient httpClient = new DefaultHttpClient(); for (int i = 0; i < 10; i++) { String url = "http://www.yoursite.com/yz.php"; HttpGet getMethod = new HttpGet(url); try { HttpResponse response = httpClient.execute(getMethod, new BasicHttpContext()); HttpEntity entity = response.getEntity(); InputStream instream = entity.getContent(); OutputStream outstream = new FileOutputStream(new File(DOWNLOAD_DIR, i + ".png")); int l = -1; byte[] tmp = new byte[2048]; while ((l = instream.read(tmp)) != -1) { outstream.write(tmp); } outstream.close(); } finally { getMethod.releaseConnection(); } } System.out.println("下載驗證碼完畢!"); }
下載後download目錄內容:
// 2.去除圖像干擾像素(非必須操做,只是能夠提升精度而已)。 public static BufferedImage removeInterference(BufferedImage image) throws Exception { int width = image.getWidth(); int height = image.getHeight(); for (int x = 0; x < width; ++x) { for (int y = 0; y < height; ++y) { if (isFontColor(image.getRGB(x, y))) { // 若是當前像素是字體色,則檢查周邊是否都爲白色,如都是則刪除本像素。 int roundWhiteCount = 0; if(isWhiteColor(image, x+1, y+1)) roundWhiteCount++; if(isWhiteColor(image, x+1, y-1)) roundWhiteCount++; if(isWhiteColor(image, x-1, y+1)) roundWhiteCount++; if(isWhiteColor(image, x-1, y-1)) roundWhiteCount++; if(roundWhiteCount == 4) { image.setRGB(x, y, Color.WHITE.getRGB()); } } } } return image; } // 取得指定位置的顏色是否爲白色,若是超出邊界,返回true // 本方法是從removeInterference方法中摘取出來的。單獨調用本方法無心義。 private static boolean isWhiteColor(BufferedImage image, int x, int y) throws Exception { if(x < 0 || y < 0) return true; if(x >= image.getWidth() || y >= image.getHeight()) return true; Color color = new Color(image.getRGB(x, y)); return color.equals(Color.WHITE)?true:false; }
剛下載的圖片:;通過去除圖像干擾像素的操做後:。
打開PhotoShop,對圖片進行編輯,用選擇工具(M)選擇一個數字,在信息欄中就看到當前字的寬度、高度。各數字的x、y座標值一樣能夠此方法獲取到。
對應代碼:
// 3.判斷拆分驗證碼的標準:就是定義驗證碼中包含的各數字的x、y座標值,及它們的寬度(width)、高度(height)。 private static List<BufferedImage> splitImage(BufferedImage image) throws Exception { final int DIGIT_WIDTH = 19; final int DIGIT_HEIGHT = 17; List<BufferedImage> digitImageList = new ArrayList<BufferedImage>(); digitImageList.add(image.getSubimage(2, 2, DIGIT_WIDTH, DIGIT_HEIGHT)); digitImageList.add(image.getSubimage(20, 2, DIGIT_WIDTH, DIGIT_HEIGHT)); digitImageList.add(image.getSubimage(40, 2, DIGIT_WIDTH, DIGIT_HEIGHT)); digitImageList.add(image.getSubimage(60, 2, DIGIT_WIDTH, DIGIT_HEIGHT)); return digitImageList; }
3.4 判斷字體的顏色含義:正常能夠用rgb三種顏色加起來表示,字與非字應該有顯示的區別,找出來。
一樣經過PhotoShop,用吸管工具(I)選擇有顏色的部分,在信息欄中能夠看到當前的RGB值,由於是純色,記錄三值相加結果便可。我這裏R+G+B是340。
對應代碼(若是不是純色,能夠用大於、小於某一範圍之類的判斷,而不是用等於):
// 4.判斷字體的顏色含義:正常能夠用rgb三種顏色加起來表示,字與非字應該有顯示的區別,找出來。 private static boolean isFontColor(int colorInt) { Color color = new Color(colorInt); return color.getRed() + color.getGreen() + color.getBlue() == 340; }
3.5 將下載的驗證碼圖片所有拆分到另外一個目錄。
// 5.將下載的驗證碼圖片所有拆分到另外一個目錄。 public void generateStdDigitImgage() throws Exception { File dir = new File(DOWNLOAD_DIR); File[] files = dir.listFiles(new ImageFileFilter("png")); int counter = 0; for (File file : files) { BufferedImage image = ImageIO.read(file); removeInterference(image); List<BufferedImage> digitImageList = splitImage(image); for (int i = 0; i < digitImageList.size(); i++) { BufferedImage bi = digitImageList.get(i); ImageIO.write(bi, "PNG", new File(TRAIN_DIR, "temp_" + counter++ + ".png")); } } System.out.println("生成供比對的圖片完畢,請到目錄中手工識別並重命名圖片,並刪除其它無關圖片!"); }
運行後train目錄內容:
3.7 測試判斷效果:運行方法,能夠在isFontColor方法中調整rgb三值累加的範圍值,以達到高的分辨率。
// 7.測試判斷效果:運行方法,能夠調整rgb三值,以達到高的分辨率。 // 目前此方法提供在輸出判斷結果的同時,在目標目錄生成以判斷結果命名的新驗證碼圖片,以批量檢查效果。 public void testDownloadImage() throws Exception { File dir = new File(DOWNLOAD_DIR); File[] files = dir.listFiles(new ImageFileFilter("png")); for (File file : files) { String validateCode = getValidateCode(file); System.out.println(file.getName() + "=" + validateCode); } System.out.println("判斷完畢,請到相關目錄檢查效果!"); }
運行後result目錄結果以下圖(識別率100%):
3.8 開放給外界接口調用。
/** * 8.提供給外界接口調用。 * @param file * @return * @throws Exception */ public static String getValidateCode(File file) throws Exception { // 裝載圖片 BufferedImage image = ImageIO.read(file); removeInterference(image); // 拆分圖片 List<BufferedImage> digitImageList = splitImage(image); // 循環每一位數字圖進行比對 StringBuilder sb = new StringBuilder(); for (BufferedImage digitImage : digitImageList) { String result = ""; int width = digitImage.getWidth(); int height = digitImage.getHeight(); // 最小的不一樣次數(初始值爲總像素),值越小就越像。 int minDiffCount = width * height; for (BufferedImage bi : trainMap.keySet()) { // 對每一位數字圖與字典中的進行按像素比較 int currDiffCount = 0; // 按像素比較不一樣的次數 outer : for (int x = 0; x < width; ++x) { for (int y = 0; y < height; ++y) { if (isFontColor(digitImage.getRGB(x, y)) != isFontColor(bi.getRGB(x, y))) { // 按像素比較若是不一樣,則加1; currDiffCount++; // 若是值大於minDiffCount,則不用再比較了,由於咱們要找最小的minDiffCount。 if (currDiffCount >= minDiffCount) break outer; } } } if (currDiffCount < minDiffCount) { // 如今誰差異最小,就先暫時把值賦予給它 minDiffCount = currDiffCount; result = trainMap.get(bi); } } sb.append(result); } ImageIO.write(image, "PNG", new File(RESULT_DIR, sb.toString() + ".png")); return sb.toString(); }
package com.clzhang.sample.net; import java.awt.Color; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileFilter; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.imageio.ImageIO; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.protocol.BasicHttpContext; /** * 這是一個自動識別驗證碼的程序。要求是簡單的驗證碼,固定大小,固定位置,固定字體;字體純色最好,如不是須要修改代碼。 * * @author acer * */ public class ImageProcess { // 存放全部下載驗證碼的目錄 private static final String DOWNLOAD_DIR = "D:\\Work\\helloworld\\resources\\validate\\download"; // 存放已經拆分開的單個數字圖片的目錄,供比對用 private static final String TRAIN_DIR = "D:\\Work\\helloworld\\resources\\validate\\train"; // 存放比對結果的目錄(從新以驗證碼所含數字命名文件,很是直觀) private static final String RESULT_DIR = "D:\\Work\\helloworld\\resources\\validate\\result"; // 存放比對圖片與表明數字的Map private static Map<BufferedImage, String> trainMap = new HashMap<BufferedImage, String>(); // 圖片過濾器,想要什麼樣的圖片,傳進名稱便可。如:png/gif/.png static class ImageFileFilter implements FileFilter { private String postfix = ".png"; public ImageFileFilter(String postfix) { if(!postfix.startsWith(".")) postfix = "." + postfix; this.postfix = postfix; } @Override public boolean accept(File pathname) { return pathname.getName().toLowerCase().endsWith(postfix); } } static { try { // 將TRAIN_DIR目錄的供比對的圖片裝載進來 File dir = new File(TRAIN_DIR); File[] files = dir.listFiles(new ImageFileFilter("png")); for (File file : files) { trainMap.put(ImageIO.read(file), file.getName().charAt(0) + ""); } } catch (IOException e) { e.printStackTrace(); } } // 1.下載驗證碼:將多個驗證碼圖片下載到指定目錄,要求各類可能的驗證碼(單個數字)都應該有,好比:0-9。 private void downloadImage() throws Exception { HttpClient httpClient = new DefaultHttpClient(); for (int i = 0; i < 10; i++) { String url = "http://www.yoursite.com/yz.php"; HttpGet getMethod = new HttpGet(url); try { HttpResponse response = httpClient.execute(getMethod, new BasicHttpContext()); HttpEntity entity = response.getEntity(); InputStream instream = entity.getContent(); OutputStream outstream = new FileOutputStream(new File(DOWNLOAD_DIR, i + ".png")); int l = -1; byte[] tmp = new byte[2048]; while ((l = instream.read(tmp)) != -1) { outstream.write(tmp); } outstream.close(); } finally { getMethod.releaseConnection(); } } System.out.println("下載驗證碼完畢!"); } // 2.去除圖像干擾像素(非必須操做,只是能夠提升精度而已)。 public static BufferedImage removeInterference(BufferedImage image) throws Exception { int width = image.getWidth(); int height = image.getHeight(); for (int x = 0; x < width; ++x) { for (int y = 0; y < height; ++y) { if (isFontColor(image.getRGB(x, y))) { // 若是當前像素是字體色,則檢查周邊是否都爲白色,如都是則刪除本像素。 int roundWhiteCount = 0; if(isWhiteColor(image, x+1, y+1)) roundWhiteCount++; if(isWhiteColor(image, x+1, y-1)) roundWhiteCount++; if(isWhiteColor(image, x-1, y+1)) roundWhiteCount++; if(isWhiteColor(image, x-1, y-1)) roundWhiteCount++; if(roundWhiteCount == 4) { image.setRGB(x, y, Color.WHITE.getRGB()); } } } } return image; } // 取得指定位置的顏色是否爲白色,若是超出邊界,返回true // 本方法是從removeInterference方法中摘取出來的。單獨調用本方法無心義。 private static boolean isWhiteColor(BufferedImage image, int x, int y) throws Exception { if(x < 0 || y < 0) return true; if(x >= image.getWidth() || y >= image.getHeight()) return true; Color color = new Color(image.getRGB(x, y)); return color.equals(Color.WHITE)?true:false; } // 3.判斷拆分驗證碼的標準:就是定義驗證碼中包含的各數字的x、y座標值,及它們的寬度(width)、高度(height)。 private static List<BufferedImage> splitImage(BufferedImage image) throws Exception { final int DIGIT_WIDTH = 19; final int DIGIT_HEIGHT = 17; List<BufferedImage> digitImageList = new ArrayList<BufferedImage>(); digitImageList.add(image.getSubimage(2, 2, DIGIT_WIDTH, DIGIT_HEIGHT)); digitImageList.add(image.getSubimage(20, 2, DIGIT_WIDTH, DIGIT_HEIGHT)); digitImageList.add(image.getSubimage(40, 2, DIGIT_WIDTH, DIGIT_HEIGHT)); digitImageList.add(image.getSubimage(60, 2, DIGIT_WIDTH, DIGIT_HEIGHT)); return digitImageList; } // 4.判斷字體的顏色含義:正常能夠用rgb三種顏色加起來表示,字與非字應該有顯示的區別,找出來。 private static boolean isFontColor(int colorInt) { Color color = new Color(colorInt); return color.getRed() + color.getGreen() + color.getBlue() == 340; } // 5.將下載的驗證碼圖片所有拆分到另外一個目錄。 public void generateStdDigitImgage() throws Exception { File dir = new File(DOWNLOAD_DIR); File[] files = dir.listFiles(new ImageFileFilter("png")); int counter = 0; for (File file : files) { BufferedImage image = ImageIO.read(file); removeInterference(image); List<BufferedImage> digitImageList = splitImage(image); for (int i = 0; i < digitImageList.size(); i++) { BufferedImage bi = digitImageList.get(i); ImageIO.write(bi, "PNG", new File(TRAIN_DIR, "temp_" + counter++ + ".png")); } } System.out.println("生成供比對的圖片完畢,請到目錄中手工識別並重命名圖片,並刪除其它無關圖片!"); } // 7.測試判斷效果:運行方法,能夠調整rgb三值,以達到高的分辨率。 // 目前此方法提供在輸出判斷結果的同時,在目標目錄生成以判斷結果命名的新驗證碼圖片,以批量檢查效果。 public void testDownloadImage() throws Exception { File dir = new File(DOWNLOAD_DIR); File[] files = dir.listFiles(new ImageFileFilter("png")); for (File file : files) { String validateCode = getValidateCode(file); System.out.println(file.getName() + "=" + validateCode); } System.out.println("判斷完畢,請到相關目錄檢查效果!"); } /** * 8.提供給外界接口調用。 * @param file * @return * @throws Exception */ public static String getValidateCode(File file) throws Exception { // 裝載圖片 BufferedImage image = ImageIO.read(file); removeInterference(image); // 拆分圖片 List<BufferedImage> digitImageList = splitImage(image); // 循環每一位數字圖進行比對 StringBuilder sb = new StringBuilder(); for (BufferedImage digitImage : digitImageList) { String result = ""; int width = digitImage.getWidth(); int height = digitImage.getHeight(); // 最小的不一樣次數(初始值爲總像素),值越小就越像。 int minDiffCount = width * height; for (BufferedImage bi : trainMap.keySet()) { // 對每一位數字圖與字典中的進行按像素比較 int currDiffCount = 0; // 按像素比較不一樣的次數 outer : for (int x = 0; x < width; ++x) { for (int y = 0; y < height; ++y) { if (isFontColor(digitImage.getRGB(x, y)) != isFontColor(bi.getRGB(x, y))) { // 按像素比較若是不一樣,則加1; currDiffCount++; // 若是值大於minDiffCount,則不用再比較了,由於咱們要找最小的minDiffCount。 if (currDiffCount >= minDiffCount) break outer; } } } if (currDiffCount < minDiffCount) { // 如今誰差異最小,就先暫時把值賦予給它 minDiffCount = currDiffCount; result = trainMap.get(bi); } } sb.append(result); } ImageIO.write(image, "PNG", new File(RESULT_DIR, sb.toString() + ".png")); return sb.toString(); } public static void main(String[] args) throws Exception { ImageProcess ins = new ImageProcess(); // 第1步,下載驗證碼到DOWNLOAD_DIR // ins.downloadImage(); // 第2步,去除干擾的像素 // File dir = new File(DOWNLOAD_DIR); // File[] files = dir.listFiles(new ImageFileFilter("png")); // for (File file : files) { // BufferedImage image = ImageIO.read(file); // removeInterference(image); // ImageIO.write(image, "PNG", file); // System.out.println("成功處理:" + file.getName()); // } // 第3步,判斷拆分驗證碼的標準 // 經過PhotoShop打開驗證碼並放大觀察,我這兒的結果參考splitImage()方法中的變量 // 第4步,判斷字體的顏色含義 // 經過PhotoShop打開驗證碼並放大觀察,我這兒字體顏色的rgb總值加起來在340。由於是純色。 // 第5步,將下載的驗證碼圖片所有拆分到TRAIN_DIR目錄。 // ins.generateStdDigitImgage(); // 第6步,手工命名文件 // 打開資源管理器,選擇TRAIN_DIR,分別找出顯示0-9數字的文件,以它的名字從新命名,刪除其它全部的。 // 第7步,測試判斷效果,運行後打開RESULT_DIR,檢查文件名是否與驗證碼內容一致。 ins.testDownloadImage(); // 第8步,提供給外界接口調用。 // String validateCode = ImageProcess.getValidateCode(new File(DOWNLOAD_DIR, "0.png")); // System.out.println("驗證碼爲:" + validateCode); } }