http://www.ibm.com/developerworks/cn/opensource/os-cn-crawler/html
http://blog.csdn.net/dancen/article/details/7570911java
HttpClient 與 HtmlParser 簡介
本小結簡單的介紹一下 HttpClinet 和 HtmlParser 兩個開源的項目,以及他們的網站和提供下載的地址。node
HttpClient 簡介
HTTP 協議是如今的因特網最重要的協議之一。除了 WEB 瀏覽器以外, WEB 服務,基於網絡的應用程序以及日益增加的網絡計算不斷擴展着 HTTP 協議的角色,使得愈來愈多的應用程序須要 HTTP 協議的支持。雖然 JAVA 類庫 .net 包提供了基本功能,來使用 HTTP 協議訪問網絡資源,可是其靈活性和功能遠不能知足不少應用程序的須要。而 Jakarta Commons HttpClient 組件尋求提供更爲靈活,更加高效的 HTTP 協議支持,簡化基於 HTTP 協議的應用程序的建立。 HttpClient 提供了不少的特性,支持最新的 HTTP 標準,能夠訪問這裏瞭解更多關於 HttpClinet 的詳細信息。目前有不少的開源項目都用到了 HttpClient 提供的 HTTP功能,登錄網址能夠查看這些項目。本文中使用 HttpClinet 提供的類庫來訪問和下載 Internet上面的網頁,在後續部分會詳細介紹到其提供的兩種請求網絡資源的方法: Get 請求和 Post 請求。Apatche 提供免費的 HTTPClien t源碼和 JAR 包下載,能夠登錄這裏 下載最新的HttpClient 組件。筆者使用的是 HttpClient3.1。算法
HtmlParser 簡介
當今的 Internet 上面有數億記的網頁,愈來愈多應用程序將這些網頁做爲分析和處理的數據對象。這些網頁多爲半結構化的文本,有着大量的標籤和嵌套的結構。當咱們本身開發一些處理網頁的應用程序時,會想到要開發一個單獨的網頁解析器,這一部分的工做一定須要付出至關的精力和時間。事實上,作爲 JAVA 應用程序開發者, HtmlParser 爲其提供了強大而靈活易用的開源類庫,大大節省了寫一個網頁解析器的開銷。 HtmlParser 是 http://sourceforge.net 上活躍的一個開源項目,它提供了線性和嵌套兩種方式來解析網頁,主要用於 html 網頁的轉換(Transformation) 以及網頁內容的抽取 (Extraction)。HtmlParser 有以下一些易於使用的特性:過濾器 (Filters),訪問者模式 (Visitors),處理自定義標籤以及易於使用的 JavaBeans。正如 HtmlParser 首頁所說:它是一個快速,健壯以及嚴格測試過的組件;以它設計的簡潔,程序運行的速度以及處理 Internet 上真實網頁的能力吸引着愈來愈多的開發者。 本文中就是利用HtmlParser 裏提取網頁裏的連接,實現簡易爬蟲裏的關鍵部分。HtmlParser 最新的版本是HtmlParser1.6,能夠登錄這裏下載其源碼、 API 參考文檔以及 JAR 包。
開發環境的搭建
筆者所使用的開發環境是 Eclipse Europa,此開發工具能夠在 www.eclipse.org 免費的下載;JDK是1.6,你也能夠在 www.java.sun.com 站點下載,而且在操做系統中配置好環境變量。在 Eclipse 中建立一個 JAVA 工程,在工程的 Build Path 中導入下載的Commons-httpClient3.1.Jar,htmllexer.jar 以及 htmlparser.jar 文件。
圖 1. 開發環境搭建
HttpClient 基本類庫使用
HttpClinet 提供了幾個類來支持 HTTP 訪問。下面咱們經過一些示例代碼來熟悉和說明這些類的功能和使用。 HttpClient 提供的 HTTP 的訪問主要是經過 GetMethod 類和 PostMethod 類來實現的,他們分別對應了 HTTP Get 請求與 Http Post 請求。apache
GetMethod
使用 GetMethod 來訪問一個 URL 對應的網頁,須要以下一些步驟。
生成一個 HttpClinet 對象並設置相應的參數。
生成一個 GetMethod 對象並設置響應的參數。
用 HttpClinet 生成的對象來執行 GetMethod 生成的 Get 方法。
處理響應狀態碼。
若響應正常,處理 HTTP 響應內容。
釋放鏈接。
清單 1 的代碼展現了這些步驟,其中的註釋對代碼進行了較詳細的說明。
清單 1.數組
- HttpClient httpClient=new HttpClient();
-
- httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(5000);
-
-
- GetMethod getMethod=new GetMethod(url);
-
- getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT,5000);
-
- getMethod.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,
- new DefaultHttpMethodRetryHandler());
-
-
- try{
- int statusCode = httpClient.executeMethod(getMethod);
-
- if (statusCode != HttpStatus.SC_OK)
- {
- System.err.println("Method failed: "+ getMethod.getStatusLine());
- }
-
-
-
- Header[] headers=getMethod.getResponseHeaders();
- for(Header h: headers)
- System.out.println(h.getName()+" "+h.getValue());*/
-
- byte[] responseBody = getMethod.getResponseBody();
- System.out.println(new String(responseBody));
-
- InputStream response = getMethod.getResponseBodyAsStream();
- …
- }
- catch (HttpException e)
- {
-
- System.out.println("Please check your provided http address!");
- e.printStackTrace();
- }
- catch (IOException e)
- {
-
- e.printStackTrace();
- } finally {
-
- getMethod.releaseConnection();
- }
這裏值得注意的幾個地方是:
設置鏈接超時和請求超時,這兩個超時的意義不一樣,須要分別設置。
響應狀態碼的處理。
返回的結果能夠爲字節數組,也能夠爲 InputStream,然後者在網頁內容數據量較大的時候推薦使用。
在處理返回結果的時候能夠根據本身的須要,進行相應的處理。如筆者是須要保存網頁
到本地,所以就能夠寫一個 saveToLocaleFile(byte[] data, String filePath) 的方法,將字節數組保存成本地文件。後續的簡易爬蟲部分會有相應的介紹。
PostMethod
PostMethod 方法與 GetMethod 方法的使用步驟大致相同。可是因爲 PostMethod 使用的是HTTP 的 Post 請求,於是請求參數的設置與 GetMethod 有所不一樣。在 GetMethod 中,請求的參數直接寫在 URL 裏,通常以這樣形式出現:http://hostname:port//file?name1=value1&name2=value …。請求參數是 name,value 對。好比我想獲得百度搜索「Thinking In Java」的結果網頁,就可使 GetMethod 的構造方法中的 url 爲:http://www.baidu.com/s?wd=Thinking+In+Java 。而 PostMethod 則能夠模擬網頁裏表單提交的過程,經過設置表單裏 post 請求參數的值,來動態的得到返回的網頁結果。清單 2 中的代碼展現瞭如何建立一個 Post 對象,並設置相應的請求參數。
清單2瀏覽器
- PostMethod postMethod = new PostMethod("http://dict.cn/");
- postMethod.setRequestBody(new NameValuePair[]{new NameValuePair("q","java")});
HtmlParser 基本類庫使用
HtmlParser 提供了強大的類庫來處理 Internet 上的網頁,能夠實現對網頁特定內容的提取和修改。下面經過幾個例子來介紹 HtmlParser 的一些使用。這些例子其中的代碼,有部分用在了後面介紹的簡易爬蟲中。如下全部的代碼和方法都在在類 HtmlParser.Test.java 裏,這是筆者編寫的一個用來測試 HtmlParser 用法的類。
迭代遍歷網頁全部節點
網頁是一個半結構化的嵌套文本文件,有相似 XML 文件的樹形嵌套結構。使用HtmlParser 可讓咱們輕易的迭代遍歷網頁的全部節點。清單 3 展現瞭如何來實現這個功能。
清單 3網絡
- public static void extractKeyWordText(String url, String keyword) {
- try {
-
- Parser parser = new Parser(url);
-
- parser.setEncoding("gb2312");
-
- NodeList list = parser.parse(null);
-
- processNodeList(list, keyword);
- } catch (ParserException e) {
- e.printStackTrace();
- }
- }
-
- private static void processNodeList(NodeList list, String keyword) {
-
- SimpleNodeIterator iterator = list.elements();
- while (iterator.hasMoreNodes()) {
- Node node = iterator.nextNode();
-
- NodeList childList = node.getChildren();
-
- if (null == childList)
- {
-
- String result = node.toPlainTextString();
-
- if (result.indexOf(keyword) != -1)
- System.out.println(result);
- }
-
- else
- {
- processNodeList(childList, keyword);
- }
- }
- }
上面的中有兩個方法:
private static void processNodeList(NodeList list, String keyword)
該方法是用相似深度優先的方法來迭代遍歷整個網頁節點,將那些包含了某個關鍵字的值節點的值打印出來。
public static void extractKeyWordText(String url, String keyword)
該方法生成針對 String 類型的 url 變量表明的某個特定網頁的解析器,調用 1中的方法實現簡單的遍歷。
清單 3 的代碼展現瞭如何迭代全部的網頁,更多的工做能夠在此基礎上展開。好比找到某個特定的網頁內部節點,其實就能夠在遍歷全部的節點基礎上來判斷,看被迭代的節點是否知足特定的須要。數據結構
使用 NodeFilter
NodeFilter 是一個接口,任何一個自定義的 Filter 都須要實現這個接口中的 boolean accept() 方法。若是但願迭代網頁節點的時候保留當前節點,則在節點條件知足的狀況下返回 true;不然返回 false。HtmlParse 裏提供了不少實現了 NodeFilter 接口的類,下面就一些筆者所用到的,以及經常使用的 Filter 作一些介紹:
對 Filter 作邏輯操做的 Fitler 有:AndFilter,NotFilter ,OrFilter,XorFilter。
這些 Filter 來組合不一樣的 Filter,造成知足兩個 Filter 邏輯關係結果的 Filter。
判斷節點的孩子,兄弟,以及父親節點狀況的 Filter 有:HasChildFilter HasParentFilter,HasSiblingFilter。
判斷節點自己狀況的 Filter 有 HasAttributeFilter:判讀節點是否有特定屬性;LinkStringFilter:判斷節點是不是具備特定模式 (pattern) url 的節點;
TagNameFilter:判斷節點是否具備特定的名字;NodeClassFilter:判讀節點是不是某個 HtmlParser 定義好的 Tag 類型。在 org.htmlparser.tags 包下有對應 Html標籤的各類 Tag,例如 LinkTag,ImgeTag 等。
還有其餘的一些 Filter 在這裏不一一列舉了,能夠在 org.htmlparser.filters 下找到。
清單 4 展現瞭如何使用上面提到過的一些 filter 來抽取網頁中的 <a> 標籤裏的 href屬性值,<img> 標籤裏的 src 屬性值,以及 <frame> 標籤裏的 src 的屬性值。
清單4app
- public static void extracLinks(String url) {
- try {
- Parser parser = new Parser(url);
- parser.setEncoding("gb2312");
- NodeFilter frameFilter = new NodeFilter() {
- public boolean accept(Node node) {
- if (node.getText().startsWith("frame src=")) {
- return true;
- } else {
- return false;
- }
- }
- };
- OrFilte rorFilter = new OrFilter(new NodeClassFilter(LinkTag.class), new
- NodeClassFilter(ImageTag.class));
- OrFilter linkFilter = new OrFilter(orFilter, frameFilter);
-
- NodeList list = parser.extractAllNodesThatMatch(linkFilter);
- for (int i = 0; i < list.size(); i++) {
- Node tag = list.elementAt(i);
- if (tag instanceof LinkTag)
- {
- LinkTag link = (LinkTag) tag;
- String linkUrl = link.getLink();
- String text = link.getLinkText();
- System.out.println(linkUrl + "**********" + text);
- }
- else if (tag instanceof ImageTag)
- {
- ImageTag image = (ImageTag) list.elementAt(i);
- System.out.print(image.getImageURL() + "********");
- System.out.println(image.getText());
- }
- else
- {
- String frame = tag.getText();
- int start = frame.indexOf("src=");
- frame = frame.substring(start);
- int end = frame.indexOf(" ");
- if (end == -1)
- end = frame.indexOf(">");
- frame = frame.substring(5, end - 1);
- System.out.println(frame);
- }
- }
- } catch (ParserException e) {
- e.printStackTrace();
- }
- }
簡單強大的 StringBean
若是你想要網頁中去掉全部的標籤後剩下的文本,那就是用 StringBean 吧。如下簡單的代碼能夠幫你解決這樣的問題:
清單5
- StringBean sb = new StringBean();
- sb.setLinks(false);
- sb.setURL(url);
- System.out.println(sb.getStrings());
HtmlParser 提供了強大的類庫來處理網頁,因爲本文旨在簡單的介紹,所以只是將與筆者後續爬蟲部分有關的關鍵類庫進行了示例說明。感興趣的讀者能夠專門來研究一下 HtmlParser 更爲強大的類庫。
簡易爬蟲的實現
HttpClient 提供了便利的 HTTP 協議訪問,使得咱們能夠很容易的獲得某個網頁的源碼並保存在本地;HtmlParser 提供瞭如此簡便靈巧的類庫,能夠從網頁中便捷的提取出指向其餘網頁的超連接。筆者結合這兩個開源包,構建了一個簡易的網絡爬蟲。
爬蟲 (Crawler) 原理
學過數據結構的讀者都知道有向圖這種數據結構。以下圖所示,若是將網頁當作是圖中的某一個節點,而將網頁中指向其餘網頁的連接當作是這個節點指向其餘節點的邊,那麼咱們很容易將整個 Internet 上的網頁建模成一個有向圖。理論上,經過遍歷算法遍歷該圖,能夠訪問到Internet 上的幾乎全部的網頁。最簡單的遍歷就是寬度優先以及深度優先。如下筆者實現的簡易爬蟲就是使用了寬度優先的爬行策略。
圖 2. 網頁關係的建模圖
簡易爬蟲實現流程
在看簡易爬蟲的實現代碼以前,先介紹一下簡易爬蟲爬取網頁的流程。
圖 3. 爬蟲流程圖
各個類的源碼以及說明
對應上面的流程圖,簡易爬蟲由下面幾個類組成,各個類職責以下:
Crawler.java:爬蟲的主方法入口所在的類,實現爬取的主要流程。
LinkDb.java:用來保存已經訪問的 url 和待爬取的 url 的類,提供url出對入隊操做。
Queue.java: 實現了一個簡單的隊列,在 LinkDb.java 中使用了此類。
FileDownloader.java:用來下載 url 所指向的網頁。
HtmlParserTool.java: 用來抽取出網頁中的連接。
LinkFilter.java:一個接口,實現其 accept() 方法用來對抽取的連接進行過濾。
下面是各個類的源碼,代碼中的註釋有比較詳細的說明。
清單6 Crawler.java
- package com.ie;
-
- import java.util.Set;
- public class Crawler {
-
- private void initCrawlerWithSeeds(String[] seeds)
- {
- for(int i=0;i<seeds.length;i++)
- LinkDB.addUnvisitedUrl(seeds[i]);
- }
-
-
- public void crawling(String[] seeds)
- {
- LinkFilter filter = new LinkFilter(){
-
- public boolean accept(String url) {
- if(url.startsWith("http://www.twt.edu.cn"))
- return true;
- else
- return false;
- }
- };
-
- initCrawlerWithSeeds(seeds);
-
- while(!LinkDB.unVisitedUrlsEmpty()&&LinkDB.getVisitedUrlNum()<=1000)
- {
-
- String visitUrl=LinkDB.unVisitedUrlDeQueue();
- if(visitUrl==null)
- continue;
- FileDownLoader downLoader=new FileDownLoader();
-
- downLoader.downloadFile(visitUrl);
-
- LinkDB.addVisitedUrl(visitUrl);
-
-
- Set<String> links=HtmlParserTool.extracLinks(visitUrl,filter);
-
- for(String link:links)
- {
- LinkDB.addUnvisitedUrl(link);
- }
- }
- }
-
- public static void main(String[]args)
- {
- Crawler crawler = new Crawler();
- crawler.crawling(new String[]{"http://www.twt.edu.cn"});
- }
- }
清單7 LinkDb.java
- package com.ie;
-
- import java.util.HashSet;
- import java.util.Set;
-
- public class LinkDB {
-
-
- private static Set<String> visitedUrl = new HashSet<String>();
-
- private static Queue<String> unVisitedUrl = new Queue<String>();
-
-
- public static Queue<String> getUnVisitedUrl() {
- return unVisitedUrl;
- }
-
- public static void addVisitedUrl(String url) {
- visitedUrl.add(url);
- }
-
- public static void removeVisitedUrl(String url) {
- visitedUrl.remove(url);
- }
-
- public static String unVisitedUrlDeQueue() {
- return unVisitedUrl.deQueue();
- }
-
-
- public static void addUnvisitedUrl(String url) {
- if (url != null && !url.trim().equals("")
- && !visitedUrl.contains(url)
- && !unVisitedUrl.contians(url))
- unVisitedUrl.enQueue(url);
- }
-
- public static int getVisitedUrlNum() {
- return visitedUrl.size();
- }
-
- public static boolean unVisitedUrlsEmpty() {
- return unVisitedUrl.empty();
- }
- }
清單8 Queue.java
- package com.ie;
-
- import java.util.LinkedList;
- public class Queue<T> {
-
- private LinkedList<T> queue=new LinkedList<T>();
-
- public void enQueue(T t)
- {
- queue.addLast(t);
- }
-
- public T deQueue()
- {
- return queue.removeFirst();
- }
-
- public boolean isQueueEmpty()
- {
- return queue.isEmpty();
- }
-
- public boolean contians(T t)
- {
- return queue.contains(t);
- }
-
- public boolean empty()
- {
- return queue.isEmpty();
- }
- }
清單 9 FileDownLoader.java
清單 10 HtmlParserTool.java
- package com.ie;
-
- import java.util.HashSet;
- import java.util.Set;
-
- import org.htmlparser.Node;
- import org.htmlparser.NodeFilter;
- import org.htmlparser.Parser;
- import org.htmlparser.filters.NodeClassFilter;
- import org.htmlparser.filters.OrFilter;
- import org.htmlparser.tags.LinkTag;
- import org.htmlparser.util.NodeList;
- import org.htmlparser.util.ParserException;
-
- public class HtmlParserTool {
-
- public static Set<String> extracLinks(String url,LinkFilter filter) {
-
- Set<String> links = new HashSet<String>();
- try {
- Parser parser = new Parser(url);
- parser.setEncoding("gb2312");
-
- NodeFilter frameFilter = new NodeFilter() {
- public boolean accept(Node node) {
- if (node.getText().startsWith("frame src=")) {
- return true;
- } else {
- return false;
- }
- }
- };
-
- OrFilter linkFilter = new OrFilter(new NodeClassFilter(
- LinkTag.class), frameFilter);
-
- NodeList list = parser.extractAllNodesThatMatch(linkFilter);
- for (int i = 0; i < list.size(); i++) {
- Node tag = list.elementAt(i);
- if (tag instanceof LinkTag)
- {
- LinkTag link = (LinkTag) tag;
- String linkUrl = link.getLink();
- if(filter.accept(linkUrl))
- links.add(linkUrl);
- } else
- {
-
- String frame = tag.getText();
- int start = frame.indexOf("src=");
- frame = frame.substring(start);
- int end = frame.indexOf(" ");
- if (end == -1)
- end = frame.indexOf(">");
- String frameUrl = frame.substring(5, end - 1);
- if(filter.accept(frameUrl))
- links.add(frameUrl);
- }
- }
- } catch (ParserException e) {
- e.printStackTrace();
- }
- return links;
- }
-
- public static void main(String[]args)
- {
- Set<String> links = HtmlParserTool.extracLinks(
- "http://www.twt.edu.cn",new LinkFilter()
- {
-
- public boolean accept(String url) {
- if(url.startsWith("http://www.twt.edu.cn"))
- return true;
- else
- return false;
- }
-
- });
- for(String link : links)
- System.out.println(link);
- }
- }
清單11 LinkFilter.java
- package com.ie;
-
- public interface LinkFilter {
- public boolean accept(String url);
- }
這些代碼中關鍵的部分都在 HttpClient 和 HtmlParser 介紹中說明過了,其餘部分也比較容易,請感興趣的讀者自行理解。
參考資料學習Developworks 學習其餘關於 HttpClien t和 HtmlParser 的技術文章。Developworks 其它專區學習更多的最新技術。得到技術和產品在 www.eclipse.org 得到免費的 IDE討論在 Developerworks 社區參與更多的討論