(點選上方公眾號,可快速關註)
來源:saymagic,
blog.saymagic.cn/2016/10/02/understand-htmlparser.html
最近有解析HTML的需求,在Java中,好用的HTML解析框架也比較多,如JSoup,HTMLParser, JTidy等等。在對比幾款框架之後,最終選取了HTMLParser做為第一版實現的框架。所以對HTMLParser的原始碼進行了一次整理。由於這種解析類的框架內部細節特別多,所以這裡並不會特別的關註所有細節,而是側重梳理HTMLParser整個解析的流程。
類圖
對我而言,畫類圖是學習一個框架原始碼比較直接的方式,一是有利於自己梳理邏輯,二是以後自己看類圖還是會很容易聯想起其中的一些細節。所以,這裡放出類圖,下麵會對主要的類原始碼進行分析。
整體介紹
HTMLParser主要靠Node來表示一個節點,細分為Text、Remark和Tag,透過以上三種形式的組合來表示Html,其中Text介面表示純文字,Remark表示註釋,Tag表示標簽。
Parser是直接對外提供服務的類,其parse方法可以傳回整個HTML檔案被轉換後的NodeList。
Lexer直譯過來為詞法分析程式,它主要負責將Html轉換為Node節點的,其nextNode方法使我們後文分析的重點。Lexer與Parser的關係就像老闆與員工,Parser是對外談生意的,Lexer才是實打實幹活的夥計。
Page表示整個HTML檔案,但它裡面的主要邏輯都交由Source負責,Source是對HTML源的一個抽象,HTMLParser中有InputStreamSource與StringSource兩種實現,前一種可以處理網路或者檔案類的流資訊,後者可以處理純文字的HTML。
解析流程
HTMLParser的使用非常簡單,如下就是最基本的形式:
Parser parser = new Parser(TEXT);
NodeList list = parser.parse(null);
其中的TEXT可以使純文字HTML,也可以是一個url,HTMLParser內部會自動判斷,但是其判斷的邏輯非常簡單:
length = resource.length ();
html = false;
for (int i = 0; i < length; i++)
{
ch = resource.charAt (i);
if (!Character.isWhitespace (ch))
{
if (‘
html = true;
break;
}
}
只是判斷了首個不為空白的字元是否為
接下來我們主要看Parser的parse方法:
public NodeList parse (NodeFilter filter) throws ParserException
{
NodeIterator e;
Node node;
NodeList ret;
ret = new NodeList ();
for (e = elements (); e.hasMoreNodes (); )
{
node = e.nextNode ();
if (null != filter)
node.collectInto (ret, filter);
else
ret.add (node);
}
return (ret);
}
整體來看,這個函式並沒有做什麼東西,唯一可能複雜的就是在變數e(NodeIterator)的獲取上,我們追進elements方法:
public NodeIterator elements () throws ParserException
{
return (new IteratorImpl (getLexer (), getFeedback ()));
}
這個方法最終傳回了IteratorImpl實體,getLexer方法獲取了前面說過負責將Html轉換為Node節點的Lexer,Lexer的實體是在Parser的建構式中建立的,getFeedback方法傳回的是ParserFeedback的一個實體,它的主要作用就是輸出一些資訊。所以,我們主要來看下IteratorImpl的建構式的實現:
public IteratorImpl (Lexer lexer, ParserFeedback fb)
{
mLexer = lexer;
mFeedback = fb;
mCursor = new Cursor (mLexer.getPage (), 0);
}
首先,快取變數lexer與fb,緊接著,生成Cursor變數,這個Cursor用來表示當前處理的位置資訊。
緊接著,我們來看IteratorImpl的hasMoreNodes方法:
public boolean hasMoreNodes() throws ParserException
{
boolean ret;
mCursor.setPosition (mLexer.getPosition ());
ret = Page.EOF != mLexer.getPage ().getCharacter (mCursor); // more characters?
return (ret);
}
這裡需要明確的是,mLexer是用來處理HTML的,所以它知道當前處理的位置,而這個位置,就用cursor表示。Page表示整個HTML檔案,所以,它可以根據cursor的資訊來查詢當前cursor所對應的字元。因此,上述函式翻譯過來就是檢視當前處理的節點是否為結束符,如果是,則表示沒有更多節點了,傳回false。
接下來,來看nextNode函式,這裡省略一些異常處理:
ret = mLexer.nextNode ();
if (null != ret)
{
// kick off recursion for the top level node
if (ret instanceof Tag)
{
tag = (Tag)ret;
if (!tag.isEndTag ())
{
// now recurse if there is a scanner for this type of tag
scanner = tag.getThisScanner ();
if (null != scanner)
{
stack = new NodeList ();
ret = scanner.scan (tag, mLexer, stack);
}
}
}
}
return ret;
首先,這個函式的前面邏輯交由了lexer的nextNode函式,所以,lexer的nextNode函式我們肯定要跟進,但這裡我們先存個檔,記為A,因為一會會回到這裡。我們追進nextNode,
public Node nextNode (boolean quotesmart)
throws
ParserException
{
int start;
char ch;
Node ret;
// debugging suppport
if (-1 != mDebugLineTrigger)
{
Page page = getPage ();
int lineno = page.row (mCursor);
if (mDebugLineTrigger < lineno)
mDebugLineTrigger = lineno + 1; // trigger on next line too
}
start = mCursor.getPosition ();
ch = mPage.getCharacter (mCursor);
switch (ch)
{
case Page.EOF:
ret = null;
break;
case ‘
ch = mPage.getCharacter (mCursor);
if (Page.EOF == ch)
ret = makeString (start, mCursor.getPosition ());
else if (‘%’ == ch)
{
mPage.ungetCharacter (mCursor);
ret = parseJsp (start);
}
else if (‘?’ == ch)
{
mPage.ungetCharacter (mCursor);
ret = parsePI (start);
}
else if (‘/’ == ch || ‘%’ == ch || Character.isLetter (ch))
{
mPage.ungetCharacter (mCursor);
ret = parseTag (start);
}
else if (‘!’ == ch)
{
…
}
else
{
mPage.ungetCharacter (mCursor); // see bug #1547354 <
parsed as text ret = parseString (start, quotesmart);
}
break;
default:
mPage.ungetCharacter (mCursor); // string needs to see leading foreslash
ret = parseString (start, quotesmart);
break;
}
return (ret);
}
首先,說明一點,對於mPage.getCharacter (mCursor)這段程式碼而言,首先會傳回當前cursor對應的字元,緊接著getCharacter函式內部還會對cursor的位置只能的進行加一,所以這就是整個nextNode函式內部都沒有看到對cursor位置移動相關的程式碼的原因。
對於正常的HTML檔案而言,頭一個字元都會是
mPage.ungetCharacter (mCursor);
ret = parseTag (start);
ungetCharacter會智慧的回退cursor的字元,執行過後,cursor會回到上一步getCharacter之前的狀態。按理說我們此時需要追入parseTag方法, 但這個方法非常長,並且邏輯就比較噁心了,可能會引起您的不適,所以這裡就不貼parseTag的程式碼了,它所做的主要功能是提取出一個標簽的名稱和標簽內的屬性。但它並沒有徹底解析整個標簽。還記得剛剛存檔A嗎?解析整個標簽的邏輯在A那裡:
if (ret instanceof Tag)
{
tag = (Tag)ret;
if (!tag.isEndTag ())
{
// now recurse if there is a scanner for this type of tag
scanner = tag.getThisScanner ();
if (null != scanner)
{
stack = new NodeList ();
ret = scanner.scan (tag, mLexer, stack);
}
}
}
可以看到,如果nextNode傳回的ret為Tag且部位endTag的話,會執行一個scanner的scan方法,那這個scanner是什麼鬼呢?它就是負責解析整個tag標簽的神奇boss。它的實現比較多,一般而言,我們更長接觸的是CompositeTagScanner,所以我們來看一下CompositeTagScanner的scan方法,這個方法也非常的長,這裡我省略了非常多:
do
{
node = lexer.nextNode (false);
if (null != node)
{
if (node instanceof Tag)
{
next = (Tag)node;
name = next.getTagName ();
// check for normal end tag
if (next.isEndTag () && name.equals (ret.getTagName ()))
{
ret.setEndTag (next);
node = null;
}
else if (!next.isEndTag ())
{
// now recurse if there is a scanner for this type of tag
scanner = next.getThisScanner ();
if (null != scanner)
{
node = scanner.scan (next, lexer, stack);
addChild (ret, node);
}
else
addChild (ret, next);
}
}
}
while (null != node);
首先,lexer會迭代出下一個node,如果這個node為Tag,首先會判斷是否為當前node的endTag,如果是則表示可以結束當前node的scan。將node制空,結束當前迴圈。否則的話,會走到else if (!next.isEndTag ())的分支,當心迭代的node可以被scan的話,會執行到node = scanner.scan (next, lexer, stack);這句,相當於遞迴的進行節點的掃描。知道找到最終節點的endTag,結束當前迴圈。表示遍歷完一個Node。
當然,上面的scan函式做了非常多的精簡,真實的scan函式,因為細節點很多,所以實現遠比這複雜的多。
綜上,透過對Lexer的nextNode函式與Scan的scan函式之間的不斷配合,HTMLParser就完成了整個解析。我們也可以得到一個非常重要的結論: HTMLParser的遍歷是深度優先的!
參考
-
http://htmlparser.sourceforge.net/
看完本文有收穫?請轉發分享給更多人
關註「ImportNew」,提升Java技能