Java爬蟲學習:使用HtmlUnit獲取html頁面

使用HtmlUnit獲取html頁面

HtmlUnit簡介

  • 官網介紹
HtmlUnit is a "GUI-Less browser for Java programs". It models HTML documents and provides an API that allows you to invoke pages, fill out forms, click links, etc... just like you do in your "normal" browser.

It has fairly good JavaScript support (which is constantly improving) and is able to work even with quite complex AJAX libraries, simulating Chrome, Firefox or Internet Explorer depending on the configuration used.

It is typically used for testing purposes or to retrieve information from web sites.

HtmlUnit is not a generic unit testing framework. It is specifically a way to simulate a browser for testing purposes and is intended to be used within another testing framework such as JUnit or TestNG. Refer to the document "Getting Started with HtmlUnit" for an introduction.

HtmlUnit is used as the underlying "browser" by different Open Source tools like Canoo WebTest, JWebUnit, WebDriver, JSFUnit, WETATOR, Celerity, Spring MVC Test HtmlUnit, ...

HtmlUnit was originally written by Mike Bowler of Gargoyle Software and is released under the Apache 2 license. Since then, it has received many contributions from other developers, and would not be where it is today without their assistance.
  • 中文翻譯

HtmlUnit是一個無界面瀏覽器Java程序。它爲HTML文檔建模,提供了調用頁面、填寫表單、單擊鏈接等操作的API。就跟你在瀏覽器裏做的操作一樣。

HtmlUnit不錯的JavaScript支持(不斷改進),甚至可以使用相當複雜的AJAX庫,根據配置的不同模擬Chrome、Firefox或Internet Explorer等瀏覽器。

HtmlUnit通常用於測試或從web站點檢索信息。

HtmlUnit使用場景

  • httpClient的侷限性

對於使用java實現的網頁爬蟲程序,我們一般可以使用apache的HttpClient組件進行HTML頁面信息的獲取,HttpClient實現的http請求返回的響應一般是純文本的document頁面,即最原始的html頁面。

對於一個靜態的html頁面來說,使用httpClient足夠將我們所需要的信息爬取出來了。但是對於現在越來越多的動態網頁來說,更多的數據是通過異步JS代碼獲取並渲染到的,最開始的html頁面是不包含這部分數據的。

這裏寫圖片描述

上圖我們所見到的網頁,在最初的document加載完成之後,並不會看到紅框中的數據列表。瀏覽器通過執行異步JS請求,將獲取到的動態數據,渲染到最初的document頁面中,才最終變成了我們看到的網頁。而對於這部分需要執行JS代碼獲取的數據,httpClient就顯得無能爲力了。雖然我們可以通過研究拿到JS執行的請求路徑再用java代碼獲取我們需要的這部分數據,且不說我們能不能夠從JS腳本中分析到這個請求路徑和請求參數,光是分析這部分源碼的代價就已經很高了。

  • HtmlUnit來解決

通過上面的介紹,我們瞭解了現在很大一部分動態網頁,展現的數據都是通過異步JS請求獲取,然後再通過JS對頁面進行渲染得到的。那我們是不是可以進行這麼一個假設,假設我們的爬蟲程序模擬了一個瀏覽器,在獲取html頁面之後,像瀏覽器一樣執行異步JS代碼,等到JS將html頁面渲染完成之後,就可以愉快的獲取頁面上的節點信息了。那麼有沒有這樣的java程序呢?

答案是有的。

HtmlUnit就是這麼一個程序庫,用來做出了界面展示意外所有的異步工作。由於沒有了展示這一塊耗時的工作,HtmlUnit加載完成一個完整的網頁要比實際的瀏覽器塊多了。並且根據不同配置,HtmlUnit可以模擬市面上常用的瀏覽器如Chrome、Firefox、IE瀏覽器等。

通過HtmlUnit庫,加載一個完整的Html頁面(圖片視頻除外),然後就可以將其轉換成我們常用的字串格式,用其他工具如Jsoup來獲取其中的元素了。當然也可以直接在HtmlUnit提供的對象中獲取網頁元素,甚至是操作如按鈕、表單等控件。除了不能像可見瀏覽器一樣用鼠標鍵盤瀏覽網頁之外,我們可以用HtmlUnit來模擬操作其他的一切操作,像登錄網站,撰寫博客等等都是可以完成的。當然網頁內容爬取是最簡單的一個應用了。

HtmlUnit使用方法

1.新建maven工程,添加HtmlUnit依賴:

