`
duanhengbin
  • 浏览: 383418 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

jsoup 源码阅读

 
阅读更多

最近做网页分析时接触了一些 包括jsoup在内开源工具。 今天有时间读了下jsoup的源码,记录一下心得。

 

【特色】

作为html 解析工具,jsoup 出现的时间远不如大名鼎鼎的HttpClient。但是他有一些不错的特色:

 

1.实现了CSS选择器语法,有了这个页面内容提取真不是一般的方便。

2.解析算法不使用递归,而是enum配合状态模式遍历数据(先预设所有语法组合),减少性能瓶颈。另外,不需要任何第三方依赖。

 

【示例】

比如要想要过滤一个网页上所有的jpeg图片的链接,只需要下面几句即可。

Document doc = Jsoup.connect("http://www.mafengwo.cn/i/760809.html").get();
Elements jpegs = doc.select("img[src$=.jpeg]");

for (Element jpeg : jpegs) {
    System.out.println(jpeg.attr("abs:src"));
}
 没错,这个select()方法的参数用的就是CSS选择器的语法。熟悉JQuery的开发者会觉得非常亲切。

 

【流程分析】

上面的代码可分为三个步骤,后面的源码分析也按照这个思路来走。

1.根据输入构建DOM树

2.解析CSS选择字符串到 过滤表中

3.用深度优先算法将 树状节点逐一过滤

 

【源码分析】

1.DOM树构造

先说一下容器是位于org.jsoup.nodes 下 抽象类 Node及其派生类。看名字就知道意思,典型的组合模式。每个都可以包含子Node列表。其中最重要的是 Element类,代表一个html元素,包含一个tag,多个属性值及子元素。

 

树构造的关键代码位于 模板类TreeBuilder 中:

TreeBuilder类

    protected void runParser() {
        while (true) {
            Token token = tokeniser.read();  // 这里读入所有的Token 
            process(token);

            if (token.type == Token.TokenType.EOF)
                break;
        }
    }
 该类有两个派生类 HtmlTreeBuilder ,XmlTreeBuilder,看名字就知道用途了。这里只看下HtmlTreeBuilder。

 Tokeniser 类

    Token read() {
        if (!selfClosingFlagAcknowledged) {
            error("Self closing flag not acknowledged");
            selfClosingFlagAcknowledged = true;
        }

        while (!isEmitPending)
            state.read(this, reader); //此处 在做预设好的各种状态转移 以遍历所有标签

        // if emit is pending, a non-character token was found: return any chars in buffer, and leave token for next read:
        if (charBuffer.length() > 0) {
            String str = charBuffer.toString();
            charBuffer.delete(0, charBuffer.length());
            return new Token.Character(str);
        } else {
            isEmitPending = false;
            return emitPending;
        }
    }
这里 state 类型 enum TokeniserState,关键地方到了。
enum TokeniserState {
    Data {
        // in data state, gather characters until a character reference or tag is found
        void read(Tokeniser t, CharacterReader r) {
            switch (r.current()) {
                case '&':
                    t.advanceTransition(CharacterReferenceInData); // 这里做状态切换
                    break;
                case '<':
                    t.advanceTransition(TagOpen); // 这里也是状态切换
                    break;
                case nullChar:
                    t.error(this); // NOT replacement character (oddly?)
                    t.emit(r.consume());  // emit()方法会将 isEmitPending 设为 true,循环结束
                    break;
                case eof:
                    t.emit(new Token.EOF());
                    break;
                default:
                    String data = r.consumeToAny('&', '<', nullChar);
                    t.emit(data);
                    break;
            }
        }
    },
。。。(略)
上面的语义是当前的预期是“DATA”,如果读到'&'字符,也就是后面预期就是一个引用字符串,就把状态切换到“CharacterReferenceInData”状态,如此这般不断的切换解析,就能把整个文本分析出来。当然这样实现的前提对HTML语法要有比较深刻的理解,将所有的状态前后关联整理完善才行。

TokeniserState 包含多达67个成员,即67种解析的中间状态。可以说整个html语法解析的核心。

TokeniserState,Tokeniser 是强耦合关系,循环遍历在Tokeniser 中进行,循环终止条件在TokeniserState中调用。

Token解析完就要装载,这里又用到了一个enum HtmlTreeBuilderState,也有类似的状态切换,这里不再累述。

 HtmlTreeBuilder类

    @Override
    protected boolean process(Token token) {
        currentToken = token;
        return this.state.process(token, this);
    }
 

2.CSS选择字符串的解析

解析后的容器是  Evaluator 匹配器类,它自身为抽象类,包含为数众多的子类实现。

 这些类分别对应CSS的匹配语法,涉及细节较多。每一种实现表示一种语义并实现的对应的匹配方法:

public abstract boolean matches(Element root, Element element);

 作者可能处于减少类文件数量的考虑除了CombiningEvaluator 和 StructuralEvaluator 其他都实现为 Evaluator 的内部静态公有派生子类。

 

解析代码位于QueryParser类中,还是采用消费模式。

QueryParser类

    Evaluator parse() {
        tq.consumeWhitespace();

        if (tq.matchesAny(combinators)) { // if starts with a combinator, use root as elements
            evals.add(new StructuralEvaluator.Root());
            combinator(tq.consume());
        } else {
            findElements();
        }

        while (!tq.isEmpty()) {  // 不断消费直到消费空
            // hierarchy and extras
            boolean seenWhite = tq.consumeWhitespace();

            if (tq.matchesAny(combinators)) {
                combinator(tq.consume()); //这里处理组合语义关联的符号 ",", ">", "+", "~", " "
            } else if (seenWhite) {
                combinator(' ');
            } else { // E.class, E#id, E[attr] etc. AND
                findElements(); // take next el, #. etc off queue
            }
        }

        if (evals.size() == 1)
            return evals.get(0);

        return new CombiningEvaluator.And(evals);  // 这里将组合的选择器组装为list
    }

最后得到了一个 匹配器列表。

 

3.用非递归的深度优先算法将 树状节点逐一过滤

 

这里是一个访问者模式的应用。

NodeTraversor 类

    private NodeVisitor visitor;   //这个visitor 即是下面的Accumulator类
    public NodeTraversor(NodeVisitor visitor) {
        this.visitor = visitor;
    }

    public void traverse(Node root) {
        Node node = root;
        int depth = 0;
        
        while (node != null) {
            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.parentNode();
                    depth--;
                }
                visitor.tail(node, depth);
                if (node == root)
                    break;
                node = node.nextSibling();
            }
        }
    } 

 

Collector 类

    public static Elements collect (Evaluator eval, Element root) {
        Elements elements = new Elements();
        //这里是访问者的入口,注意 NodeTraversor 仅仅是个“媒介类”,利用其构造方法关联观察者
        //Accumulator就是访问者
        // elements 是访问者的出口
        new NodeTraversor(new Accumulator(root, elements, eval)).traverse(root);
        return elements;
    }

    private static class Accumulator implements NodeVisitor {
        private final Element root;
        private final Elements elements;
        private final Evaluator eval;

        Accumulator(Element root, Elements elements, Evaluator eval) {
            this.root = root;
            this.elements = elements;
            this.eval = eval;
        }

        public void head(Node node, int depth) {
            if (node instanceof Element) {
                Element el = (Element) node;
                if (eval.matches(root, el))
                    elements.add(el);  //这里是访问者在收集 elements 了
            }
        }

        public void tail(Node node, int depth) {
            // void
        }
    }

 

【CSS选择器的扩展】

以下是一些有用的选择器扩展,基本上能满足所有的需求。(参考org.jsoup.select.Selector 的API)

 

:contains(text) 匹配包含字符 范围:对元素及其所有子元素

:matches(regex) 匹配这则表达式 范围:对元素及其所有子元素

:containsOwn(text) 匹配包含字符 范围:仅对本元素(不包含子元素)

:matchesOwn(regex) 匹配这则表达式 范围:仅对本元素(不包含子元素)

 

【总结】

用过 jsoup 的人一定对其方便实用的功能印象深刻,采用CSS选择器语法确实是一个创意之举。不过如果要将 jsoup正式应用于项目还需要谨慎。

一是,现在开发只有一个人 Jonathan Hedley。不管这么说太少了点,长远来看项目后期维护有一定风险。

(源码中todo数目不少就是佐证)

二是,该工具是将整个html解析后,再进行搜索,有一定的解析成本。如果是很简单查询还是不如直接正则来得快。

 

【资源】

项目网站: http://jsoup.org/

值得一提的是 http://try.jsoup.org/ 可以直接拿你要用的html内容或Url,来测试css选择语法,非常实用。

另外,有中文翻译CookBook网址:http://www.open-open.com/jsoup/

  • 大小: 21 KB
  • 大小: 105.7 KB
分享到:
评论
1 楼 飞天奔月 2015-11-30  
哈哈  我也是看到 Jonathan Hedley  搜索到这个博客的

相关推荐

    jsoup源码和jar包

    jsoup源码和jar包

    jsoup源代码

    页面解析用工具类Jsoup源码,爬虫必备!

    2010最新解析html开源项目jsoup源码及api下载及jsoup.jar

    包含源码及api,要学习实例,可到官方http://jsoup.org/cookbook/查看各种实例

    jsoup包和源码

    jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。...jsoup的主要功能如下: 1. 从一个URL,文件或字符串中解析HTML; 2. 使用DOM或CSS选择器来查找、取出数据; 3. 可操作HTML元素、属性、文本;

    Jsoup源码以及chm文件

    轻量级的解析html网页的Jsoup框架源码以及所对应的chm文件,欢迎下载。

    jsoup支持包_教程_源代码

    但现在我已经不再使用htmlparser 了,原因是htmlparser 很少更新,但最重要的是有了jsoup 。 jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及...

    jsoup1.6源码及API

    jsoup1.6源码及API jsoup-1.6.1.jar jsoup-1.6.1-javadoc.jar jsoup-1.6.1-sources.jar

    jsoup-1.8.3.zip

    jsoup-1.8.3.zip,全套jsoup,包括jar包:jsoup-1.8.3.jar;源码:jsoup-1.8.3-sources;文档:jsoup-1.8.3-javadoc。

    jsoup源码与文档

    jsoup一个强大的解析html的工具,工具包里jar与源码还有文档,一应俱全

    jsoup-1.14.3-API文档-中文版.zip

    赠送源代码:jsoup-1.14.3-sources.jar; 赠送Maven依赖信息文件:jsoup-1.14.3.pom; 包含翻译后的API文档:jsoup-1.14.3-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.jsoup:jsoup:1.14.3; 标签:jsoup、...

    Android+jsoup Java爬虫做的一个 阅读app。(有源代码,随手写的 可能代码有点乱)

    无聊用Jsoup做的一个 小的阅读软件。只用于学习,不用于商用。如有问题请联系我

    jsoup操作实例源码

    获得http://www.weather.com.cn/forecast/ 天气预报 通过jsoup工具实现,获得如上网站天气预实时显示

    jsoup-1.7.3源代码

    ... 比如它可以处理: 1 没有关闭的标签 比如: &lt;p&gt;Lorem &lt;p&gt;Ipsum parses to &lt;...3 一个Element包含一个子节点集合 并拥有一个父Element 他们还提供了一个唯一的子元素过滤列表 ... [更多]

    Jsoup 1.5.2 和jsoup 1.6

    Jsoup 1.5.2 和jsoup 1.6 开发包jar包,开发文档,源码包

    java爬虫jsoup 源码

    使用于java爬虫解析html DOM,一个强大的解析工具,可以爬取数据

    jsoup-1.11.3-API文档-中文版.zip

    赠送源代码:jsoup-1.11.3-sources.jar; 赠送Maven依赖信息文件:jsoup-1.11.3.pom; 包含翻译后的API文档:jsoup-1.11.3-javadoc-API文档-中文(简体)版.zip; Maven坐标:org.jsoup:jsoup:1.11.3; 标签:jsoup、...

    jsoup-1.8.3(含源码)

    Jsoup可以从网站URL,文件或字符串中获取或解析HTML,提供完整的方法操作HTML元素,属性和文本(含源码)

    jsoup-jsoup-1.6.1.zip

    jsoup是一款Java的HTML解析器,主要用来对HTML解析。其解析器能够尽最大可能从你提供的HTML文档来创见一个干净的解析结果,无论HTML的格式是否完整。

    基于SSM+maven+httpClient+jsoup实现小说网站项目源码.zip

    基于SSM+maven+httpClient+jsoup实现小说网站项目源码.zip 基于SSM+maven+httpClient+jsoup实现小说网站项目源码.zip 基于SSM+maven+httpClient+jsoup实现小说网站项目源码.zip 基于SSM+maven+httpClient+jsoup实现...

Global site tag (gtag.js) - Google Analytics