💧 Posted on 

撸一个JSON解析器

JSON(JavaScript Object Notation, JS 对象简谱) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。采用完全独立于语言的文本格式,但是也使用了类似于 C 语言家族的习惯(包括 C, C++, C#, Java, JavaScript, Perl, Python 等)。这些特性使 JSON 成为理想的数据交换语言。

JSON 与 JS 的区别以及和 XML 的区别具体请参考百度百科

JSON 有两种结构:

第一种:对象

“名称 / 值” 对的集合不同的语言中,它被理解为对象(object),纪录(record),结构(struct),字典(dictionary),哈希表(hash table),有键列表(keyed list),或者关联数组 (associative array)。

对象是一个无序的 “‘名称 / 值’对” 集合。一个对象以 “{”(左括号)开始,“}”(右括号)结束。每个“名称” 后跟一个 “:”(冒号);“‘名称 / 值’ 对” 之间使用“,”(逗号)分隔。

1
{"姓名": "张三", "年龄": "18"}

第二种:数组

值的有序列表(An ordered list of values)。在大部分语言中,它被理解为数组(array)。

数组是值(value)的有序集合。一个数组以 “[”(左中括号)开始,“]”(右中括号)结束。值之间使用 “,”(逗号)分隔。

值(value)可以是双引号括起来的字符串(string)、数值 (number)、true、false、 null、对象(object)或者数组(array)。这些结构可以嵌套。

1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
"姓名": "张三",
"年龄":"18"
},

{
"姓名": "里斯",
"年龄":"19"

}
]

通过上面的了解可以看出,JSON 存在以下几种数据类型(以 Java 做类比):

json java
string Java 中的 String
number Java 中的 Long 或 Double
true/false Java 中的 Boolean
null Java 中的 null
[array] Java 中的 List 或 Object[]
{“key”:”value”} Java 中的 Map<String, Object>

解析 JSON

输入一串 JSON 字符串,输出一个 JSON 对象。

步骤

JSON 解析的过程主要分以下两步:

第一步: 对于输入的一串 JSON 字符串我们需要将其解析成一组 token 流。

例如 JSON 字符串 {“姓名”: “张三”, “年龄”: “18”} 我们需要将它解析成

1
{、 姓名、 :、 张三、 ,、 年龄、 :、 18、 }

这样一组 token 流

第二步:根据得到的 token 流将其解析成对应的 JSON 对象(JSONObject)或者 JSON 数组(JSONArray)

下面我们来详细分析下这两个步骤:

获取 token 流

根据 JSON 格式的定义,token 可以分为以下几种类型

token 含义
NULL null
NUMBER 数字
STRING 字符串
BOOLEAN true/false
SEP_COLON :
SEP_COMMA ,
BEGIN_OBJECT {
END_OBJECT }
BEGIN_ARRAY [
END_ARRAY ]
END_DOCUMENT 表示 JSON 数据结束

根据以上的 JSON 类型,我们可以将其封装成 enum 类型的 TokenType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.json.demo.tokenizer;
/**
BEGIN_OBJECT({)
END_OBJECT(})
BEGIN_ARRAY([)
END_ARRAY(])
NULL(null)
NUMBER(数字)
STRING(字符串)
BOOLEAN(true/false)
SEP_COLON(:)
SEP_COMMA(,)
END_DOCUMENT(表示JSON文档结束)
*/

public enum TokenType {
BEGIN_OBJECT(1),
END_OBJECT(2),
BEGIN_ARRAY(4),
END_ARRAY(8),
NULL(16),
NUMBER(32),
STRING(64),
BOOLEAN(128),
SEP_COLON(256),
SEP_COMMA(512),
END_DOCUMENT(1024);

private int code; // 每个类型的编号

TokenType(int code) {
this.code = code;
}

public int getTokenCode() {
return code;
}
}

在 TokenType 中我们为每一种类型都赋一个数字,目的是在 Parser 做一些优化操作(通过位运算来判断是否是期望出现的类型)

在进行第一步之前 JSON 串对计算机来说只是一串没有意义的字符而已。第一步的作用就是把这些无意义的字符串变成一个一个的 token,上面我们已经为每一种 token 定义了相应的类型和值。所以计算机能够区分不同的 token,并能以 token 为单位解读 JSON 数据。

下面我们封装一个 token 类来存储每一个 token 对应的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.json.demo.tokenizer;

/**
* 存储对应类型的字面量
*/

