最近生活上有點忙,女兒總是半夜不睡,精神狀態也不是很好。工做上的事情也談不上順心,有不少想法可是沒有幾個被承認,有些事情也不是說代碼寫得好就行的。算了,仍是端正態度,畢竟資歷尚淺,我仍是繼續個人。javascript
讀Jsoup源碼並不是無聊,目的實際上是爲了將webmagic作的更好一點,畢竟parser也是爬蟲的重要組成部分之一。讀了代碼後,收穫也很多,對HTML的知識也更進一步了。html
這裏單獨將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
有兩個子類,HtmlTreeBuilder
和XmlTreeBuilder
。XmlTreeBuilder
天然是構建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; }
相比XmlTreeBuilder
,HtmlTreeBuilder
則實現較爲複雜,除了相似的棧結構之外,還用到了HtmlTreeBuilderState
來構建了一個狀態機來分析HTML。這是爲何呢?不妨看看HtmlTreeBuilderState
到底用到了哪些狀態吧(在代碼中中用<!-- State: -->標明狀態):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>
標籤爲例:
漏寫了開始標籤,只寫告終束標籤
<!-- 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>
的故事)
寫了開始標籤,漏寫告終束標籤
這個狀況分析起來更復雜一點。若是是沒法在內部嵌套內容的標籤,那麼在遇到不可接受的標籤時,會進行閉合。而<div>
標籤能夠包括大多數標籤,這種狀況下,其做用域會持續到HTML結束。
好了,parser系列算是分析結束了,其間學到很多HTML及狀態機內容,可是離實際使用比較遠。下面開始select部分,這部分可能對平常使用更有意義一點。
最後附上個人Jsoup系列博客及源碼地址:http://github.com/code4craft/jsoup-learning