本文转自:
https://blog.csdn.net/zstu_cc/article/details/39250903
https://blog.csdn.net/zstu_cc/article/details/39738117
大家在做爬虫、网页采集、通过网页自动写入数据时基本上都接触过这两个组件(权且称之为组件吧),网上入门资料已经很多了,我想从实际的应用角度谈谈我对于这两个组件的看法,并记录在博客中,以便日后翻阅,欢迎大家批评指正。
本文主要比较两者的优劣性以及介绍应用中的使用技巧,推荐一些入门资料以及非常实用的辅助工具,希望能对大家有所帮助。
大家有任何疑问或者建议希望留言给我,一起交流学习。
下面我们首先来看下2个组件的区别和优劣性:
HtmlUnit:
HtmlUnit本来是一款自动化测试的工具,它采用了HttpClient和java自带的网络api结合来实现,它与HttpClient的不同之处在于,它比HttpClient更“人性化”。
在写HtmlUnit代码的时候,仿佛感觉到的就是在操作浏览器而非写代码,得到页面(getPage)-- 寻找到文本框(getElementByID || getElementByName || getElementByXPath 等等)-- 输入文字(type,setValue,setText等等)-- 其他一些类似操作 -- 找到提交按钮 -- 提交 -- 得到新的Page,这样就非常像一个人在后台帮你操作浏览器一样,而你要做的就是告诉他如何操作以及需要填入哪些值。
优点:
一、网页的模拟化
首先说说HtmlUnit相对于HttpClient的最明显的一个好处,HtmlUnit更好的将一个网页封装成了一个对象,如果你非要说HttpClient返回的接口HttpResponse实际上也是存储了一个对象那也可以,但是HtmlUnit不仅保存了这个网页对象,更难能可贵的是它还存有这个网页的所有基本操作甚至事件。这就是说,我们对于操作这个网页可以像在jsp中写js一样,这是非常方便的,比如:你想某个节点的上一个节点,查找所有的按钮,查找样式为“bt-style”的所有元素,对于某些元素先进行一些改造,然后再转成String,或者我直接得到这个网页之后操作这个网页,完成一次提交都是非常方便的。这意味着你如果想分析一个网页会来的非常的容易,比如我附上一段百度新闻高级搜索的代码:
// 得到浏览器对象,直接New一个就能得到,现在就好比说你得到了一个浏览器了
WebClient webclient = new WebClient();
// 这里是配置一下不加载css和javaScript,配置起来很简单,是不是
webclient.getOptions().setCssEnabled(false);
webclient.getOptions().setJavaScriptEnabled(false);
// 做的第一件事,去拿到这个网页,只需要调用getPage这个方法即可
HtmlPage htmlpage = webclient.getPage("http://news.baidu.com/advanced_news.html");
// 根据名字得到一个表单,查看上面这个网页的源代码可以发现表单的名字叫“f”
final HtmlForm form = htmlpage.getFormByName("f");
// 同样道理,获取”百度一下“这个按钮
final HtmlSubmitInput button = form.getInputByValue("百度一下");
// 得到搜索框
final HtmlTextInput textField = form.getInputByName("q1");
// 最近周星驰比较火呀,我这里设置一下在搜索框内填入”周星驰“
textField.setValueAttribute("周星驰");
// 输入好了,我们点一下这个按钮
final HtmlPage nextPage = button.click();
// 我把结果转成String
String result = nextPage.asXml();
System.out.println(result);
然后你可以把得到的result结果复制到一个文本,然后用浏览器打开该文本,是不是想要的东西(如图结),很简单对吧,为什么会感觉简单,因为它完全符合我们操作浏览器的习惯,当然最终它也是用HttpClient和其它一些工具类实现的,但是这样的封装是非常人性化和令人惊叹的。
Htmlunit可以有效的分析出 dom标签,并且可以有效的运行页面上的js以便得到一些需要执行JS才能得到的值,你仅仅需要做的就是执行executeJavaScript()这个方法而已,这些都是HtmlUnit帮我们封装好,我们要做的仅仅是告诉它需要做什么。
WebClient webclient = new WebClient();
HtmlPage htmlpage = webclient.getPage("you url");
htmlpage.executeJavaScript("the function name you want to execute");
对于使用Java程序员来说,对对象的操作就再熟悉不过了,HtmlUnit所做的正是帮我们把网页封装成一个对象,一个功能丰富的,透明的对象。
二、网络响应的自动化处理
HtmlUnit拥有强大的响应处理机制,我们知道:常见的404是找不到资源,100等是继续,300等是跳转...我们在使用HttpClient的时候它会把响应结果告诉我们,当然,你可以自己来判断,比如说,你发现响应码是302的时候,你就在响应头去找到新的地址并自动再跳过去,发现是100的时候就再发一次请求,你如果使用HttpClient,你可以这么去做,也可以写的比较完善,但是,HtmlUnit已经较为完整的实现了这一功能,甚至说,他还包括了页面JS的自动跳转(响应码是200,但是响应的页面就是一个JS),天涯的登录就是这么一个情况,让我们一起来看下。
/**
* @author CaiBo
* @date 2014年9月15日 上午9:16:36
* @version $Id$
*
*/
public class TianyaTest {
/**
*
*/
public static void main(String[] args) throws Exception {
// 这是一个测试,也是为了让大家看的更清楚,请暂时抛开代码规范性,不要纠结于我多建了一个局部变量等
// 得到认证https的浏览器对象
HttpClient client = getSSLInsecureClient();
// 得到我们需要的post流
HttpPost post = getPost();
// 使用我们的浏览器去执行这个流,得到我们的结果
HttpResponse hr = client.execute(post);
// 在控制台输出我们想要的一些信息
showResponseInfo(hr);
}
private static void showResponseInfo(HttpResponse hr) throws ParseException, IOException {
System.out.println("响应状态行信息:" + hr.getStatusLine());
System.out.println("---------------------------------------------------------------");
System.out.println("响应头信息:");
Header[] allHeaders = hr.getAllHeaders();
for (int i = 0; i < allHeaders.length; i++) {
System.out.println(allHeaders[i].getName() + ":" + allHeaders[i].getValue());
}
System.out.println("---------------------------------------------------------------");
System.out.println("响应正文:");
System.out.println(EntityUtils.toString(hr.getEntity()));
}
// 得到一个认证https链接的HttpClient对象(因为我们将要的天涯登录是Https的)
// 具体是如何工作的我们后面会提到的
private static HttpClient getSSLInsecureClient() throws Exception {
// 建立一个认证上下文,认可所有安全链接,当然,这是因为我们仅仅是测试,实际中认可所有安全链接是危险的
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
return true;
}
}).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);
return HttpClients.custom().//
setSSLSocketFactory(sslsf)//
// .setProxy(new HttpHost("127.0.0.1", 8888))
.build();
}
// 获取我们需要的Post流,如果你是把我的代码复制过去,请记得更改为你的用户名和密码
private static HttpPost getPost() {
HttpPost post = new HttpPost("https://passport.tianya.cn/login");
// 首先我们初始化请求头
post.addHeader("Referer", "https://passport.tianya.cn/login.jsp");
post.addHeader("Host", "passport.tianya.cn");
post.addHeader("Origin", "http://passport.tianya.cn");
// 然后我们填入我们想要传递的表单参数(主要也就是传递我们的用户名和密码)
// 我们可以先建立一个List,之后通过post.setEntity方法传入即可
// 写在一起主要是为了大家看起来方便,大家在正式使用的当然是要分开处理,优化代码结构的
List<NameValuePair> paramsList = new ArrayList<NameValuePair>();
/*
* 添加我们要的参数,这些可以通过查看浏览器中的网络看到,如下面我的截图中看到的一样
* 不论你用的是firebut,httpWatch或者是谷歌自带的查看器也好,都能查看到(后面会推荐辅助工具来查看)
* 要把表单需要的参数都填齐,顺序不影响
*/
paramsList.add(new BasicNameValuePair("Submit", ""));
paramsList.add(new BasicNameValuePair("fowardURL", "http://www.tianya.cn"));
paramsList.add(new BasicNameValuePair("from", ""));
paramsList.add(new BasicNameValuePair("method", "name"));
paramsList.add(new BasicNameValuePair("returnURL", ""));
paramsList.add(new BasicNameValuePair("rmflag", "1"));
paramsList.add(new BasicNameValuePair("__sid", "1#1#1.0#a6c606d9-1efa-4e12-8ad5-3eefd12b8254"));
// 你可以申请一个天涯的账号 并在下两行代码中替换为你的用户名和密码
paramsList.add(new BasicNameValuePair("vwriter", "ifugletest2014"));// 替换为你的用户名
paramsList.add(new BasicNameValuePair("vpassword", "test123456"));// 你的密码
// 将这个参数list设置到post中
post.setEntity(new UrlEncodedFormEntity(paramsList, Consts.UTF_8));
return post;
}
}
执行上面这个Main函数你会得到一下的结果:
我们看到,响应码确实是200,表明成功了,其实这个响应相当于是302,它是需要跳转的,只不过它的跳转写到了body部分的js里面而已。
<script>
location.href="http://passport.tianya.cn:80/online/loginSuccess.jsp?fowardurl=http%3A%2F%2Fwww.tianya.cn%2F94693372&userthird=®Orlogin=%E7%99%BB%E5%BD%95%E4%B8%AD......&t=1410746182629&k=8cd4d967491c44c5eab1097e0f30c054&c=6fc7ebf8d782a07bb06624d9c6fbbf3f";
</script>
它这是一个页面上的跳转
那这个时候如果你使用HttpClient就头疼了(当然也是可以处理的,后面讲到)。如果你使用的是HtmlUnit,整个过程显得简单轻松。
public class TianyaTestByHtmlUnit {
public static void main(String[] args) throws Exception {
WebClient webClient = new WebClient();
// 拿到这个网页
HtmlPage page = webClient.getPage("http://passport.tianya.cn/login.jsp");
// 填入用户名和密码
HtmlInput username = (HtmlInput) page.getElementById("userName");
username.type("ifugletest2014");
HtmlInput password = (HtmlInput) page.getElementById("password");
password.type("test123456");
// 提交
HtmlButton submit = (HtmlButton) page.getElementById("loginBtn");
HtmlPage nextPage = submit.click();
System.out.println(nextPage.asXml());
}
}
这样简单的几行代码就完成了。
三、并行控制 和串行控制
既然HtmlUnit封装了那么多的底层api和hHttpClient操作,那么它有没有给我们提供自定义各种响应策略和监听整个执行过程的方法呢?,答案是肯定的。由于HtmlUnit提供的监听和控制方法比较多,我说几个大家可能接触比较少,但很有用的方法。其他的类似于:设置CSS有效,设置不抛出JS异常,设置使用SSL安全链接,诸如此类,大家通过webClient.getOptions().set***,就可以设置了,这种大家都比较熟了。
(1)首先来看一下JS错误处理监听机制,我们打开HtmlUnit源码可以看到(该源码位置在JavaScriptEngine类中的handleJavaScriptException方法处)
protected void handleJavaScriptException(final ScriptException scriptException, final boolean triggerOnError) {
// Trigger window.onerror, if it has been set.
final HtmlPage page = scriptException.getPage();
if (triggerOnError && page != null) {
final WebWindow window = page.getEnclosingWindow();
if (window != null) {
final Window w = (Window) window.getScriptObject();
if (w != null) {
try {
w.triggerOnError(scriptException);
}
catch (final Exception e) {
handleJavaScriptException(new ScriptException(page, e, null), false);
}
}
}
}
// 这里尝试去取我们设置的JavaScript错误处理器
final JavaScriptErrorListener javaScriptErrorListener = getWebClient().getJavaScriptErrorListener();
if (javaScriptErrorListener != null) {
javaScriptErrorListener.scriptException(page, scriptException);
}
// Throw a Java exception if the user wants us to.
if (getWebClient().getOptions().isThrowExceptionOnScriptError()) {
throw scriptException;
}
// Log the error; ScriptException instances provide good debug info.
LOG.info("Caught script exception", scriptException);
}
也就是说我们它在发现JS错误的时候会自动去寻找我们是否有处理器,有的话就会用我们设置的处理器来处理,要在webClient里加一个处理器也非常的方便。使用:
webClient.setJavaScriptErrorListener(new 你自己的JavaScriptErrorListener());即可。自己的JavaScriptErrorListener也很好实现,直接继承JavaScriptErrorListener接口即可,然后你就可以在javaScript出错时自行处理,你可以选择分析它的url、修正它的url、重新再获取或者直接忽略等等。有js错误处理器,当然也还有别的了,这一类型的我就只说一个了。为了防止有小白不明白,我还是贴出一个简单的实现好了。
/**
* @author CaiBo
* @date 2014年8月12日 上午12:32:08
* @version $Id: WaiJavaScriptErrorListener.java 3943 2014-08-12 03:54:25Z CaiBo $
*
*/
public class WaiJavaScriptErrorListener implements JavaScriptErrorListener {
public WaiJavaScriptErrorListener() {
}
@Override
public void scriptException(HtmlPage htmlPage, ScriptException scriptException) {
// TODO Auto-generated method stub
}
@Override
public void timeoutError(HtmlPage htmlPage, long allowedTime, long executionTime) {
// TODO Auto-generated method stub
}
@Override
public void malformedScriptURL(HtmlPage htmlPage, String url, MalformedURLException malformedURLException) {
// TODO Auto-generated method stub
}
@Override
public void loadScriptError(HtmlPage htmlPage, URL scriptUrl, Exception exception) {
// TODO Auto-generated method stub
}
public static void main(String[] args) {
WebClient webClient = new WebClient();
webClient.setJavaScriptErrorListener(new WaiJavaScriptErrorListener());
}
}
Main方法处实现了JS错误自定义处理的webClient
(2)链接响应监听
很多时候我们想看看HtmlUnit到底去拿了什么东西,或者说我想对它拿的东西过滤一下,再或者我想把它拿到的某些东西存起来,那这个时候响应监听就很必要了。比如说一个最简单的响应监听。
/**
* @author CaiBo
* @date 2014年9月15日 上午10:59:30
* @version $Id$
*
*/
public class SimpleConectionListener extends FalsifyingWebConnection {
private static final Logger LOGGER = LoggerFactory.getLogger(SimpleConectionListener.class);
public SimpleConectionListener(WebClient webClient) throws IllegalArgumentException {
super(webClient);
}
@Override
public WebResponse getResponse(WebRequest request) throws IOException {
// 得到了这个响应,你想怎么处理就怎么处理了,不多写了
WebResponse response = super.getResponse(request);
String url = response.getWebRequest().getUrl().toString();
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("下载文件链接:" + url);
}
if (check(url)) {
return createWebResponse(response.getWebRequest(), "", "application/javascript", 200, "Ok");
}
return response;
}
private boolean check(String url) {
// TODO 加入你自己的判断什么的
return false;
}
}
这样我们就实现了一个自己的监听器,虽然比较简陋。现在我们把它设置到我们的webClient里面去。
WebClient webClient = new WebClient();
// 如果你好奇这里仅仅传进去没有返回,怎么webClient就改变了,你可以到这个实例化里面看下就明白了
new WebConnectionListener(webClient);
// 这个webClient在上一步之后,已经被监听了
webClient.getPage("someUrl");
结果就如上图所示了。
HtmlUnit还有其他许多并、串行控制方法,统一cookie,统一连接池等等,就不一一叙述了。
四、强大的缓存机制
为什么第一次获取一个网页可能会比较慢,但是第二次来拿就特别快呢?在HtmlUnit源码webClient类中的loadWebResponseFromWebConnection方法中我们可以看到。
final WebResponse fromCache = getCache().getCachedResponse(webRequest);
final WebResponse webResponse;
if (fromCache != null) {
webResponse = new WebResponseFromCache(fromCache, webRequest);
}
else {
try {
webResponse = getWebConnection().getResponse(webRequest);
}
catch (final NoHttpResponseException e) {
return new WebResponse(responseDataNoHttpResponse_, webRequest, 0);
}
getCache().cacheIfPossible(webRequest, webResponse, null);
}
当然,它还有许多别的缓存机制来加快我们的访问速度,减少带宽压力。
劣势:
相对于HttpClient来说,HtmlUnit的优点大致就这么多了,那相对于HttpClient来说,短程距离上(访问量小的情况下),HtmlUnit的性能是不如HttpClient的,这也很容易理解,HtmlUnit把HttpClient封装了一层嘛,在短程距离行不如HttpClient就很正常了,在具体的业务下,那就要看程序员水平了。
写太长我自己容易疏忽,大家看着也累,所以第一篇就只谈一下HtmlUnit的优势和劣势了,下一篇将讲述HttpClient的优势和劣势,之后再对他们进行详细比较以及介绍技巧。
HttpClient 是 Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包。它采用是大家再熟悉不过的Scoket编程,我们在HttpClientConnectionOperator类中可以看到。
<span style="font-size:14px;"> public void connect(
final ManagedHttpClientConnection conn,
final HttpHost host,
final InetSocketAddress localAddress,
final int connectTimeout,
final SocketConfig socketConfig,
final HttpContext context) throws IOException {
// 它先去找找这个Socket工厂,看看有没有
final Lookup<ConnectionSocketFactory> registry = getSocketFactoryRegistry(context);
final ConnectionSocketFactory sf = registry.lookup(host.getSchemeName());
if (sf == null) {
throw new UnsupportedSchemeException(host.getSchemeName() +
" protocol is not supported");
}
// InetAddress这货也在。。。
final InetAddress[] addresses = this.dnsResolver.resolve(host.getHostName());
final int port = this.schemePortResolver.resolve(host);
for (int i = 0; i < addresses.length; i++) {
final InetAddress address = addresses[i];
final boolean last = i == addresses.length - 1;
Socket sock = sf.createSocket(context);
sock.setReuseAddress(socketConfig.isSoReuseAddress());
conn.bind(sock);
final InetSocketAddress remoteAddress = new InetSocketAddress(address, port);
if (this.log.isDebugEnabled()) {
this.log.debug("Connecting to " + remoteAddress);
}
try {
sock.setSoTimeout(socketConfig.getSoTimeout());
// 这里就是它打开这个链接了,建立链接之后就可以接收流了,然后HttpClient再用自己的流接收方式接收进去。。。
sock = sf.connectSocket(
connectTimeout, sock, host, remoteAddress, localAddress, context);
sock.setTcpNoDelay(socketConfig.isTcpNoDelay());
sock.setKeepAlive(socketConfig.isSoKeepAlive());
final int linger = socketConfig.getSoLinger();
if (linger >= 0) {
sock.setSoLinger(linger > 0, linger);
}
conn.bind(sock);
if (this.log.isDebugEnabled()) {
this.log.debug("Connection established " + conn);
}
return;
} catch (final SocketTimeoutException ex) {
if (last) {
throw new ConnectTimeoutException(ex, host, addresses);
}
} catch (final ConnectException ex) {
if (last) {
final String msg = ex.getMessage();
if ("Connection timed out".equals(msg)) {
throw new ConnectTimeoutException(ex, host, addresses);
} else {
throw new HttpHostConnectException(ex, host, addresses);
}
}
}
if (this.log.isDebugEnabled()) {
this.log.debug("Connect to " + remoteAddress + " timed out. " +
"Connection will be retried using another IP address");
}
}
}
</span>
就是说HttpClient和URLConnection一样是通过Socket编程来实现网络通信的,相比来说当然是JDK的东西效率什么的更高了,但是我们选择前者,其实就是主要原因就是因为----懒!HttpClient是对Socket/HTTP协议恰到好处的封装,它不像HtmlUnit那样高度,也不像URLConnection用起来比较麻烦,它兼有简单、强扩展等特性,所以和它同Apache Jakarta开发项目组的HtmlUnit也采用了HttpClient。
优点(从百科中说的优点来看):
1、实现了所有 HTTP 的方法(GET,POST,PUT,HEAD 等):实现倒是实现了,笔者就用过GET、POST、PUT,其它的可能用的比较少了。现在Java Servlet服务器不是整天都GET、POST的,倒也不是特别关心其它的方式
2、支持自动转向:这句话说的也点坑,因为它其实是想说,支持200以下的响应码自动向前,这点可以参考HttpRequestExecutor类中的doReceiveResponse方法。
protected HttpResponse doReceiveResponse(
final HttpRequest request,
final HttpClientConnection conn,
final HttpContext context) throws HttpException, IOException {
// 他们老喜欢把这个检测写成Args类了。。。。
Args.notNull(request, "HTTP request");
Args.notNull(conn, "Client connection");
Args.notNull(context, "HTTP context");
HttpResponse response = null;
int statusCode = 0;
// 这里开始接收响应(路由已经在MainClientExec中的establishRoute建立并连接)
// 它判断了下状态码是不是小于200,小于200就要继续接收
while (response == null || statusCode < HttpStatus.SC_OK) {
response = conn.receiveResponseHeader();
if (canResponseHaveBody(request, response)) {
conn.receiveResponseEntity(response);
}
statusCode = response.getStatusLine().getStatusCode();
} // while intermediate response
return response;
}
最开始的时候我还以为是支持自动跳转,就像上一篇文章中的例子一样。
3、支持 HTTPS 协议:HttpClient对SSL的支持是比较全面的,最简单的:
private static HttpClient getSSLInsecureClient() throws Exception {
43. // 建立一个认证上下文,认可所有安全链接,当然,这是因为我们仅仅是测试,实际中认可所有安全链接是危险的
44. SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() {
45. public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
46. return true;
47. }
48. }).build();
49. SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext);
50. return HttpClients.custom().//
51. setSSLSocketFactory(sslsf)//
52. // .setProxy(new HttpHost("127.0.0.1", 8888))
53. .build();
54. }
HttpClient还支持各种各样的证书验证方式,还有服务器认证,看这个就可以了:http://baike.baidu.com/view/2476238.htm?fr=aladdin
4、支持代理服务器:支持代理,就实用的,你可以把HttpClient的代理设置为Filder(一款功能非常强大的网络监听软件,WebDebugger),这样所有的HttpClient发出的请求都会被Filder所接收和管理,这是在代码测试阶段一个非常好的方式。