Skip to content

Commit 73f9cc2

Browse files
committed
syntax hightlight
1 parent 655ff43 commit 73f9cc2

File tree

5 files changed

+47
-23
lines changed

5 files changed

+47
-23
lines changed

blogs/jsoup2.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ Node类是一个抽象类,它代表DOM树中的一个节点,它包含:
1919

2020
Node里面包含一些获取属性、父子节点、修改元素的方法,其中比较有意思的是`absUrl()`。我们知道,在很多html页面里,链接会使用相对地址,我们有时会需要将其转变为绝对地址。Jsoup的解决方案是在attr()的参数开始加"abs:",例如attr("abs:href"),而`absUrl()`就是其实现方式。我写的爬虫框架[webmagic](http://www.oschina.net/p/webmagic)里也用到了类似功能,当时是自己手写的,看到Jsoup的实现,才发现自己是白费劲了,代码如下:
2121

22-
<!-- lang: java -->
22+
```java
2323
URL base;
2424
try {
2525
try {
@@ -38,7 +38,8 @@ Node里面包含一些获取属性、父子节点、修改元素的方法,其
3838
} catch (MalformedURLException e) {
3939
return "";
4040
}
41-
41+
```
42+
4243
Node还有一个比较值得一提的方法是`abstract String nodeName()`,这个相当于定义了节点的类型名(例如`Document`是'#Document',`Element`则是对应的TagName)。
4344

4445
Element也是一个重要的类,它代表的是一个HTML元素。它包含一个字段`tag``classNames`。classNames是"class"属性解析出来的集合,因为CSS规范里,"class"属性允许设置多个,并用空格隔开,而在用Selector选择的时候,即使只指定其中一个,也能够选中其中的元素。所以这里就把"class"属性展开了。Element还有选取元素的入口,例如`select``getElementByXXX`,这些都用到了select包中的内容,这个留到下篇文章select再说。
@@ -51,7 +52,7 @@ Document还有一个属性`quirksMode`,大致意思是定义处理非标准HTM
5152

5253
Node还有一些方法,例如`outerHtml()`,用作节点及文档HTML的输出,用到了树的遍历。在DOM树的遍历上,用到了`NodeVisitor``NodeTraversor`来对树的进行遍历。`NodeVisitor`在上一篇文章提到过了,head()和tail()分别是遍历开始和结束时的方法,而`NodeTraversor`的核心代码如下:
5354

54-
<!-- lang: java -->
55+
```java
5556
public void traverse(Node root) {
5657
Node node = root;
5758
int depth = 0;
@@ -78,6 +79,7 @@ Node还有一些方法,例如`outerHtml()`,用作节点及文档HTML的输
7879
}
7980
}
8081
}
82+
```
8183

8284
这里使用循环+回溯来替换掉了我们常用的递归方式,从而避免了栈溢出的风险。
8385

@@ -91,21 +93,24 @@ Jsoup官方说明里,一个重要的功能就是***output tidy HTML***。这
9193

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

94-
<!-- lang: java -->
96+
```java
9597
private void html(StringBuilder accum) {
9698
for (Node node : childNodes)
9799
node.outerHtml(accum);
98100
}
101+
```
99102

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

102-
<!-- lang: java -->
105+
```java
103106
protected void outerHtml(StringBuilder accum) {
104107
new NodeTraversor(new OuterHtmlVisitor(accum, getOutputSettings())).traverse(this);
105108
}
109+
```
106110

107111
OuterHtmlVisitor会对所有子节点做遍历,并调用`node.outerHtmlHead()``node.outerHtmlTail`两个方法。
108-
112+
113+
```java
109114
private static class OuterHtmlVisitor implements NodeVisitor {
110115
private StringBuilder accum;
111116
private Document.OutputSettings out;
@@ -119,6 +124,7 @@ OuterHtmlVisitor会对所有子节点做遍历,并调用`node.outerHtmlHead()`
119124
node.outerHtmlTail(accum, depth, out);
120125
}
121126
}
127+
```
122128

123129
好了,现在我们找到了真正干活的代码,`node.outerHtmlHead()``node.outerHtmlTail`。分析代码前,我们不妨先想想,"tidy HTML"到底包括哪些东西:
124130

blogs/jsoup3.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Jsoup官方说明里,一个重要的功能就是***output tidy HTML***。这
1414

1515
这里要补充一下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开发者非常好的学习资料。
1616

17-
<!-- lang: java -->
17+
```java
1818
// internal static initialisers:
1919
// prepped from http://www.w3.org/TR/REC-html40/sgml/dtd.html and other sources
2020
//block tags,需要换行
@@ -46,6 +46,7 @@ Jsoup官方说明里,一个重要的功能就是***output tidy HTML***。这
4646
private static final String[] preserveWhitespaceTags = {
4747
"pre", "plaintext", "title", "textarea"
4848
};
49+
```
4950

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

@@ -57,22 +58,24 @@ Jsoup官方说明里,一个重要的功能就是***output tidy HTML***。这
5758

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

60-
<!-- lang: java -->
61+
```java
6162
private void html(StringBuilder accum) {
6263
for (Node node : childNodes)
6364
node.outerHtml(accum);
6465
}
66+
```
6567

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

68-
<!-- lang: java -->
70+
```java
6971
protected void outerHtml(StringBuilder accum) {
7072
new NodeTraversor(new OuterHtmlVisitor(accum, getOutputSettings())).traverse(this);
7173
}
72-
74+
```
75+
7376
OuterHtmlVisitor会对所有子节点做遍历,并调用`node.outerHtmlHead()``node.outerHtmlTail`两个方法。
7477

75-
<!-- lang: java -->
78+
```java
7679
private static class OuterHtmlVisitor implements NodeVisitor {
7780
private StringBuilder accum;
7881
private Document.OutputSettings out;
@@ -86,10 +89,11 @@ OuterHtmlVisitor会对所有子节点做遍历,并调用`node.outerHtmlHead()`
8689
node.outerHtmlTail(accum, depth, out);
8790
}
8891
}
92+
```
8993

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

92-
<!-- lang: java -->
96+
```java
9397
void outerHtmlHead(StringBuilder accum, int depth, Document.OutputSettings out) {
9498
if (accum.length() > 0 && out.prettyPrint()
9599
&& (tag.formatAsBlock() || (parent() != null && parent().tag().formatAsBlock()) || out.outline()) )
@@ -116,14 +120,16 @@ OuterHtmlVisitor会对所有子节点做遍历,并调用`node.outerHtmlHead()`
116120
accum.append("</").append(tagName()).append(">");
117121
}
118122
}
123+
```
119124

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

122-
<!-- lang: java -->
127+
```java
123128
protected void indent(StringBuilder accum, int depth, Document.OutputSettings out) {
124129
//out.indentAmount()是缩进长度,默认是1
125130
accum.append("\n").append(StringUtil.padding(depth * out.indentAmount()));
126131
}
132+
```
127133

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

blogs/jsoup4.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ Jsoup的词法分析和语法分析都用到了状态机。状态机可以理解
3232

3333
状态机本身是一个编程模型,这里我们尝试用程序去实现它,那么最直接的方式大概是这样:
3434

35-
<!-- lang: java -->
35+
```java
3636
public void process(StringReader reader) throws StringReader.EOFException {
3737
char ch;
3838
switch (state) {
@@ -54,13 +54,15 @@ Jsoup的词法分析和语法分析都用到了状态机。状态机可以理解
5454
break;
5555
}
5656
}
57+
```
5758

5859
这样写简单的状态机倒没有问题,但是复杂情况下就有点难受了。还有一种标准的状态机解法,先建立状态转移表,然后使用这个表建立状态机。这个方法的问题就是,只能做纯状态转移,无法在代码级别操作输入输出。
5960

6061
Jsoup里则使用了状态模式来实现状态机,初次看到时,确实让人眼前一亮。状态模式是设计模式的一种,它将状态和对应的行为绑定在一起。而在状态机的实现过程中,使用它来实现状态转移时的处理再合适不过了。
6162

6263
"a[b]*"的例子的状态模式实现如下,这里采用了与Jsoup相同的方式,用到了枚举来实现状态模式:
6364

65+
```java
6466
public class StateModelABStateMachine implements ABStateMachine {
6567

6668
State state;
@@ -96,7 +98,7 @@ Jsoup里则使用了状态模式来实现状态机,初次看到时,确实让
9698
state.process(this, reader);
9799
}
98100
}
99-
101+
```
100102

101103
PS:我在github上fork了一份Jsoup的代码,把这系列文章提交了上去,并且给一些代码增加了中文注释,有兴趣的可以看看[https://github.com/code4craft/jsoup](https://github.com/code4craft/jsoup)。本文中提到的几种状态机的完整实现在这个仓库的[https://github.com/code4craft/jsoup/tree/master/src/main/java/us/codecraft/learning](https://github.com/code4craft/jsoup/tree/master/src/main/java/us/codecraft/learning)路径下。
102104

blogs/jsoup5.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Jsoup代码解读之五-parser(中)
5050

5151
Jsoup里词法分析比较复杂,我从里面抽取出了对应的部分,就成了我们的miniSoupLexer(这里省略了部分代码,完整代码可以看这里[`MiniSoupTokeniserState`](https://github.com/code4craft/jsoup/blob/master/src/main/java/org/jsoup/parser/MiniSoupTokeniserState.java)):
5252

53+
```java
5354
enum MiniSoupTokeniserState implements ITokeniserState {
5455
/**
5556
* 什么层级都没有的状态
@@ -98,6 +99,7 @@ Jsoup里词法分析比较复杂,我从里面抽取出了对应的部分,就
9899
};
99100

100101
}
102+
```
101103

102104
参考这个程序,可以看到Jsoup的词法分析的大致思路。分析器本身的编写是比较繁琐的过程,涉及属性值(区分单双引号)、DocType、注释、HTML实体,以及一些错误情况。不过了解了其思路,代码实现也是按部就班的过程。
103105

blogs/jsoup6.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Jsoup代码解读之六-parser(下)
1010

1111
`TreeBuilder`同样是一个facade对象,真正进行语法解析的是以下一段代码:
1212

13-
<!-- lang: java -->
13+
```java
1414
protected void runParser() {
1515
while (true) {
1616
Token token = tokeniser.read();
@@ -21,10 +21,11 @@ Jsoup代码解读之六-parser(下)
2121
break;
2222
}
2323
}
24+
```
2425

2526
`TreeBuilder`有两个子类,`HtmlTreeBuilder``XmlTreeBuilder``XmlTreeBuilder`自然是构建XML树的类,实现颇为简单,基本上是维护一个栈,并根据不同Token插入节点即可:
2627

27-
<!-- lang: java -->
28+
```java
2829
@Override
2930
protected boolean process(Token token) {
3031
// start tag, end tag, doctype, comment, character, eof
@@ -51,10 +52,11 @@ Jsoup代码解读之六-parser(下)
5152
}
5253
return true;
5354
}
55+
```
5456

5557
`insertNode`的代码大致是这个样子(为了便于展示,对方法进行了一些整合):
5658

57-
<!-- lang: java -->
59+
```java
5860
Element insert(Token.StartTag startTag) {
5961
Tag tag = Tag.valueOf(startTag.name());
6062
Element el = new Element(tag, baseUri, startTag.attributes);
@@ -68,12 +70,13 @@ Jsoup代码解读之六-parser(下)
6870
}
6971
return el;
7072
}
73+
```
7174

7275
## HTML解析状态机
7376

7477
相比`XmlTreeBuilder``HtmlTreeBuilder`则实现较为复杂,除了类似的栈结构以外,还用到了`HtmlTreeBuilderState`来构建了一个状态机来分析HTML。这是为什么呢?不妨看看`HtmlTreeBuilderState`到底用到了哪些状态吧(在代码中中用&lt;!-- State: --\&gt;标明状态):
7578

76-
<!-- lang: html -->
79+
```html
7780
<!-- State: Initial -->
7881
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
7982
<!-- State: BeforeHtml -->
@@ -113,14 +116,15 @@ Jsoup代码解读之六-parser(下)
113116
</tr>
114117
</table>
115118
</html>
119+
```
116120

117121
这里可以看到,HTML标签是有嵌套要求的,例如`<tr>`,`<td>`需要组合`<table>`来使用。根据Jsoup的代码,可以发现,`HtmlTreeBuilderState`做了以下一些事情:
118122

119123
* ### 语法检查
120124

121125
例如`tr`没有嵌套在`table`标签内,则是一个语法错误。当`InBody`状态直接出现以下tag时,则出错。Jsoup里遇到这种错误,会发现这个Token的解析并记录错误,然后继续解析下面内容,并不会直接退出。
122126

123-
<!-- lang: java -->
127+
```java
124128
InBody {
125129
boolean process(Token t, HtmlTreeBuilder tb) {
126130
if (StringUtil.in(name,
@@ -129,26 +133,29 @@ Jsoup代码解读之六-parser(下)
129133
return false;
130134
}
131135
}
136+
```
132137

133138
* ### 标签补全
134139

135140
例如`head`标签没有闭合,就写入了一些只有body内才允许出现的标签,则自动闭合`</head>`。`HtmlTreeBuilderState`有的方法`anythingElse()`就提供了自动补全标签,例如`InHead`状态的自动闭合代码如下:
136141

137-
<!-- lang: java -->
142+
```java
138143
private boolean anythingElse(Token t, TreeBuilder tb) {
139144
tb.process(new Token.EndTag("head"));
140145
return tb.process(t);
141146
}
147+
```
142148

143149
还有一种标签闭合方式,例如下面的代码:
144150

145-
<!-- lang: java -->
151+
```java
146152
private void closeCell(HtmlTreeBuilder tb) {
147153
if (tb.inTableScope("td"))
148154
tb.process(new Token.EndTag("td"));
149155
else
150156
tb.process(new Token.EndTag("th")); // only here if th or td in scope
151157
}
158+
```
152159

153160
## 实例研究
154161

@@ -160,14 +167,15 @@ Jsoup代码解读之六-parser(下)
160167

161168
1. 漏写了开始标签,只写了结束标签
162169

163-
<!-- lang: java -->
170+
```java
164171
case EndTag:
165172
if (StringUtil.in(name,"div","dl", "fieldset", "figcaption", "figure", "footer", "header", "pre", "section", "summary", "ul")) {
166173
if (!tb.inScope(name)) {
167174
tb.error(this);
168175
return false;
169176
}
170177
}
178+
```
171179

172180
恭喜你,这个`</div>`会被当做错误处理掉,于是你的页面就毫无疑问的乱掉了!当然,如果单纯多写了一个`</div>`,好像也不会有什么影响哦?(记得有人跟我讲过为了防止标签未闭合,而在页面底部多写了几个`</div>`的故事)
173181

0 commit comments

Comments
 (0)