其他
手把手教你实现一个 JSON 解析器!
脚本之家
你与百万开发者在一起
本文经由博客园作者 田小波⊰ 授权转载
转载自:www.cnblogs.com/nullllun/p/8358146.html
1. 背景
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。相对于另一种数据交换格式 XML,JSON 有着诸多优点。比如易读性更好,占用空间更少等。
本着探究 JSON 原理的目的,我将会在这篇文章中详细向大家介绍一个简单的JSON解析器的解析流程和实现细节。
由于 JSON 本身比较简单,解析起来也并不复杂。所以如果大家感兴趣的话,在看完本文后,不妨自己动手实现一个 JSON 解析器。
2. JSON 解析器实现原理
{
"name" : "小明",
"age": 18
}
{、 name、 :、 小明、 ,、 age、 :、 18、 }
object = {string : value}
。如果传入了一个格式错误的字符串,比如:{
"name", "小明"
}
:
。但当它读取了这个 Token,发现这个 Token 是,
,并非其期望的:
,于是文法分析器就会报错误。2.1 词法分析
http://www.json.org/
对 JSON 的定义,罗列一下 JSON 所规定的数据类型:BEGIN_OBJECT({) END_OBJECT(}) BEGIN_ARRAY([) END_ARRAY(]) NULL(null) NUMBER(数字) STRING(字符串) BOOLEAN(true/false) SEP_COLON(:) SEP_COMMA(,)
当词法分析器读取的词是上面类型中的一种时,即可将其解析成一个 Token。我们可以定义一个枚举类来表示上面的数据类型,如下:
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);
TokenType(int code) {
this.code = code;
}
private int code;
public int getTokenCode() {
return code;
}
}
public class Token {
private TokenType tokenType;
private String value;
// 省略不重要的代码
}
public CharReader(Reader reader) {
this.reader = reader;
buffer = new char[BUFFER_SIZE];
}
/**
* 返回 pos 下标处的字符,并返回
* @return
* @throws IOException
*/
public char peek() throws IOException {
if (pos - 1 >= size) {
return (char) -1;
}
return buffer[Math.max(0, pos - 1)];
}
/**
* 返回 pos 下标处的字符,并将 pos + 1,最后返回字符
* @return
* @throws IOException
*/
public char next() throws IOException {
if (!hasMore()) {
return (char) -1;
}
return buffer[pos++];
}
public void back() {
pos = Math.max(0, --pos);
}
public boolean hasMore() throws IOException {
if (pos < size) {
return true;
}
fillBuffer();
return pos < size;
}
void fillBuffer() throws IOException {
int n = reader.read(buffer);
if (n == -1) {
return;
}
pos = 0;
size = n;
}
}
public class Tokenizer {
private CharReader charReader;
private TokenList tokens;
public TokenList tokenize(CharReader charReader) throws IOException {
this.charReader = charReader;
tokens = new TokenList();
tokenize();
return tokens;
}
private void tokenize() throws IOException {
// 使用do-while处理空文件
Token token;
do {
token = start();
tokens.add(token);
} while (token.getTokenType() != TokenType.END_DOCUMENT);
}
private Token start() throws IOException {
char ch;
for(;;) {
if (!charReader.hasMore()) {
return new Token(TokenType.END_DOCUMENT, null);
}
ch = charReader.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");
}
private Token readNull() {...}
private Token readBoolean() {...}
private Token readString() {...}
private Token readNumber() {...}
}
第一个字符是 {
、}
、[
、]
、,
、:
,直接封装成相应的 Token 返回即可第一个字符是 n
,期望这个词是null
,Token 类型是NULL
第一个字符是 t
或f
,期望这个词是true
或者false
,Token 类型是BOOLEAN
第一个字符是 "
,期望这个词是字符串,Token 类型为String
第一个字符是 0~9
或-
,期望这个词是数字,类型为NUMBER
private Token readNull() throws IOException {
if (!(charReader.next() == 'u' && charReader.next() == 'l' && charReader.next() == 'l')) {
throw new JsonParseException("Invalid json string");
}
return new Token(TokenType.NULL, "null");
}
private Token readString() throws IOException {
StringBuilder sb = new StringBuilder();
for (;;) {
char ch = charReader.next();
// 处理转义字符
if (ch == '\\') {
if (!isEscape()) {
throw new JsonParseException("Invalid escape character");
}
sb.append('\\');
ch = charReader.peek();
sb.append(ch);
// 处理 Unicode 编码,形如 \u4e2d。且只支持 \u0000 ~ \uFFFF 范围内的编码
if (ch == 'u') {
for (int i = 0; i < 4; i++) {
ch = charReader.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);
}
}
}
private boolean isEscape() throws IOException {
char ch = charReader.next();
return (ch == '"' || ch == '\\' || ch == 'u' || ch == 'r'
|| ch == 'n' || ch == 'b' || ch == 't' || ch == 'f');
}
private boolean isHex(char ch) {
return ((ch >= '0' && ch <= '9') || ('a' <= ch && ch <= 'f')
|| ('A' <= ch && ch <= 'F'));
}
\"
\
\b
\f
\n
\r
\t
\u four-hex-digits
\/
\/
代码中未做处理,其他字符均做了判断,判断逻辑在 isEscape 方法中。在传入 JSON 字符串中,仅允许字符串包含上面所列的转义字符。如果乱传转义字符,解析时会报错。"
,也终于"
。所以在解析的过程中,当再次遇到字符"
,readString 方法会认为本次的字符串解析过程结束,并返回相应类型的 Token。2.2 语法分析
object = {} | { members }
members = pair | pair , members
pair = string : value
array = [] | [ elements ]
elements = value | value , elements
value = string | number | object | array | true | false | null
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);
}
public List<Map.Entry<String, Object>> getAllKeyValue() {
return new ArrayList<>(map.entrySet());
}
public JsonObject getJsonObject(String key) {
if (!map.containsKey(key)) {
throw new IllegalArgumentException("Invalid key");
}
Object obj = map.get(key);
if (!(obj instanceof JsonObject)) {
throw new JsonTypeException("Type of value is not JsonObject");
}
return (JsonObject) obj;
}
public JsonArray getJsonArray(String key) {
if (!map.containsKey(key)) {
throw new IllegalArgumentException("Invalid key");
}
Object obj = map.get(key);
if (!(obj instanceof JsonArray)) {
throw new JsonTypeException("Type of value is not JsonArray");
}
return (JsonArray) obj;
}
@Override
public String toString() {
return BeautifyJsonUtils.beautify(this);
}
}
public class JsonArray implements Iterable {
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();
}
public JsonObject getJsonObject(int index) {
Object obj = list.get(index);
if (!(obj instanceof JsonObject)) {
throw new JsonTypeException("Type of value is not JsonObject");
}
return (JsonObject) obj;
}
public JsonArray getJsonArray(int index) {
Object obj = list.get(index);
if (!(obj instanceof JsonArray)) {
throw new JsonTypeException("Type of value is not JsonArray");
}
return (JsonArray) obj;
}
@Override
public String toString() {
return BeautifyJsonUtils.beautify(this);
}
public Iterator iterator() {
return list.iterator();
}
}
private JsonObject parseJsonObject() {
JsonObject jsonObject = new JsonObject();
int expectToken = STRING_TOKEN | END_OBJECT_TOKEN;
String key = null;
Object value = null;
while (tokens.hasMore()) {
Token token = tokens.next();
TokenType tokenType = token.getTokenType();
String tokenValue = token.getValue();
switch (tokenType) {
case BEGIN_OBJECT:
checkExpectToken(tokenType, expectToken);
jsonObject.put(key, parseJsonObject()); // 递归解析 json object
expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN;
break;
case END_OBJECT:
checkExpectToken(tokenType, expectToken);
return jsonObject;
case BEGIN_ARRAY: // 解析 json array
checkExpectToken(tokenType, expectToken);
jsonObject.put(key, parseJsonArray());
expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN;
break;
case NULL:
checkExpectToken(tokenType, expectToken);
jsonObject.put(key, null);
expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN;
break;
case NUMBER:
checkExpectToken(tokenType, expectToken);
if (tokenValue.contains(".") || tokenValue.contains("e") || tokenValue.contains("E")) {
jsonObject.put(key, Double.valueOf(tokenValue));
} else {
Long num = Long.valueOf(tokenValue);
if (num > Integer.MAX_VALUE || num < Integer.MIN_VALUE) {
jsonObject.put(key, num);
} else {
jsonObject.put(key, num.intValue());
}
}
expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN;
break;
case BOOLEAN:
checkExpectToken(tokenType, expectToken);
jsonObject.put(key, Boolean.valueOf(token.getValue()));
expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN;
break;
case STRING:
checkExpectToken(tokenType, expectToken);
Token preToken = tokens.peekPrevious();
/*
* 在 JSON 中,字符串既可以作为键,也可作为值。
* 作为键时,只期待下一个 Token 类型为 SEP_COLON。
* 作为值时,期待下一个 Token 类型为 SEP_COMMA 或 END_OBJECT
*/
if (preToken.getTokenType() == TokenType.SEP_COLON) {
value = token.getValue();
jsonObject.put(key, value);
expectToken = SEP_COMMA_TOKEN | END_OBJECT_TOKEN;
} else {
key = token.getValue();
expectToken = SEP_COLON_TOKEN;
}
break;
case SEP_COLON:
checkExpectToken(tokenType, expectToken);
expectToken = NULL_TOKEN | NUMBER_TOKEN | BOOLEAN_TOKEN | STRING_TOKEN
| BEGIN_OBJECT_TOKEN | BEGIN_ARRAY_TOKEN;
break;
case SEP_COMMA:
checkExpectToken(tokenType, expectToken);
expectToken = STRING_TOKEN;
break;
case END_DOCUMENT:
checkExpectToken(tokenType, expectToken);
return jsonObject;
default:
throw new JsonParseException("Unexpected Token.");
}
}
throw new JsonParseException("Parse error, invalid Token.");
}
private void checkExpectToken(TokenType tokenType, int expectToken) {
if ((tokenType.getTokenCode() & expectToken) == 0) {
throw new JsonParseException("Parse error, invalid Token.");
}
}
读取一个 Token,检查这个 Token 是否是其所期望的类型 如果是,更新期望的 Token 类型。否则,抛出异常,并退出 重复步骤1和2,直至所有的 Token 都解析完,或出现异常
{、 id、 :、 1、 }
{
Token 后,接下来它将期待 STRING 类型的 Token 或者 END_OBJECT 类型的 Token 出现。于是 parseJsonObject 读取了一个新的 Token,发现这个 Token 的类型是 STRING 类型,满足期望。:
。如此循环下去,直至 Token 序列解析结束或者抛出异常退出。:
,那么此处的字符串只能作为值了。否则,则只能做为键。[Integer.MIN_VALUE, Integer.MAX_VALUE]
范围内的整数来说,解析成 Integer 更为合适,所以解析的过程中也需要注意一下。3. 测试及效果展示
4. 写作最后
更多精彩
在公众号后台对话框输入以下关键词
查看更多优质内容!
女朋友 | 大数据 | 运维 | 书单 | 算法
大数据 | JavaScript | Python | 黑客
AI | 人工智能 | 5G | 区块链
机器学习 | 数学 | 送书
●