public class Token {
private TokenType tokenType;
private String value;

public Token(TokenType tokenType, String value) {
this.tokenType = tokenType;
this.value = value;
}

public TokenType getTokenType() {
return tokenType;
}

public void setTokenType(TokenType tokenType) {
this.tokenType = tokenType;
}

public String getValue() {
return value;
}

public void setValue(String value) {
this.value = value;
}

@Override
public String toString() {
return "Token{" +
"tokenType=" + tokenType +
", value='" + value + '\'' +
'}';
}
}

在解析的过程中我们通过字符流来不断的读取字符,并且需要经常根据相应的字符来判断状态的跳转。所以我们需要自己封装一个 ReaderChar 类,以便我们更好的操作字符流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.json.demo.tokenizer;

import java.io.IOException;
import java.io.Reader;

public class ReaderChar {
private static final int BUFFER_SIZE = 1024;
private Reader reader;
private char[] buffer;
private int index; // 下标
private int size;

public ReaderChar(Reader reader) {
this.reader = reader;
buffer = new char[BUFFER_SIZE];
}

/**
* 返回 pos 下标处的字符,并返回
* @return
*/
public char peek() {
if (index - 1 >= size) {
return (char) -1;
}

return buffer[Math.max(0, index - 1)];
}

/**
* 返回 pos 下标处的字符,并将 pos + 1,最后返回字符
* @return
* @throws IOException
*/
public char next() throws IOException {
if (!hasMore()) {
return (char) -1;
}

return buffer[index++];
}

/**
* 下标回退
*/
public void back() {
index = Math.max(0, --index);
}

/**
* 判断流是否结束
*/
public boolean hasMore() throws IOException {
if (index < size) {
return true;
}

fillBuffer();
return index < size;
}

/**
* 填充buffer数组
* @throws IOException
*/
void fillBuffer() throws IOException {
int n = reader.read(buffer);
if (n == -1) {
return;
}

index = 0;
size = n;
}
}

另外我们还需要一个 TokenList 来存储解析出来的 token 流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package com.json.demo.tokenizer;

import java.util.ArrayList;
import java.util.List;

/**
* 存储词法解析所得的token流
*/
public class TokenList {
private List<Token> tokens = new ArrayList<Token>();
private int index = 0;

public void add(Token token) {
tokens.add(token);
}

public Token peek() {
return index < tokens.size() ? tokens.get(index) : null;
}

public Token peekPrevious() {
return index - 1 < 0 ? null : tokens.get(index - 2);
}

public Token next() {
return tokens.get(index++);
}

public boolean hasMore() {
return index < tokens.size();
}

@Override
public String toString() {
return "TokenList{" +
"tokens=" + tokens +
'}';
}
}

JSON 解析比其他文本解析要简单的地方在于,我们只需要根据下一个字符就可知道接下来它所期望读取的到的内容是什么样的。如果满足期望了,则返回 Token,否则返回错误。

为了方便程序出错时更好的 debug,程序中自定义了两个 exception 类来处理错误信息。(具体实现参考 exception 包)

