HTMLParser具有小巧,快速的優(yōu)點,缺點是相關(guān)文檔比較少(英文的也少),很多功能需要自己摸索。對于初學(xué)者還是要費一些功夫的,而一旦上手以后,會發(fā)現(xiàn)HTMLParser的結(jié)構(gòu)設(shè)計很巧妙,非常實用,基本你的各種需求都可以滿足。
這里我根據(jù)自己這幾個月來的經(jīng)驗,寫了一點入門的東西,希望能對新學(xué)習(xí)HTMLParser的朋友們有所幫助。(不過當(dāng)年高考本人語文只比及格高一分,所以文法方面的問題還希望大家多多擔(dān)待)
HTMLParser的核心模塊是org.htmlparser.Parser類,這個類實際完成了對于HTML頁面的分析工作。這個類有下面幾個構(gòu)造函數(shù):
public Parser ();
public Parser (Lexer lexer, ParserFeedback fb);
public Parser (URLConnection connection, ParserFeedback fb) throws ParserException;
public Parser (String resource, ParserFeedback feedback) throws ParserException;
public Parser (String resource) throws ParserException;
public Parser (Lexer lexer);
public Parser (URLConnection connection) throws ParserException;
和一個靜態(tài)類 public static Parser createParser (String html, String charset);
對于大多數(shù)使用者來說,使用最多的是通過一個URLConnection或者一個保存有網(wǎng)頁內(nèi)容的字符串來初始化Parser,或者使用靜態(tài)函數(shù)來生成一個Parser對象。ParserFeedback的代碼很簡單,是針對調(diào)試和跟蹤分析過程的,一般不需要改變。而使用Lexer則是一個相對比較高級的話題,放到以后再討論吧。
這里比較有趣的一點是,如果需要設(shè)置頁面的編碼方式的話,不使用Lexer就只有靜態(tài)函數(shù)一個方法了。對于大多數(shù)中文頁面來說,好像這是應(yīng)該用得比較多的一個方法。
下面是初始化Parser的例子。
package com.baizeju.htmlparsertester;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.io.File;
import java.net.HttpURLConnection;
import java.net.URL;
import org.htmlparser.visitors.TextExtractingVisitor;
import org.htmlparser.Parser;
/**
* @author www.
*/
public class Main {
private static String ENCODE = "GBK";
private static void message( String szMsg ) {
try{System.out.println(new String(szMsg.getBytes(ENCODE), System.getProperty("file.encoding"))); } catch(Exception e ){}
}
public static String openFile( String szFileName ) {
try {
BufferedReader bis = new BufferedReader(new InputStreamReader(new FileInputStream( new File(szFileName)), ENCODE) );
String szContent="";
String szTemp;
while ( (szTemp = bis.readLine()) != null) {
szContent+=szTemp+"\n";
}
bis.close();
return szContent;
}
catch( Exception e ) {
return "";
}
}
public static void main(String[] args) {
String szContent = openFile( "E:/My Sites/HTMLParserTester.html");
try{
//Parser parser = Parser.createParser(szContent, ENCODE);
//Parser parser = new Parser( szContent );
Parser parser = new Parser( (HttpURLConnection) (new URL("http://127.0.0.1:8080/HTMLParserTester.html")).openConnection() );
TextExtractingVisitor visitor = new TextExtractingVisitor();
parser.visitAllNodesWith(visitor);
String textInPage = visitor.getExtractedText();
message(textInPage);
}
catch( Exception e ) {
}
}
}
加重的部分測試了幾種不同的初始化方法,后面的顯示了結(jié)果。大家看到能Parser出內(nèi)容就可以了,如何操作訪問Parser的內(nèi)容我們在后面討論。
HTMLParser將解析過的信息保存為一個樹的結(jié)構(gòu)。Node是信息保存的數(shù)據(jù)類型基礎(chǔ)。
請看Node的定義:
public interface Node extends Cloneable;
Node中包含的方法有幾類:
對于樹型結(jié)構(gòu)進(jìn)行遍歷的函數(shù),這些函數(shù)最容易理解:
Node getParent ():取得父節(jié)點
NodeList getChildren ():取得子節(jié)點的列表
Node getFirstChild ():取得第一個子節(jié)點
Node getLastChild ():取得最后一個子節(jié)點
Node getPreviousSibling ():取得前一個兄弟(不好意思,英文是兄弟姐妹,直譯太麻煩而且不符合習(xí)慣,對不起女同胞了)
Node getNextSibling ():取得下一個兄弟節(jié)點
取得Node內(nèi)容的函數(shù):
String getText ():取得文本
String toPlainTextString():取得純文本信息。
String toHtml () :取得HTML信息(原始HTML)
String toHtml (boolean verbatim):取得HTML信息(原始HTML)
String toString ():取得字符串信息(原始HTML)
Page getPage ():取得這個Node對應(yīng)的Page對象
int getStartPosition ():取得這個Node在HTML頁面中的起始位置
int getEndPosition ():取得這個Node在HTML頁面中的結(jié)束位置
用于Filter過濾的函數(shù):
void collectInto (NodeList list, NodeFilter filter):基于filter的條件對于這個節(jié)點進(jìn)行過濾,符合條件的節(jié)點放到list中。
用于Visitor遍歷的函數(shù):
void accept (NodeVisitor visitor):對這個Node應(yīng)用visitor
用于修改內(nèi)容的函數(shù),這類用得比較少:
void setPage (Page page):設(shè)置這個Node對應(yīng)的Page對象
void setText (String text):設(shè)置文本
void setChildren (NodeList children):設(shè)置子節(jié)點列表
其他函數(shù):
void doSemanticAction ():執(zhí)行這個Node對應(yīng)的操作(只有少數(shù)Tag有對應(yīng)的操作)
Object clone ():接口Clone的抽象函數(shù)。
實際我們用HTMLParser最多的是處理HTML頁面,Filter或Visitor相關(guān)的函數(shù)是必須的,然后第一類和第二類函數(shù)是用得最多的。第一類函數(shù)比較容易理解,下面用例子說明一下第二類函數(shù)。
下面是用于測試的HTML文件:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd">
<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白澤居-www.</title></head>
<html xmlns="http://www./1999/xhtml">
<body >
<div id="top_main">
<div id="logoindex">
<!--這是注釋-->
白澤居-www.
<a href="http://www.">白澤居-www.</a>
</div>
白澤居-www.
</div>
</body>
</html>
測試代碼:
/**
* @author www.
*/
package com.baizeju.htmlparsertester;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.io.File;
import java.net.HttpURLConnection;
import java.net.URL;
import org.htmlparser.Node;
import org.htmlparser.util.NodeIterator;
import org.htmlparser.Parser;
/**
* @author www.
*/
public class Main {
private static String ENCODE = "GBK";
private static void message( String szMsg ) {
try{ System.out.println(new String(szMsg.getBytes(ENCODE), System.getProperty("file.encoding"))); } catch(Exception e ){}
}
public static String openFile( String szFileName ) {
try {
BufferedReader bis = new BufferedReader(new InputStreamReader(new FileInputStream( new File(szFileName)), ENCODE) );
String szContent="";
String szTemp;
while ( (szTemp = bis.readLine()) != null) {
szContent+=szTemp+"\n";
}
bis.close();
return szContent;
}
catch( Exception e ) {
return "";
}
}
public static void main(String[] args) {
try{
Parser parser = new Parser( (HttpURLConnection) (new URL("http://127.0.0.1:8080/HTMLParserTester.html")).openConnection() );
for (NodeIterator i = parser.elements (); i.hasMoreNodes(); ) {
Node node = i.nextNode();
message("getText:"+node.getText());
message("getPlainText:"+node.toPlainTextString());
message("toHtml:"+node.toHtml());
message("toHtml(true):"+node.toHtml(true));
message("toHtml(false):"+node.toHtml(false));
message("toString:"+node.toString());
message("=================================================");
}
}
catch( Exception e ) {
System.out.println( "Exception:"+e );
}
}
}
輸出結(jié)果:
getText:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd"
getPlainText:
toHtml:<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd">
toHtml(true):<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd">
toHtml(false):<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd">
toString:Doctype Tag : !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd; begins at : 0; ends at : 121
=================================================
getText:
getPlainText:
toHtml:
toHtml(true):
toHtml(false):
toString:Txt (121[0,121],123[1,0]): \n
=================================================
getText:head
getPlainText:白澤居-www.
toHtml:<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白澤居-www.</title></head>
toHtml(true):<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白澤居-www.</title></head>
toHtml(false):<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白澤居-www.</title></head>
toString:HEAD: Tag (123[1,0],129[1,6]): head
Tag (129[1,6],197[1,74]): meta http-equiv="Content-Type" content="text/html; ...
Tag (197[1,74],204[1,81]): title
Txt (204[1,81],223[1,100]): 白澤居-www.
End (223[1,100],231[1,108]): /title
End (231[1,108],238[1,115]): /head
=================================================
getText:
getPlainText:
toHtml:
toHtml(true):
toHtml(false):
toString:Txt (238[1,115],240[2,0]): \n
=================================================
getText:html xmlns="http://www./1999/xhtml"
getPlainText:
白澤居-www.
白澤居-www.
白澤居-www.
toHtml:<html xmlns="http://www./1999/xhtml">
<body >
<div id="top_main">
<div id="logoindex">
<!--這是注釋-->
白澤居-www.
<a href="http://www.">白澤居-www.</a>
</div>
白澤居-www.
</div>
</body>
</html>
toHtml(true):<html xmlns="http://www./1999/xhtml">
<body >
<div id="top_main">
<div id="logoindex">
<!--這是注釋-->
白澤居-www.
<a href="http://www.">白澤居-www.</a>
</div>
白澤居-www.
</div>
</body>
</html>
toHtml(false):<html xmlns="http://www./1999/xhtml">
<body >
<div id="top_main">
<div id="logoindex">
<!--這是注釋-->
白澤居-www.
<a href="http://www.">白澤居-www.</a>
</div>
白澤居-www.
</div>
</body>
</html>
toString:Tag (240[2,0],283[2,43]): html xmlns="http://www./1999/xhtml"
Txt (283[2,43],285[3,0]): \n
Tag (285[3,0],292[3,7]): body
Txt (292[3,7],294[4,0]): \n
Tag (294[4,0],313[4,19]): div id="top_main"
Txt (313[4,19],316[5,1]): \n\t
Tag (316[5,1],336[5,21]): div id="logoindex"
Txt (336[5,21],340[6,2]): \n\t\t
Rem (340[6,2],351[6,13]): 這是注釋
Txt (351[6,13],376[8,0]): \n\t\t白澤居-www.\n
Tag (376[8,0],409[8,33]): a href="http://www."
Txt (409[8,33],428[8,52]): 白澤居-www.
End (428[8,52],432[8,56]): /a
Txt (432[8,56],435[9,1]): \n\t
End (435[9,1],441[9,7]): /div
Txt (441[9,7],465[11,0]): \n\t白澤居-www.\n
End (465[11,0],471[11,6]): /div
Txt (471[11,6],473[12,0]): \n
End (473[12,0],480[12,7]): /body
Txt (480[12,7],482[13,0]): \n
End (482[13,0],489[13,7]): /html
=================================================
對于第一個Node的內(nèi)容,對應(yīng)的就是第一行<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd">,這個比較好理解。
從這個輸出結(jié)果中,也可以看出內(nèi)容的樹狀結(jié)構(gòu)。或者說是樹林結(jié)構(gòu)。在Page內(nèi)容的第一層Tag,如DOCTYPE,head和html,分別形成了一個最高層的Node節(jié)點(很多人可能對第二個和第四個Node的內(nèi)容有點奇怪。實際上這兩個Node就是兩個換行符號。HTMLParser把HTML頁面內(nèi)容中的所有換行,空格,Tab等都轉(zhuǎn)換成了相應(yīng)的Tag,所以就出現(xiàn)了這樣的Node。雖然內(nèi)容少但是級別高,呵呵)
getPlainTextString是把用戶可以看到的內(nèi)容都包含了。有趣的有兩點,一是<head>標(biāo)簽中的Title內(nèi)容是在plainText中的,可能在標(biāo)題中可見的也算可見吧。另外就是象前面說的,HTML內(nèi)容中的換行符什么的,也都成了plainText,這個邏輯上好像有點問題。
另外可能大家發(fā)現(xiàn)toHtml,toHtml(true)和toHtml(false)的結(jié)果沒什么區(qū)別。實際也是這樣的,如果跟蹤HTMLParser的代碼就可以發(fā)現(xiàn),Node的子類是AbstractNode,其中實現(xiàn)了toHtml()的代碼,直接調(diào)用toHtml(false),而AbstractNode的三個子類RemarkNode,TagNode和TextNode中,toHtml(boolean verbatim)的實現(xiàn)中,都沒有處理verbatim參數(shù),所以三個函數(shù)的結(jié)果是一模一樣的。如果你不需要實現(xiàn)你自己的什么特殊處理,簡單使用toHtml就可以了。
HTML的Node類繼承關(guān)系如下圖(這個是從別的文章Copy的):

