轻松把玩HttpClient之封装HttpClient工具类(七),新增验证码识别功能

这个HttpClientUtil工具类分享在GitHub上已经半年多的时间了,并且得到了不小的关注,有25颗star,被fork了38次。有了大家的鼓励,工具类一直也在完善中。最近比较忙,两个多月前的修改在今天刚修改测试完成,今天再次分享给大家。

验证码识别这项技术并不是本工具类的功能,而是通过一个开源的api来识别验证码的。这里做了一个简单的封装,主要是用来解决登陆时的验证码的问题。在线验证码识别官网:http://lab.ocrking.com/,github地址:https://github.com/AvensLab/OcrKing/,是一个功能非常强大的工具。

好了,言归正传,本次封装的工具重要代码如下:

/**
 * 识别验证码
 *
 * @author arron
 * @date 2016年3月24日 上午9:44:35
 * @version 1.0
 */
public class OCR {

	/**
	 * 接口说明:
	 * https://github.com/AvensLab/OcrKing/blob/master/线上识别http接口说明.txt
	 */
	private static final String apiUrl = "http://lab.ocrking.com/ok.html";
	private static final String apiKey = PropertiesUtil.getProperty("OCR.key");
	private static final String boundary = "----------------------------OcrKing_Client_Aven_s_Lab";
	private static final String end="\r\n--" + boundary + "--\r\n";
	private static final Header[] headers = HttpHeader.custom()										 	.referer("http://lab.ocrking.com/?javaclient0.1)")
																					.build();
	private static final Map<String, Object> map = getParaMap();
	private static HttpClient client  =null; //=HCB.custom().proxy("127.0.0.1", 8888).build();

	public static void debug(){
		client =HCB.custom().proxy("127.0.0.1", 8888).build();
	}
	public static void exitDebug(){
		client =null;
	}

	//获取固定参数
	private static Map<String, Object> getParaMap(){
		//加载所有参数
		Map<String , Object> map = new HashMap<String, Object>();
		map.put("service", "OcrKingForCaptcha");
		map.put("language", "eng");
		map.put("charset", "7");//7-数字大写小写,5-数字大写字母
		map.put("type", "http://www.unknown.com");
		map.put("apiKey", apiKey);
		return map;
	}

	/**
	 * 识别本地校验码(英文:字母+大小写)
	 *
	 * @param imgFilePath	验证码地址
	 * @return
	 */
	public static String ocrCode(String filePath){
		return ocrCode(filePath, 0);
	}
	/**
	 * 识别本地校验码(英文:字母+大小写)
	 *
	 * @param imgFilePath	验证码地址
	 * @param limitCodeLen	验证码长度(如果结果与设定长度不一致,则返回获取失败的提示)
	 * @return
	 */
	@SuppressWarnings("resource")
	public static String ocrCode(String imgFilePath, int limitCodeLen){
		byte[] data = null;
		String fileName = imgFilePath.replaceAll("[^/]*/|[^\\\\]*\\\\", "");

		StringBuffer strBuf = new StringBuffer();
		for (Entry<String, Object> entry : map.entrySet()) {
			strBuf.append("\r\n").append("--").append(boundary).append("\r\n");
			strBuf.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"\r\n\r\n");
			strBuf.append(entry.getValue());
		}
		strBuf.append("\r\n").append("--").append(boundary).append("\r\n");
		strBuf.append("Content-Disposition: form-data; name=\"ocrfile\"; filename=\"" + fileName + "\"\r\n");
		strBuf.append("Content-Type:application/octet-stream\r\n\r\n");

		//读取文件
		File f = new File(imgFilePath);
		if(!f.exists()){
			return "Error:文件不存在!";
		}

		//内容长度=参数长度+文件长度+结尾字符串长度
		ByteArrayOutputStream bos = new ByteArrayOutputStream(strBuf.length()+(int)f.length()+end.length());
        try {
        	bos.write(strBuf.toString().getBytes());//转化参数内容
        	BufferedInputStream in = new BufferedInputStream(new FileInputStream(f));
            int buf_size = 1024;
            int len = 0;
            byte[] buf = new byte[buf_size];
            while (-1 != (len = in.read(buf, 0, buf_size))) {
                bos.write(buf, 0, len);
            }
            bos.write(end.getBytes());
            data= bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }

