Jsoup代碼解讀之六-parser(下)

最近生活上有點忙,女兒總是半夜不睡,精神狀態也不是很好。工做上的事情也談不上順心,有不少想法可是沒有幾個被承認,有些事情也不是說代碼寫得好就行的。算了,仍是端正態度,畢竟資歷尚淺,我仍是繼續個人。javascript

讀Jsoup源碼並不是無聊,目的實際上是爲了將webmagic作的更好一點,畢竟parser也是爬蟲的重要組成部分之一。讀了代碼後,收穫也很多,對HTML的知識也更進一步了。html

DOM樹產生過程

這裏單獨將TreeBuilder部分抽出來叫作語法分析過程可能稍微不妥,其實就是根據Token生成DOM樹的過程,不過我仍是沿用這個編譯器裏的稱呼了。java

TreeBuilder一樣是一個facade對象,真正進行語法解析的是如下一段代碼:git

<!-- lang: java -->
protected void runParser() {
    while (true) {
        Token token = tokeniser.read();
        
        process(token);

        if (token.type == Token.TokenType.EOF)
            break;
    }
}

TreeBuilder有兩個子類,HtmlTreeBuilderXmlTreeBuilderXmlTreeBuilder天然是構建XML樹的類,實現頗爲簡單,基本上是維護一個棧,並根據不一樣Token插入節點便可:github

<!-- lang: java -->
@Override
protected boolean process(Token token) {
    // start tag, end tag, doctype, comment, character, eof
    switch (token.type) {
        case StartTag:
            insert(token.asStartTag());
            break;
        case EndTag:
            popStackToClose(token.asEndTag());
            break;
        case Comment:
            insert(token.asComment());
            break;
        case Character:
            insert(token.asCharacter());
            break;
        case Doctype:
            insert(token.asDoctype());
            break;
        case EOF: // could put some normalisation here if desired
            break;
        default:
            Validate.fail("Unexpected token type: " + token.type);
    }
    return true;
}

insertNode的代碼大體是這個樣子(爲了便於展現,對方法進行了一些整合):web

<!-- lang: java -->
Element insert(Token.StartTag startTag) {
    Tag tag = Tag.valueOf(startTag.name());
    Element el = new Element(tag, baseUri, startTag.attributes);
    stack.getLast().appendChild(el);
    if (startTag.isSelfClosing()) {
        tokeniser.acknowledgeSelfClosingFlag();
        if (!tag.isKnownTag()) // unknown tag, remember this is self closing for output. see above.
            tag.setSelfClosing();
    } else {
        stack.add(el);
    }
    return el;
}

HTML解析狀態機

相比XmlTreeBuilderHtmlTreeBuilder則實現較爲複雜,除了相似的棧結構之外,還用到了HtmlTreeBuilderState來構建了一個狀態機來分析HTML。這是爲何呢?不妨看看HtmlTreeBuilderState到底用到了哪些狀態吧(在代碼中中用<!-- State: --&gt;標明狀態):app

<!-- lang: html -->
<!-- State: Initial -->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- State: BeforeHtml -->
<html lang='zh-CN' xml:lang='zh-CN' xmlns='http://www.w3.org/1999/xhtml'>
<!-- State: BeforeHead -->
<head>
  <!-- State: InHead -->
  <script type="text/javascript">
  //<!-- State: Text -->
    function xx(){
    }
  </script>
  <noscript>
    <!-- State: InHeadNoscript -->
    Your browser does not support JavaScript!
  </noscript>
</head>
<!-- State: AfterHead -->
<body>
<!-- State: InBody -->
<textarea>
    <!-- State: Text -->
    xxx
</textarea>
<table>
    <!-- State: InTable -->
    <!-- State: InTableText -->
    xxx
    <tbody>
    <!-- State: InTableBody -->
    </tbody>
    <tr>
        <!-- State: InRow -->
        <td>
            <!-- State: InCell -->
        </td>
    </tr>    
</table>
</html>

這裏能夠看到,HTML標籤是有嵌套要求的,例如<tr>,<td>須要組合<table>來使用。根據Jsoup的代碼,能夠發現,HtmlTreeBuilderState作了如下一些事情:ide

  • 語法檢查

    例如tr沒有嵌套在table標籤內,則是一個語法錯誤。當InBody狀態直接出現如下tag時,則出錯。Jsoup裏遇到這種錯誤,會發現這個Token的解析並記錄錯誤,而後繼續解析下面內容,並不會直接退出。ui

    <!-- lang: java -->
      InBody {
          boolean process(Token t, HtmlTreeBuilder tb) {
      		if (StringUtil.in(name,
      		"caption", "col", "colgroup", "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr")) {
      		tb.error(this);
      		return false;
      		}
          }
  • 標籤補全

    例如head標籤沒有閉合,就寫入了一些只有body內才容許出現的標籤,則自動閉合</head>HtmlTreeBuilderState有的方法anythingElse()就提供了自動補全標籤,例如InHead狀態的自動閉合代碼以下:this

    <!-- lang: java -->
          private boolean anythingElse(Token t, TreeBuilder tb) {
              tb.process(new Token.EndTag("head"));
              return tb.process(t);
          }

    還有一種標籤閉合方式,例以下面的代碼:

    <!-- lang: java -->
      private void closeCell(HtmlTreeBuilder tb) {
          if (tb.inTableScope("td"))
              tb.process(new Token.EndTag("td"));
          else
              tb.process(new Token.EndTag("th")); // only here if th or td in scope
      }

實例研究

缺乏標籤時,會發生什麼事?

好了,看了這麼多parser的源碼,不妨回到咱們的平常應用上來。咱們知道,在頁面裏多寫一個兩個未閉合的標籤是很正常的事,那麼它們會被怎麼解析呢?

就拿<div>標籤爲例:

  1. 漏寫了開始標籤,只寫告終束標籤

    <!-- lang: java -->
     case EndTag:
     	if (StringUtil.in(name,"div","dl", "fieldset", "figcaption", "figure", "footer", "header", "pre", "section", "summary", "ul")) {                
     		if (!tb.inScope(name)) {
     		tb.error(this);
     		return false;
     		} 
     	}

    恭喜你,這個</div>會被當作錯誤處理掉,因而你的頁面就毫無疑問的亂掉了!固然,若是單純多寫了一個</div>,好像也不會有什麼影響哦?(記得有人跟我講過爲了防止標籤未閉合,而在頁面底部多寫了幾個</div>的故事)

  2. 寫了開始標籤,漏寫告終束標籤

    這個狀況分析起來更復雜一點。若是是沒法在內部嵌套內容的標籤,那麼在遇到不可接受的標籤時,會進行閉合。而<div>標籤能夠包括大多數標籤,這種狀況下,其做用域會持續到HTML結束。

好了,parser系列算是分析結束了,其間學到很多HTML及狀態機內容,可是離實際使用比較遠。下面開始select部分,這部分可能對平常使用更有意義一點。

最後附上個人Jsoup系列博客及源碼地址:http://github.com/code4craft/jsoup-learning

相關文章
相關標籤/搜索