AbstractNodes是Node的直接子類,也是一個抽象類。它的三個直接子類實現(xiàn)是RemarkNode,用于保存注釋。在輸出結(jié)果的toString部分中可以看到有一個"Rem (345[6,2],356[6,13]): 這是注釋",就是一個RemarkNode。TextNode也很簡單,就是用戶可見的文字信息。TagNode是最復(fù)雜的,包含了HTML語言中的所有標(biāo)簽,而且可以擴展(擴展 HTMLParser 對自定義標(biāo)簽的處理能力)。TagNode包含兩類,一類是簡單的Tag,實際就是不能包含其他Tag的標(biāo)簽,只能做葉子節(jié)點。另一類是CompositeTag,就是可以包含其他Tag,是分支節(jié)點
HTMLParser遍歷了網(wǎng)頁的內(nèi)容以后,以樹(森林)結(jié)構(gòu)保存了結(jié)果。HTMLParser訪問結(jié)果內(nèi)容的方法有兩種。使用Filter和使用Visitor。
(一)Filter類
顧名思義,Filter就是對于結(jié)果進(jìn)行過濾,取得需要的內(nèi)容。HTMLParser在org.htmlparser.filters包之內(nèi)一共定義了16個不同的Filter,也可以分為幾類。
判斷類Filter:
TagNameFilter
HasAttributeFilter
HasChildFilter
HasParentFilter
HasSiblingFilter
IsEqualFilter
邏輯運算Filter:
AndFilter
NotFilter
OrFilter
XorFilter
其他Filter:
NodeClassFilter
StringFilter
LinkStringFilter
LinkRegexFilter
RegexFilter
CssSelectorNodeFilter
所有的Filter類都實現(xiàn)了org.htmlparser.NodeFilter接口。這個接口只有一個主要函數(shù):
boolean accept (Node node);
各個子類分別實現(xiàn)這個函數(shù),用于判斷輸入的Node是否符合這個Filter的過濾條件,如果符合,返回true,否則返回false。
(二)判斷類Filter
2.1 TagNameFilter
TabNameFilter是最容易理解的一個Filter,根據(jù)Tag的名字進(jìn)行過濾。
下面是用于測試的HTML文件:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd">
<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白澤居-www.</title>< /head>
<html xmlns="http://www./1999/xhtml">
<body >
<div id="top_main">
<div id="logoindex">
<!--這是注釋-->
白澤居-www.
<a href="http://www.">白澤居-www.</a>
</div>
白澤居-www.
</div>
</body>
</html>
測試代碼:(這里只列出了Main函數(shù),全部代碼請參考 HTMLParser使用入門(2)- Node內(nèi)容,自己添加import部分)
public static void main(String[] args) {
try{
Parser parser = new Parser( (HttpURLConnection) (new URL("http://127.0.0.1:8080/HTMLParserTester.html")).openConnection() );
// 這里是控制測試的部分,后面的例子修改的就是這個地方。
NodeFilter filter = new TagNameFilter ("DIV");
NodeList nodes = parser.extractAllNodesThatMatch(filter);
if(nodes!=null) {
for (int i = 0; i < nodes.size(); i++) {
Node textnode = (Node) nodes.elementAt(i);
message("getText:"+textnode.getText());
message("=================================================");
}
}
}
catch( Exception e ) {
e.printStackTrace();
}
}
輸出結(jié)果:
getText:div id="top_main"
=================================================
getText:div id="logoindex"
=================================================
可以看出文件中兩個Div節(jié)點都被取出了。下面可以針對這兩個DIV節(jié)點進(jìn)行操作
2.2 HasChildFilter
下面讓我們看看HasChildFilter。剛剛看到這個Filter的時候,我想當(dāng)然地認(rèn)為這個Filter返回的是有Child的Tag。直接初始化了一個
NodeFilter filter = new HasChildFilter();
結(jié)果調(diào)用NodeList nodes = parser.extractAllNodesThatMatch(filter);的時候HasChildFilter內(nèi)部直接發(fā)生NullPointerException。讀了一下HasChildFilter的代碼,才發(fā)現(xiàn),實際HasChildFilter是返回有符合條件的子節(jié)點的節(jié)點,需要另外一個Filter作為過濾子節(jié)點的參數(shù)。缺省的構(gòu)造函數(shù)雖然可以初始化,但是由于子節(jié)點的Filter是null,所以使用的時候發(fā)生了Exception。從這點來看,HTMLParser的代碼還有很多可以優(yōu)化的的地方。呵呵。
修改代碼:
NodeFilter innerFilter = new TagNameFilter ("DIV");
NodeFilter filter = new HasChildFilter(innerFilter);
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結(jié)果:
getText:body
=================================================
getText:div id="top_main"
=================================================
可以看到,輸出的是兩個有DIV子Tag的Tag節(jié)點。(body有子節(jié)點DIV "top_main","top_main"有子節(jié)點"logoindex"。
注意HasChildFilter還有一個構(gòu)造函數(shù):
public HasChildFilter (NodeFilter filter, boolean recursive)
如果recursive是false,則只對第一級子節(jié)點進(jìn)行過濾。比如前面的例子,body和top_main都是在第一級的子節(jié)點里就有DIV節(jié)點,所以匹配上了。如果我們用下面的方法調(diào)用:
NodeFilter filter = new HasChildFilter( innerFilter, true );
輸出結(jié)果:
getText:html xmlns="http://www./1999/xhtml"
=================================================
getText:body
=================================================
getText:div id="top_main"
=================================================
可以看到輸出結(jié)果中多了一個html xmlns="http://www./1999/xhtml",這個是整個HTML頁面的節(jié)點(根節(jié)點),雖然這個節(jié)點下直接沒有DIV節(jié)點,但是它的子節(jié)點body下面有DIV節(jié)點,所以它也被匹配上了。
2.3 HasAttributeFilter
HasAttributeFilter有3個構(gòu)造函數(shù):
public HasAttributeFilter ();
public HasAttributeFilter (String attribute);
public HasAttributeFilter (String attribute, String value);
這個Filter可以匹配出包含制定名字的屬性,或者制定屬性為指定值的節(jié)點。還是用例子說明比較容易。
調(diào)用方法1:
NodeFilter filter = new HasAttributeFilter();
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結(jié)果:
什么也沒有輸出。
調(diào)用方法2:
NodeFilter filter = new HasAttributeFilter( "id" );
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結(jié)果:
getText:div id="top_main"
=================================================
getText:div id="logoindex"
=================================================
調(diào)用方法3:
NodeFilter filter = new HasAttributeFilter( "id", "logoindex" );
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結(jié)果:
getText:div id="logoindex"
=================================================
很簡單吧。呵呵
2.4 其他判斷列Filter
HasParentFilter和HasSiblingFilter的功能與HasChildFilter類似,大家自己試一下就應(yīng)該了解了。
IsEqualFilter的構(gòu)造函數(shù)參數(shù)是一個Node:
public IsEqualFilter (Node node) {
mNode = node;
}
accept函數(shù)也很簡單:
public boolean accept (Node node) {
return (mNode == node);
}
不需要過多說明了。
(三)邏輯運算Filter
前面介紹的都是簡單的Filter,只能針對某種單一類型的條件進(jìn)行過濾。HTMLParser支持對于簡單類型的Filter進(jìn)行組合,從而實現(xiàn)復(fù)雜的條件。原理和一般編程語言的邏輯運算是一樣的。
3.1 AndFilter
AndFilter可以把兩種Filter進(jìn)行組合,只有同時滿足條件的Node才會被過濾。
測試代碼:
NodeFilter filterID = new HasAttributeFilter( "id" );
NodeFilter filterChild = new HasChildFilter(filterA);
NodeFilter filter = new AndFilter(filterID, filterChild);
輸出結(jié)果:
getText:div id="logoindex"
=================================================
3.2 OrFilter
把前面的AndFilter換成OrFilter
測試代碼:
NodeFilter filterID = new HasAttributeFilter( "id" );
NodeFilter filterChild = new HasChildFilter(filterA);
NodeFilter filter = new OrFilter(filterID, filterChild);
輸出結(jié)果:
getText:div id="top_main"
=================================================
getText:div id="logoindex"
=================================================
3.3 NotFilter
把前面的AndFilter換成NotFilter
測試代碼:
NodeFilter filterID = new HasAttributeFilter( "id" );
NodeFilter filterChild = new HasChildFilter(filterA);
NodeFilter filter = new NotFilter(new OrFilter(filterID, filterChild));
輸出結(jié)果:
getText:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd"
=================================================
getText:
=================================================
getText:head
=================================================
getText:meta http-equiv="Content-Type" content="text/html; charset=gb2312"
=================================================
getText:title
=================================================
getText:白澤居-www.
=================================================
getText:/title
=================================================
getText:/head
=================================================
getText:
=================================================
getText:html xmlns="http://www./1999/xhtml"
=================================================
getText:
=================================================
getText:body
=================================================
getText:
=================================================
getText:
=================================================
getText:
=================================================
getText:這是注釋
=================================================
getText:
白澤居-www.
=================================================
getText:a href="http://www."
=================================================
getText:白澤居-www.
=================================================
getText:/a
=================================================
getText:
=================================================
getText:/div
=================================================
getText:
白澤居-www.
=================================================
getText:/div
=================================================
getText:
=================================================
getText:/body
=================================================
getText:
=================================================
getText:/html
=================================================
getText:
=================================================
除了前面3.2中輸出的幾個Tag,其余的Tag都在這里了。
3.4 XorFilter
把前面的AndFilter換成NotFilter
測試代碼:
NodeFilter filterID = new HasAttributeFilter( "id" );
NodeFilter filterChild = new HasChildFilter(filterA);
NodeFilter filter = new XorFilter(filterID, filterChild);
輸出結(jié)果:
getText:div id="top_main"
=================================================
(四)其他Filter:
4.1 NodeClassFilter
這個Filter用于判斷節(jié)點類型是否是某個特定的Node類型。在HTMLParser使用入門(2)- Node內(nèi)容 中我們已經(jīng)了解了Node的不同類型,這個Filter就可以針對類型進(jìn)行過濾。
測試代碼:
NodeFilter filter = new NodeClassFilter(RemarkNode.class);
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結(jié)果:
getText:這是注釋
=================================================
可以看到只有RemarkNode(注釋)被輸出了。
4.2 StringFilter
這個Filter用于過濾顯示字符串中包含制定內(nèi)容的Tag。注意是可顯示的字符串,不可顯示的字符串中的內(nèi)容(例如注釋,鏈接等等)不會被顯示。
修改一下例子代碼:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd">
<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白澤居-title-www.</title></head>
<html xmlns="http://www./1999/xhtml">
<body >
<div id="top_main">
<div id="logoindex">
<!--這是注釋 白澤居-www. -->
白澤居-字符串1-www.
<a href="http://www.">白澤居-鏈接文本-www.</a>
</div>
白澤居-字符串2-www.
</div>
</body>
</html>
測試代碼:
NodeFilter filter = new StringFilter("www.");
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結(jié)果:
getText:白澤居-title-www.
=================================================
getText:
白澤居-字符串1-www.
=================================================
getText:白澤居-鏈接文本-www.
=================================================
getText:
白澤居-字符串2-www.
=================================================
可以看到包含title,兩個內(nèi)容字符串和鏈接的文本字符串的Tag都被輸出了,但是注釋和鏈接Tag本身沒有輸出。
4.3 LinkStringFilter
這個Filter用于判斷鏈接中是否包含某個特定的字符串,可以用來過濾出指向某個特定網(wǎng)站的鏈接。
測試代碼:
NodeFilter filter = new LinkStringFilter("www.");
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結(jié)果:
getText:a href="http://www."
=================================================
4.4 其他幾個Filter
其他幾個Filter也是根據(jù)字符串對不同的域進(jìn)行判斷,與前面這些的區(qū)別主要就是支持正則表達(dá)式。這個不在本文的討論范圍以內(nèi),大家可以自己實驗一下。
HTMLParser遍歷了網(wǎng)頁的內(nèi)容以后,以樹(森林)結(jié)構(gòu)保存了結(jié)果。HTMLParser訪問結(jié)果內(nèi)容的方法有兩種。使用Filter和使用Visitor。
下面介紹使用Visitor訪問內(nèi)容的方法。
4.1 NodeVisitor
從簡單方面的理解,Filter是根據(jù)某種條件過濾取出需要的Node再進(jìn)行處理。Visitor則是遍歷內(nèi)容樹的每一個節(jié)點,對于符合條件的節(jié)點進(jìn)行處理。實際的結(jié)果異曲同工,兩種不同的方法可以達(dá)到相同的結(jié)果。
下面是一個最常見的NodeVisitro的例子。
測試代碼:
public static void main(String[] args) {
try{
Parser parser = new Parser( (HttpURLConnection) (new URL("http://127.0.0.1:8080/HTMLParserTester.html")).openConnection() );
NodeVisitor visitor = new NodeVisitor( false, false ) {
public void visitTag(Tag tag) {
message("This is Tag:"+tag.getText());
}
public void visitStringNode (Text string) {
message("This is Text:"+string);
}
public void visitRemarkNode (Remark remark) {
message("This is Remark:"+remark.getText());
}
public void beginParsing () {
message("beginParsing");
}
public void visitEndTag (Tag tag){
message("visitEndTag:"+tag.getText());
}
public void finishedParsing () {
message("finishedParsing");
}
};
parser.visitAllNodesWith(visitor);
}
catch( Exception e ) {
e.printStackTrace();
}
}
輸出結(jié)果:
beginParsing
This is Tag:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd"
This is Text:Txt (121[0,121],123[1,0]): \n
This is Text:Txt (244[1,121],246[2,0]): \n
finishedParsing
可以看到,開始遍歷所以的節(jié)點以前,beginParsing先被調(diào)用,然后處理的是中間的Node,最后在結(jié)束遍歷以前,finishParsing被調(diào)用。因為我設(shè)置的 recurseChildren和recurseSelf都是false,所以Visitor沒有訪問子節(jié)點也沒有訪問根節(jié)點的內(nèi)容。中間輸出的兩個\n就是我們在HTMLParser使用詳解(1)- 初始化Parser 中討論過的最高層的那兩個換行。
我們先把recurseSelf設(shè)置成true,看看會發(fā)生什么。
NodeVisitor visitor = new NodeVisitor( false, true) {
輸出結(jié)果:
beginParsing
This is Tag:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd"
This is Text:Txt (121[0,121],123[1,0]): \n
This is Tag:head
This is Text:Txt (244[1,121],246[2,0]): \n
This is Tag:html xmlns="http://www./1999/xhtml"
finishedParsing
可以看到,HTML頁面的第一層節(jié)點都被調(diào)用了。
我們再用下面的方法調(diào)用看看:
NodeVisitor visitor = new NodeVisitor( true, false) {
輸出結(jié)果:
beginParsing
This is Tag:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd"
This is Text:Txt (121[0,121],123[1,0]): \n
This is Tag:meta http-equiv="Content-Type" content="text/html; charset=gb2312"
This is Text:Txt (204[1,81],229[1,106]): 白澤居-title-www.
visitEndTag:/title
visitEndTag:/head
This is Text:Txt (244[1,121],246[2,0]): \n
This is Text:Txt (289[2,43],291[3,0]): \n
This is Text:Txt (298[3,7],300[4,0]): \n
This is Text:Txt (319[4,19],322[5,1]): \n\t
This is Text:Txt (342[5,21],346[6,2]): \n\t\t
This is Remark:這是注釋 白澤居-www.
This is Text:Txt (378[6,34],408[8,0]): \n\t\t白澤居-字符串1-www.\n
This is Text:Txt (441[8,33],465[8,57]): 白澤居-鏈接文本-www.
visitEndTag:/a
This is Text:Txt (469[8,61],472[9,1]): \n\t
visitEndTag:/div
This is Text:Txt (478[9,7],507[11,0]): \n\t白澤居-字符串2-www.\n
visitEndTag:/div
This is Text:Txt (513[11,6],515[12,0]): \n
visitEndTag:/body
This is Text:Txt (522[12,7],524[13,0]): \n
visitEndTag:/html
finishedParsing
可以看到,所有的子節(jié)點都出現(xiàn)了,除了剛剛例子里面的兩個最上層節(jié)點This is Tag:head和This is Tag:html xmlns="http://www./1999/xhtml"。
想讓它們都出來,只需要
NodeVisitor visitor = new NodeVisitor( true, true) {
輸出結(jié)果:
beginParsing
This is Tag:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www./TR/xhtml1/DTD/xhtml1-transitional.dtd"
This is Text:Txt (121[0,121],123[1,0]): \n
This is Tag:head
This is Tag:meta http-equiv="Content-Type" content="text/html; charset=gb2312"
This is Tag:title
This is Text:Txt (204[1,81],229[1,106]): 白澤居-title-www.
visitEndTag:/title
visitEndTag:/head
This is Text:Txt (244[1,121],246[2,0]): \n
This is Tag:html xmlns="http://www./1999/xhtml"
This is Text:Txt (289[2,43],291[3,0]): \n
This is Tag:body
This is Text:Txt (298[3,7],300[4,0]): \n
This is Tag:div id="top_main"
This is Text:Txt (319[4,19],322[5,1]): \n\t
This is Tag:div id="logoindex"
This is Text:Txt (342[5,21],346[6,2]): \n\t\t
This is Remark:這是注釋 白澤居-www.
This is Text:Txt (378[6,34],408[8,0]): \n\t\t白澤居-字符串1-www.\n
This is Tag:a href="http://www."
This is Text:Txt (441[8,33],465[8,57]): 白澤居-鏈接文本-www.
visitEndTag:/a
This is Text:Txt (469[8,61],472[9,1]): \n\t
visitEndTag:/div
This is Text:Txt (478[9,7],507[11,0]): \n\t白澤居-字符串2-www.\n
visitEndTag:/div
This is Text:Txt (513[11,6],515[12,0]): \n
visitEndTag:/body
This is Text:Txt (522[12,7],524[13,0]): \n
visitEndTag:/html
finishedParsing
哈哈,這下調(diào)用清楚了,大家在需要處理的地方增加自己的代碼好了。
4.2 其他Visitor
HTMLParser還定義了幾個其他的Visitor。HtmlPage,NodeVisitor,ObjectFindingVisitor,StringFindingVisitor,TagFindingVisitor,TextExtractingVisitor,UrlModifyingVisitor,它們都是NodeVisitor的子類,實現(xiàn)了一些特定的功能。筆者個人的感覺是沒什么用處,如果你需要什么特定的功能,還不如自己寫一個,想在這些里面找到適合你需要的,化的時間可能更多。反正大家看看代碼就發(fā)現(xiàn),它們每個都沒幾行真正有效的代碼。HTMLParser 是一個用來解析 HTML 文檔的開放源碼項目,它具有小巧、快速、使用簡單的特點以及擁有強大的功能。對該項目還不了解的朋友可以參照 2004 年三月份我發(fā)表的文章--《從HTML中攫取你所需的信息》,這篇文章介紹如何通過 HTMLParser 來提取 HTML 文檔中的文本數(shù)據(jù)以及提取出文檔中的所有鏈接或者是圖片等信息。
現(xiàn)在該項目的最新版本是 Integration Build 1.6,與之前版本的差別在于代碼結(jié)構(gòu)的調(diào)整、當(dāng)然也有一些功能的提升以及 BugFix,同時對字符集的處理也更加自動了。比較遺憾的該項目并沒有詳盡的使用文檔,你只能借助于它的 API 文檔、一兩個簡單例子以及源碼來熟悉它。
如果是 HTML 文檔,那么用 HTMLParser 已經(jīng)差不多可以滿足你至少 90% 的需求。一個 HTML 文檔中可能出現(xiàn)的標(biāo)簽差不多在 HTMLParser 中都有對應(yīng)的類,甚至包括一些動態(tài)的腳本標(biāo)簽,例如 <%...%> 這種 JSP 和 ASP 用到的標(biāo)簽都有相應(yīng)的 JspTag 對應(yīng)。HTMLParser 的強大功能還體現(xiàn)在你可以修改每個標(biāo)簽的屬性或者它所包含的文本內(nèi)容并生成新的 HTML 文檔,比如你可以文檔中的鏈接地址偷偷的改成你自己的地址等等。關(guān)于 HTMLParser 的強大功能,其實上一篇文章已經(jīng)介紹很多,這里不再累贅,我們今天要講的是另外一個用途--處理自定義標(biāo)簽。
首先我們先解釋一下什么叫自定義標(biāo)簽,我把所有不是 HTML 腳本語言中定義的標(biāo)簽稱之為自定義標(biāo)簽,比如可以是 <scriptlet>、<book> 等等,這是我們自己創(chuàng)造出來的標(biāo)簽。你可能會很奇怪,因為這些標(biāo)簽一旦用在 HTML 文檔中是沒有任何效果的,那么我們換另外一個例子,假如你要解析的不是 HTML 文檔,而是一個 WML(Wireless Markup Lauguage)文檔呢?WML 文檔中的 card,anchor 等標(biāo)簽 HTMLParser 是沒有現(xiàn)成的標(biāo)簽類來處理的。還有就是你同樣可以用 HTMLParser 來處理 XML 文檔,而 XML 文檔中所有的標(biāo)簽都是你自己定義的。
為了使我們的例子更具有代表意義,接下來我們將給出一段代碼用來解析出 WML 文檔中的所有鏈接,了解 WML 文檔的人都知道,WML 文檔中除了與 HTML 文檔相同的鏈接寫法外,還多了一種標(biāo)簽叫 <anchor>,例如在一個 WML 文檔我們可以用下面兩種方式來表示一個鏈接。
|
<a href="http://www.?cat_id=1">Java自由人</a>
或者:
<anchor>
Java自由人
<go href="http://www." method="get">
<postfield name="cat_id" value="1"/>
</go>
</anchor> |
(更多的時候使用 anchor 的鏈接用來提交一個表單。) 如果我們還是使用 LinkTag 來遍歷整個 WML 文檔的話,那 Anchor 中的鏈接將會被我們所忽略掉。
下面我們先給出一個簡單的例子,然后再敘述其中的道理。這個例子包含兩個文件,一個是WML 的測試腳本文件 test.wml,另外一個是 Java 程序文件 HyperLinkTrace.java,內(nèi)容如下:
1. test.wml
|
<?xml version="1.0"?>
<!DOCTYPE wml PUBLIC "-//WAPFORUM//DTD WML 1.1//EN"
"http://www./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>現(xiàn)在登錄
<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 中的粗體部分是我們需要提取出來的鏈接。
2. HyperLinkTrace.java
|
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());
//注冊新的結(jié)點解析器
PrototypicalNodeFactory factory = new PrototypicalNodeFactory ();
factory.registerTag(new WmlGoTag ());
parser.setNodeFactory(factory);
//遍歷符合條件的所有節(jié)點
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腳本內(nèi)容
* @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標(biāo)簽解析器
* @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腳本文件的內(nèi)容并返回字符串。
2. 靜態(tài)屬性lnkFilter:這是一個NodeFilter的匿名類所構(gòu)造的實例。該實例用來傳遞給HTMLParser告知需要提取哪些節(jié)點。在這個例子中我們僅需要提取鏈接標(biāo)簽以及我們自定義的一個GO標(biāo)簽。
3. 嵌套類WmlGoTag:這也是最為重要的一部分,這個類用來告訴HTMLParser如何去解析<go>這樣一個節(jié)點。我們先看看下面這個HTMLParser的節(jié)點類層次圖:
如上圖所示,HTMLParser將一個文檔分成三種節(jié)點分別是:Remark(注釋);Text(文本);Tag(標(biāo)簽)。而標(biāo)簽又分成兩種分別是簡單標(biāo)簽(Tag)和復(fù)合標(biāo)簽(CompositeTag),像<img><br/>這種標(biāo)簽稱為簡單標(biāo)簽,因為標(biāo)簽不會再包含其它內(nèi)容。而像<a href="xxxx">Home</a>這種類型的標(biāo)簽,因為標(biāo)簽會嵌套文本或者其他標(biāo)簽的稱為復(fù)合標(biāo)簽,也就是對應(yīng)著CompositeTag這個類。簡單標(biāo)簽的實現(xiàn)類很簡單,只需要擴展Tag類并覆蓋getIds方法以返回標(biāo)簽的識別文本,例如<img>標(biāo)簽應(yīng)該返回包含"img"字符串的數(shù)組,具體的代碼可以參考HTMLParser自帶的ImageTag標(biāo)簽類的實現(xiàn)。
從上圖可清楚看出,復(fù)合標(biāo)簽事實上是對簡單標(biāo)簽的擴展,HTMLParser在處理一個復(fù)合標(biāo)簽時需要知道該標(biāo)簽的起始標(biāo)識以及結(jié)束標(biāo)識,也就是我們在前面給出的源碼中的兩個方法getIds和getEnders,一般來講,標(biāo)簽出現(xiàn)都是成對的,因此這兩個方法一般返回相同的值。另外一個方法getEndTagEnders,這個方法用來返回父一級的標(biāo)簽名稱,例如<tr>的父一級標(biāo)簽應(yīng)該是<table>。這個方法的必要性在于HTML對格式的要求很不嚴(yán)格,在很多的HTML文檔中的一些標(biāo)簽經(jīng)常是有開始標(biāo)識,但是沒有結(jié)束標(biāo)識,由于瀏覽器的超強適應(yīng)能力使這種情況出現(xiàn)的很頻繁,因此HTMLParser利用這個方法來輔助判斷一個標(biāo)簽是否已經(jīng)結(jié)束。由于WML文檔的格式要求非常嚴(yán)格,因此上例源碼中的getEndTagEnders方法事實上可有可無。
4. 入口方法main:該方法初始化HTMLParser并注冊新的節(jié)點解析器,解析文檔并打印運行結(jié)果。
最后我們編譯并運行這個例子,便可以得到下面的運行結(jié)果:
|
GO: /wap/user.do
LINK: /wap/index.vm |
HTMLParser本身就是一個開放源碼的項目,它對于HTML文檔中出現(xiàn)的標(biāo)簽定義已經(jīng)應(yīng)有盡有,我們盡可以參考這些標(biāo)簽解析類的源碼來學(xué)習(xí)如何實現(xiàn)一個標(biāo)簽的解析類,從而擴展出更豐富多彩的應(yīng)用程序。