作者:实验室2012级刘力超

网页爬虫不管是在学术上还是在系统开发方面都有广泛的应用,Java中对单页面的爬虫使用主要有HttpClient与HtmlUnit两种工具,但两种工具的操作使用都比较繁杂,每次请求读取都需要编写大量的代码,因此有必要将其进行封装,使其能够模块化操作。

整体应用架构,详细的代码不罗列了,如果不懂Spring就先去看看书吧。

1、HttpClient简介

HttpClient是Apache Jakarta Common下的子项目,用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本。

【技术教程】对基于HttpClient的网页爬虫封装-Web开发实验室

工具特性:

  • 基于标准、纯净的java语言。实现了Http1.0和Http1.1
  •  以可扩展的面向对象的结构实现了Http全部的方法(GET, POST, PUT, DELETE, HEAD, OPTIONS, and TRACE)。
  • 支持HTTPS协议。
  • 连接管理器支持多线程应用。支持设置最大连接数,同时支持设置每个主机的最大连接数,发现并关闭过期的连接。
  • 自动处理Set-Cookie中的Cookie。
  • 插件式的自定义Cookie策略。
  • 直接获取服务器发送的response code和 headers。
  • 设置连接超时的能力。
  • 源代码基于Apache License 可免费获取。

项目地址:http://hc.apache.org/httpcomponents-client-4.3.x/download.html

2、HttpClient使用方法

使用HttpClient原生方法执行爬取请求,可分为以下简要步骤:

  • 1. 创建HttpClient对象。
  • 2. 创建请求方法的实例,并指定请求URL。如果需要发送GET请求,创建HttpGet对象;如果需要发送POST请求,创建HttpPost对象。
  • 3. 如果需要发送请求参数,可调用HttpGet、HttpPost共同的 setParams(HetpParams params)方法来添加请求参数;对于HttpPost对象而言,也可调用setEntity(HttpEntity entity)方法来设置请求参数。
  • 4. 调用HttpClient对象的execute(HttpUriRequest request)发送请求,该方法返回一个HttpResponse。
  • 5. 调用HttpResponse的getAllHeaders()、 getHeaders(String name)等方法可获取服务器的响应头;调用HttpResponse的getEntity()方法可获取HttpEntity对象,该对象包装了服务器 的响应内容。程序可通过该对象获取服务器的响应内容。
  • 6. 释放连接。无论执行方法是否成功,都必须释放连接

maven引入:

<!-- HttpClient -->
<dependency>
	<groupId>org.apache.httpcomponents</groupId>
	<artifactId>httpclient</artifactId>
	<version>4.5.2</version>
</dependency>

更多文档参见:http://hc.apache.org/httpcomponents-client-4.3.x/examples.html

3、对HttpClient进行封装

基本思路是定义一个Request的POJO,这个POJO包含一次Http请求所需要的所有参数,将此POJO传入curlGet()、curlPost()接口中,在接口的具体实现中应用HttpClient对请求进行处理,再返回执行Http请求过后的内容字符串

3.1 Request.java

在Request.java中包含一次请求所需要的所有参数信息,包括请求的唯一标志id、请求的url、返回内容的编码、请求的参数以及请求的header头信息

Request.java:

package org.weblab.spider.pojo;

import java.util.Map;

public class Request {
	
	private String id;
	
	private String url;
	
	private String charSet = "utf-8";
	
	//请求参数
	private Map<String, String> params;
	
	//请求header
	private Map<String, String> header;
	
	public Request(String url){
		this.url = url;
	}

	//省略以下的get以及set方法,自己添加

}

 

3.2 定义接口HttpCrawler

在基本的爬取请求中,请求类型主要包括get方式、post方式以及请求文件,因此我们定义Crawler的接口为curlGet()、curlPost()以及curlFile()

HttpCrawler.java:

package org.weblab.spider.spider;

import org.weblab.spider.pojo.Request;

/**
 * @desp 基于HttpClient的spider接口
 * @author liulichao <liulichao@ruc.edu.cn>
 * @date 2016年10月11日
 *
 */
public interface HttpCrawler {
	
	/**
	 * @desp Get请求
	 * @param id cookie标志
	 * @param url 访问地址
	 * @return
	 */
	public String curlGet(Request request);
	
	/**
	 * @desp Post请求
	 * @param id cookie标志
	 * @param url 访问地址
	 * @return
	 */
	public String curlPost(Request request);
	