        Map<String , Object> m = new HashMap<String, Object>();
  		m.put(Utils.ENTITY_BYTES, data);

  		String html;
		try {
			html = HttpClientUtil.post(HttpConfig.custom().client(client).url(apiUrl).headers(headers).map(m));
			//System.out.println(html);
			String[] results = StringUtil.regex("<Result>([^<]*)</Result>\\s*<Status>([^<]*)</Status>", html);
			if(results.length>0){
				//System.out.println(results[0]);
				if(limitCodeLen<=0 || limitCodeLen==results[0].length()){//不判断长度或者长度一致时,直接返回
					return results[0];
				}
			}
		} catch (HttpProcessException e) {
			e.printStackTrace();
		}

		return "Error:获取失败!";
	}

	/**
	 * 直接获取网络验证码(验证码不刷新)
	 *
	 * @param imgUrl			验证码地址
	 * @return
	 */
	public static String ocrCode4Net(String imgUrl){
		return ocrCode4Net(imgUrl, 0);
	}
	/**
	 * 直接获取网络验证码(验证码不刷新)
	 *
	 * @param imgUrl			验证码地址
	 * @param limitCodeLen	验证码长度
	 * @return
	 */
	public static String ocrCode4Net(String imgUrl, int limitCodeLen){
		Map<String, Object> map = getParaMap();
		map.put("url", imgUrl);

		Header[] headers = HttpHeader.custom().userAgent("Mozilla/5.0 (Windows NT 5.1; zh-CN; rv:1.9.1.3) Gecko/20100101 Firefox/8.0").build();

		try {
			String html = HttpClientUtil.post(HttpConfig.custom().client(client).url(apiUrl).headers(headers).map(map));
			//System.out.println(html);
			String[] results = StringUtil.regex("<Result>([^<]*)</Result>\\s*<Status>([^<]*)</Status>", html);
			if(results.length>0){
				//System.out.println(results[0]);
				if(limitCodeLen<=0 || limitCodeLen==results[0].length()){//不判断长度或者长度一致时,直接返回
					return results[0];
				}
			}
		} catch (HttpProcessException e) {
			e.printStackTrace();
		}

		return "Error:获取失败!";
	}

	/**
	 * 直接获取网络验证码(通过获取图片流,然后识别验证码)
	 *
	 * @param config				HttpConfig对象(设置cookie)
	 * @param savePath		图片保存的完整路径(值为null时,不保存),如:c:/1.png
	 * @return
	 */
	public static String ocrCode4Net(HttpConfig config, String savePath){
		return ocrCode4Net(config, savePath, 0);
	}
	/**
	 * 直接获取网络验证码(通过获取图片流,然后识别验证码)
	 *
	 * @param config				HttpConfig对象(设置cookie)
	 * @param savePath		图片保存的完整路径(值为null时,不保存),如:c:/1.png
	 * @param limitCodeLen	验证码长度
	 * @return
	 */
	@SuppressWarnings("resource")
	public static String ocrCode4Net(HttpConfig config, String savePath, int limitCodeLen){
		byte[] data = null;

		StringBuffer strBuf = new StringBuffer();
		for (Entry<String, Object> entry : map.entrySet()) {
			strBuf.append("\r\n").append("--").append(boundary).append("\r\n");
			strBuf.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"\r\n\r\n");
			strBuf.append(entry.getValue());
		}
		strBuf.append("\r\n").append("--").append(boundary).append("\r\n");
		strBuf.append("Content-Disposition: form-data; name=\"ocrfile\"; filename=\"" + "aaa" + "\"\r\n");
		strBuf.append("Content-Type:application/octet-stream\r\n\r\n");

		//下载图片
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		try {
			out = (ByteArrayOutputStream) HttpClientUtil.down(config.client(client).out(out));
			if(savePath==null || savePath.equals("")){
			}else{
				//本地测试,可以保存一下图片,方便核验
				FileOutputStream fos = new FileOutputStream(savePath);
				fos.write(out.toByteArray());
			}

			ByteArrayOutputStream bos = new ByteArrayOutputStream(out.size()+strBuf.length()+end.length());
			bos.write(strBuf.toString().getBytes());
			bos.write(out.toByteArray());
			bos.write(end.getBytes());
			data= bos.toByteArray();
		} catch (HttpProcessException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}

		Map<String , Object> m = new HashMap<String, Object>();
		m.put(Utils.ENTITY_BYTES, data);

		String html;
		try {
			html = HttpClientUtil.post(config.client(client).url(apiUrl).headers(headers).map(m));
			//System.out.println(html);
			String[] results = StringUtil.regex("<Result>([^<]*)</Result>\\s*<Status>([^<]*)</Status>", html);
			if(results.length>0){
				//System.out.println(results[0]);
				if(limitCodeLen<=0 || limitCodeLen==results[0].length()){//不判断长度或者长度一致时,直接返回
					return results[0];
				}
			}
		} catch (HttpProcessException e) {
			e.printStackTrace();
		}

		return "Error:获取失败!";
	}
}

