JAVA抓取通過JS渲染的網站(動態)網頁數據
使用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(" ", ""); 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(); } } }
1.HtmlUnit簡要介紹
HtmlUnit是一款java的無界面瀏覽器程序庫。它模擬HTML文檔,並提供相應的API,允許您調用頁面,填寫表單,點擊鏈接等操作,就像您在“正常”瀏覽器中做的一樣。它有相當不錯的JavaScript支持(還在不斷改進),甚至能夠處理相當復雜的AJAX庫,模擬Chrome,Firefox或Internet Explorer取決於使用的配置。它通常用於測試目的或從網站檢索信息。
HtmlUnit不是一個通用的單元測試框架。它是一種模擬瀏覽器以用於測試目的的方法,並且旨在用於另一個測試框架(如JUnit或TestNG)中。有關簡介,請參閱文檔“HtmlUnit入門”。HtmlUnit用作不同的開源工具,如Canoo WebTest,JWebUnit,WebDriver,JSFUnit,WETATOR,Celerity,Spring MVC Test HtmlUnit作為底層的“瀏覽器”。
HtmlUnit最初由Gargoyle Software的Mike Bowler編寫,並根據Apache 2許可證發布。從那時起,它已經收到了許多來自其他開發商的貢獻,今天也會得到他們的幫助。
幾年前在做一個購物網站的數據抓取工作中,偶然的機會邂逅了HtmlUnit了。記得當時怎么也捉取不到頁面上的價格數據,而httpfox也追蹤不到價格數據的URL,正當我一愁莫展的時個,HtmlUnit出現並幫我解決了問題。所以今天我要說聲謝謝,也將HtmlUnit推薦給大家。
官網地址:htmlunit@sourceforge
2.HtmlUnit動一小手
下載所有Jar文件到類路徑中,所有Jar文件可在地址:HtmlUnitJars 下載到。或者利用Maven依賴,如下:
<dependency> <groupId>net.sourceforge.htmlunit</groupId> <artifactId>htmlunit</artifactId> <version>2.25</version> </dependency>
下面以某購物網站列表頁為例,做一個基本的數據捉取的例子,輸入的條件為網站的列表頁URL地址,輸出的結果為該頁面所有產品的名稱,價格和評論數並以//做為分隔。不包括推廣產品。
例如輸入:京東(JD.COM)-正品低價、品質保障、配送及時、輕松購物!
結果輸出(多條):
魅族 魅藍metal 16GB 灰色 電信4G手機 雙卡雙待//¥699.00 ¥699.00//已有5.2萬+人評價
OPPO R9s 全網通4G+64G 雙卡雙待手機 玫瑰金//¥2799.00 ¥2799.00//已有8.6萬+人評價
示例程序代碼:
package com.du42.htmlunit; import com.gargoylesoftware.htmlunit.BrowserVersion; import com.gargoylesoftware.htmlunit.NicelyResynchronizingAjaxController; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.html.DomNode; import com.gargoylesoftware.htmlunit.html.DomNodeList; import com.gargoylesoftware.htmlunit.html.HtmlDivision; import com.gargoylesoftware.htmlunit.html.HtmlPage; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import java.util.List; /** * Created by iFat3 on 2017/3/15 42du.cn. */ public class JDListTools { public static void getItems() throws Exception { WebClient webClient = new WebClient(BrowserVersion.BEST_SUPPORTED); webClient.getOptions().setCssEnabled(false); webClient.getOptions().setJavaScriptEnabled(true); webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); webClient.getOptions().setThrowExceptionOnScriptError(false); webClient.waitForBackgroundJavaScript(600*1000); webClient.setAjaxController(new NicelyResynchronizingAjaxController()); HtmlPage page = webClient.getPage("https://list.jd.com/list.html?cat=9987,653,655"); List<HtmlDivision> divs = (List) page.getByXPath("//div[@id='plist']//ul//li[@class='gl-item']//div[@class='gl-i-wrap j-sku-item']"); for(HtmlDivision div :divs) { DomNodeList<DomNode> childs = div.getChildNodes(); String name = ""; String price = ""; String comments = ""; for(DomNode dn : childs) { NamedNodeMap map = dn.getAttributes(); Node node = map.getNamedItem("class"); if(node != null) { String value = node.getNodeValue(); if(value.contains("p-name")) { name = dn.asText(); } else if(value.contains("p-price")) { price = dn.asText(); } else if(value.contains("p-commit"))