歡迎光臨
每天分享高質量文章

HTMLParser 原始碼解析

(點選上方公眾號,可快速關註)


來源: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技能

贊(0)

分享創造快樂