其实这个类中,主要用3个方法,第一个是识别本地图片,第二个是识别网络上的固定图片,第三个是识别网络上可刷新的验证码图片。当然不管哪个方法,核心代码都是读取图片字节流,上传到api接口,通过接口进行识别。

上面代码中用到了StringUtil.regex方法,具体如下:

	/**
	 * 通过正则表达式获取内容
	 *
	 * @param regex		正则表达式
	 * @param from		原字符串
	 * @return
	 */
	public static String[] regex(String regex, String from){
		Pattern pattern = Pattern.compile(regex);
		Matcher matcher = pattern.matcher(from);
		List<String> results = new ArrayList<String>();
		while(matcher.find()){
			for (int i = 0; i < matcher.groupCount(); i++) {
				results.add(matcher.group(i+1));
			}
		}
		return results.toArray(new String[]{});
	}

由于识别验证码的api需要用到ke‘y,所以就写了一个配置文件config.properties,用于设置ke‘y的值。同时提供了一个简单的配置文件工具类:

/**
 * 最简单的属性文件读取工具类
 *
 * @author arron
 * @date 2016年1月14日 下午5:37:18
 * @version 1.0
 */
public class PropertiesUtil {

	/**
	 * 默认属性集合(文件在Constants中配置)
	 */
	protected static Properties defaultProp = null;
	/**
	 * 所有读取过的属性集合
	 * 文件名 <-> 属性集合
	 */
	protected static Map<String, Properties> allProps = new HashMap<String, Properties>();

	// 初始化默认的属性集合
	static {
		if (defaultProp == null) {
			defaultProp = loadProperties("config.properties");
			allProps.put("config.properties", defaultProp);
		}
	}

	/**
	 * 读取属性文件,并将读出来的属性集合添加到【allProps】当中
	 * 如果该属性文件之前已读取过,则直接从【allProps】获得
	 */
	public static Properties getProperties(String fileName) {
		if (fileName==null || "".equals(fileName)) {
			return defaultProp;
		} else {
			Properties prop = allProps.get(fileName);
			if(prop == null) {
				prop = loadProperties(fileName);
				allProps.put(fileName, prop);
			}

			return prop;
		}
	}		

	/**
	 * 解析属性文件,将文件中的所有属性都读取到【Properties】当中
	 */
	protected static Properties loadProperties (String fileName) {
		Properties prop = new Properties();
		InputStream ins = null;
		ins = PropertiesUtil.class.getClassLoader().getResourceAsStream(fileName);
		if (ins == null) {
		    System.err.println("Can not find the resource!");
		} else {
			try {
				prop.load(ins);
			} catch (IOException e) {
	            System.err.println("An error occurred when reading from the input stream, "+e.getMessage());
			} catch (IllegalArgumentException e) {
                System.err.println("The input stream contains a malformed Unicode escape sequence, "+e.getMessage());
			}
		}
		return prop;
	}