<dependencies>
    <dependency>
        <groupId>net.sourceforge.htmlunit</groupId>
        <artifactId>htmlunit</artifactId>
        <version>2.27</version>
    </dependency>
</dependencies>

2.新建一個Junit TestCase來嘗試一下程序庫的使用

程序代碼註釋如下:

package xuyihao.util.depend;

import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.junit.Test;

import java.util.List;

/**
 * Created by xuyh at 2017/11/6 14:03.
 */
public class HtmlUtilTest {
    @Test
    public void test() {
        final WebClient webClient = new WebClient(BrowserVersion.CHROME);//新建一個模擬谷歌Chrome瀏覽器的瀏覽器客戶端對象

        webClient.getOptions().setThrowExceptionOnScriptError(false);//當JS執行出錯的時候是否拋出異常, 這裏選擇不需要
        webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);//當HTTP的狀態非200時是否拋出異常, 這裏選擇不需要
        webClient.getOptions().setActiveXNative(false);
        webClient.getOptions().setCssEnabled(false);//是否啓用CSS, 因爲不需要展現頁面, 所以不需要啓用
        webClient.getOptions().setJavaScriptEnabled(true); //很重要,啓用JS
        webClient.setAjaxController(new NicelyResynchronizingAjaxController());//很重要,設置支持AJAX

        HtmlPage page = null;
        try {
            page = webClient.getPage("http://ent.sina.com.cn/film/");//嘗試加載上面圖片例子給出的網頁
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            webClient.close();
        }

        webClient.waitForBackgroundJavaScript(30000);//異步JS執行需要耗時,所以這裏線程要阻塞30秒,等待異步JS執行結束

        String pageXml = page.asXml();//直接將加載完成的頁面轉換成xml格式的字符串

        //TODO 下面的代碼就是對字符串的操作了,常規的爬蟲操作,用到了比較好用的Jsoup庫

        Document document = Jsoup.parse(pageXml);//獲取html文檔
        List<Element> infoListEle = document.getElementById("feedCardContent").getElementsByAttributeValue("class", "feed-card-item");//獲取元素節點等
        infoListEle.forEach(element -> {
            System.out.println(element.getElementsByTag("h2").first().getElementsByTag("a").text());
            System.out.println(element.getElementsByTag("h2").first().getElementsByTag("a").attr("href"));
        });
    }
}

上面的例子將獲取到的頁面中消息列表的標題和超鏈接URL打印到控制檯,操作HTML文檔的庫是Jsoup,需要添加依賴:

<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.8.3</version>
</dependency>

經過三十秒的等待,控制檯輸出的結果是這樣的:

十一月 06, 2017 2:17:05 下午 com.gargoylesoftware.htmlunit.IncorrectnessListenerImpl notify
警告: Obsolete content type encountered: 'application/x-javascript'.
十一月 06, 2017 2:17:06 下午 com.gargoylesoftware.htmlunit.javascript.StrictErrorReporter runtimeError
嚴重: runtimeError: message=[An invalid or illegal selector was specified (selector: '*,:x' error: Invalid selector: :x).] sourceName=[http://n.sinaimg.cn/lib/core/core.js] line=[1] lineSource=[null] lineOffset=[0]
十一月 06, 2017 2:17:06 下午 com.gargoylesoftware.htmlunit.IncorrectnessListenerImpl notify
警告: Obsolete content type encountered: 'application/x-javascript'.
2017-11-06 14:17:11.003:INFO::JS executor for com.gargoylesoftware.htmlunit.WebClient@618c5d94: Logging initialized @7179ms to org.eclipse.jetty.util.log.StdErrLog
十一月 06, 2017 2:17:11 下午 com.gargoylesoftware.htmlunit.javascript.host.WebSocket run
嚴重: WS connect error
java.util.concurrent.ExecutionException: org.eclipse.jetty.websocket.api.UpgradeException: 0 null
    at java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:357)
    at java.util.concurrent.CompletableFuture.get(CompletableFuture.java:1895)
    at com.gargoylesoftware.htmlunit.javascript.host.WebSocket$1.run(WebSocket.java:151)
    at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:672)
    at org.eclipse.jetty.util.thread.QueuedThreadPool$2.run(QueuedThreadPool.java:590)
    at java.lang.Thread.run(Thread.java:748)
