Jsoup代码解读之三-Document的输出

 

Jsoup官方说明里,一个重要的功能就是output tidy HTML。这里我们看看Jsoup是如何输出HTML的。

HTML相关知识

分析代码前,我们不妨先想想,“tidy HTML"到底包括哪些东西:

  • 换行,块级标签习惯上都会独占一行
  • 缩进,根据HTML标签嵌套层数,行首缩进会不同
  • 严格的标签闭合,如果是可以自闭合的标签并且没有内容,则进行自闭合
  • HTML实体的转义

这里要补充一下HTML标签的知识。HTML Tag可以分为block和inline两类。关于Tag的inline和block的定义可以参考http://www.w3schools.com/html/html_blocks.asp,而Jsoup的Tag类则是对Java开发者非常好的学习资料。


// internal static initialisers:
// prepped from http://www.w3.org/TR/REC-html40/sgml/dtd.html and other sources
//block tags,需要换行
private static final String[] blockTags = {
        "html", "head", "body", "frameset", "script", "noscript", "style", "meta", "link", "title", "frame",
        "noframes", "section", "nav", "aside", "hgroup", "header", "footer", "p", "h1", "h2", "h3", "h4", "h5", "h6",
        "ul", "ol", "pre", "div", "blockquote", "hr", "address", "figure", "figcaption", "form", "fieldset", "ins",
        "del", "s", "dl", "dt", "dd", "li", "table", "caption", "thead", "tfoot", "tbody", "colgroup", "col", "tr", "th",
        "td", "video", "audio", "canvas", "details", "menu", "plaintext"
};
//inline tags,无需换行
private static final String[] inlineTags = {
        "object", "base", "font", "tt", "i", "b", "u", "big", "small", "em", "strong", "dfn", "code", "samp", "kbd",
        "var", "cite", "abbr", "time", "acronym", "mark", "ruby", "rt", "rp", "a", "img", "br", "wbr", "map", "q",
        "sub", "sup", "bdo", "iframe", "embed", "span", "input", "select", "textarea", "label", "button", "optgroup",
        "option", "legend", "datalist", "keygen", "output", "progress", "meter", "area", "param", "source", "track",
        "summary", "command", "device"
};
//emptyTags是不能有内容的标签,这类标签都是可以自闭合的
private static final String[] emptyTags = {
        "meta", "link", "base", "frame", "img", "br", "wbr", "embed", "hr", "input", "keygen", "col", "command",
        "device"
};
private static final String[] formatAsInlineTags = {
        "title", "a", "p", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "address", "li", "th", "td", "script", "style",
        "ins", "del", "s"
};
//在这些标签里,需要保留空格
private static final String[] preserveWhitespaceTags = {
        "pre", "plaintext", "title", "textarea"
};

另外,Jsoup的Entities类里包含了一些HTML实体转义的东西。这些转义的对应数据保存在entities-full.propertiesentities-base.properties里。

Jsoup的格式化实现

在Jsoup里,直接调用Document.toString()(继承自Element),即可对文档进行输出。另外OutputSettings可以控制输出格式,主要是prettyPrint(是否重新格式化)、outline(是否强制所有标签换行)、indentAmount(缩进长度)等。

里面的继承和互相调用关系略微复杂,大概是这样子:

Document.toString()=>Document.outerHtml()=>Element.html(),最终Element.html()又会循环调用所有子元素的outerHtml(),拼接起来作为输出。


private void html(StringBuilder accum) {
    for (Node node : childNodes)
        node.outerHtml(accum);
}

outerHtml()会使用一个OuterHtmlVisitor对所以子节点做遍历,并拼装起来作为结果。


protected void outerHtml(StringBuilder accum) {
    new NodeTraversor(new OuterHtmlVisitor(accum, getOutputSettings())).traverse(this);
}

OuterHtmlVisitor会对所有子节点做遍历,并调用node.outerHtmlHead()node.outerHtmlTail两个方法。


private static class OuterHtmlVisitor implements NodeVisitor {
    private StringBuilder accum;
    private Document.OutputSettings out;

    public void head(Node node, int depth) {
        node.outerHtmlHead(accum, depth, out);
    }

    public void tail(Node node, int depth) {
        if (!node.nodeName().equals("#text")) // saves a void hit.
            node.outerHtmlTail(accum, depth, out);
    }
}

我们终于找到了真正工作的代码,node.outerHtmlHead()node.outerHtmlTail。Jsoup里每种Node的输出方式都不太一样,这里只讲讲两种主要节点:ElementTextNodeElement是格式化的主要对象,它的两个方法代码如下:


void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) {
    if (accum.length() > 0 && out.prettyPrint()
            && (tag.formatAsBlock() || (parent() != null && parent().tag().formatAsBlock()) || out.outline()) )
        //换行并调整缩进
        indent(accum, depth, out);
    accum
            .append("<")
            .append(tagName());
    attributes.html(accum, out);

    if (childNodes.isEmpty() && tag.isSelfClosing())
        accum.append(" />");
    else
        accum.append(">");
}

void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) {
    if (!(childNodes.isEmpty() && tag.isSelfClosing())) {
        if (out.prettyPrint() && (!childNodes.isEmpty() && (
                tag.formatAsBlock() || (out.outline() && (childNodes.size()>1 || (childNodes.size()==1 && !(childNodes.get(0) instanceof TextNode))))
        )))
            //换行并调整缩进
            indent(accum, depth, out);
        accum.append("</").append(tagName()).append(">");
    }
}