	/**
	 * @desp File请求
	 * @param id cookie标志
	 * @param url 访问地址
	 * @return
	 */
	public String curlFile(Request request);
	
}

 

3.3 接口实现HttpClientCrawlerImpl

该部分为封装的核心,在接收到包含请求参数的Request对象后,我们利用HttpClient中的接口进行网页抓取操作,同时对编写自定义的CookieManager对Cookie进行处理,实现会话的保持,

package org.weblab.spider.spider.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.weblab.spider.pojo.Request;
import org.weblab.spider.spider.CookieManager;
import org.weblab.spider.spider.HttpCrawler;

@Service
public class HttpClientCrawlerImpl implements HttpCrawler {

	@Autowired
	private CookieManager cookieManager;

	@Override
	public String curlGet(Request request) {
		String result = new String();

		// 定义Cookie
		CookieStore cookieStore = cookieManager.loadCookies(request.getId());
		// 允许多次重定向
		RequestConfig config = RequestConfig.custom()
				.setCircularRedirectsAllowed(true).build();

		try (final CloseableHttpClient client = HttpClientBuilder.create()
				.setDefaultCookieStore(cookieStore)
				.setDefaultRequestConfig(config).build()) {
			// 构造url参数
			String paramStr = new String();
			if (request.getParams() != null) {
				paramStr = EntityUtils.toString(new UrlEncodedFormEntity(
						paramsAdapter(request.getParams()), "UTF-8"));
			}
			// 新建Get请求
			HttpGet get = new HttpGet(request.getUrl() + "?" + paramStr);

			// 添加Header模拟浏览器
			get.addHeader(
					"User-Agent",
					"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36");

			// 添加自定义请求头
			Map<String, String> header = request.getHeader();
			if (header != null) {
				for (Entry<String, String> entry : header.entrySet()) {
					// 将自定义的header加入请求头
					get.addHeader(new BasicHeader(entry.getKey(), entry
							.getValue()));
				}
			}

			// 执行请求
			CloseableHttpResponse response = client.execute(get);
			// 保存新的Cookie
			cookieManager.saveCookie(request.getId(), cookieStore);
			// 获取返回实体
			HttpEntity entity = response.getEntity();

			if (entity != null) {
				result = EntityUtils.toString(entity, request.getCharSet());
			}

			response.close();
			client.close();
			get.abort();

		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}
		return result;
	}

	@Override
	public String curlPost(Request request) {
		String result = new String();

		/* 新建一个HttpClient的Post方法对象,其参数为request对象中的url */
		HttpPost post = new HttpPost(request.getUrl());

		// 添加Header模拟浏览器
		post.addHeader(
				"User-Agent",
				"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.143 Safari/537.36");

		/* 设置请求header */
		Map<String, String> header = request.getHeader();
		if (header != null) {
			for (Entry<String, String> entry : header.entrySet()) {
				/* 以BasicHeader形式向post对象中加入请求header */
				post.addHeader(new BasicHeader(entry.getKey(), entry.getValue()));
			}
		}

		/* 设置cookie,此处loadCookies方法需要自己定义,具体可用数据库实现 */
		CookieStore cookieStore = cookieManager.loadCookies(request.getId());
		/* 允许多次重定向 */
		RequestConfig config = RequestConfig.custom()
				.setCircularRedirectsAllowed(true).build();

		try (final CloseableHttpClient client = HttpClientBuilder.create()
				.setDefaultCookieStore(cookieStore)
				.setDefaultRequestConfig(config).build()) {
			
			//构造访问请求POST的参数
			if (request.getParams() != null) {
				UrlEncodedFormEntity uefEntity = new UrlEncodedFormEntity(
						paramsAdapter(request.getParams()), "UTF-8");
				post.setEntity(uefEntity);
			}

			//执行post请求
			CloseableHttpResponse response = client.execute(post);
			//保存Cookie
			cookieManager.saveCookie(request.getId(),cookieStore);

			HttpEntity entity = response.getEntity();

			if (entity != null) {
				result = EntityUtils.toString(entity, request.getCharSet());
			}
			response.close();
			client.close();
			post.abort();
		} catch (Exception e) {
			e.printStackTrace();
			return null;
		}

		return result;
	}

	@Override
	public String curlFile(Request request) {
		// TODO Auto-generated method stub
		return null;
	}

