jsoup源码阅读

github详细源码分析:https://github.com/code4craft/jsoup-learning


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

 

【特色】

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

 

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

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

 

【示例】

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

Java代码  
  1. Document doc = Jsoup.connect("http://www.mafengwo.cn/i/760809.html").get();  
  2. Elements jpegs = doc.select("img[src$=.jpeg]");  
  3.   
  4. for (Element jpeg : jpegs) {  
  5.     System.out.println(jpeg.attr("abs:src"));  // 获取图片的绝对路径
  6. }  
 没错,这个select()方法的参数用的就是CSS选择器的语法。熟悉JQuery的开发者会觉得非常亲切。

 

【流程分析】

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

1.根据输入构建DOM树

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

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

 

【源码分析】

1.DOM树构造

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

 

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

TreeBuilder类

Java代码  
  1. protected void runParser() {  
  2.     while (true) {  
  3.         Token token = tokeniser.read();  // 这里读入所有的Token   
  4.         process(token);  
  5.   
  6.         if (token.type == Token.TokenType.EOF)  
  7.             break;  
  8.     }  
  9. }  
 该类有两个派生类 HtmlTreeBuilder ,XmlTreeBuilder,看名字就知道用途了。这里只看下HtmlTreeBuilder。

 Tokeniser 类

Java代码  
  1. Token read() {  
  2.     if (!selfClosingFlagAcknowledged) {  
  3.         error("Self closing flag not acknowledged");  
  4.         selfClosingFlagAcknowledged = true;  
  5.     }  
  6.   
  7.     while (!isEmitPending)  
  8.         state.read(this, reader); //此处 在做预设好的各种状态转移 以遍历所有标签  
  9.   
  10.     // if emit is pending, a non-character token was found: return any chars in buffer, and leave token for next read:  
  11.     if (charBuffer.length() > 0) {  
  12.         String str = charBuffer.toString();  
  13.         charBuffer.delete(0, charBuffer.length());  
  14.         return new Token.Character(str);  
  15.     } else {  
  16.         isEmitPending = false;  
  17.         return emitPending;  
  18.     }  
  19. }  
这里 state 类型 enum TokeniserState,关键地方到了。
Java代码  
  1. enum TokeniserState {  
  2.     Data {  
  3.         // in data state, gather characters until a character reference or tag is found  
  4.         void read(Tokeniser t, CharacterReader r) {  
  5.             switch (r.current()) {  
  6.                 case '&':  
  7.                     t.advanceTransition(CharacterReferenceInData); // 这里做状态切换  
  8.                     break;  
  9.                 case '<':  
  10.                     t.advanceTransition(TagOpen); // 这里也是状态切换  
  11.                     break;  
  12.                 case nullChar:  
  13.                     t.error(this); // NOT replacement character (oddly?)  
  14.                     t.emit(r.consume());  // emit()方法会将 isEmitPending 设为 true,循环结束  
  15.                     break;  
  16.                 case eof:  
  17.                     t.emit(new Token.EOF());  
  18.                     break;  
  19.                 default:  
  20.                     String data = r.consumeToAny('&''<', nullChar);  
  21.                     t.emit(data);  
  22.                     break;  
  23.             }  
  24.         }  
  25.     },  
  26. 。。。(略)  
上面的语义是当前的预期是“DATA”,如果读到'&'字符,也就是后面预期就是一个引用字符串,就把状态切换到“CharacterReferenceInData”状态,如此这般不断的切换解析,就能把整个文本分析出来。当然这样实现的前提对HTML语法要有比较深刻的理解,将所有的状态前后关联整理完善才行。

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

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

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

 HtmlTreeBuilder类

Java代码 
  1. @Override  
  2. protected boolean process(Token token) {  
  3.     currentToken = token;  
  4.     return this.state.process(token, this);  
  5. }  
 

2.CSS选择字符串的解析

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

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

Java代码   收藏代码
  1. public abstract boolean matches(Element root, Element element);  

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

 

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

QueryParser类

Java代码  
  1. Evaluator parse() {  
  2.     tq.consumeWhitespace();  
  3.   
  4.     if (tq.matchesAny(combinators)) { // if starts with a combinator, use root as elements  
  5.         evals.add(new StructuralEvaluator.Root());  
  6.         combinator(tq.consume());  
  7.     } else {  
  8.         findElements();  
  9.     }  
  10.   
  11.     while (!tq.isEmpty()) {  // 不断消费直到消费空  
  12.         // hierarchy and extras  
  13.         boolean seenWhite = tq.consumeWhitespace();  
  14.   
  15.         if (tq.matchesAny(combinators)) {  
  16.             combinator(tq.consume()); //这里处理组合语义关联的符号 ",", ">", "+", "~", " "  
  17.         } else if (seenWhite) {  
  18.             combinator(' ');  
  19.         } else { // E.class, E#id, E[attr] etc. AND  
  20.             findElements(); // take next el, #. etc off queue  
  21.         }  
  22.     }  
  23.   
  24.     if (evals.size() == 1)  
  25.         return evals.get(0);  
  26.   
  27.     return new CombiningEvaluator.And(evals);  // 这里将组合的选择器组装为list  
  28. }  

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

 

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

 

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

NodeTraversor 类

Java代码   收藏代码
  1. private NodeVisitor visitor;   //这个visitor 即是下面的Accumulator类  
  2. public NodeTraversor(NodeVisitor visitor) {  
  3.     this.visitor = visitor;  
  4. }  
  5.   
  6. public void traverse(Node root) {  
  7.     Node node = root;  
  8.     int depth = 0;  
  9.       
  10.     while (node != null) {  
  11.         visitor.head(node, depth);  
  12.         if (node.childNodeSize() > 0) {  //有子元素先处理(深度优先)  
  13.             node = node.childNode(0);  
  14.             depth++;  
  15.         } else {  
  16.             while (node.nextSibling() == null && depth > 0) {  
  17.                 visitor.tail(node, depth);  
  18.                 node = node.parentNode();  
  19.                 depth--;  
  20.             }  
  21.             visitor.tail(node, depth);  
  22.             if (node == root)  
  23.                 break;  
  24.             node = node.nextSibling();  
  25.         }  
  26.     }  
  27. }   

 

Collector 类

Java代码  
  1. public static Elements collect (Evaluator eval, Element root) {  
  2.     Elements elements = new Elements();  
  3.     //这里是访问者的入口,注意 NodeTraversor 仅仅是个“媒介类”,利用其构造方法关联观察者  
  4.     //Accumulator就是访问者  
  5.     // elements 是访问者的出口  
  6.     new NodeTraversor(new Accumulator(root, elements, eval)).traverse(root);  
  7.     return elements;  
  8. }  
  9.   
  10. private static class Accumulator implements NodeVisitor {  
  11.     private final Element root;  
  12.     private final Elements elements;  
  13.     private final Evaluator eval;  
  14.   
  15.     Accumulator(Element root, Elements elements, Evaluator eval) {  
  16.         this.root = root;  
  17.         this.elements = elements;  
  18.         this.eval = eval;  
  19.     }  
  20.   
  21.     public void head(Node node, int depth) {  
  22.         if (node instanceof Element) {  
  23.             Element el = (Element) node;  
  24.             if (eval.matches(root, el))  
  25.                 elements.add(el);  //这里是访问者在收集 elements 了  
  26.         }  
  27.     }  
  28.   
  29.     public void tail(Node node, int depth) {  
  30.         // void  
  31.     }  
  32. }  

 

【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/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值