Jsoup代碼解讀之二-DOM相關對象

以前在文章中說到,Jsoup使用了一套本身的DOM對象體系,和Java XML API互不兼容。這樣作的好處是從XML的API裏解脫出來,使得代碼精煉了不少。這篇文章會說明Jsoup的DOM結構,DOM的遍歷方式。在下一篇文章,我會並結合這兩個基礎,分析一下Jsoup的HTML輸出功能。html

DOM結構相關類

咱們先來看看nodes包的類圖:java

node類圖

這裏能夠看到,核心無疑是Node類。node

Node類是一個抽象類,它表明DOM樹中的一個節點,它包含:web

  • 父節點parentNode以及子節點childNodes的引用
  • 屬性值集合attributes
  • 頁面的uribaseUri,用於修正相對地址爲絕對地址
  • 在兄弟節點中的位置siblingIndex,用於進行DOM操做

Node裏面包含一些獲取屬性、父子節點、修改元素的方法,其中比較有意思的是absUrl()。咱們知道,在不少html頁面裏,連接會使用相對地址,咱們有時會須要將其轉變爲絕對地址。Jsoup的解決方案是在attr()的參數開始加"abs:",例如attr("abs:href"),而absUrl()就是其實現方式。我寫的爬蟲框架webmagic裏也用到了相似功能,當時是本身手寫的,看到Jsoup的實現,才發現本身是白費勁了,代碼以下:框架

<!-- lang: java -->
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元素。它包含一個字段tagclassNames。classNames是"class"屬性解析出來的集合,由於CSS規範裏,"class"屬性容許設置多個,並用空格隔開,而在用Selector選擇的時候,即便只指定其中一個,也可以選中其中的元素。因此這裏就把"class"屬性展開了。Element還有選取元素的入口,例如selectgetElementByXXX,這些都用到了select包中的內容,這個留到下篇文章select再說。.net

Document是表明整個文檔,它也是一個特殊的Element,即根節點。Document除了Element的內容,還包括一些輸出的方法。code

Document還有一個屬性quirksMode,大體意思是定義處理非標準HTML的幾個級別,這個留到之後分析parser的時候再說。orm

DOM樹的遍歷

Node還有一些方法,例如outerHtml(),用做節點及文檔HTML的輸出,用到了樹的遍歷。在DOM樹的遍歷上,用到了NodeVisitorNodeTraversor來對樹的進行遍歷。NodeVisitor在上一篇文章提到過了,head()和tail()分別是遍歷開始和結束時的方法,而NodeTraversor的核心代碼以下:htm

<!-- lang: java -->
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。

在下一篇博客我會講講Document的輸出。

相關文章
相關標籤/搜索