以前在文章中說到,Jsoup使用了一套本身的DOM對象體系,和Java XML API互不兼容。這樣作的好處是從XML的API裏解脫出來,使得代碼精煉了不少。這篇文章會說明Jsoup的DOM結構,DOM的遍歷方式。在下一篇文章,我會並結合這兩個基礎,分析一下Jsoup的HTML輸出功能。html
咱們先來看看nodes包的類圖:java
這裏能夠看到,核心無疑是Node
類。node
Node類是一個抽象類,它表明DOM樹中的一個節點,它包含:web
parentNode
以及子節點childNodes
的引用attributes
baseUri
,用於修正相對地址爲絕對地址siblingIndex
,用於進行DOM操做Node裏面包含一些獲取屬性、父子節點、修改元素的方法,其中比較有意思的是absUrl()
。咱們知道,在不少html頁面裏,連接會使用相對地址,咱們有時會須要將其轉變爲絕對地址。Jsoup的解決方案是在attr()的參數開始加"abs:「,例如attr(「abs:href」),而absUrl()
就是其實現方式。我寫的爬蟲框架webmagic裏也用到了相似功能,當時是本身手寫的,看到Jsoup的實現,才發現本身是白費勁了,代碼以下:框架
URL base;
try { try { base = new URL(baseUri); } catch (MalformedURLException e) { // the base is unsuitable, but the attribute may be abs on its own, so try that URL abs = new URL(relUrl); return abs.toExternalForm(); } // workaround: java resolves '//path/file + ?foo' to '//path/?foo', not '//path/file?foo' as desired if (relUrl.startsWith("?")) relUrl = base.getPath() + relUrl; // java URL自帶的相對路徑解析 URL abs = new URL(base, relUrl); return abs.toExternalForm(); } catch (MalformedURLException e) { return ""; }
Node還有一個比較值得一提的方法是abstract String nodeName()
,這個至關於定義了節點的類型名(例如Document
是'#Document',Element
則是對應的TagName)。ui
Element也是一個重要的類,它表明的是一個HTML元素。它包含一個字段tag
和classNames
。classNames是"class"屬性解析出來的集合,由於CSS規範裏,「class"屬性容許設置多個,並用空格隔開,而在用Selector選擇的時候,即便只指定其中一個,也可以選中其中的元素。因此這裏就把"class"屬性展開了。Element還有選取元素的入口,例如select
、getElementByXXX
,這些都用到了select包中的內容,這個留到下篇文章select再說。spa
Document是表明整個文檔,它也是一個特殊的Element,即根節點。Document除了Element的內容,還包括一些輸出的方法。.net
Document還有一個屬性quirksMode
,大體意思是定義處理非標準HTML的幾個級別,這個留到之後分析parser的時候再說。code
Node還有一些方法,例如outerHtml()
,用做節點及文檔HTML的輸出,用到了樹的遍歷。在DOM樹的遍歷上,用到了NodeVisitor
和NodeTraversor
來對樹的進行遍歷。NodeVisitor
在上一篇文章提到過了,head()和tail()分別是遍歷開始和結束時的方法,而NodeTraversor
的核心代碼以下:orm
public void traverse(Node root) { Node node = root; int depth = 0; //這裏對樹進行後序(深度優先)遍歷 while (node != null) { //開始遍歷node visitor.head(node, depth); if (node.childNodeSize() > 0) { node = node.childNode(0); depth++; } else { //沒有下一個兄弟節點,退棧 while (node.nextSibling() == null && depth > 0) { visitor.tail(node, depth); node = node.parent(); depth--; } //結束遍歷 visitor.tail(node, depth); if (node == root) break; node = node.nextSibling(); } } }
這裏使用循環+回溯來替換掉了咱們經常使用的遞歸方式,從而避免了棧溢出的風險。
實際上,Jsoup的Selector機制也是基於NodeVisitor
來實現的,能夠說NodeVisitor
是更加底層和靈活的API。