而ident方法的代码只有一行:


protected void indent(StringBuilder accum, int depth, Document.OutputSettings out) {
    //out.indentAmount()是缩进长度,默认是1
    accum.append("\n").append(StringUtil.padding(depth * out.indentAmount()));
}

代码简单明了,就没什么好说的了。值得一提的是,StringUtil.padding()方法为了减少字符串生成,把常用的缩进保存到了一个数组中。

好了,水了一篇文章,下一篇将比较有技术含量的parser部分。

另外,通过本节的学习,我们学到了要把StringBuilder命名为accum,而不是sb

Jsoup代码解读之三-Document的输出的更多相关文章

  1. Jsoup代码解读之六-防御XSS攻击

    Jsoup代码解读之八-防御XSS攻击 防御XSS攻击的一般原理 cleaner是Jsoup的重要功能之一,我们常用它来进行富文本输入中的XSS防御. 我们知道,XSS攻击的一般方式是,通过在页面输入 ...

  2. Jsoup代码解读之二-DOM相关对象

    Jsoup代码解读之二-DOM相关对象   之前在文章中说到,Jsoup使用了一套自己的DOM对象体系,和Java XML API互不兼容.这样做的好处是从XML的API里解脱出来,使得代码精炼了很多 ...

  3. Jsoup代码解读之四-parser

    Jsoup代码解读之四-parser 作为Java世界最好的HTML 解析库,Jsoup的parser实现非常具有代表性.这部分也是Jsoup最复杂的部分,需要一些数据结构.状态机乃至编译器的知识.好 ...

  4. Jsoup代码解读之一-概述

    Jsoup代码解读之一-概述 今天看到一个用python写的抽取正文的东东,美滋滋的用Java实现了一番,放到了webmagic里,然后发现Jsoup里已经有了…觉得自己各种不靠谱啊!算了,静下心来学 ...

  5. Jsoup代码解读之五-实现一个CSS Selector

    Jsoup代码解读之七-实现一个CSS Selector 当当当!终于来到了Jsoup的特色:CSS Selector部分.selector也是我写的爬虫框架webmagic开发的一个重点.附上一张s ...

  6. 去掉你代码里的 document.write("<script...

    在传统的浏览器中,同步的 script 标签是会阻塞 HTML 解析器的,无论是内联的还是外链的,比如: <script src="a.js"></script& ...

  7. 优秀开源代码解读之JS与iOS Native Code互调的优雅实现方案

    简介 本篇为大家介绍一个优秀的开源小项目:WebViewJavascriptBridge. 它优雅地实现了在使用UIWebView时JS与ios 的ObjC nativecode之间的互调,支持消息发 ...

  8. Hybrid----优秀开源代码解读之JS与iOS Native Code互调的优雅实现方案-备

    本篇为大家介绍一个优秀的开源小项目:WebViewJavascriptBridge. 它优雅地实现了在使用UIWebView时JS与ios 的ObjC nativecode之间的互调,支持消息发送.接 ...

  9. iOS开发CoreAnimation解读之三——几种常用Layer的使用解析

    iOS开发CoreAnimation解读之三——几种常用Layer的使用解析 一.CAEmitterLayer 二.CAGradientLayer 三.CAReplicatorLayer 四.CASh ...

随机推荐

  1. Java 工厂模式学习

    工厂模式分三种:简单工厂.工厂方法.抽象工厂.其中抽象工厂是用于多个产品族的情况.所谓产品族就是不同产品组成的有机整体,各不同产品之间相互依赖.打个比方,一台电脑有CPU.主板.内存和硬盘等,这些不同 ...

  2. Android 更好的Activity生命周期回调

    /** * This class allows you to listen to when the user is entering the background (i.e. after a home ...

  3. js previousSibling兼容使用方法

    使用previousSibling的时候发现当前元素跟上一个元素之间有空格就不获取不到对象, 查资料才知道除了ie外js的previousSibling获取的对象包括空格! 兼容方法如下: funct ...

  4. 对Msn协议的一点点研究

    这个也是好奇msn的协议还是2011年的时候写的, 就在网上找啊找啊, 可惜要不是不能用就是C++代码还有就是不完整, 到最后我也没弄成功,只到了下面这步就挂掉了...... 登录成功<=SBS ...

  5. windows 8 metro 开发学习资源链接

    原文 http://www.cnblogs.com/icuit/archive/2012/05/30/2525979.html windows8 metro开发资源目前还是以MSDN为主,做了一个li ...

  6. java.util.Timer分析源码了解原理

    Timer中最主要由三个部分组成: 任务 TimerTask .  任务队列: TaskQueue queue 和 任务调试者:TimerThread thread 他们之间的关系可以通过下面图示: ...

  7. 用OO方式写键盘字母小游戏

    <html>  <head>   <title>0.0</title>   <script>    window.onload=functi ...

  8. hadoop源码eclipse环境搭建-源码获取阶段

    就目前了解,结合eclipse阅读和编译hadoop源码用两种方式:svn和git. 根据官方指南http://wiki.apache.org/hadoop/EclipseEnvironment 推荐 ...

  9. 【MongoDB数据库】MongoDB 命令入门初探

    MongoDB是一款NoSql数据库,使用了"面向集合"(Collection-Oriented)原理,意思是数据被分组存储在数据集中,被称为一个集合(Collection).每一 ...

  10. SQL学习之数据列去空格函数

    1.LTRIM()---去掉列值左边的空格  如下代码: select * from dbo.course where tno='t003' and cno='c0013'