	/**
	 * 参数适配器,将系统定义的请求参数转换成HttpClient能够接受的参数类型
	 * 
	 * @param params
	 *            系统定义的请求参数
	 * @return HttpClient要求的参数类型
	 */
	private List<NameValuePair> paramsAdapter(Map<String, String> map) {
		List<NameValuePair> nvps = new ArrayList<NameValuePair>();

		for (Entry<String, String> entry : map.entrySet()) {
			nvps.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
		}
		return nvps;
	}

}

3.4 CookieManager.java

本地浏览器与访问系统会话依靠的是服务器分配给本地所保存的cookie,如果要想持久与服务器保持会话(比如一直登陆某一系统),则需要在登录过后获取Cookie,然后一直带着这个Cookie与服务器进行访问,如此我们则需要编写CookieManager,对Cookie进行读取保存持久化等操作。

在CookieManager中,我们定义了两个接口,分别是loadCookie()以及saveCookie()

HttpClient中的Cookie都保存在CookieStore对象中,因此Cookie操作都是基于CookieStore,并且一旦CookieStore建立,每次访问执行之后,CookieStore中所保存的Cookie都会自动更新为新的Cookie。

CookieManager.java:

package org.weblab.spider.spider;

import org.apache.http.client.CookieStore;

/**
 * @desp Cookie管理器
 * @author liulichao <liulichao@ruc.edu.cn>
 * @date 2016年10月11日
 *
 */
public interface CookieManager {
	
	/**
	 * @desp 保存Cookie
	 * @param id
	 * @param cookieStore
	 */
	public void saveCookie(String id,CookieStore cookieStore);
	
	/**
	 * @desp 加载Cookie
	 * @param id
	 * @return
	 */
	public CookieStore loadCookies(String id);

}

接口实现CookieManagerImpl.java:

package org.weblab.spider.spider.impl;

import java.util.List;

import net.sf.json.JSONArray;
import net.sf.json.JSONObject;

import org.apache.http.client.CookieStore;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.weblab.spider.service.CookieService;
import org.weblab.spider.spider.CookieManager;

@Service
public class CookieManagerImpl implements CookieManager{

	@Autowired
	private CookieService cookieService;
	
	@Override
	public void saveCookie(String id, CookieStore cookieStore) {
		List<Cookie> cookies = cookieStore.getCookies();
		if(cookies.size() != 0){
			JSONArray cookieArray = JSONArray.fromObject(cookies);
			cookieService.setCookie(id, cookieArray.toString());
		}
	}

	@Override
	public CookieStore loadCookies(String id) {
		CookieStore cookieStore = new BasicCookieStore();
		JSONArray cookieArray = JSONArray.fromObject(cookieService.getCookie(id));
		System.out.println(cookieArray);
		if(cookieArray.size() != 0 && !cookieArray.get(0).equals("null")){
			for(int i=0;i<cookieArray.size();i++){
				JSONObject cookieObject = cookieArray.getJSONObject(i);
				BasicClientCookie cookie = new BasicClientCookie(cookieObject.getString("name"), cookieObject.getString("value"));
				cookie.setDomain(cookieObject.getString("domain"));
				cookie.setPath(cookieObject.getString("path"));
				cookie.setVersion(0);
				cookieStore.addCookie(cookie);
			}
		}
		return cookieStore;
	}

}

注:CookieService中定义了数据库访问的接口,实现cookie字符串的保存于读取,读者可以自行编写实现。

由此,CookieManager则实现了每次访问都能加载该访问id的cookie以及更新并持久化cookie。

最后,整体主要的包结构如下:

【技术教程】对基于HttpClient的网页爬虫封装-Web开发实验室

 

4、应用

那么这玩意儿封装好了之后有什么用呢,当然应用很广泛了啊,在这个架构中,我们通过curlGet(), curlPost()接口,获取到网页源代码,再通过Jsoup对网页DOM对象进行解析,抽取中页面的相关信息。

比如一个例子,利用这个爬虫自动抓取微人大上的课程成绩以及课表,通过一定微信或者网页直接展示

【技术教程】对基于HttpClient的网页爬虫封装-Web开发实验室【技术教程】对基于HttpClient的网页爬虫封装-Web开发实验室

 

这里的实现思路就是我们通过发送自己的微人大账号密码,然后利用curlPost()接口访问微人大登录链接,实现登录后本地已保存微人大分配的cookie,再以此访问各个链接,到达课表以及成绩的页面,通过Jsoup解析页面DOM,将各个文档节点的信息抽取出来后序列化并保存,再通过相应的方式展示。