原文:http://www.cnblogs.com/nayitian/p/3282862.html
1. 需求
因為項目需要,需要多次登錄某網站抓取信息。所以學習了驗證碼的一些小知識。文章參考http://blog.csdn.net/problc/article/details/5794460的部分內容。
需要程序識別的驗證碼格式如圖所示:
,這個圖片符合固定大小,固定位置,固定字體,固定顏色的范圍,實現起來相對簡單。
驗證碼識別基本分四步,圖片預處理,分割,訓練,識別。為便於演示,我這里分更多的步驟。
BTW:
如果是形如:
的驗證碼,請參考:http://blog.csdn.net/problc/article/details/5797507
如果是形如:
的驗證碼,請參考:http://blog.csdn.net/problc/article/details/5800093
如果是形如:
的驗證碼,請參考:http://blog.csdn.net/problc/article/details/5846614
更多驗證碼相關內容,請參考:http://blog.csdn.net/problc/article/details/5983276
2. 環境
目錄結構:download目錄用於存放下載的驗證碼;train用於存放供比對的標准圖片;result用於存放比對結果。

包:HttpClient4.2(用於抓取圖片)
3. 步驟
3.1 下載驗證碼:將多個驗證碼圖片下載到指定目錄,要求各種可能的驗證碼(單個數字)都應該有,比如:0-9。
// 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目錄內容:
3.2 去除圖像干擾像素(非必須操作,只是可以提高精度而已;可以按照自己的需求進行更改)。
// 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.3 判斷拆分驗證碼的標准:就是定義驗證碼中包含的各數字的x、y坐標值,及它們的寬度(width)、高度(height)。
打開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.6 手工命名文件:在資源管理器中,切換到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();
}
4. 完整代碼
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);
}
}