Caused by: org.eclipse.jetty.websocket.api.UpgradeException: 0 null
    at org.eclipse.jetty.websocket.client.WebSocketUpgradeRequest.onComplete(WebSocketUpgradeRequest.java:513)
    at org.eclipse.jetty.client.ResponseNotifier.notifyComplete(ResponseNotifier.java:193)
    at org.eclipse.jetty.client.ResponseNotifier.notifyComplete(ResponseNotifier.java:185)
    at org.eclipse.jetty.client.HttpExchange.notifyFailureComplete(HttpExchange.java:269)
    at org.eclipse.jetty.client.HttpExchange.abort(HttpExchange.java:240)
    at org.eclipse.jetty.client.HttpConversation.abort(HttpConversation.java:141)
    at org.eclipse.jetty.client.HttpRequest.abort(HttpRequest.java:748)
    at org.eclipse.jetty.client.HttpDestination.abort(HttpDestination.java:444)
    at org.eclipse.jetty.client.HttpDestination.failed(HttpDestination.java:224)
    at org.eclipse.jetty.client.AbstractConnectionPool$1.failed(AbstractConnectionPool.java:122)
    at org.eclipse.jetty.util.Promise$Wrapper.failed(Promise.java:136)
    at org.eclipse.jetty.client.HttpClient$1$1.failed(HttpClient.java:588)
    at org.eclipse.jetty.client.AbstractHttpClientTransport.connectFailed(AbstractHttpClientTransport.java:154)
    at org.eclipse.jetty.client.AbstractHttpClientTransport$ClientSelectorManager.connectionFailed(AbstractHttpClientTransport.java:199)
    at org.eclipse.jetty.io.ManagedSelector$Connect.failed(ManagedSelector.java:655)
    at org.eclipse.jetty.io.ManagedSelector$Connect.access$1300(ManagedSelector.java:622)
    at org.eclipse.jetty.io.ManagedSelector$1.failed(ManagedSelector.java:364)
    at org.eclipse.jetty.io.ManagedSelector$CreateEndPoint.run(ManagedSelector.java:604)
    ... 3 more
Caused by: java.lang.NullPointerException
    at org.eclipse.jetty.io.ssl.SslClientConnectionFactory.newConnection(SslClientConnectionFactory.java:59)
    at org.eclipse.jetty.client.AbstractHttpClientTransport$ClientSelectorManager.newConnection(AbstractHttpClientTransport.java:191)
    at org.eclipse.jetty.io.ManagedSelector.createEndPoint(ManagedSelector.java:420)
    at org.eclipse.jetty.io.ManagedSelector.access$1600(ManagedSelector.java:61)
    at org.eclipse.jetty.io.ManagedSelector$CreateEndPoint.run(ManagedSelector.java:599)
    ... 3 more

十一月 06, 2017 2:17:16 下午 com.gargoylesoftware.htmlunit.IncorrectnessListenerImpl notify
警告: Obsolete content type encountered: 'application/x-javascript'.
十一月 06, 2017 2:17:21 下午 com.gargoylesoftware.htmlunit.IncorrectnessListenerImpl notify
警告: Obsolete content type encountered: 'text/javascript'.
十一月 06, 2017 2:17:21 下午 com.gargoylesoftware.htmlunit.IncorrectnessListenerImpl notify
警告: Obsolete content type encountered: 'text/javascript'.
時隔17年重溫《EUREKA》 宮崎葵:這次哭得很兇
http://ent.sina.com.cn/m/f/2017-11-06/doc-ifynmzrs7411439.shtml
模式單一成審美疲勞 超級英雄電影該如何突圍?
http://ent.sina.com.cn/m/f/2017-11-06/doc-ifynmnae2196060.shtml
組圖:《天生不對》首映 薛凱琪不規則紅裙優雅可人 13
http://slide.ent.sina.com.cn/film/slide_4_704_247725.html
電影資料館達成線上售票合作 影迷不必排隊買票
http://ent.sina.com.cn/m/c/2017-11-06/doc-ifynmvuq8917282.shtml
組圖:詹妮弗加納去教堂路遇好友 白裙清新心情靚 4
http://slide.ent.sina.com.cn/film/h/slide_4_704_247702.html
《東方快車》發幕後特輯 唯美復古凸顯品質
http://ent.sina.com.cn/m/f/2017-11-06/doc-ifynnnsc7188105.shtml
組圖:梅根福克斯穿緊身衣身材火辣 踩拖鞋抱瑜伽墊 4
http://slide.ent.sina.com.cn/film/slide_4_704_247699.html

忽略HtmlUnit執行時候的報錯信息,可以看到最後還是成功的將結果打印了出來了。

3.編寫工具類

嘗試了一下HtmlUnit加載網頁並解析之後,我們可以編寫一個工具類爲之後的爬蟲程序的使用鋪路了,代碼如下:


