HTMLParser 是一個用來解析 HTML 文檔的開放源碼項目,它具有小巧、快速、使用簡單的特點以及擁有強大的功能。對該項目還不瞭解的朋友可以參照 2004 年三月份我發表的文章--《從HTML中攫取你所需的信息》,這篇文章介紹如何通過 HTMLParser 來提取 HTML 文檔中的文本數據以及提取出文檔中的所有鏈接或者是圖片等信息。
現在該項目的最新版本是 Integration Build 1.6,與之前版本的差別在於代碼結構的調整、當然也有一些功能的提升以及 BugFix,同時對字符集的處理也更加自動了。比較遺憾的該項目並沒有詳盡的使用文檔,你只能藉助於它的 API 文檔、一兩個簡單例子以及源碼來熟悉它。
如果是 HTML 文檔,那麼用 HTMLParser 已經差不多可以滿足你至少 90% 的需求。一個 HTML 文檔中可能出現的標籤差不多在 HTMLParser 中都有對應的類,甚至包括一些動態的腳本標籤,例如 <%...%> 這種 JSP 和 ASP 用到的標籤都有相應的 JspTag 對應。HTMLParser 的強大功能還體現在你可以修改每個標籤的屬性或者它所包含的文本內容並生成新的 HTML 文檔,比如你可以文檔中的鏈接地址偷偷的改成你自己的地址等等。關於 HTMLParser 的強大功能,其實上一篇文章已經介紹很多,這裏不再累贅,我們今天要講的是另外一個用途--處理自定義標籤。
首先我們先解釋一下什麼叫自定義標籤,我把所有不是 HTML 腳本語言中定義的標籤稱之爲自定義標籤,比如可以是 <scriptlet>、<book> 等等,這是我們自己創造出來的標籤。你可能會很奇怪,因爲這些標籤一旦用在 HTML 文檔中是沒有任何效果的,那麼我們換另外一個例子,假如你要解析的不是 HTML 文檔,而是一個 WML(Wireless Markup Lauguage)文檔呢?WML 文檔中的 card,anchor 等標籤 HTMLParser 是沒有現成的標籤類來處理的。還有就是你同樣可以用 HTMLParser 來處理 XML 文檔,而 XML 文檔中所有的標籤都是你自己定義的。
爲了使我們的例子更具有代表意義,接下來我們將給出一段代碼用來解析出 WML 文檔中的所有鏈接,瞭解 WML 文檔的人都知道,WML 文檔中除了與 HTML 文檔相同的鏈接寫法外,還多了一種標籤叫 <anchor>,例如在一個 WML 文檔我們可以用下面兩種方式來表示一個鏈接。
<a href="http://www.javayou.com?cat_id=1">Java自由人</a> 或者: <anchor> Java自由人 <go href="http://www.javayou.com" method="get"> <postfield name="cat_id" value="1"/> </go> </anchor> |
(更多的時候使用 anchor 的鏈接用來提交一個表單。) 如果我們還是使用 LinkTag 來遍歷整個 WML 文檔的話,那 Anchor 中的鏈接將會被我們所忽略掉。
下面我們先給出一個簡單的例子,然後再敘述其中的道理。這個例子包含兩個文件,一個是WML 的測試腳本文件 test.wml,另外一個是 Java 程序文件 HyperLinkTrace.java,內容如下:
<?xml version="1.0"?> <!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN" "http://www.wapforum.org/DTD/wml_1.1.xml"> <wml> <card title="Java自由人登錄"> <p> 用戶名:<input type="text" name="username" size="15"/> 密碼:<input type="text" name="password" size="15"/> <br/> <anchor>現在登錄 <go href="/wap/user.do" method="get"> <postfield name="name" value="$(username)"/> <postfield name="password" value="$(password)"/> <postfield name="eventSubmit_Login" value="WML"/> </go> </anchor><br/> <a href="/wap/index.vm">返回首頁</a> </p> </card> </wml> |
test.wml 中的粗體部分是我們需要提取出來的鏈接。
package demo.htmlparser; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.net.URL; import org.htmlparser.Node; import org.htmlparser.NodeFilter; import org.htmlparser.Parser; import org.htmlparser.PrototypicalNodeFactory; import org.htmlparser.tags.CompositeTag; import org.htmlparser.tags.LinkTag; import org.htmlparser.util.NodeList; /** * 用來遍歷WML文檔中的所有超鏈接 * @author Winter Lau */ public class HyperLinkTrace { public static void main(String[] args) throws Exception { //初始化HTMLParser Parser parser = new Parser(); parser.setEncoding("8859_1"); parser.setInputHTML(getWmlContent()); //註冊新的結點解析器 PrototypicalNodeFactory factory = new PrototypicalNodeFactory (); factory.registerTag(new WmlGoTag ()); parser.setNodeFactory(factory); //遍歷符合條件的所有節點 NodeList nlist = parser.extractAllNodesThatMatch(lnkFilter); for(int i=0;i<nlist.size();i++){ CompositeTag node = (CompositeTag)nlist.elementAt(i); if(node instanceof LinkTag){ LinkTag link = (LinkTag)node; System.out.println("LINK: /t" + link.getLink()); } else if(node instanceof WmlGoTag){ WmlGoTag go = (WmlGoTag)node; System.out.println("GO: /t" + go.getLink()); } } } /** * 獲取測試的WML腳本內容 * @return * @throws Exception */ static String getWmlContent() throws Exception{ URL url = ParserTester.class.getResource("/demo/htmlparser/test.wml"); File f = new File(url.toURI()); BufferedReader in = new BufferedReader(new FileReader(f)); StringBuffer wml = new StringBuffer(); do{ String line = in.readLine(); if(line==null) break; if(wml.length()>0) wml.append("/r/n"); wml.append(line); }while(true); return wml.toString(); } /** * 解析出所有的鏈接,包括行爲<a>與<go> */ static NodeFilter lnkFilter = new NodeFilter() { public boolean accept(Node node) { if(node instanceof WmlGoTag) return true; if(node instanceof LinkTag) return true; return false; } }; /** * WML文檔的GO標籤解析器 * @author Winter Lau */ static class WmlGoTag extends CompositeTag { private static final String[] mIds = new String[] {"GO"}; private static final String[] mEndTagEnders = new String[] {"ANCHOR"}; public String[] getIds (){ return (mIds); } public String[] getEnders (){ return (mIds); } public String[] getEndTagEnders (){ return (mEndTagEnders); } public String getLink(){ return super.getAttribute("href"); } public String getMethod(){ return super.getAttribute("method"); } } } |
上面這段代碼比較長,可以分成下面幾部分來看:
1. getWmlContent方法: 該方法用來獲取在同一個包中的test.wml腳本文件的內容並返回字符串。
2. 靜態屬性lnkFilter:這是一個NodeFilter的匿名類所構造的實例。該實例用來傳遞給HTMLParser告知需要提取哪些節點。在這個例子中我們僅需要提取鏈接標籤以及我們自定義的一個GO標籤。
3. 嵌套類WmlGoTag:這也是最爲重要的一部分,這個類用來告訴HTMLParser如何去解析<go>這樣一個節點。我們先看看下面這個HTMLParser的節點類層次圖:
如上圖所示,HTMLParser將一個文檔分成三種節點分別是:Remark(註釋);Text(文本);Tag(標籤)。而標籤又分成兩種分別是簡單標籤(Tag)和複合標籤(CompositeTag),像<img><br/>這種標籤稱爲簡單標籤,因爲標籤不會再包含其它內容。而像<a href="xxxx">Home</a>這種類型的標籤,因爲標籤會嵌套文本或者其他標籤的稱爲複合標籤,也就是對應着CompositeTag這個類。簡單標籤的實現類很簡單,只需要擴展Tag類並覆蓋getIds方法以返回標籤的識別文本,例如<img>標籤應該返回包含"img"字符串的數組,具體的代碼可以參考HTMLParser自帶的ImageTag標籤類的實現。
從上圖可清楚看出,複合標籤事實上是對簡單標籤的擴展,HTMLParser在處理一個複合標籤時需要知道該標籤的起始標識以及結束標識,也就是我們在前面給出的源碼中的兩個方法getIds和getEnders,一般來講,標籤出現都是成對的,因此這兩個方法一般返回相同的值。另外一個方法getEndTagEnders,這個方法用來返回父一級的標籤名稱,例如<tr>的父一級標籤應該是<table>。這個方法的必要性在於HTML對格式的要求很不嚴格,在很多的HTML文檔中的一些標籤經常是有開始標識,但是沒有結束標識,由於瀏覽器的超強適應能力使這種情況出現的很頻繁,因此HTMLParser利用這個方法來輔助判斷一個標籤是否已經結束。由於WML文檔的格式要求非常嚴格,因此上例源碼中的getEndTagEnders方法事實上可有可無。
4. 入口方法main:該方法初始化HTMLParser並註冊新的節點解析器,解析文檔並打印運行結果。
最後我們編譯並運行這個例子,便可以得到下面的運行結果:
GO: /wap/user.do LINK: /wap/index.vm |
HTMLParser本身就是一個開放源碼的項目,它對於HTML文檔中出現的標籤定義已經應有盡有,我們儘可以參考這些標籤解析類的源碼來學習如何實現一個標籤的解析類,從而擴展出更豐富多彩的應用程序。