	/**
	 * 从指定的属性文件中获取某一属性值
	 * 如果属性文件不存在该属性则返回 null
	 */
	public static String getProperty(String fileName, String name){
		return getProperties(fileName).getProperty(name);
	}

	/**
	 * 从默认的属性文件中获取某一属性值
	 * 如果属性文件不存在该属性则返回 null
	 */
	public static String getProperty(String name){
		return getProperties(null).getProperty(name);
	}
}

代码也就这么多。在最后,提供一段测试代码来测试该功能:核心逻辑就是通过HttpClientUtil的download方法获取图片,然后通过api进行识别,然后通过请求特定网址进行验证识别的结果是否正确。

	public static void main(String[] args) throws InterruptedException, HttpProcessException {
		String qq = "123456789";//qq号
		String imgUrl = "http://qqxoo.com/include/vdimgvt.php?t="+Math.random(); //获取验证码图片地址
		String verifyUrl = "http://qqxoo.com/include/vdcheck.php";
		String saveCodePath = "C:/1.png";//保存验证码图片路径

		Header[] headers = HttpHeader.custom().referer("http://qqxoo.com/main.html?qqid="+qq).build();//设置referer,是为了获取对应qq号的验证码,否则报错
		HttpConfig config = HttpConfig.custom().headers(headers).context(HttpCookies.custom().getContext());//必须设置context,是为了携带cookie进行操作

		String result =null;//识别结果

		do {
			if(result!=null){
				System.err.println("本次识别失败!");
			}

			//获取验证码
			//OCR.debug(); //开始Fiddler4抓包(127.0.0.1:8888)
			String code = OCR.ocrCode4Net(config.url(imgUrl), saveCodePath);

			while(code.length()!=5){//如果识别的验证码位数不等于5,则重新识别
				if(code.equals("亲,apiKey已经过期或错误,请重新获取")){
					System.err.println(code);
					return;
				}
				code = OCR.ocrCode4Net(config.url(imgUrl), saveCodePath);
			}

			System.out.println("本地识别的验证码为:"+code);
			System.out.println("验证码已保存到:"+saveCodePath);

			//开始验证识别的验证码是否正确
			result = HttpClientUtil.get(config.url(verifyUrl+"?vc="+code+"&qqid="+qq));

		} while (result.contains("succeed"));

		System.out.println("识别验证码成功!反馈信息如下:\n" + result);
	}

运行结果如下:

最新的完整代码请到GitHub上进行下载:https://github.com/Arronlong/httpclientUtil 。

时间: 2024-10-13 00:16:13

轻松把玩HttpClient之封装HttpClient工具类(七),新增验证码识别功能的相关文章

DAO设计模式实现数据库的增删改查(进一步封装JDBC工具类)

一.DAO模式简介 DAO即Data Access Object,数据访问接口.数据访问:故名思义就是与数据库打交道.夹在业务逻辑与数据库资源中间. DAO模式实际上是两个模式的组合,即Data Accessor (数据访问者)模式和 Active Domain Object(领域对象)模式.Data Accessor 模式实现了数据访问和业务逻辑的分离:Active Domain Object 模式实现了业务数据的对象化封装. 需要注意的是,DAO设计模式是Java EE中的设计模式,而非Ja

MySQL数据库学习笔记(十)----JDBC事务处理、封装JDBC工具类

