├── .gitignore ├── CHANGES ├── LICENSE ├── README.md ├── blogs ├── htmlcleaner │ └── htmlcleaner.md ├── images │ ├── compiler.pages │ ├── compiler.png │ ├── hacker.png │ ├── html compiler.png │ ├── select uml.png │ ├── streetfighter.jpg │ ├── uml.zargo │ ├── uml.zargo~ │ └── 类图.png ├── jsoup1.md ├── jsoup2.md ├── jsoup3.md ├── jsoup4.md ├── jsoup5.md ├── jsoup6.md ├── jsoup7.md └── jsoup8.md ├── pom.xml └── src ├── main ├── java │ ├── org │ │ └── jsoup │ │ │ ├── Connection.java │ │ │ ├── HttpStatusException.java │ │ │ ├── Jsoup.java │ │ │ ├── UnsupportedMimeTypeException.java │ │ │ ├── examples │ │ │ ├── HtmlToPlainText.java │ │ │ ├── ListLinks.java │ │ │ └── package-info.java │ │ │ ├── helper │ │ │ ├── DataUtil.java │ │ │ ├── DescendableLinkedList.java │ │ │ ├── HttpConnection.java │ │ │ ├── StringUtil.java │ │ │ └── Validate.java │ │ │ ├── nodes │ │ │ ├── Attribute.java │ │ │ ├── Attributes.java │ │ │ ├── Comment.java │ │ │ ├── DataNode.java │ │ │ ├── Document.java │ │ │ ├── DocumentType.java │ │ │ ├── Element.java │ │ │ ├── Entities.java │ │ │ ├── FormElement.java │ │ │ ├── Node.java │ │ │ ├── TextNode.java │ │ │ ├── XmlDeclaration.java │ │ │ ├── entities-base.properties │ │ │ ├── entities-full.properties │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ ├── parser │ │ │ ├── CharacterReader.java │ │ │ ├── HtmlTreeBuilder.java │ │ │ ├── HtmlTreeBuilderState.java │ │ │ ├── ITokeniserState.java │ │ │ ├── MiniSoupTokeniserState.java │ │ │ ├── ParseError.java │ │ │ ├── ParseErrorList.java │ │ │ ├── Parser.java │ │ │ ├── Tag.java │ │ │ ├── Token.java │ │ │ ├── TokenQueue.java │ │ │ ├── Tokeniser.java │ │ │ ├── TokeniserState.java │ │ │ ├── TreeBuilder.java │ │ │ ├── XmlTreeBuilder.java │ │ │ └── package-info.java │ │ │ ├── safety │ │ │ ├── Cleaner.java │ │ │ ├── Whitelist.java │ │ │ └── package-info.java │ │ │ └── select │ │ │ ├── Collector.java │ │ │ ├── CombiningEvaluator.java │ │ │ ├── Elements.java │ │ │ ├── Evaluator.java │ │ │ ├── NodeTraversor.java │ │ │ ├── NodeVisitor.java │ │ │ ├── QueryParser.java │ │ │ ├── Selector.java │ │ │ ├── StructuralEvaluator.java │ │ │ └── package-info.java │ └── us │ │ └── codecraft │ │ └── learning │ │ ├── automata │ │ ├── ABStateMachine.java │ │ ├── StateModelABStateMachine.java │ │ ├── StringReader.java │ │ └── SwitchABStateMachine.java │ │ ├── parser │ │ ├── PageErrorChecker.java │ │ └── ParserCorrectorTest.java │ │ └── select │ │ └── SelectorTest.java └── javadoc │ └── overview.html └── test ├── java └── org │ └── jsoup │ ├── TextUtil.java │ ├── helper │ ├── DataUtilTest.java │ ├── HttpConnectionTest.java │ └── StringUtilTest.java │ ├── integration │ ├── Benchmark.java │ ├── ParseTest.java │ └── UrlConnectTest.java │ ├── nodes │ ├── AttributeTest.java │ ├── AttributesTest.java │ ├── DocumentTest.java │ ├── DocumentTypeTest.java │ ├── ElementTest.java │ ├── EntitiesTest.java │ ├── FormElementTest.java │ ├── NodeTest.java │ └── TextNodeTest.java │ ├── parser │ ├── AttributeParseTest.java │ ├── CharacterReaderTest.java │ ├── HtmlParserTest.java │ ├── TagTest.java │ ├── TokenQueueTest.java │ └── XmlTreeBuilderTest.java │ ├── safety │ └── CleanerTest.java │ └── select │ ├── CssTest.java │ ├── ElementsTest.java │ ├── QueryParserTest.java │ └── SelectorTest.java └── resources └── htmltests ├── README ├── baidu-cn-home.html ├── baidu-variant.html ├── google-ipod.html ├── meta-charset-1.html ├── meta-charset-2.html ├── meta-charset-3.html ├── news-com-au-home.html ├── nyt-article-1.html ├── smh-biz-article-1.html ├── thumb.jpg ├── xml-test.xml ├── yahoo-article-1.html └── yahoo-jp.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | jsoup.iml 3 | jsoup.ipr 4 | jsoup.iws 5 | target/ 6 | .classpath 7 | .project 8 | .settings/ 9 | *Thrash* 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2009, 2010, 2011, 2012, 2013 Jonathan Hedley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Jsoup学习笔记 2 | ------ 3 | **Jsoup**是Java世界的一款HTML解析工具,它支持用CSS Selector方式选择DOM元素,也可过滤HTML文本,防止XSS攻击。 4 | 5 | 学习Jsoup是为了更好的开发我的另一个爬虫框架[webmagic](https://github.com/code4craft/webmagic),为了学的比较详细,就强制自己用很规范的方式写出这部分文章。 6 | 7 | 代码部分来自[https://github.com/jhy/jsoup](https://github.com/jhy/jsoup),添加了一些中文注释以及示例代码。 8 | 9 | --------------- 10 | 11 | ## 提纲 12 | 13 | 1. [概述](https://github.com/code4craft/jsoup-learning/blob/master/blogs/jsoup1.md) 14 | 15 | 2. [DOM相关对象](https://github.com/code4craft/jsoup-learning/blob/master/blogs/jsoup2.md) 16 | 17 | 3. [Document的输出](https://github.com/code4craft/jsoup-learning/blob/master/blogs/jsoup3.md) 18 | 19 | 4. HTML语法分析parser 20 | 21 | 1. [语法分析与状态机基础](https://github.com/code4craft/jsoup-learning/blob/master/blogs/jsoup4.md) 22 | 2. [词法分析Tokenizer](https://github.com/code4craft/jsoup-learning/blob/master/blogs/jsoup5.md) 23 | 3. [语法检查及DOM树构建](https://github.com/code4craft/jsoup-learning/blob/master/blogs/jsoup6.md) 24 | 25 | 5. [CSS Selector](https://github.com/code4craft/jsoup-learning/blob/master/blogs/jsoup7.md) 26 | 27 | 6. [防御XSS攻击](https://github.com/code4craft/jsoup-learning/blob/master/blogs/jsoup8.md) 28 | 29 | 7. [为Jsoup增加XPath选择功能](https://github.com/code4craft/xsoup) 30 | 31 | Jsoup默认没有XPath功能,我写了一个项目[Xsoup](https://github.com/code4craft/xsoup),可以使用XPath来选择HTML文本。Java里较常用的XPath抽取器是HtmlCleaner,Xsoup的性能比它快了一倍。 32 | 33 | ------- 34 | 35 | ## 协议: 36 | 37 | 相关代码遵循MIT协议。 38 | 39 | 文档遵循CC-BYNC协议。 40 | 41 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/code4craft/jsoup-learning/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 42 | 43 | -------------------------------------------------------------------------------- /blogs/htmlcleaner/htmlcleaner.md: -------------------------------------------------------------------------------- 1 | htmlcleaner代码学习 2 | --- 3 | 相比Jsoup,htmlcleaner支持XPath进行抽取,也是挺有用的。 4 | 5 | htmlcleaner托管在sourceforge下[http://htmlcleaner.sourceforge.net/‎](http://htmlcleaner.sourceforge.net/‎ 6 | ),由于某种原因,访问sourceforge不是那么顺畅,最后选了这个比较新的github上的fork:[https://github.com/amplafi/htmlcleaner](https://github.com/amplafi/htmlcleaner)。 7 | 8 | htmlcleaner的包结构与Jsoup还是有些差距,一开始就被一字排开的类给吓到了。 9 | 10 | htmlcleaner仍然有一套自己的树结构,继承自:`HtmlNode`。但是它提供了到`org.w3c.dom.Document`和`org.jdom2.Document`的转换。 11 | 12 | `HtmlTokenizer`是词法分析部分,有状态但是没用状态机,而是用了一些基本类型来保存状态,例如: 13 | 14 | public class HtmlTokenizer { 15 | 16 | private BufferedReader _reader; 17 | private char[] _working = new char[WORKING_BUFFER_SIZE]; 18 | 19 | private transient int _pos; 20 | private transient int _len = -1; 21 | private transient int _row = 1; 22 | private transient int _col = 1; 23 | 24 | 25 | private transient StringBuffer _saved = new StringBuffer(512); 26 | 27 | private transient boolean _isLateForDoctype; 28 | private transient DoctypeToken _docType; 29 | private transient TagToken _currentTagToken; 30 | private transient List _tokenList = new ArrayList(); 31 | private transient Set _namespacePrefixes = new HashSet(); 32 | 33 | private boolean _asExpected = true; 34 | 35 | private boolean _isScriptContext; 36 | } 37 | 38 | 浓烈的面向过程编程的味道。 39 | 40 | `Tokenize`之后就是简单的用栈将树组合起来。 41 | 42 | 测试了一下,一个44k的文档,用Jsoup做parse是3.5ms,而htmlcleaner是7.9ms,差距在一倍左右。 43 | 44 | XPath部分也是云里雾里, -------------------------------------------------------------------------------- /blogs/images/compiler.pages: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code4craft/jsoup-learning/2c0580fdd895cabedeb5eee14241bf511270dc61/blogs/images/compiler.pages -------------------------------------------------------------------------------- /blogs/images/compiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code4craft/jsoup-learning/2c0580fdd895cabedeb5eee14241bf511270dc61/blogs/images/compiler.png -------------------------------------------------------------------------------- /blogs/images/hacker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code4craft/jsoup-learning/2c0580fdd895cabedeb5eee14241bf511270dc61/blogs/images/hacker.png -------------------------------------------------------------------------------- /blogs/images/html compiler.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code4craft/jsoup-learning/2c0580fdd895cabedeb5eee14241bf511270dc61/blogs/images/html compiler.png -------------------------------------------------------------------------------- /blogs/images/select uml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code4craft/jsoup-learning/2c0580fdd895cabedeb5eee14241bf511270dc61/blogs/images/select uml.png -------------------------------------------------------------------------------- /blogs/images/streetfighter.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code4craft/jsoup-learning/2c0580fdd895cabedeb5eee14241bf511270dc61/blogs/images/streetfighter.jpg -------------------------------------------------------------------------------- /blogs/images/uml.zargo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code4craft/jsoup-learning/2c0580fdd895cabedeb5eee14241bf511270dc61/blogs/images/uml.zargo -------------------------------------------------------------------------------- /blogs/images/uml.zargo~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code4craft/jsoup-learning/2c0580fdd895cabedeb5eee14241bf511270dc61/blogs/images/uml.zargo~ -------------------------------------------------------------------------------- /blogs/images/类图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/code4craft/jsoup-learning/2c0580fdd895cabedeb5eee14241bf511270dc61/blogs/images/类图.png -------------------------------------------------------------------------------- /blogs/jsoup1.md: -------------------------------------------------------------------------------- 1 | Jsoup代码解读之一-概述 2 | ------ 3 | >今天看到一个用python写的抽取正文的东东,美滋滋的用Java实现了一番,放到了webmagic里,然后发现Jsoup里已经有了…觉得自己各种不靠谱啊!算了,静下心来学学好东西吧! 4 | 5 | Jsoup是Java世界用作html解析和过滤的不二之选。支持将html解析为DOM树、支持CSS Selector形式选择、支持html过滤,本身还附带了一个Http下载器。 6 | 7 | ## 概述 8 | 9 | Jsoup的代码相当简洁,Jsoup总共53个类,且没有任何第三方包的依赖,对比最终发行包9.8M的SAXON,实在算得上是短小精悍了。 10 | 11 | ```shell 12 | jsoup 13 | ├── examples #样例,包括一个将html转为纯文本和一个抽取所有链接地址的例子。 14 | ├── helper #一些工具类,包括读取数据、处理连接以及字符串转换的工具 15 | ├── nodes #DOM节点定义 16 | ├── parser #解析html并转换为DOM树 17 | ├── safety #安全相关,包括白名单及html过滤 18 | └── select #选择器,支持CSS Selector以及NodeVisitor格式的遍历 19 | ``` 20 | 21 | ## 使用 22 | 23 | Jsoup的入口是`Jsoup`类。examples包里提供了两个例子,解析html后,分别用CSS Selector以及NodeVisitor来操作Dom元素。 24 | 25 | 这里用`ListLinks`里的例子来说明如何调用Jsoup: 26 | 27 | ```java 28 | public static void main(String[] args) throws IOException { 29 | Validate.isTrue(args.length == 1, "usage: supply url to fetch"); 30 | String url = args[0]; 31 | print("Fetching %s...", url); 32 | 33 | // 下载url并解析成html DOM结构 34 | Document doc = Jsoup.connect(url).get(); 35 | // 使用select方法选择元素,参数是CSS Selector表达式 36 | Elements links = doc.select("a[href]"); 37 | 38 | print("\nLinks: (%d)", links.size()); 39 | for (Element link : links) { 40 | //使用abs:前缀取绝对url地址 41 | print(" * a: <%s> (%s)", link.attr("abs:href"), trim(link.text(), 35)); 42 | } 43 | } 44 | ``` 45 | 46 | Jsoup使用了自己的一套DOM代码体系,这里的Elements、Element等虽然名字和概念都与Java XML API`org.w3c.dom`类似,但并没有代码层面的关系。就是说你想用XML的一套API来操作Jsoup的结果是办不到的,但是正因为如此,才使得Jsoup可以抛弃xml里一些繁琐的API,使得代码更加简单。 47 | 48 | 还有一种方式是通过`NodeVisitor`来遍历DOM树,这个在对整个html做分析和替换时比较有用: 49 | 50 | ```java 51 | public interface NodeVisitor { 52 | 53 | //遍历到节点开始时,调用此方法 54 | public void head(Node node, int depth); 55 | 56 | //遍历到节点结束时(所有子节点都已遍历完),调用此方法 57 | public void tail(Node node, int depth); 58 | } 59 | ``` 60 | 61 | `HtmlToPlainText`的例子说明了如何使用NodeVisitor来遍历DOM树,将html转化为纯文本,并将需要换行的标签替换为换行\\n: 62 | 63 | ```java 64 | public static void main(String... args) throws IOException { 65 | Validate.isTrue(args.length == 1, "usage: supply url to fetch"); 66 | String url = args[0]; 67 | 68 | // fetch the specified URL and parse to a HTML DOM 69 | Document doc = Jsoup.connect(url).get(); 70 | 71 | HtmlToPlainText formatter = new HtmlToPlainText(); 72 | String plainText = formatter.getPlainText(doc); 73 | System.out.println(plainText); 74 | } 75 | 76 | public String getPlainText(Element element) { 77 | //自定义一个NodeVisitor - FormattingVisitor 78 | FormattingVisitor formatter = new FormattingVisitor(); 79 | //使用NodeTraversor来装载FormattingVisitor 80 | NodeTraversor traversor = new NodeTraversor(formatter); 81 | //进行遍历 82 | traversor.traverse(element); 83 | return formatter.toString(); 84 | } 85 | ``` 86 | 87 | 下一节将从DOM结构开始对Jsoup代码进行分析。 -------------------------------------------------------------------------------- /blogs/jsoup2.md: -------------------------------------------------------------------------------- 1 | Jsoup代码解读之二-DOM相关对象 2 | ------- 3 | 之前在文章中说到,Jsoup使用了一套自己的DOM对象体系,和Java XML API互不兼容。这样做的好处是从XML的API里解脱出来,使得代码精炼了很多。这篇文章会说明Jsoup的DOM结构,DOM的遍历方式。在下一篇文章,我会并结合这两个基础,分析一下Jsoup的HTML输出功能。 4 | ## DOM结构相关类 5 | 6 | 我们先来看看nodes包的类图: 7 | 8 | ![node类图][1] 9 | 10 | 这里可以看到,核心无疑是`Node`类。 11 | 12 | Node类是一个抽象类,它代表DOM树中的一个节点,它包含: 13 | 14 | * 父节点`parentNode`以及子节点`childNodes`的引用 15 | * 属性值集合`attributes` 16 | * 页面的uri`baseUri`,用于修正相对地址为绝对地址 17 | * 在兄弟节点中的位置`siblingIndex`,用于进行DOM操作 18 | 19 | Node里面包含一些获取属性、父子节点、修改元素的方法,其中比较有意思的是`absUrl()`。我们知道,在很多html页面里,链接会使用相对地址,我们有时会需要将其转变为绝对地址。Jsoup的解决方案是在attr()的参数开始加"abs:",例如attr("abs:href"),而`absUrl()`就是其实现方式。我写的爬虫框架[webmagic](http://www.oschina.net/p/webmagic)里也用到了类似功能,当时是自己手写的,看到Jsoup的实现,才发现自己是白费劲了,代码如下: 20 | 21 | ```java 22 | URL base; 23 | try { 24 | try { 25 | base = new URL(baseUri); 26 | } catch (MalformedURLException e) { 27 | // the base is unsuitable, but the attribute may be abs on its own, so try that 28 | URL abs = new URL(relUrl); 29 | return abs.toExternalForm(); 30 | } 31 | // workaround: java resolves '//path/file + ?foo' to '//path/?foo', not '//path/file?foo' as desired 32 | if (relUrl.startsWith("?")) 33 | relUrl = base.getPath() + relUrl; 34 | // java URL自带的相对路径解析 35 | URL abs = new URL(base, relUrl); 36 | return abs.toExternalForm(); 37 | } catch (MalformedURLException e) { 38 | return ""; 39 | } 40 | ``` 41 | 42 | Node还有一个比较值得一提的方法是`abstract String nodeName()`,这个相当于定义了节点的类型名(例如`Document`是'#Document',`Element`则是对应的TagName)。 43 | 44 | Element也是一个重要的类,它代表的是一个HTML元素。它包含一个字段`tag`和`classNames`。classNames是"class"属性解析出来的集合,因为CSS规范里,"class"属性允许设置多个,并用空格隔开,而在用Selector选择的时候,即使只指定其中一个,也能够选中其中的元素。所以这里就把"class"属性展开了。Element还有选取元素的入口,例如`select`、`getElementByXXX`,这些都用到了select包中的内容,这个留到下篇文章select再说。 45 | 46 | Document是代表整个文档,它也是一个特殊的Element,即根节点。Document除了Element的内容,还包括一些输出的方法。 47 | 48 | Document还有一个属性`quirksMode`,大致意思是定义处理非标准HTML的几个级别,这个留到以后分析parser的时候再说。 49 | 50 | ## DOM树的遍历 51 | 52 | Node还有一些方法,例如`outerHtml()`,用作节点及文档HTML的输出,用到了树的遍历。在DOM树的遍历上,用到了`NodeVisitor`和`NodeTraversor`来对树的进行遍历。`NodeVisitor`在上一篇文章提到过了,head()和tail()分别是遍历开始和结束时的方法,而`NodeTraversor`的核心代码如下: 53 | 54 | ```java 55 | public void traverse(Node root) { 56 | Node node = root; 57 | int depth = 0; 58 | 59 | //这里对树进行后序(深度优先)遍历 60 | while (node != null) { 61 | //开始遍历node 62 | visitor.head(node, depth); 63 | if (node.childNodeSize() > 0) { 64 | node = node.childNode(0); 65 | depth++; 66 | } else { 67 | //没有下一个兄弟节点,退栈 68 | while (node.nextSibling() == null && depth > 0) { 69 | visitor.tail(node, depth); 70 | node = node.parent(); 71 | depth--; 72 | } 73 | //结束遍历 74 | visitor.tail(node, depth); 75 | if (node == root) 76 | break; 77 | node = node.nextSibling(); 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | 这里使用循环+回溯来替换掉了我们常用的递归方式,从而避免了栈溢出的风险。 84 | 85 | 实际上,Jsoup的Selector机制也是基于`NodeVisitor`来实现的,可以说`NodeVisitor`是更加底层和灵活的API。 86 | 87 | 在下一篇博客我会讲讲Document的输出。 88 | 89 | 90 | 91 | [1]: http://static.oschina.net/uploads/space/2013/0825/221021_wQvT_190591.png -------------------------------------------------------------------------------- /blogs/jsoup3.md: -------------------------------------------------------------------------------- 1 | Jsoup代码解读之三-Document的输出 2 | ------- 3 | 4 | Jsoup官方说明里,一个重要的功能就是***output tidy HTML***。这里我们看看Jsoup是如何输出HTML的。 5 | 6 | ## HTML相关知识 7 | 8 | 分析代码前,我们不妨先想想,"tidy HTML"到底包括哪些东西: 9 | 10 | * 换行,块级标签习惯上都会独占一行 11 | * 缩进,根据HTML标签嵌套层数,行首缩进会不同 12 | * 严格的标签闭合,如果是可以自闭合的标签并且没有内容,则进行自闭合 13 | * HTML实体的转义 14 | 15 | 这里要补充一下HTML标签的知识。HTML Tag可以分为block和inline两类。关于Tag的inline和block的定义可以参考[http://www.w3schools.com/html/html_blocks.asp](http://www.w3schools.com/html/html_blocks.asp),而Jsoup的`Tag`类则是对Java开发者非常好的学习资料。 16 | 17 | ```java 18 | // internal static initialisers: 19 | // prepped from http://www.w3.org/TR/REC-html40/sgml/dtd.html and other sources 20 | //block tags,需要换行 21 | private static final String[] blockTags = { 22 | "html", "head", "body", "frameset", "script", "noscript", "style", "meta", "link", "title", "frame", 23 | "noframes", "section", "nav", "aside", "hgroup", "header", "footer", "p", "h1", "h2", "h3", "h4", "h5", "h6", 24 | "ul", "ol", "pre", "div", "blockquote", "hr", "address", "figure", "figcaption", "form", "fieldset", "ins", 25 | "del", "s", "dl", "dt", "dd", "li", "table", "caption", "thead", "tfoot", "tbody", "colgroup", "col", "tr", "th", 26 | "td", "video", "audio", "canvas", "details", "menu", "plaintext" 27 | }; 28 | //inline tags,无需换行 29 | private static final String[] inlineTags = { 30 | "object", "base", "font", "tt", "i", "b", "u", "big", "small", "em", "strong", "dfn", "code", "samp", "kbd", 31 | "var", "cite", "abbr", "time", "acronym", "mark", "ruby", "rt", "rp", "a", "img", "br", "wbr", "map", "q", 32 | "sub", "sup", "bdo", "iframe", "embed", "span", "input", "select", "textarea", "label", "button", "optgroup", 33 | "option", "legend", "datalist", "keygen", "output", "progress", "meter", "area", "param", "source", "track", 34 | "summary", "command", "device" 35 | }; 36 | //emptyTags是不能有内容的标签,这类标签都是可以自闭合的 37 | private static final String[] emptyTags = { 38 | "meta", "link", "base", "frame", "img", "br", "wbr", "embed", "hr", "input", "keygen", "col", "command", 39 | "device" 40 | }; 41 | private static final String[] formatAsInlineTags = { 42 | "title", "a", "p", "h1", "h2", "h3", "h4", "h5", "h6", "pre", "address", "li", "th", "td", "script", "style", 43 | "ins", "del", "s" 44 | }; 45 | //在这些标签里,需要保留空格 46 | private static final String[] preserveWhitespaceTags = { 47 | "pre", "plaintext", "title", "textarea" 48 | }; 49 | ``` 50 | 51 | 另外,Jsoup的`Entities`类里包含了一些HTML实体转义的东西。这些转义的对应数据保存在`entities-full.properties`和`entities-base.properties`里。 52 | 53 | ## Jsoup的格式化实现 54 | 55 | 在Jsoup里,直接调用`Document.toString()`(继承自Element),即可对文档进行输出。另外`OutputSettings`可以控制输出格式,主要是`prettyPrint`(是否重新格式化)、`outline`(是否强制所有标签换行)、`indentAmount`(缩进长度)等。 56 | 57 | 里面的继承和互相调用关系略微复杂,大概是这样子: 58 | 59 | `Document.toString()`=>`Document.outerHtml()`=>`Element.html()`,最终`Element.html()`又会循环调用所有子元素的`outerHtml()`,拼接起来作为输出。 60 | 61 | ```java 62 | private void html(StringBuilder accum) { 63 | for (Node node : childNodes) 64 | node.outerHtml(accum); 65 | } 66 | ``` 67 | 68 | 而`outerHtml()`会使用一个`OuterHtmlVisitor`对所以子节点做遍历,并拼装起来作为结果。 69 | 70 | ```java 71 | protected void outerHtml(StringBuilder accum) { 72 | new NodeTraversor(new OuterHtmlVisitor(accum, getOutputSettings())).traverse(this); 73 | } 74 | ``` 75 | 76 | OuterHtmlVisitor会对所有子节点做遍历,并调用`node.outerHtmlHead()`和`node.outerHtmlTail`两个方法。 77 | 78 | ```java 79 | private static class OuterHtmlVisitor implements NodeVisitor { 80 | private StringBuilder accum; 81 | private Document.OutputSettings out; 82 | 83 | public void head(Node node, int depth) { 84 | node.outerHtmlHead(accum, depth, out); 85 | } 86 | 87 | public void tail(Node node, int depth) { 88 | if (!node.nodeName().equals("#text")) // saves a void hit. 89 | node.outerHtmlTail(accum, depth, out); 90 | } 91 | } 92 | ``` 93 | 94 | 我们终于找到了真正工作的代码,`node.outerHtmlHead()`和`node.outerHtmlTail`。Jsoup里每种Node的输出方式都不太一样,这里只讲讲两种主要节点:`Element`和`TextNode`。`Element`是格式化的主要对象,它的两个方法代码如下: 95 | 96 | ```java 97 | void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) { 98 | if (accum.length() > 0 && out.prettyPrint() 99 | && (tag.formatAsBlock() || (parent() != null && parent().tag().formatAsBlock()) || out.outline()) ) 100 | //换行并调整缩进 101 | indent(accum, depth, out); 102 | accum 103 | .append("<") 104 | .append(tagName()); 105 | attributes.html(accum, out); 106 | 107 | if (childNodes.isEmpty() && tag.isSelfClosing()) 108 | accum.append(" />"); 109 | else 110 | accum.append(">"); 111 | } 112 | 113 | void outerHtmlTail(StringBuilder accum, int depth, Document.OutputSettings out) { 114 | if (!(childNodes.isEmpty() && tag.isSelfClosing())) { 115 | if (out.prettyPrint() && (!childNodes.isEmpty() && ( 116 | tag.formatAsBlock() || (out.outline() && (childNodes.size()>1 || (childNodes.size()==1 && !(childNodes.get(0) instanceof TextNode)))) 117 | ))) 118 | //换行并调整缩进 119 | indent(accum, depth, out); 120 | accum.append(""); 121 | } 122 | } 123 | ``` 124 | 125 | 而ident方法的代码只有一行: 126 | 127 | ```java 128 | protected void indent(StringBuilder accum, int depth, Document.OutputSettings out) { 129 | //out.indentAmount()是缩进长度,默认是1 130 | accum.append("\n").append(StringUtil.padding(depth * out.indentAmount())); 131 | } 132 | ``` 133 | 134 | 代码简单明了,就没什么好说的了。值得一提的是,`StringUtil.padding()`方法为了减少字符串生成,把常用的缩进保存到了一个数组中。 135 | 136 | 好了,水了一篇文章,下一篇将比较有技术含量的parser部分。 137 | 138 | 另外,通过本节的学习,我们学到了要把StringBuilder命名为**accum**,而不是**sb**。 -------------------------------------------------------------------------------- /blogs/jsoup4.md: -------------------------------------------------------------------------------- 1 | Jsoup代码解读之四-parser(上) 2 | ------- 3 | 作为Java世界最好的HTML 解析库,Jsoup的parser实现非常具有代表性。这部分也是Jsoup最复杂的部分,需要一些数据结构、状态机乃至编译器的知识。好在HTML语法不复杂,解析只是到DOM树为止,所以作为编译器入门倒是挺合适的。这一块不要指望囫囵吞枣,我们还是泡一杯咖啡,细细品味其中的奥妙吧。 4 | 5 | ## 基础知识 6 | 7 | ### 编译器 8 | 9 | 将计算机语言转化为另一种计算机语言(通常是更底层的语言,例如机器码、汇编、或者JVM字节码)的过程就叫做编译(compile)。编译器(Compiler)是计算机科学的一个重要领域,已经有很多年历史了,而最近各种通用语言层出不穷,加上跨语言编译的兴起、DSL概念的流行,都让编译器变成了一个很时髦的东西。 10 | 11 | 编译器领域相关有三本公认的经典书籍,龙书[《Compilers: Principles, Techniques, and Tools 》](http://book.douban.com/subject/1866231/),虎书[《Modern Compiler Implementation in X (X表示各种语言)》](http://book.douban.com/subject/1923484/),鲸书[《Advanced Compiler Design and Implementation》](http://book.douban.com/subject/1821532/)。其中龙书是编译理论方面公认的不二之选,而后面两本则对实践更有指导意义。另外[@装配脑袋](http://www.cnblogs.com/Ninputer)有个很好的编译器入门系列博客:[http://www.cnblogs.com/Ninputer/archive/2011/06/07/2074632.html](http://www.cnblogs.com/Ninputer/archive/2011/06/07/2074632.html) 12 | 13 | 编译器的基本流程如下: 14 | 15 | ![compiler][1] 16 | 17 | 其中词法分析、语法分析、语义分析这部分又叫编译器的前端(front-end),而此后的中间代码生成直到目标生成、优化等属于编译器的后端(back-end)。编译器的前端技术已经很成熟了,也有yacc这样的工具来自动进行词法、语法分析(Java里也有一个类似的工具ANTLR),而后端技术更加复杂,也是目前编译器研究的重点。 18 | 19 | 说了这么多,回到咱们的HTML上来。HTML是一种声明式的语言,可以理解它的最终的输出是浏览器里图形化的页面,而并非可执行的目标语言,因此我将这里的Translate改为了Render。 20 | 21 | ![html compiler][2] 22 | 23 | 在Jsoup(包括类似的HTML parser)里,只做了Lex(词法分析)、Parse(语法分析)两步,而HTML parse最终产出结果,就是DOM树。至于HTML的语义解析以及渲染,不妨看看携程UED团队的这篇文章:[《浏览器是怎样工作的:渲染引擎,HTML解析》](http://ued.ctrip.com/blog/?p=3295)。 24 | 25 | ### 状态机 26 | 27 | Jsoup的词法分析和语法分析都用到了状态机。状态机可以理解为一个特殊的程序模型,例如经常跟我们打交道的正则表达式就是用状态机实现的。 28 | 29 | 它由状态(state)和转移(transition)两部分构成。根据状态转移的可能性,状态机又分为DFA(确定有限状态机)和NFA(非确定有限状态自动机)。这里拿一个最简单的正则表达式"a[b]*"作为例子,我们先把它映射到一个状态机DFA,大概是这样子: 30 | 31 | ![state machine][3] 32 | 33 | 状态机本身是一个编程模型,这里我们尝试用程序去实现它,那么最直接的方式大概是这样: 34 | 35 | ```java 36 | public void process(StringReader reader) throws StringReader.EOFException { 37 | char ch; 38 | switch (state) { 39 | case Init: 40 | ch = reader.read(); 41 | if (ch == 'a') { 42 | state = State.AfterA; 43 | accum.append(ch); 44 | } 45 | break; 46 | case AfterA: 47 | ... 48 | break; 49 | case AfterB: 50 | ... 51 | break; 52 | case Accept: 53 | ... 54 | break; 55 | } 56 | } 57 | ``` 58 | 59 | 这样写简单的状态机倒没有问题,但是复杂情况下就有点难受了。还有一种标准的状态机解法,先建立状态转移表,然后使用这个表建立状态机。这个方法的问题就是,只能做纯状态转移,无法在代码级别操作输入输出。 60 | 61 | Jsoup里则使用了状态模式来实现状态机,初次看到时,确实让人眼前一亮。状态模式是设计模式的一种,它将状态和对应的行为绑定在一起。而在状态机的实现过程中,使用它来实现状态转移时的处理再合适不过了。 62 | 63 | "a[b]*"的例子的状态模式实现如下,这里采用了与Jsoup相同的方式,用到了枚举来实现状态模式: 64 | 65 | ```java 66 | public class StateModelABStateMachine implements ABStateMachine { 67 | 68 | State state; 69 | 70 | StringBuilder accum; 71 | 72 | enum State { 73 | Init { 74 | @Override 75 | public void process(StateModelABStateMachine stateModelABStateMachine, StringReader reader) throws StringReader.EOFException { 76 | char ch = reader.read(); 77 | if (ch == 'a') { 78 | stateModelABStateMachine.state = AfterA; 79 | stateModelABStateMachine.accum.append(ch); 80 | } 81 | } 82 | }, 83 | Accept { 84 | ... 85 | }, 86 | AfterA { 87 | ... 88 | }, 89 | AfterB { 90 | ... 91 | }; 92 | 93 | public void process(StateModelABStateMachine stateModelABStateMachine, StringReader reader) throws StringReader.EOFException { 94 | } 95 | } 96 | 97 | public void process(StringReader reader) throws StringReader.EOFException { 98 | state.process(this, reader); 99 | } 100 | } 101 | ``` 102 | 103 | 本文中提到的几种状态机的完整实现在这个仓库的[https://github.com/code4craft/jsoup-learning/tree/master/src/main/java/us/codecraft/learning/automata](https://github.com/code4craft/jsoup-learning/tree/master/src/main/java/us/codecraft/learning/automata)路径下。 104 | 105 | 下一篇文章将从Jsoup的词法分析器开始来讲状态机的使用。 106 | 107 | 108 | 109 | [1]: http://static.oschina.net/uploads/space/2013/0828/081055_j2Xy_190591.png 110 | [2]: http://static.oschina.net/uploads/space/2013/0828/103726_uejc_190591.png 111 | [3]: http://static.oschina.net/uploads/space/2013/0828/131113_nyHh_190591.png -------------------------------------------------------------------------------- /blogs/jsoup5.md: -------------------------------------------------------------------------------- 1 | Jsoup代码解读之五-parser(中) 2 | ------- 3 | 上一篇文章讲到了状态机和词法分析的基本知识,这一节我们来分析Jsoup是如何进行词法分析的。 4 | 5 | ## 代码结构 6 | 7 | 先介绍以下parser包里的主要类: 8 | 9 | * `Parser` 10 | 11 | Jsoup parser的入口facade,封装了常用的parse静态方法。可以设置`maxErrors`,用于收集错误记录,默认是0,即不收集。与之相关的类有`ParseError`,`ParseErrorList`。基于这个功能,我写了一个[`PageErrorChecker`](https://github.com/code4craft/jsoup-learning/tree/master/src/main/java/us/codecraft/learning/parser)来对页面做语法检查,并输出语法错误。 12 | 13 | * `Token` 14 | 15 | 保存单个的词法分析结果。Token是一个抽象类,它的实现有`Doctype`,`StartTag`,`EndTag`,`Comment`,`Character`,`EOF`6种,对应6种词法类型。 16 | 17 | * `Tokeniser` 18 | 19 | 保存词法分析过程的状态及结果。比较重要的两个字段是`state`和`emitPending`,前者保存状态,后者保存输出。其次还有`tagPending`/`doctypePending`/`commentPending`,保存还没有填充完整的Token。 20 | 21 | * `CharacterReader` 22 | 23 | 对读取字符的逻辑的封装,用于Tokenize时候的字符输入。CharacterReader包含了类似NIO里ByteBuffer的`consume()`、`unconsume()`、`mark()`、`rewindToMark()`,还有高级的`consumeTo()`这样的用法。 24 | 25 | * `TokeniserState` 26 | 27 | 用枚举实现的词法分析状态机。 28 | 29 | * `HtmlTreeBuilder` 30 | 31 | 语法分析,通过token构建DOM树的类。 32 | 33 | * `HtmlTreeBuilderState` 34 | 35 | 语法分析状态机。 36 | 37 | * `TokenQueue` 38 | 39 | 虽然披了个Token的马甲,其实是在query的时候用到,留到select部分再讲。 40 | 41 | ## 词法分析状态机 42 | 43 | 现在我们来讲讲HTML的词法分析过程。这里借用一下[http://ued.ctrip.com/blog/?p=3295](http://ued.ctrip.com/blog/?p=3295)里的图,图中描述了一个Tag标签的状态转移过程, 44 | 45 | ![lexer][1] 46 | 47 | 这里忽略了HTML注释、实体以及属性,只保留基本的开始/结束标签,例如下面的HTML: 48 | 49 |
test
50 | 51 | Jsoup里词法分析比较复杂,我从里面抽取出了对应的部分,就成了我们的miniSoupLexer(这里省略了部分代码,完整代码可以看这里[`MiniSoupTokeniserState`](https://github.com/code4craft/jsoup-learning/blob/master/src/main/java/org/jsoup/parser/MiniSoupTokeniserState.java)): 52 | 53 | ```java 54 | enum MiniSoupTokeniserState implements ITokeniserState { 55 | /** 56 | * 什么层级都没有的状态 57 | * ⬇ 58 | *
test
59 | * ⬇ 60 | *
test
61 | */ 62 | Data { 63 | // in data state, gather characters until a character reference or tag is found 64 | public void read(Tokeniser t, CharacterReader r) { 65 | switch (r.current()) { 66 | case '<': 67 | t.advanceTransition(TagOpen); 68 | break; 69 | case eof: 70 | t.emit(new Token.EOF()); 71 | break; 72 | default: 73 | String data = r.consumeToAny('&', '<', nullChar); 74 | t.emit(data); 75 | break; 76 | } 77 | } 78 | }, 79 | /** 80 | * ⬇ 81 | *
test
82 | */ 83 | TagOpen { 84 | ... 85 | }, 86 | /** 87 | * ⬇ 88 | *
test
89 | */ 90 | EndTagOpen { 91 | ... 92 | }, 93 | /** 94 | * ⬇ 95 | *
test
96 | */ 97 | TagName { 98 | ... 99 | }; 100 | 101 | } 102 | ``` 103 | 104 | 参考这个程序,可以看到Jsoup的词法分析的大致思路。分析器本身的编写是比较繁琐的过程,涉及属性值(区分单双引号)、DocType、注释、HTML实体,以及一些错误情况。不过了解了其思路,代码实现也是按部就班的过程。 105 | 106 | 下一节开始介绍语法分析部分。 107 | 108 | [1]: http://taligarsiel.com/Projects/image019.png -------------------------------------------------------------------------------- /blogs/jsoup6.md: -------------------------------------------------------------------------------- 1 | Jsoup代码解读之六-parser(下) 2 | -------- 3 | 最近生活上有点忙,女儿老是半夜不睡,精神状态也不是很好。工作上的事情也谈不上顺心,有很多想法但是没有几个被认可,有些事情也不是说代码写得好就行的。算了,还是端正态度,毕竟资历尚浅,我还是继续我的。 4 | 5 | 读Jsoup源码并非无聊,目的其实是为了将webmagic做的更好一点,毕竟parser也是爬虫的重要组成部分之一。读了代码后,收获也不少,对HTML的知识也更进一步了。 6 | 7 | ## DOM树产生过程 8 | 9 | 这里单独将`TreeBuilder`部分抽出来叫做语法分析过程可能稍微不妥,其实就是根据Token生成DOM树的过程,不过我还是沿用这个编译器里的称呼了。 10 | 11 | `TreeBuilder`同样是一个facade对象,真正进行语法解析的是以下一段代码: 12 | 13 | ```java 14 | protected void runParser() { 15 | while (true) { 16 | Token token = tokeniser.read(); 17 | 18 | process(token); 19 | 20 | if (token.type == Token.TokenType.EOF) 21 | break; 22 | } 23 | } 24 | ``` 25 | 26 | `TreeBuilder`有两个子类,`HtmlTreeBuilder`和`XmlTreeBuilder`。`XmlTreeBuilder`自然是构建XML树的类,实现颇为简单,基本上是维护一个栈,并根据不同Token插入节点即可: 27 | 28 | ```java 29 | @Override 30 | protected boolean process(Token token) { 31 | // start tag, end tag, doctype, comment, character, eof 32 | switch (token.type) { 33 | case StartTag: 34 | insert(token.asStartTag()); 35 | break; 36 | case EndTag: 37 | popStackToClose(token.asEndTag()); 38 | break; 39 | case Comment: 40 | insert(token.asComment()); 41 | break; 42 | case Character: 43 | insert(token.asCharacter()); 44 | break; 45 | case Doctype: 46 | insert(token.asDoctype()); 47 | break; 48 | case EOF: // could put some normalisation here if desired 49 | break; 50 | default: 51 | Validate.fail("Unexpected token type: " + token.type); 52 | } 53 | return true; 54 | } 55 | ``` 56 | 57 | `insertNode`的代码大致是这个样子(为了便于展示,对方法进行了一些整合): 58 | 59 | ```java 60 | Element insert(Token.StartTag startTag) { 61 | Tag tag = Tag.valueOf(startTag.name()); 62 | Element el = new Element(tag, baseUri, startTag.attributes); 63 | stack.getLast().appendChild(el); 64 | if (startTag.isSelfClosing()) { 65 | tokeniser.acknowledgeSelfClosingFlag(); 66 | if (!tag.isKnownTag()) // unknown tag, remember this is self closing for output. see above. 67 | tag.setSelfClosing(); 68 | } else { 69 | stack.add(el); 70 | } 71 | return el; 72 | } 73 | ``` 74 | 75 | ## HTML解析状态机 76 | 77 | 相比`XmlTreeBuilder`,`HtmlTreeBuilder`则实现较为复杂,除了类似的栈结构以外,还用到了`HtmlTreeBuilderState`来构建了一个状态机来分析HTML。这是为什么呢?不妨看看`HtmlTreeBuilderState`到底用到了哪些状态吧(在代码中中用``标明状态): 78 | 79 | ```html 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 92 | 96 | 97 | 98 | 99 | 100 | 104 | 105 | 106 | 107 | xxx 108 | 109 | 110 | 111 | 112 | 113 | 116 | 117 |
114 | 115 |
118 | 119 | ``` 120 | 121 | 这里可以看到,HTML标签是有嵌套要求的,例如``,``需要组合``来使用。根据Jsoup的代码,可以发现,`HtmlTreeBuilderState`做了以下一些事情: 122 | 123 | * ### 语法检查 124 | 125 | 例如`tr`没有嵌套在`table`标签内,则是一个语法错误。当`InBody`状态直接出现以下tag时,则出错。Jsoup里遇到这种错误,会发现这个Token的解析并记录错误,然后继续解析下面内容,并不会直接退出。 126 | 127 | ```java 128 | InBody { 129 | boolean process(Token t, HtmlTreeBuilder tb) { 130 | if (StringUtil.in(name, 131 | "caption", "col", "colgroup", "frame", "head", "tbody", "td", "tfoot", "th", "thead", "tr")) { 132 | tb.error(this); 133 | return false; 134 | } 135 | } 136 | ``` 137 | 138 | * ### 标签补全 139 | 140 | 例如`head`标签没有闭合,就写入了一些只有body内才允许出现的标签,则自动闭合``。`HtmlTreeBuilderState`有的方法`anythingElse()`就提供了自动补全标签,例如`InHead`状态的自动闭合代码如下: 141 | 142 | ```java 143 | private boolean anythingElse(Token t, TreeBuilder tb) { 144 | tb.process(new Token.EndTag("head")); 145 | return tb.process(t); 146 | } 147 | ``` 148 | 149 | 还有一种标签闭合方式,例如下面的代码: 150 | 151 | ```java 152 | private void closeCell(HtmlTreeBuilder tb) { 153 | if (tb.inTableScope("td")) 154 | tb.process(new Token.EndTag("td")); 155 | else 156 | tb.process(new Token.EndTag("th")); // only here if th or td in scope 157 | } 158 | ``` 159 | 160 | ## 实例研究 161 | 162 | ### 缺少标签时,会发生什么事? 163 | 164 | 好了,看了这么多parser的源码,不妨回到我们的日常应用上来。我们知道,在页面里多写一个两个未闭合的标签是很正常的事,那么它们会被怎么解析呢? 165 | 166 | 就拿`
`标签为例: 167 | 168 | 1. 漏写了开始标签,只写了结束标签 169 | 170 | ```java 171 | case EndTag: 172 | if (StringUtil.in(name,"div","dl", "fieldset", "figcaption", "figure", "footer", "header", "pre", "section", "summary", "ul")) { 173 | if (!tb.inScope(name)) { 174 | tb.error(this); 175 | return false; 176 | } 177 | } 178 | ``` 179 | 180 | 恭喜你,这个`
`会被当做错误处理掉,于是你的页面就毫无疑问的乱掉了!当然,如果单纯多写了一个``,好像也不会有什么影响哦?(记得有人跟我讲过为了防止标签未闭合,而在页面底部多写了几个``的故事) 181 | 182 | 2. 写了开始标签,漏写了结束标签 183 | 184 | 这个情况分析起来更复杂一点。如果是无法在内部嵌套内容的标签,那么在遇到不可接受的标签时,会进行闭合。而`
`标签可以包括大多数标签,这种情况下,其作用域会持续到HTML结束。 185 | 186 | 好了,parser系列算是分析结束了,其间学到不少HTML及状态机内容,但是离实际使用比较远。下面开始select部分,这部分可能对日常使用更有意义一点。 -------------------------------------------------------------------------------- /blogs/jsoup7.md: -------------------------------------------------------------------------------- 1 | Jsoup代码解读之七-实现一个CSS Selector 2 | ----- 3 | 4 | ![street fighter][1] 5 | 6 | 当当当!终于来到了Jsoup的特色:CSS Selector部分。selector也是我写的爬虫框架[webmagic](https://github.com/code4craft/webmagic)开发的一个重点。附上一张street fighter的图,希望以后webmagic也能挑战Jsoup! 7 | 8 | ## select机制 9 | 10 | Jsoup的select包里,类结构如下: 11 | 12 | ![uml][2] 13 | 14 | 在最开始介绍Jsoup的时候,就已经说过`NodeVisitor`和`Selector`了。`Selector`是select部分的对外facade,而`NodeVisitor`则是遍历树的底层API,CSS Selector也是根据`NodeVisitor`实现的遍历。 15 | 16 | Jsoup的select核心是`Evaluator`。Selector所传递的表达式,会经过`QueryParser`,最终编译成一个`Evaluator`。`Evaluator`是一个抽象类,它只有一个方法: 17 | 18 | ```java 19 | public abstract boolean matches(Element root, Element element); 20 | ``` 21 | 22 | 注意这里传入了root,是为了某些情况下对树进行遍历时用的。 23 | 24 | Evaluator的设计简洁明了,所有的Selector表达式单词都会编译到对应的Evaluator。例如`#xx`对应`Id`,`.xx`对应`Class`,`[]`对应`Attribute`。这里补充一下w3c的CSS Selector规范:[http://www.w3.org/TR/CSS2/selector.html](http://www.w3.org/TR/CSS2/selector.html) 25 | 26 | 当然,只靠这几个还不够,Jsoup还定义了`CombiningEvaluator`(对Evaluator进行And/Or组合),`StructuralEvaluator`(结合DOM树结构进行筛选)。 27 | 28 | 这里我们可能最关心的是,“div ul li”这样的父子结构是如何实现的。这个的实现方式在`StructuralEvaluator.Parent`中,贴一下代码了: 29 | 30 | ```java 31 | static class Parent extends StructuralEvaluator { 32 | public Parent(Evaluator evaluator) { 33 | this.evaluator = evaluator; 34 | } 35 | 36 | public boolean matches(Element root, Element element) { 37 | if (root == element) 38 | return false; 39 | 40 | Element parent = element.parent(); 41 | while (parent != root) { 42 | if (evaluator.matches(root, parent)) 43 | return true; 44 | parent = parent.parent(); 45 | } 46 | return false; 47 | } 48 | } 49 | ``` 50 | 51 | 这里Parent包含了一个`evaluator`属性,会根据这个evaluator去验证所有父节点。注意Parent是可以嵌套的,所以这个表达式"div ul li"最终会编译成`And(Parent(And(Parent(Tag("div")),Tag("ul")),Tag("li")))`这样的Evaluator组合。 52 | 53 | select部分比想象的要简单,代码可读性也很高。经过了parser部分的研究,这部分应该算是驾轻就熟了。 54 | 55 | ## 关于webmagic的后续打算 56 | 57 | webmagic是一个爬虫框架,它的Selector是用于抓取HTML中指定的文本,其机制和Jsoup的Evaluator非常像,只不过webmagic暂时是将Selector封装成较简单的API,而Evaluator直接上了表达式。之前也考虑过自己定制DSL来写一个HTML,现在看了Jsoup的源码,实现能力算是有了,但是引入DSL,实现只是一小部分,如何让DSL易写易懂才是难点。 58 | 59 | 其实看了Jsoup的源码,精细程度上比webmagic要好得多了,基本每个类都对应一个真实的概念抽象,可能以后会在这方面下点工夫。 60 | 61 | 下篇文章将讲最后一部分:白名单及HTML过滤机制。 62 | 63 | [1]: http://static.oschina.net/uploads/space/2013/0830/180244_r1Vb_190591.jpg 64 | 65 | [2]: http://static.oschina.net/uploads/space/2013/0830/184337_j85b_190591.png -------------------------------------------------------------------------------- /blogs/jsoup8.md: -------------------------------------------------------------------------------- 1 | Jsoup代码解读之八-防御XSS攻击 2 | -------- 3 | ![hacker][1] 4 | 5 | ## 防御XSS攻击的一般原理 6 | 7 | cleaner是Jsoup的重要功能之一,我们常用它来进行富文本输入中的XSS防御。 8 | 9 | 我们知道,XSS攻击的一般方式是,通过在页面输入中嵌入一段恶意脚本,对输出时的DOM结构进行修改,从而达到执行这段脚本的目的。对于纯文本输入,过滤/转义HTML特殊字符`<`,`>`,`"`,`'`是行之有效的办法,但是如果本身用户输入的就是一段HTML文本(例如博客文章),这种方式就不太有效了。这个时候,就是Jsoup大显身手的时候了。 10 | 11 | 在前面,我们已经知道了,Jsoup里怎么将HTML变成一棵DOM树,怎么对DOM树进行遍历,怎么对DOM文档进行输出,那么其实cleaner的实现方式,也能猜出大概了。使用Jsoup进行XSS防御,大致分为三个步骤: 12 | 13 | 1. 将HTML解析为DOM树 14 | 15 | 这一步可以过滤掉一些企图搞破坏的非闭合标签、非正常语法等。例如一些输入,会尝试用``闭合当前Tag,然后写入攻击脚本。而根据前面对Jsoup的parser的分析,这种时候,这些非闭合标签会被当做错误并丢弃。 16 | 17 | 2. 过滤高风险标签/属性/属性值 18 | 19 | 高风险标签是指`

three

four

"); 62 | assertEquals("

two

three

four

", TextUtil.stripNewlines(doc.html())); 63 | } 64 | 65 | @Test public void testClone() { 66 | Document doc = Jsoup.parse("Hello

One

Two"); 67 | Document clone = doc.clone(); 68 | 69 | assertEquals("Hello

One

Two

", TextUtil.stripNewlines(clone.html())); 70 | clone.title("Hello there"); 71 | clone.select("p").first().text("One more").attr("id", "1"); 72 | assertEquals("Hello there

One more

Two

", TextUtil.stripNewlines(clone.html())); 73 | assertEquals("Hello

One

Two

", TextUtil.stripNewlines(doc.html())); 74 | } 75 | 76 | @Test public void testClonesDeclarations() { 77 | Document doc = Jsoup.parse("Doctype test"); 78 | Document clone = doc.clone(); 79 | 80 | assertEquals(doc.html(), clone.html()); 81 | assertEquals("<!DOCTYPE html><html><head><title>Doctype test", 82 | TextUtil.stripNewlines(clone.html())); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /src/test/java/org/jsoup/nodes/DocumentTypeTest.java: -------------------------------------------------------------------------------- 1 | package org.jsoup.nodes; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Tests for the DocumentType node 9 | * 10 | * @author Jonathan Hedley, http://jonathanhedley.com/ 11 | */ 12 | public class DocumentTypeTest { 13 | @Test(expected = IllegalArgumentException.class) 14 | public void constructorValidationThrowsExceptionOnBlankName() { 15 | DocumentType fail = new DocumentType("","", "", ""); 16 | } 17 | 18 | @Test(expected = IllegalArgumentException.class) 19 | public void constructorValidationThrowsExceptionOnNulls() { 20 | DocumentType fail = new DocumentType("html", null, null, ""); 21 | } 22 | 23 | @Test 24 | public void constructorValidationOkWithBlankPublicAndSystemIds() { 25 | DocumentType fail = new DocumentType("html","", "",""); 26 | } 27 | 28 | @Test public void outerHtmlGeneration() { 29 | DocumentType html5 = new DocumentType("html", "", "", ""); 30 | assertEquals("", html5.outerHtml()); 31 | 32 | DocumentType publicDocType = new DocumentType("html", "-//IETF//DTD HTML//", "", ""); 33 | assertEquals("", publicDocType.outerHtml()); 34 | 35 | DocumentType systemDocType = new DocumentType("html", "", "http://www.ibm.com/data/dtd/v11/ibmxhtml1-transitional.dtd", ""); 36 | assertEquals("", systemDocType.outerHtml()); 37 | 38 | DocumentType combo = new DocumentType("notHtml", "--public", "--system", ""); 39 | assertEquals("", combo.outerHtml()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/org/jsoup/nodes/EntitiesTest.java: -------------------------------------------------------------------------------- 1 | package org.jsoup.nodes; 2 | 3 | import org.jsoup.Jsoup; 4 | import org.junit.Test; 5 | 6 | import static org.junit.Assert.*; 7 | 8 | import java.nio.charset.Charset; 9 | 10 | public class EntitiesTest { 11 | @Test public void escape() { 12 | String text = "Hello &<> Å å π 新 there ¾ ©"; 13 | String escapedAscii = Entities.escape(text, Charset.forName("ascii").newEncoder(), Entities.EscapeMode.base); 14 | String escapedAsciiFull = Entities.escape(text, Charset.forName("ascii").newEncoder(), Entities.EscapeMode.extended); 15 | String escapedAsciiXhtml = Entities.escape(text, Charset.forName("ascii").newEncoder(), Entities.EscapeMode.xhtml); 16 | String escapedUtfFull = Entities.escape(text, Charset.forName("UTF-8").newEncoder(), Entities.EscapeMode.base); 17 | String escapedUtfMin = Entities.escape(text, Charset.forName("UTF-8").newEncoder(), Entities.EscapeMode.xhtml); 18 | 19 | assertEquals("Hello &<> Å å π 新 there ¾ ©", escapedAscii); 20 | assertEquals("Hello &<> Å å π 新 there ¾ ©", escapedAsciiFull); 21 | assertEquals("Hello &<> Å å π 新 there ¾ ©", escapedAsciiXhtml); 22 | assertEquals("Hello &<> Å å π 新 there ¾ ©", escapedUtfFull); 23 | assertEquals("Hello &<> Å å π 新 there ¾ ©", escapedUtfMin); 24 | // odd that it's defined as aring in base but angst in full 25 | 26 | // round trip 27 | assertEquals(text, Entities.unescape(escapedAscii)); 28 | assertEquals(text, Entities.unescape(escapedAsciiFull)); 29 | assertEquals(text, Entities.unescape(escapedAsciiXhtml)); 30 | assertEquals(text, Entities.unescape(escapedUtfFull)); 31 | assertEquals(text, Entities.unescape(escapedUtfMin)); 32 | } 33 | 34 | @Test public void escapeSupplementaryCharacter(){ 35 | String text = new String(Character.toChars(135361)); 36 | String escapedAscii = Entities.escape(text, Charset.forName("ascii").newEncoder(), Entities.EscapeMode.base); 37 | assertEquals("𡃁", escapedAscii); 38 | String escapedUtf = Entities.escape(text, Charset.forName("UTF-8").newEncoder(), Entities.EscapeMode.base); 39 | assertEquals(text, escapedUtf); 40 | } 41 | 42 | @Test public void unescape() { 43 | String text = "Hello &<> ® Å &angst π π 新 there &! ¾ © ©"; 44 | assertEquals("Hello &<> ® Å &angst π π 新 there &! ¾ © ©", Entities.unescape(text)); 45 | 46 | assertEquals("&0987654321; &unknown", Entities.unescape("&0987654321; &unknown")); 47 | } 48 | 49 | @Test public void strictUnescape() { // for attributes, enforce strict unescaping (must look like &#xxx; , not just &#xxx) 50 | String text = "Hello &= &"; 51 | assertEquals("Hello &= &", Entities.unescape(text, true)); 52 | assertEquals("Hello &= &", Entities.unescape(text)); 53 | assertEquals("Hello &= &", Entities.unescape(text, false)); 54 | } 55 | 56 | 57 | @Test public void caseSensitive() { 58 | String unescaped = "Ü ü & &"; 59 | assertEquals("Ü ü & &", Entities.escape(unescaped, Charset.forName("ascii").newEncoder(), Entities.EscapeMode.extended)); 60 | 61 | String escaped = "Ü ü & &"; 62 | assertEquals("Ü ü & &", Entities.unescape(escaped)); 63 | } 64 | 65 | @Test public void quoteReplacements() { 66 | String escaped = "\ $"; 67 | String unescaped = "\\ $"; 68 | 69 | assertEquals(unescaped, Entities.unescape(escaped)); 70 | } 71 | 72 | @Test public void letterDigitEntities() { 73 | String html = "

¹²³¼½¾

"; 74 | Document doc = Jsoup.parse(html); 75 | Element p = doc.select("p").first(); 76 | assertEquals("¹²³¼½¾", p.html()); 77 | assertEquals("¹²³¼½¾", p.text()); 78 | } 79 | 80 | @Test public void noSpuriousDecodes() { 81 | String string = "http://www.foo.com?a=1&num_rooms=1&children=0&int=VA&b=2"; 82 | assertEquals(string, Entities.unescape(string)); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/org/jsoup/nodes/FormElementTest.java: -------------------------------------------------------------------------------- 1 | package org.jsoup.nodes; 2 | 3 | import org.jsoup.Connection; 4 | import org.jsoup.Jsoup; 5 | import org.junit.Test; 6 | 7 | import java.io.IOException; 8 | import java.util.Collection; 9 | import java.util.List; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Tests for FormElement 15 | * 16 | * @author Jonathan Hedley 17 | */ 18 | public class FormElementTest { 19 | @Test public void hasAssociatedControls() { 20 | //"button", "fieldset", "input", "keygen", "object", "output", "select", "textarea" 21 | String html = "