最近做网页分析时接触了一些 包括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/
相关推荐
jsoup源码和jar包
页面解析用工具类Jsoup源码,爬虫必备!
在提供的压缩包中,`jsoup-1.3.3-sources.jar`文件包含了jsoup的源代码,这对于开发者来说极其宝贵。通过阅读源码,你可以深入了解jsoup内部的工作机制,理解其解析算法,以及如何实现对HTML元素的选取和操作。这...
这个压缩包包含的“Jsoup源码”意味着你可以深入理解其内部工作机制,这对于开发者进行二次开发或者学习网络爬虫技术非常有帮助。而“chm文件”通常是一种Windows平台下的帮助文档格式,它可能包含了Jsoup的官方文档...
**三、jsoup源码分析** `jsoup-1.7.2-sources.jar`包含jsoup的源代码,对于想要深入了解jsoup工作原理或者进行二次开发的开发者来说,这是非常宝贵的资源。通过阅读源码,我们可以学习到如何处理HTML解析的各种复杂...
jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。...jsoup的主要功能如下: 1. 从一个URL,文件或字符串中解析HTML; 2. 使用DOM或CSS选择器来查找、取出数据; 3. 可操作HTML元素、属性、文本;
但现在我已经不再使用htmlparser 了,原因是htmlparser 很少更新,但最重要的是有了jsoup 。 jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及...
这个文件包含了JSoup的源代码,对于开发者来说,这是一个宝贵的资源。通过查看源码,开发者可以深入理解库内部的工作机制,学习如何实现类似功能,甚至对库进行自定义扩展或调试。如果你在使用过程中遇到问题,查看...
提供了jsoup 1.6.1的源代码,对于开发者来说,这是深入理解jsoup内部工作原理的宝贵资源。你可以查看类的实现、方法的逻辑,甚至调试源代码以解决特定问题。 总的来说,jsoup 1.6版本虽然相对较旧,但其核心功能和...
11. **源代码管理**: 提供源代码意味着项目可以被其他开发者学习和改进。常见的源代码管理工具有Git,它可以帮助开发者追踪代码版本,协同开发,并且方便分享项目。 总结起来,这个项目展示了如何结合Android和...
在"jsoup-1.8.3(含源码)"这个压缩包中,包含了Jsoup库的1.8.3版本以及其源代码,这对于开发者来说是极其宝贵的资源,可以深入理解内部实现并进行定制开发。 ### 1. HTML解析 Jsoup能够解析HTML文档,无论是从URL...
根据提供的压缩包文件名称“KingFalse-HttpRaw2Jsoup-258eea3”,我们可以推测这可能是该项目的源代码仓库的一个特定版本。KingFalse可能是作者或者项目维护者的用户名,而“258eea3”可能是Git仓库中的一个提交哈希...
import org.jsoup.Jsoup; import org.jsoup.nodes.Document; public class WeatherFetcher { public Document fetchWeatherPage(String url) throws Exception { return Jsoup.connect(url).get(); } } ``` 在这...
**Jsoup API 深入解析** Jsoup 是一个用于处理和解析HTML的Java库,它提供了强大的功能,使得在Java程序中操作HTML文档变得简单而直观。在Jsoup 1.10.2版本中,这个API进一步优化了对HTML的处理能力,提供了丰富的...
jsoup的设计目标是处理真实世界中的HTML,它能够处理不规则、不完整的HTML代码,尽可能地恢复原始的结构,为开发者呈现一个干净、结构化的DOM树。 在jsoup 1.6.1版本中,我们可以看到这个库已经相当成熟,提供了...
赠送源代码: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、...
`jsoup-1.8.1-sources.jar`是Jsoup源代码的集合。拥有源代码,开发者可以深入理解内部工作原理,进行调试,甚至自定义或扩展Jsoup的功能。对于学习和解决问题来说,查看源码是一个极好的途径,特别是当遇到不明确的...
... 比如它可以处理: 1 没有关闭的标签 比如: <p>Lorem <p>Ipsum parses to <...3 一个Element包含一个子节点集合 并拥有一个父Element 他们还提供了一个唯一的子元素过滤列表 ... [更多]
1. HTML解析:Jsoup能够解析各种各样的HTML源码,无论是干净的结构化HTML还是充满乱七八糟标签的真实网页。它能处理HTML5和HTML4,甚至一些常见的错误格式也能被正确解析。 2. DOM操作:解析后的HTML被转化为一个...
4. **jsoup-1.11.3-sources.jar**:这个文件包含了JSoup库的源代码。开发者可以通过查看源代码来学习JSoup的工作原理,或者在遇到问题时进行调试。源代码有时也对优化性能或实现自定义功能有所帮助。 **JSoup主要...