首先需要回顾一下上一篇文章中的内容:MySQL数据库学习笔记(九)----JDBC的PreparedStatement接口重构增删改查 一.JDBC事务处理: 我们已经知道,事务的概念即:所有的操作要么同时成功,要么同时失败.在MySQL中提供了Commit.Rollback命令进行事务的提交与回滚.实际上在JDBC中也存在事务处理,如果要想进行事务处理的话,则必须按照以下的步骤完成. JDBC中事务处理的步骤: 1.要取消掉JDBC的自动提交:void setAutoCommit(boolea

封装JDBC工具类

JDBC连接数据库基本的步骤是固定的,这样就可以考虑封装一个工具类来简化数据库操作. 封装时用到了Java中的properties配置文件,是以一种键值对的形式存在的,可以把连接数据库要动态的信息保存到里面,这样比较直观,不容易出错,而且容易维护. 把配置文件放到src下就可以,如果要放到包下面就配置文件的相对路径就必须从包名开始. Demo : db.properties mysqlDriver=com.mysql.jdbc.Driver mysqlURL=jdbc:mysql://local

导入导出封装的工具类 (一) 利用POI封装

对于导入导出各个项目中几乎都会用到,记得在高校平台中封装过导入导出这部分今天看了看是利用JXL封装的而经理说让我用POI写写导出,这两个导入导出框架是目前比较流程和常用的框架,有必要都了解一下. 写了写代码觉得导入导出这一块底层都是一样的,几乎所有的框架和别的牛人也好都是底层利用POI或JXL实现,比的是谁对这部分封装的好而且每个项目中对导入导出具体的细节是不同的,因此,有必要了解了解怎么样操作POI,学学使用它的API做导入导出也许第一步你封装的没有别人那么好,你也会收获很多了解他们封装的思路

JAVA中封装JSONUtils工具类及使用

在JAVA中用json-lib-2.3-jdk15.jar包中提供了JSONObject和JSONArray基类,用于JSON的序列化和反序列化的操作.但是我们更习惯将其进一步封装,达到更好的重用. 封装后的JSON工具类JSONUtils.java代码如下: JSONUtils代码,点击展开 import java.util.ArrayList;import java.util.Collection;import java.util.HashMap;import java.util.Itera

MySQL数据库学习笔记(十一)----DAO设计模式实现数据库的增删改查(进一步封装JDBC工具类)

[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/4059514.html 联系方式:[email protected] [正文] 一.DAO模式简介 DAO即Data Access Object,数据访问接口.数据访问:故名思义就是与数据库打交道.夹在业务逻辑与数据库资源中间. DAO模式实际上是两个模式的组合,即Data Accessor (数据

轻松把玩HttpClient之封装HttpClient工具类(六),封装输入参数,简化工具类

在写这个工具类的时候发现传入的参数太多,以至于方法泛滥,只一个send方法就有30多个,所以对工具类进行了优化,把输入参数封装在一个对象里,这样以后再扩展输入参数,直接修改这个类就ok了. 不多说了,先上代码: /** * 请求配置类 * * @author arron * @date 2016年2月2日 下午3:14:32 * @version 1.0 */ public class HttpConfig { private HttpConfig(){}; /** * 获取实例 * @retu

轻松把玩HttpClient之封装HttpClient工具类(二),插件式配置HttpClient对象

上一篇文章中,简单分享一下封装HttpClient工具类的思路及部分代码,本文将分享如何实现插件式配置HttpClient对象. 如果你看过我前面的几篇关于HttpClient的文章或者官网示例,应该都知道HttpClient对象在创建时,都可以设置各种参数,但是却没有简单的进行封装,比如对我来说比较重要的3个:代理.ssl(包含绕过证书验证和自定义证书验证).超时.还需要自己写.所以这里我就简单封装了一下,顺便还封装了一个连接池的配置. 其实说是插件式配置,那是高大上的说法,说白了,就是采用了

轻松把玩HttpClient之封装HttpClient工具类(三),插件式配置Header

上篇文章介绍了插件式配置HttpClient,本文将介绍插件式配置Header. 为什么要配置header在前面已经提到了,还里再简单说一下,要使用HttpClient模拟请求,去访问各种接口或者网站资源,都有可能有各种限制,比如说java客户端模拟访问csdn博客,就必须设置User-Agent,否则就报错了.还有各种其他情况,必须的设置一些特定的Header,才能请求成功,或者才能不出问题. 好了就说这么多,本次还是采用构造者模式的级联调用方式,来完成该工具类.在该工具类中,为所有常用的Ht