import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlPage;

/**
 * <pre>
 * Http工具,包含:
 * 高級http工具(使用net.sourceforge.htmlunit獲取完整的html頁面,即完成後臺js代碼的運行)
 * </pre>
 * Created by xuyh at 2017/7/17 19:08.
 */
public class HttpUtils {
    /**
     * 請求超時時間,默認20000ms
     */
    private int timeout = 20000;
    /**
     * 等待異步JS執行時間,默認20000ms
     */
    private int waitForBackgroundJavaScript = 20000;

    private static HttpUtils httpUtils;

    private HttpUtils() {
    }

    /**
     * 獲取實例
     *
     * @return
     */
    public static HttpUtils getInstance() {
        if (httpUtils == null)
            httpUtils = new HttpUtils();
        return httpUtils;
    }

    public int getTimeout() {
        return timeout;
    }

    /**
     * 設置請求超時時間
     *
     * @param timeout
     */
    public void setTimeout(int timeout) {
        this.timeout = timeout;
    }

    public int getWaitForBackgroundJavaScript() {
        return waitForBackgroundJavaScript;
    }

    /**
     * 設置獲取完整HTML頁面時等待異步JS執行的時間
     *
     * @param waitForBackgroundJavaScript
     */
    public void setWaitForBackgroundJavaScript(int waitForBackgroundJavaScript) {
        this.waitForBackgroundJavaScript = waitForBackgroundJavaScript;
    }

    /**
     * 將網頁返回爲解析後的文檔格式
     * 
     * @param html
     * @return
     * @throws Exception
     */
    public static Document parseHtmlToDoc(String html) throws Exception {
        return removeHtmlSpace(html);
    }

    private static Document removeHtmlSpace(String str) {
        Document doc = Jsoup.parse(str);
        String result = doc.html().replace("&nbsp;", "");
        return Jsoup.parse(result);
    }

    /**
     * 獲取頁面文檔字串(等待異步JS執行)
     *
     * @param url 頁面URL
     * @return
     * @throws Exception
     */
    public String getHtmlPageResponse(String url) throws Exception {
        String result = "";

        final WebClient webClient = new WebClient(BrowserVersion.CHROME);

        webClient.getOptions().setThrowExceptionOnScriptError(false);//當JS執行出錯的時候是否拋出異常
        webClient.getOptions().setThrowExceptionOnFailingStatusCode(false);//當HTTP的狀態非200時是否拋出異常
        webClient.getOptions().setActiveXNative(false);
        webClient.getOptions().setCssEnabled(false);//是否啓用CSS
        webClient.getOptions().setJavaScriptEnabled(true); //很重要,啓用JS
        webClient.setAjaxController(new NicelyResynchronizingAjaxController());//很重要,設置支持AJAX

        webClient.getOptions().setTimeout(timeout);//設置“瀏覽器”的請求超時時間
        webClient.setJavaScriptTimeout(timeout);//設置JS執行的超時時間

        HtmlPage page;
        try {
            page = webClient.getPage(url);
        } catch (Exception e) {
            webClient.close();
            throw e;
        }
        webClient.waitForBackgroundJavaScript(waitForBackgroundJavaScript);//該方法阻塞線程

        result = page.asXml();
        webClient.close();

        return result;
    }

    /**
     * 獲取頁面文檔Document對象(等待異步JS執行)
     *
     * @param url 頁面URL
     * @return
     * @throws Exception
     */
    public Document getHtmlPageResponseAsDocument(String url) throws Exception {
        return parseHtmlToDoc(getHtmlPageResponse(url));
    }
}

可以通過這樣的方式調用本工具:


import org.jsoup.nodes.Document;
import org.junit.Test;


public class HttpUtilsTest {
    private static final String TEST_URL = "http://www.google.com/";

    @Test
    public void testGetHtmlPageResponse() {
        HttpUtils httpUtils = HttpUtils.getInstance();
        httpUtils.setTimeout(30000);
        httpUtils.setWaitForBackgroundJavaScript(30000);
        try {
            String htmlPageStr = httpUtils.getHtmlPageResponse(TEST_URL);
            //TODO
            System.out.println(htmlPageStr);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Test
    public void testGetHtmlPageResponseAsDocument() {
        HttpUtils httpUtils = HttpUtils.getInstance();
        httpUtils.setTimeout(30000);
        httpUtils.setWaitForBackgroundJavaScript(30000);
        try {
            Document document = httpUtils.getHtmlPageResponseAsDocument(TEST_URL);
            //TODO
            System.out.println(document);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

源碼地址

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