下面就是第一步中的重头戏(核心代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public TokenList getTokenStream(ReaderChar readerChar) throws IOException {
this.readerChar = readerChar;
tokenList = new TokenList();

// 词法解析,获取token流
tokenizer();

return tokenList;
}

/**
* 将JSON文件解析成token流
* @throws IOException
*/
private void tokenizer() throws IOException {
Token token;
do {
token = start();
tokenList.add(token);
} while (token.getTokenType() != TokenType.END_DOCUMENT);
}

/**
* 解析过程的具体实现方法
* @return
* @throws IOException
* @throws JsonParseException
*/
private Token start() throws IOException, JsonParseException {
char ch;
while (true){ //先读一个字符,若为空白符(ASCII码在[0, 20H]上)则接着读,直到刚读的字符非空白符
if (!readerChar.hasMore()) {
return new Token(TokenType.END_DOCUMENT, null);
}

ch = readerChar.next();
if (!isWhiteSpace(ch)) {
break;
}
}

switch (ch) {
case '{':
return new Token(TokenType.BEGIN_OBJECT, String.valueOf(ch));
case '}':
return new Token(TokenType.END_OBJECT, String.valueOf(ch));
case '[':
return new Token(TokenType.BEGIN_ARRAY, String.valueOf(ch));
case ']':
return new Token(TokenType.END_ARRAY, String.valueOf(ch));
case ',':
return new Token(TokenType.SEP_COMMA, String.valueOf(ch));
case ':':
return new Token(TokenType.SEP_COLON, String.valueOf(ch));
case 'n':
return readNull();
case 't':
case 'f':
return readBoolean();
case '"':
return readString();
case '-':
return readNumber();
}

if (isDigit(ch)) {
return readNumber();
}

throw new JsonParseException("Illegal character");
}

start 方法中,我们将每个处理方法都封装成了单独的函数。主要思想就是通过一个死循环不停的读取字符,然后再根据字符的期待值,执行不同的处理函数。

下面我们详解分析几个处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private Token readString() throws IOException {
StringBuilder sb = new StringBuilder();
while(true) {
char ch = readerChar.next();
if (ch == '\\') { // 处理转义字符
if (!isEscape()) {
throw new JsonParseException("Invalid escape character");
}
sb.append('\\');
ch = readerChar.peek();
sb.append(ch);
if (ch == 'u') { // 处理 Unicode 编码,形如 \u4e2d。且只支持 \u0000 ~ \uFFFF 范围内的编码
for (int i = 0; i < 4; i++) {
ch = readerChar.next();
if (isHex(ch)) {
sb.append(ch);
} else {
throw new JsonParseException("Invalid character");
}
}
}
} else if (ch == '"') { // 碰到另一个双引号,则认为字符串解析结束,返回 Token
return new Token(TokenType.STRING, sb.toString());
} else if (ch == '\r' || ch == '\n') { // 传入的 JSON 字符串不允许换行
throw new JsonParseException("Invalid character");
} else {
sb.append(ch);
}
}
}

该方法也是通过一个死循环来读取字符,首先判断的是 JSON 中的转义字符。

JSON 中允许出现的有以下几种

1
2
3
4
5
6
7
8
9
10
\"
\\
\b
\f
\n
\r
\t
\u four-hex-digits
\/

具体的处理方法封装在了 isEscape() 方法中,处理 Unicode 编码时要特别注意一下 u 的后面会出现四位十六进制数。当读取到一个双引号或者读取到了非法字符(’r’或’、’n’)循环退出。

判断数字的时候也要特别小心,注意负数,frac,exp 等等情况。

通过上面的解析,我们可以得到一组 token,接下来我们需要以这组 token 作为输入,解析出相应的 JSON 对象

解析出 JSON 对象

解析之前我们需要定义出 JSON 对象(JSONObject)和 JSON 数组 (JSONArray) 的实体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.json.demo.jsonstyle;

import com.json.demo.exception.JsonTypeException;
import com.json.demo.util.FormatUtil;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* JSON的对象形式
* 对象是一个无序的“‘名称/值’对”集合。一个对象以“{”(左括号)开始,“}”(右括号)结束。每个“名称”后跟一个“:”(冒号);“‘名称/值’ 对”之间使用“,”(逗号)分隔。
*/
public class JsonObject {
private Map<String, Object> map = new HashMap<String, Object>();

public void put(String key, Object value) {
map.put(key, value);
}

public Object get(String key) {
return map.get(key);
}
...

}

package com.json.demo.jsonstyle;

import com.json.demo.exception.JsonTypeException;
import com.json.demo.util.FormatUtil;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
* JSON的数组形式
* 数组是值(value)的有序集合。一个数组以“[”(左中括号)开始,“]”(右中括号)结束。值之间使用“,”(逗号)分隔。
*/
public class JsonArray {
private List list = new ArrayList();

public void add(Object obj) {
list.add(obj);
}

public Object get(int index) {
return list.get(index);
}

public int size() {
return list.size();
}
...
}

之后我们就可以写解析类了,由于代码较长,这里就不展示了。有兴趣的可以去 GitHub 上下载。实现逻辑比较简单,也易于理解。

解析类中的 parse 方法首先根据第一个 token 的类型选择调用 parseJsonObject()或者 parseJsonArray(),进而返回 JSON 对象或者 JSON 数组。上面的解析方法中利用位运算来判断字符的期待值既提高了程序的执行效率也有助于提高代码的 ke’du’xi

完成之后我们可以写一个测试类来验证下我们的解析器的运行情况。我们可以自己定义一组 JSON 串也可以通过 HttpUtil 工具类从网上获取。最后通过 FormatUtil 类来规范我们输出。

具体效果如下图所示:

参考文章