HTMLParser具有小巧,快速的優點,缺點是相關文檔比較少(英文的也少),很多功能需要自己摸索。對于初學者還是要費一些功夫的,而一旦上手以后,會發現HTMLParser的結構設計很巧妙,非常實用,基本你的各種需求都可以滿足。
這里我根據自己這幾個月來的經驗,寫了一點入門的東西,希望能對新學習HTMLParser的朋友們有所幫助。(不過當年高考本人語文只比及格高一分,所以文法方面的問題還希望大家多多擔待)
HTMLParser的核心模塊是org.htmlparser.Parser類,這個類實際完成了對于HTML頁面的分析工作。這個類有下面幾個構造函數:
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;
和一個靜態類public static Parser createParser (String html, String charset);
對于大多數使用者來說,使用最多的是通過一個URLConnection或者一個保存有網頁內容的字符串來初始化Parser,或者使用靜態函數來生成一個Parser對象。ParserFeedback的代碼很簡單,是針對調試和跟蹤分析過程的,一般不需要改變。而使用Lexer則是一個相對比較高級的話題,放到以后再討論吧。
這里比較有趣的一點是,如果需要設置頁面的編碼方式的話,不使用Lexer就只有靜態函數一個方法了。對于大多數中文頁面來說,好像這是應該用得比較多的一個方法。
下面是初始化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.baizeju.com
*/
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 ) {
}
}
}
加重的部分測試了幾種不同的初始化方法,后面的顯示了結果。大家看到能Parser出內容就可以了,如何操作訪問Parser的內容我們在后面討論。
HTMLParser將解析過的信息保存為一個樹的結構。Node是信息保存的數據類型基礎。
請看Node的定義:
public interface Node extends Cloneable;
Node中包含的方法有幾類:
對于樹型結構進行遍歷的函數,這些函數最容易理解:
Node getParent ():取得父節點
NodeList getChildren ():取得子節點的列表
Node getFirstChild ():取得第一個子節點
Node getLastChild ():取得最后一個子節點
Node getPreviousSibling ():取得前一個兄弟(不好意思,英文是兄弟姐妹,直譯太麻煩而且不符合習慣,對不起女同胞了)
Node getNextSibling ():取得下一個兄弟節點
取得Node內容的函數:
String getText ():取得文本
String toPlainTextString():取得純文本信息。
String toHtml () :取得HTML信息(原始HTML)
String toHtml (boolean verbatim):取得HTML信息(原始HTML)
String toString ():取得字符串信息(原始HTML)
Page getPage ():取得這個Node對應的Page對象
int getStartPosition ():取得這個Node在HTML頁面中的起始位置
int getEndPosition ():取得這個Node在HTML頁面中的結束位置
用于Filter過濾的函數:
void collectInto (NodeList list, NodeFilter filter):基于filter的條件對于這個節點進行過濾,符合條件的節點放到list中。
用于Visitor遍歷的函數:
void accept (NodeVisitor visitor):對這個Node應用visitor
用于修改內容的函數,這類用得比較少:
void setPage (Page page):設置這個Node對應的Page對象
void setText (String text):設置文本
void setChildren (NodeList children):設置子節點列表
其他函數:
void doSemanticAction ():執行這個Node對應的操作(只有少數Tag有對應的操作)
Object clone ():接口Clone的抽象函數。
實際我們用HTMLParser最多的是處理HTML頁面,Filter或Visitor相關的函數是必須的,然后第一類和第二類函數是用得最多的。第一類函數比較容易理解,下面用例子說明一下第二類函數。
下面是用于測試的HTML文件:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白澤居-www.baizeju.com</title></head>
<html xmlns="http://www.w3.org/1999/xhtml">
<body >
<div id="top_main">
<div id="logoindex">
<!--這是注釋-->
白澤居-www.baizeju.com
<a href="http://www.baizeju.com">白澤居-www.baizeju.com</a>
</div>
白澤居-www.baizeju.com
</div>
</body>
</html>
測試代碼:
/**
* @author www.baizeju.com
*/
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.baizeju.com
*/
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 );
}
}
}
輸出結果:
getText:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
getPlainText:
toHtml:<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
toHtml(true):<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
toHtml(false):<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
toString:Doctype Tag : !DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/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.baizeju.com
toHtml:<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白澤居-www.baizeju.com</title></head>
toHtml(true):<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白澤居-www.baizeju.com</title></head>
toHtml(false):<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白澤居-www.baizeju.com</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.baizeju.com
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.w3.org/1999/xhtml"
getPlainText:
白澤居-www.baizeju.com
白澤居-www.baizeju.com
白澤居-www.baizeju.com
toHtml:<html xmlns="http://www.w3.org/1999/xhtml">
<body >
<div id="top_main">
<div id="logoindex">
<!--這是注釋-->
白澤居-www.baizeju.com
<a href="http://www.baizeju.com">白澤居-www.baizeju.com</a>
</div>
白澤居-www.baizeju.com
</div>
</body>
</html>
toHtml(true):<html xmlns="http://www.w3.org/1999/xhtml">
<body >
<div id="top_main">
<div id="logoindex">
<!--這是注釋-->
白澤居-www.baizeju.com
<a href="http://www.baizeju.com">白澤居-www.baizeju.com</a>
</div>
白澤居-www.baizeju.com
</div>
</body>
</html>
toHtml(false):<html xmlns="http://www.w3.org/1999/xhtml">
<body >
<div id="top_main">
<div id="logoindex">
<!--這是注釋-->
白澤居-www.baizeju.com
<a href="http://www.baizeju.com">白澤居-www.baizeju.com</a>
</div>
白澤居-www.baizeju.com
</div>
</body>
</html>
toString:Tag (240[2,0],283[2,43]): html xmlns="http://www.w3.org/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.baizeju.com\n
Tag (376[8,0],409[8,33]): a href="http://www.baizeju.com"
Txt (409[8,33],428[8,52]): 白澤居-www.baizeju.com
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.baizeju.com\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的內容,對應的就是第一行<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">,這個比較好理解。
從這個輸出結果中,也可以看出內容的樹狀結構。或者說是樹林結構。在Page內容的第一層Tag,如DOCTYPE,head和html,分別形成了一個最高層的Node節點(很多人可能對第二個和第四個Node的內容有點奇怪。實際上這兩個Node就是兩個換行符號。HTMLParser把HTML頁面內容中的所有換行,空格,Tab等都轉換成了相應的Tag,所以就出現了這樣的Node。雖然內容少但是級別高,呵呵)
getPlainTextString是把用戶可以看到的內容都包含了。有趣的有兩點,一是<head>標簽中的Title內容是在plainText中的,可能在標題中可見的也算可見吧。另外就是象前面說的,HTML內容中的換行符什么的,也都成了plainText,這個邏輯上好像有點問題。
另外可能大家發現toHtml,toHtml(true)和toHtml(false)的結果沒什么區別。實際也是這樣的,如果跟蹤HTMLParser的代碼就可以發現,Node的子類是AbstractNode,其中實現了toHtml()的代碼,直接調用toHtml(false),而AbstractNode的三個子類RemarkNode,TagNode和TextNode中,toHtml(boolean verbatim)的實現中,都沒有處理verbatim參數,所以三個函數的結果是一模一樣的。如果你不需要實現你自己的什么特殊處理,簡單使用toHtml就可以了。
HTML的Node類繼承關系如下圖(這個是從別的文章Copy的):
AbstractNodes是Node的直接子類,也是一個抽象類。它的三個直接子類實現是RemarkNode,用于保存注釋。在輸出結果的toString部分中可以看到有一個"Rem (345[6,2],356[6,13]): 這是注釋",就是一個RemarkNode。TextNode也很簡單,就是用戶可見的文字信息。TagNode是最復雜的,包含了HTML語言中的所有標簽,而且可以擴展(擴展 HTMLParser 對自定義標簽的處理能力)。TagNode包含兩類,一類是簡單的Tag,實際就是不能包含其他Tag的標簽,只能做葉子節點。另一類是CompositeTag,就是可以包含其他Tag,是分支節點
HTMLParser遍歷了網頁的內容以后,以樹(森林)結構保存了結果。HTMLParser訪問結果內容的方法有兩種。使用Filter和使用Visitor。
(一)Filter類
顧名思義,Filter就是對于結果進行過濾,取得需要的內容。HTMLParser在org.htmlparser.filters包之內一共定義了16個不同的Filter,也可以分為幾類。
判斷類Filter:
TagNameFilter
HasAttributeFilter
HasChildFilter
HasParentFilter
HasSiblingFilter
IsEqualFilter
邏輯運算Filter:
AndFilter
NotFilter
OrFilter
XorFilter
其他Filter:
NodeClassFilter
StringFilter
LinkStringFilter
LinkRegexFilter
RegexFilter
CssSelectorNodeFilter
所有的Filter類都實現了org.htmlparser.NodeFilter接口。這個接口只有一個主要函數:
boolean accept (Node node);
各個子類分別實現這個函數,用于判斷輸入的Node是否符合這個Filter的過濾條件,如果符合,返回true,否則返回false。
(二)判斷類Filter
2.1 TagNameFilter
TabNameFilter是最容易理解的一個Filter,根據Tag的名字進行過濾。
下面是用于測試的HTML文件:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白澤居-www.baizeju.com</title>< /head>
<html xmlns="http://www.w3.org/1999/xhtml">
<body >
<div id="top_main">
<div id="logoindex">
<!--這是注釋-->
白澤居-www.baizeju.com
<a href="http://www.baizeju.com" rel="nofollow ugc noreferrer noopener">白澤居-www.baizeju.com</a>
</div>
白澤居-www.baizeju.com
</div>
</body>
</html>
測試代碼:(這里只列出了Main函數,全部代碼請參考 HTMLParser使用入門(2)- Node內容,自己添加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();
}
}
輸出結果:
getText:div id="top_main"
=================================================
getText:div id="logoindex"
=================================================
可以看出文件中兩個Div節點都被取出了。下面可以針對這兩個DIV節點進行操作
2.2 HasChildFilter
下面讓我們看看HasChildFilter。剛剛看到這個Filter的時候,我想當然地認為這個Filter返回的是有Child的Tag。直接初始化了一個
NodeFilter filter = new HasChildFilter();
結果調用NodeList nodes = parser.extractAllNodesThatMatch(filter);的時候HasChildFilter內部直接發生NullPointerException。讀了一下HasChildFilter的代碼,才發現,實際HasChildFilter是返回有符合條件的子節點的節點,需要另外一個Filter作為過濾子節點的參數。缺省的構造函數雖然可以初始化,但是由于子節點的Filter是null,所以使用的時候發生了Exception。從這點來看,HTMLParser的代碼還有很多可以優化的的地方。呵呵。
修改代碼:
NodeFilter innerFilter = new TagNameFilter ("DIV");
NodeFilter filter = new HasChildFilter(innerFilter);
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結果:
getText:body
=================================================
getText:div id="top_main"
=================================================
可以看到,輸出的是兩個有DIV子Tag的Tag節點。(body有子節點DIV "top_main","top_main"有子節點"logoindex"。
注意HasChildFilter還有一個構造函數:
public HasChildFilter (NodeFilter filter, boolean recursive)
如果recursive是false,則只對第一級子節點進行過濾。比如前面的例子,body和top_main都是在第一級的子節點里就有DIV節點,所以匹配上了。如果我們用下面的方法調用:
NodeFilter filter = new HasChildFilter( innerFilter, true );
輸出結果:
getText:html xmlns="http://www.w3.org/1999/xhtml"
=================================================
getText:body
=================================================
getText:div id="top_main"
=================================================
可以看到輸出結果中多了一個html xmlns="http://www.w3.org/1999/xhtml",這個是整個HTML頁面的節點(根節點),雖然這個節點下直接沒有DIV節點,但是它的子節點body下面有DIV節點,所以它也被匹配上了。
2.3 HasAttributeFilter
HasAttributeFilter有3個構造函數:
public HasAttributeFilter ();
public HasAttributeFilter (String attribute);
public HasAttributeFilter (String attribute, String value);
這個Filter可以匹配出包含制定名字的屬性,或者制定屬性為指定值的節點。還是用例子說明比較容易。
調用方法1:
NodeFilter filter = new HasAttributeFilter();
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結果:
什么也沒有輸出。
調用方法2:
NodeFilter filter = new HasAttributeFilter( "id" );
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結果:
getText:div id="top_main"
=================================================
getText:div id="logoindex"
=================================================
調用方法3:
NodeFilter filter = new HasAttributeFilter( "id", "logoindex" );
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結果:
getText:div id="logoindex"
=================================================
很簡單吧。呵呵
2.4 其他判斷列Filter
HasParentFilter和HasSiblingFilter的功能與HasChildFilter類似,大家自己試一下就應該了解了。
IsEqualFilter的構造函數參數是一個Node:
public IsEqualFilter (Node node) {
mNode = node;
}
accept函數也很簡單:
public boolean accept (Node node) {
return (mNode == node);
}
不需要過多說明了。
(三)邏輯運算Filter
前面介紹的都是簡單的Filter,只能針對某種單一類型的條件進行過濾。HTMLParser支持對于簡單類型的Filter進行組合,從而實現復雜的條件。原理和一般編程語言的邏輯運算是一樣的。
3.1 AndFilter
AndFilter可以把兩種Filter進行組合,只有同時滿足條件的Node才會被過濾。
測試代碼:
NodeFilter filterID = new HasAttributeFilter( "id" );
NodeFilter filterChild = new HasChildFilter(filterA);
NodeFilter filter = new AndFilter(filterID, filterChild);
輸出結果:
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);
輸出結果:
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));
輸出結果:
getText:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"
=================================================
getText:
=================================================
getText:head
=================================================
getText:meta http-equiv="Content-Type" content="text/html; charset=gb2312"
=================================================
getText:title
=================================================
getText:白澤居-www.baizeju.com
=================================================
getText:/title
=================================================
getText:/head
=================================================
getText:
=================================================
getText:html xmlns="http://www.w3.org/1999/xhtml"
=================================================
getText:
=================================================
getText:body
=================================================
getText:
=================================================
getText:
=================================================
getText:
=================================================
getText:這是注釋
=================================================
getText:
白澤居-www.baizeju.com
=================================================
getText:a href="http://www.baizeju.com"
=================================================
getText:白澤居-www.baizeju.com
=================================================
getText:/a
=================================================
getText:
=================================================
getText:/div
=================================================
getText:
白澤居-www.baizeju.com
=================================================
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);
輸出結果:
getText:div id="top_main"
=================================================
(四)其他Filter:
4.1 NodeClassFilter
這個Filter用于判斷節點類型是否是某個特定的Node類型。在HTMLParser使用入門(2)- Node內容 中我們已經了解了Node的不同類型,這個Filter就可以針對類型進行過濾。
測試代碼:
NodeFilter filter = new NodeClassFilter(RemarkNode.class);
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結果:
getText:這是注釋
=================================================
可以看到只有RemarkNode(注釋)被輸出了。
4.2 StringFilter
這個Filter用于過濾顯示字符串中包含制定內容的Tag。注意是可顯示的字符串,不可顯示的字符串中的內容(例如注釋,鏈接等等)不會被顯示。
修改一下例子代碼:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<head><meta http-equiv="Content-Type" content="text/html; charset=gb2312"><title>白澤居-title-www.baizeju.com</title></head>
<html xmlns="http://www.w3.org/1999/xhtml">
<body >
<div id="top_main">
<div id="logoindex">
<!--這是注釋白澤居-www.baizeju.com -->
白澤居-字符串1-www.baizeju.com
<a href="http://www.baizeju.com">白澤居-鏈接文本-www.baizeju.com</a>
</div>
白澤居-字符串2-www.baizeju.com
</div>
</body>
</html>
測試代碼:
NodeFilter filter = new StringFilter("www.baizeju.com");
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結果:
getText:白澤居-title-www.baizeju.com
=================================================
getText:
白澤居-字符串1-www.baizeju.com
=================================================
getText:白澤居-鏈接文本-www.baizeju.com
=================================================
getText:
白澤居-字符串2-www.baizeju.com
=================================================
可以看到包含title,兩個內容字符串和鏈接的文本字符串的Tag都被輸出了,但是注釋和鏈接Tag本身沒有輸出。
4.3 LinkStringFilter
這個Filter用于判斷鏈接中是否包含某個特定的字符串,可以用來過濾出指向某個特定網站的鏈接。
測試代碼:
NodeFilter filter = new LinkStringFilter("www.baizeju.com");
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結果:
getText:a href="http://www.baizeju.com"
=================================================
4.4 其他幾個Filter
其他幾個Filter也是根據字符串對不同的域進行判斷,與前面這些的區別主要就是支持正則表達式。這個不在本文的討論范圍以內,大家可以自己實驗一下。
HTMLParser遍歷了網頁的內容以后,以樹(森林)結構保存了結果。HTMLParser訪問結果內容的方法有兩種。使用Filter和使用Visitor。
下面介紹使用Visitor訪問內容的方法。
4.1 NodeVisitor
從簡單方面的理解,Filter是根據某種條件過濾取出需要的Node再進行處理。Visitor則是遍歷內容樹的每一個節點,對于符合條件的節點進行處理。實際的結果異曲同工,兩種不同的方法可以達到相同的結果。
下面是一個最常見的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();
}
}
輸出結果:
beginParsing
This is Tag:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/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
可以看到,開始遍歷所以的節點以前,beginParsing先被調用,然后處理的是中間的Node,最后在結束遍歷以前,finishParsing被調用。因為我設置的 recurseChildren和recurseSelf都是false,所以Visitor沒有訪問子節點也沒有訪問根節點的內容。中間輸出的兩個\n就是我們在HTMLParser使用詳解(1)- 初始化Parser 中討論過的最高層的那兩個換行。
我們先把recurseSelf設置成true,看看會發生什么。
NodeVisitor visitor = new NodeVisitor( false, true) {
輸出結果:
beginParsing
This is Tag:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/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.w3.org/1999/xhtml"
finishedParsing
可以看到,HTML頁面的第一層節點都被調用了。
我們再用下面的方法調用看看:
NodeVisitor visitor = new NodeVisitor( true, false) {
輸出結果:
beginParsing
This is Tag:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/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.baizeju.com
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.baizeju.com
This is Text:Txt (378[6,34],408[8,0]): \n\t\t白澤居-字符串1-www.baizeju.com\n
This is Text:Txt (441[8,33],465[8,57]): 白澤居-鏈接文本-www.baizeju.com
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.baizeju.com\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
可以看到,所有的子節點都出現了,除了剛剛例子里面的兩個最上層節點This is Tag:head和This is Tag:html xmlns="http://www.w3.org/1999/xhtml"。
想讓它們都出來,只需要
NodeVisitor visitor = new NodeVisitor( true, true) {
輸出結果:
beginParsing
This is Tag:!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/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.baizeju.com
visitEndTag:/title
visitEndTag:/head
This is Text:Txt (244[1,121],246[2,0]): \n
This is Tag:html xmlns="http://www.w3.org/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.baizeju.com
This is Text:Txt (378[6,34],408[8,0]): \n\t\t白澤居-字符串1-www.baizeju.com\n
This is Tag:a href="http://www.baizeju.com"
This is Text:Txt (441[8,33],465[8,57]): 白澤居-鏈接文本-www.baizeju.com
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.baizeju.com\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
哈哈,這下調用清楚了,大家在需要處理的地方增加自己的代碼好了。
4.2 其他Visitor
HTMLParser還定義了幾個其他的Visitor。HtmlPage,NodeVisitor,ObjectFindingVisitor,StringFindingVisitor,TagFindingVisitor,TextExtractingVisitor,UrlModifyingVisitor,它們都是NodeVisitor的子類,實現了一些特定的功能。筆者個人的感覺是沒什么用處,如果你需要什么特定的功能,還不如自己寫一個,想在這些里面找到適合你需要的,化的時間可能更多。反正大家看看代碼就發現,它們每個都沒幾行真正有效的代碼。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文檔中出現的標簽定義已經應有盡有,我們盡可以參考這些標簽解析類的源碼來學習如何實現一個標簽的解析類,從而擴展出更豐富多彩的應用程序。
來源:http://www.360doc.com/content/12/0806/10/7471983_228598216.shtml
文章列表