HttpClient和HtmlUnit的比較總結以及使用技巧(一)

大家在做爬蟲、網頁採集、通過網頁自動寫入數據時基本上都接觸過這兩個組件(權且稱之爲組件吧),網上入門資料已經很多了,我想從實際的應用角度談談我對於這兩個組件的看法,並記錄在博客中,以便日後翻閱,歡迎大家批評指正。

本文主要比較兩者的優劣性以及介紹應用中的使用技巧,推薦一些入門資料以及非常實用的輔助工具,希望能對大家有所幫助。

大家有任何疑問或者建議希望留言給我,一起交流學習。

下面我們首先來看下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=&regOrlogin=%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的優勢和劣勢,之後再對他們進行詳細比較以及介紹技巧。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章