HTMLParser 詳細 使用

原文地址 http://www.cnblogs.com/loveyakamoz/archive/2011/07/27/2118937.html

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 ()取得這個NodeHTML頁面中的起始位置
int 
getEndPosition ()取得這個NodeHTML頁面中的結束位置
用於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頁面,FilterVisitor相關的函數是必須的,然後第一類和第二類函數是用得最多的。第一類函數比較容易理解,下面用例子說明一下第二類函數。
下面是用於測試的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,如DOCTYPEheadhtml,分別形成了一個最高層的Node節點(很多人可能對第二個和第四個Node的內容有點奇怪。實際上這兩個Node就是兩個換行符號。HTMLParserHTML頁面內容中的所有換行,空格,Tab等都轉換成了相應的Tag,所以就出現了這樣的Node。雖然內容少但是級別高,呵呵)
getPlainTextString
是把用戶可以看到的內容都包含了。有趣的有兩點,一是<head>標籤中的Title內容是在plainText中的,可能在標題中可見的也算可見吧。另外就是象前面說的,HTML內容中的換行符什麼的,也都成了plainText,這個邏輯上好像有點問題。

另外可能大家發現toHtmltoHtml(true)toHtml(false)的結果沒什麼區別。實際也是這樣的,如果跟蹤HTMLParser的代碼就可以發現,Node的子類是AbstractNode,其中實現了toHtml()的代碼,直接調用toHtml(false),而AbstractNode的三個子類RemarkNodeTagNodeTextNode中,toHtml(boolean verbatim)的實現中,都沒有處理verbatim參數,所以三個函數的結果是一模一樣的。如果你不需要實現你自己的什麼特殊處理,簡單使用toHtml就可以了。

HTML
Node類繼承關係如下圖(這個是從別的文章Copy的):

 

AbstractNodesNode的直接子類,也是一個抽象類。它的三個直接子類實現是RemarkNode,用於保存註釋。在輸出結果的toString部分中可以看到有一個"Rem (345[6,2],356[6,13]): 這是註釋",就是一個RemarkNodeTextNode也很簡單,就是用戶可見的文字信息。TagNode是最複雜的,包含了HTML語言中的所有標籤,而且可以擴展(擴展 HTMLParser 對自定義標籤的處理能力)。TagNode包含兩類,一類是簡單的Tag,實際就是不能包含其他Tag的標籤,只能做葉子節點。另一類是CompositeTag,就是可以包含其他Tag,是分支節點

HTMLParser遍歷了網頁的內容以後,以樹(森林)結構保存了結果。HTMLParser訪問結果內容的方法有兩種。使用Filter和使用Visitor

(一)Filter
顧名思義,Filter就是對於結果進行過濾,取得需要的內容。HTMLParserorg.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">
白澤居-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返回的是有ChildTag。直接初始化了一個
NodeFilter filter = new HasChildFilter();
結果調用NodeList nodes = parser.extractAllNodesThatMatch(filter);的時候HasChildFilter內部直接發生NullPointerException。讀了一下HasChildFilter的代碼,才發現,實際HasChildFilter是返回有符合條件的子節點的節點,需要另外一個Filter作爲過濾子節點的參數。缺省的構造函數雖然可以初始化,但是由於子節點的Filternull,所以使用的時候發生了Exception。從這點來看,HTMLParser的代碼還有很多可以優化的的地方。呵呵。

修改代碼:
NodeFilter innerFilter = new TagNameFilter ("DIV");
NodeFilter filter = new HasChildFilter(innerFilter);
NodeList nodes = parser.extractAllNodesThatMatch(filter);
輸出結果:
getText:body 
=================================================
getText:div id="top_main"
=================================================
可以看到,輸出的是兩個有DIVTagTag節點。(body有子節點DIV "top_main""top_main"有子節點"logoindex"

注意HasChildFilter還有一個構造函數:
public HasChildFilter (NodeFilter filter, boolean recursive)
如果recursivefalse,則只對第一級子節點進行過濾。比如前面的例子,bodytop_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
HasAttributeFilter3個構造函數:
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被調用。因爲我設置的 recurseChildrenrecurseSelf都是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:headThis 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
還定義了幾個其他的VisitorHtmlPageNodeVisitorObjectFindingVisitorStringFindingVisitorTagFindingVisitorTextExtractingVisitorUrlModifyingVisitor,它們都是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 文檔,而是一個 WMLWireless Markup Lauguage)文檔呢?WML 文檔中的 cardanchor 等標籤 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,內容如下:

 


 

 

1. test.wml

 

<?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 中的粗體部分是我們需要提取出來的鏈接。

 


 

 

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());

 

  //註冊新的結點解析器

  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在處理一個複合標籤時需要知道該標籤的起始標識以及結束標識,也就是我們在前面給出的源碼中的兩個方法getIdsgetEnders,一般來講,標籤出現都是成對的,因此這兩個方法一般返回相同的值。另外一個方法getEndTagEnders,這個方法用來返回父一級的標籤名稱,例如<tr>的父一級標籤應該是<table>。這個方法的必要性在於HTML對格式的要求很不嚴格,在很多的HTML文檔中的一些標籤經常是有開始標識,但是沒有結束標識,由於瀏覽器的超強適應能力使這種情況出現的很頻繁,因此HTMLParser利用這個方法來輔助判斷一個標籤是否已經結束。由於WML文檔的格式要求非常嚴格,因此上例源碼中的getEndTagEnders方法事實上可有可無。

4. 入口方法main:該方法初始化HTMLParser並註冊新的節點解析器,解析文檔並打印運行結果。

最後我們編譯並運行這個例子,便可以得到下面的運行結果:

 

GO:  /wap/user.do

LINK:  /wap/index.vm

HTMLParser本身就是一個開放源碼的項目,它對於HTML文檔中出現的標籤定義已經應有盡有,我們儘可以參考這些標籤解析類的源碼來學習如何實現一個標籤的解析類,從而擴展出更豐富多彩的應用程序。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章