背景
作为一个程序员,心里一直有一个手撸编译器的梦,奈何技术不够一直没能付诸实践,JSON虽然不是一门语言,但很适合用来作为编译器的练手,原因在于
- 关键字较少,结构简单
- 语法简单,没有判断,循环等高级语言语法
- 文本格式,测试比较方便
虽然写代码硬解析也能做到,但总归不科学,对于复杂的语法,硬解析根本无法解决。从阮一峰的博客了解到the-super-tiny-compiler这个项目,该项目是一个迷你编译器,将lisp表达式转化为c语言表达时,代码去掉注释不到200行,很适合用来学习,这个项目给了我很多启发,开始对写一个json解析器有了一点思路,该系列博文将记录一个完成json解析器的实现过程,当然我自己也是小白,不是什么编译器专家,只是希望给同样是小白的你一点参考,大神可以绕道。
说明
如何把一个json字符串解析成一个java对象,大概要分为一下步骤
- 分词(tokenizer)将json字符串分解成一个个独立的单元,比如下面这个简单的json字符串
{
"name": "asan",
"age": 32
}
经过分词后会分解成下面这种格式
[
{"type":"object","value":"{","valueType":"object"},{"type":"key","value":"name","valueType":"string"},{"type":"value","value":"asan","valueType":"string"},{"type":"key","value":"age","valueType":"string"},{"type":"value","value":32,"valueType":"number"},{"type":"object","value":"}","valueType":"object"}
]
这期间会将无用字符过滤,分解为一个个token
- 解析抽象语法树(AST):tokenizer只是将字符串分解平铺,AST负责将平铺的各个token按照语义变成一棵树,带有层级结构,比如上面的token解析抽象语法树如下
{
"items": [
{
"name": "name",
"type": "value",
"value": "asan"
},
{
"name": "age",
"type": "value",
"value": 32
}
],
"type": "object"
}
- 对象生成,根据抽象语法树生成对象
无论是tokenizer还是ast,格式都不是固定,上面只是一个参考,但作用都是类似的,基本上解析器都要经过tokenizer和ast两个步骤。
分词(tokenizer)
示例json
如无特殊说明,后续程序都是基于以下这个json进行测试
{
"name": "asan",
"age": 32,
"mail": null,
"married": true,
"birthday": "1992-02-08",
"salary": 1234.56,
"deposit": -342.34,
"description": "a \"hundsome\" man",
"tags": [
"coder",
"mid-age"
],
"location": {
"province": "福建省",
"city": "泉州市",
"area": "晋江市"
},
"family": [
{
"relation": "couple",
"name": "Helen"
},
{
"relation": "daughter",
"name": "XiaoMan"
}
]
}
该示例基本包含了json所有常用的元素,可以满足基本的测试
- 基本数据类型:字符串,整型,浮点型,日期,null,布尔值
- 对象(location)
- 数组(family)
- 基本类型数组(tags)
分词
我们首先定义一个保存分词结果的结构,该结构至少需要包含以下两个字段
- type:token类型,包含object(对象),array(数组),key(字段名),value(字段值),kvSymbol(key-value之间的冒号:)
- value:token值
但一个type可能不足以描述json,比如json的value有字符串,整型,浮点型等,但type都是value
,你可能会说为什么不每个定义一个类型呢,如果每个定义一个类型,那么到时候判断该token是不是value类型的时候就比较麻烦,需要依次判断是不是字符串,整型,浮点型等,因此我们增加了一个字段valueType用来存储值类型
- type:token类型
- value:token值
- valueType:值类型(string,bool,null,number)
我们暂时不去定义枚举,先把解析器实现再去重构代码,暂时不考虑代码的合理性。
以下是第一版本的解析器
import java.util.ArrayList;
import java.util.List;
/**
* @Description:
* @author: jianfeng.zheng
* @since: 2022/12/20 11:56
* @history: 1.2022/12/20 created by jianfeng.zheng
*/
public class JSONParser {
//currentIndex保存当前字符串扫描的位置,字符串是逐字符进行扫描
private int currentIndex = 0;
/**
* 对json字符串进行分词
*
* @param json json字符串
* @return token列表
*/
public List tokenizer(String json) {
// 保存分词结果
List tokens = new ArrayList<>();
while (currentIndex < json.length()) {
char c = json.charAt(currentIndex);
//对于空白符可以直接跳过,如果有更多的空白符只需添加新的判断即可
if (c == ' ' || c == '\r' || c == '\n' || c == ',') {
//字符只要处理过了必须要将当前位置移动到下一个
++currentIndex;
continue;
} else if (c == '{' || c == '}') {
//对象
tokens.add(new Token("object", c));
} else if (c == '[' || c == ']') {
//数组
tokens.add(new Token("array", c));
} else if (c == '"') {
//字符串
StringBuffer value = new StringBuffer();
char cc = json.charAt(++currentIndex);
// 这里以"作为字符串结束符的标志
// 当然这个不严谨因为没考虑到转义,但这个问题留着后面解决,我们暂时忽略
while (cc != '"') {
value.append(cc);
cc = json.charAt(++currentIndex);
}
tokens.add(new Token("string", value.toString()));
} else if (c == ':') {
// key-value中间的分隔符
tokens.add(new Token("kvSymbol", "kvSymbol", c));
} else if (c >= '0' && c <= '9') {
//数字
StringBuffer value = new StringBuffer();
value.append(c);
char cc = json.charAt(++currentIndex);
//这里考虑到带有小数点的浮点数
while (cc == '.' || (cc >= '0' && cc <= '9')) {
value.append(cc);
cc = json.charAt(++currentIndex);
}
//数字暂时统一用浮点数进行表示
tokens.add(new Token("value", "number", Float.parseFloat(value.toString())));
}
++currentIndex;
}
return tokens;
}
}
代码流程如下
- 循环遍历json字符串
- 检测关键字,并识别出关键字以token保存
- 对于字符串的处理目前不论是key还是value统一保存的是string类型
- 对于数字类型的处理目前都是以Float浮点数进行保存
测试
我们写一个程序程序进行测试
public class Main {
public static void main(String[] args) {
String json = "{\"name\": \"asan\", \"age\": 32}";
JSONParser parser = new JSONParser();
List tokens = parser.tokenizer(json);
System.out.println(String.format("|%-12s|%-12s|%-15s|", "type", "valueType", "value"));
System.out.println("-------------------------------------------");
for (Token t : tokens) {
System.out.println(String.format("|%-12s|%-12s|%-15s|",
t.getType(),
t.getValueType(),
t.getValue()));
}
System.out.println("-------------------------------------------");
}
}
我们先拿一个比较简单的json{"name": "asan", "age": 32}
进行测试,测试结果如下
|type |valueType |value |
-------------------------------------------
|object |object |{ |
|string |string |name |
|kvSymbol |kvSymbol |: |
|string |string |asan |
|string |string |age |
|kvSymbol |kvSymbol |: |
|value |number |32.0 |
-------------------------------------------
目前这个结果符合我们的预期。
优化
其他基本类型
目前程序value类型程序只处理了字符串和数字,bool和null类型未处理,由于程序是一个字符一个字符对字符串进行扫描,但要判断bool和null必须往后进行扫描。
- 判断null
if ((c == 'n') &&
json.startsWith("null", currentIndex)) {
tokens.add(new Token("value", "null", null));
//如果读取到null值需要将当前指针往前移动3个字符(null占4个字符,除去已经读取到的1个字符串还需要移动3个字符)
currentIndex += 3;
}
- 判断bool值
bool值的判断就是判断true和false两个字符串,和判断空值类似
if ((c == 't') &&
json.startsWith("true", currentIndex)) {
tokens.add(new Token("value", "bool", true));
currentIndex += 3;
}
if ((c == 'f') &&
json.startsWith("false", currentIndex)) {
tokens.add(new Token("value", "bool", false));
//false是5个字符因此需要移动4位
currentIndex += 4;
}
我们将测试的json字符串修改为{"name": "asan", "age": 32,"mail": null,"married": true}
再进行测试,结果如下
|type |valueType |value |
-------------------------------------------
|object |object |{ |
|string |string |name |
|kvSymbol |kvSymbol |: |
|string |string |asan |
|string |string |age |
|kvSymbol |kvSymbol |: |
|value |number |32.0 |
|string |string |mail |
|kvSymbol |kvSymbol |: |
|value |null |null |
|string |string |married |
|kvSymbol |kvSymbol |: |
|value |bool |true |
|object |object |} |
-------------------------------------------
结果符合预期。
字符串处理
字符串目前的处理方式是检测到"
就当作是字符串,直到下一个"
出现,但这种处理方式是不严谨的,有可能字符串本身就包含了"
,因此需要对转义字符进行处理,我们修改字符串的处理函数
if (c == '"') {
//字符串
StringBuffer value = new StringBuffer();
char cc = json.charAt(++currentIndex);
// 这里以"作为字符串结束符的标志
while (cc != '"') {
if (cc == '\\') {
cc = json.charAt(++currentIndex);
}
value.append(cc);
cc = json.charAt(++currentIndex);
}
tokens.add(new Token("string", value.toString()));
}
我们将测试字符串改为{"name": "asan", "age": 32,"description": "a \"hudsom\" man","married": true}
再测试,结果如下
|type |valueType |value |
-------------------------------------------
|object |object |{ |
|string |string |name |
|kvSymbol |kvSymbol |: |
|string |string |asan |
|string |string |age |
|kvSymbol |kvSymbol |: |
|value |number |32.0 |
|string |string |description |
|kvSymbol |kvSymbol |: |
|string |string |a "hudsom" man |
|string |string |married |
|kvSymbol |kvSymbol |: |
|value |bool |true |
|object |object |} |
-------------------------------------------
成功识别到了转义字符串
数字处理
数字处理目前也有一些问题
- 没有处理负数情况
- 没有处理科学技术法
- 没有区分浮点和整型统一都是浮点数
程序修改如下
if ((c >= '0' && c <= '9') || c == '-') {
// 数字
StringBuffer value = new StringBuffer();
value.append(c);
// 判断是不是浮点数
boolean isFloat = false;
//如果json是一位整型比如:1,那么这里不判断就会报错
if (currentIndex + 1 < json.length()) {
char cc = json.charAt(++currentIndex);
// 判断包含浮点型,整型,科学技术法
while (cc == '.' || (cc >= '0' && cc <= '9') || cc == 'e' || cc == 'E' || cc == '+' || cc == '-') {
value.append(cc);
if (cc == '.') {
isFloat = true;
}
cc = json.charAt(++currentIndex);
}
}
if (isFloat) {
//浮点数
tokens.add(new Token("value", "float", Float.parseFloat(value.toString())));
} else {
//整型
tokens.add(new Token("value", "long", Long.parseLong(value.toString())));
}
}
我们用字符串{"age":32,"deposit": -342.34}
进行测试,测试结果如下
|type |valueType |value |
-------------------------------------------
|object |object |{ |
|string |string |age |
|kvSymbol |kvSymbol |: |
|value |long |32 |
|string |string |deposit |
|kvSymbol |kvSymbol |: |
|value |float |-342.34 |
-------------------------------------------
完整测试
我们用完整的字符串进行测试,结果如下
|type |valueType |value |
-------------------------------------------
|object |object |{ |
|string |string |name |
|kvSymbol |kvSymbol |: |
|string |string |asan |
|string |string |age |
|kvSymbol |kvSymbol |: |
|value |long |32 |
|string |string |married |
|kvSymbol |kvSymbol |: |
|value |bool |true |
|string |string |birthday |
|kvSymbol |kvSymbol |: |
|string |string |1992-02-08 |
|string |string |salary |
|kvSymbol |kvSymbol |: |
|value |float |1234.56 |
|string |string |description |
|kvSymbol |kvSymbol |: |
|string |string |a "hudsom" man |
|string |string |tags |
|kvSymbol |kvSymbol |: |
|array |array |[ |
|string |string |coder |
|string |string |mid-age |
|array |array |] |
|string |string |location |
|kvSymbol |kvSymbol |: |
|object |object |{ |
|string |string |province |
|kvSymbol |kvSymbol |: |
|string |string |福建省 |
|string |string |city |
|kvSymbol |kvSymbol |: |
|string |string |泉州市 |
|string |string |area |
|kvSymbol |kvSymbol |: |
|string |string |晋江市 |
|object |object |} |
|string |string |family |
|kvSymbol |kvSymbol |: |
|array |array |[ |
|object |object |{ |
|string |string |relation |
|kvSymbol |kvSymbol |: |
|string |string |couple |
|string |string |name |
|kvSymbol |kvSymbol |: |
|string |string |Helen |
|object |object |} |
|object |object |{ |
|string |string |relation |
|kvSymbol |kvSymbol |: |
|string |string |daughter |
|string |string |name |
|kvSymbol |kvSymbol |: |
|string |string |XiaoMan |
|object |object |} |
|array |array |] |
|object |object |} |
-------------------------------------------
完整代码
public class JSONParser {
//currentIndex保存当前字符串扫描的位置,字符串是逐字符进行扫描
private int currentIndex = 0;
/**
* 对json字符串进行分词
*
* @param json json字符串
* @return token列表
*/
public List tokenizer(String json) {
// 保存分词结果
List tokens = new ArrayList<>();
while (currentIndex < json.length()) {
char c = json.charAt(currentIndex);
//对于空白符可以直接跳过,如果有更多的空白符只需添加新的判断即可
if (c == ' ' || c == '\r' || c == '\n' || c == ',') {
//字符只要处理过了必须要将当前位置移动到下一个
++currentIndex;
continue;
} else if (c == '{' || c == '}') {
//对象
tokens.add(new Token("object", c));
} else if (c == '[' || c == ']') {
//数组
tokens.add(new Token("array", c));
} else if (c == '"') {
//字符串
StringBuffer value = new StringBuffer();
char cc = json.charAt(++currentIndex);
// 这里以"作为字符串结束符的标志
while (cc != '"') {
if (cc == '\\') {
cc = json.charAt(++currentIndex);
}
value.append(cc);
cc = json.charAt(++currentIndex);
}
tokens.add(new Token("string", value.toString()));
} else if (c == ':') {
// key-value中间的分隔符
tokens.add(new Token("kvSymbol", "kvSymbol", c));
} else if ((c >= '0' && c <= '9') || c == '-') {
// 数字
StringBuffer value = new StringBuffer();
value.append(c);
// 判断是不是浮点数
boolean isFloat = false;
//如果json是一位整型比如:1,那么这里不判断就会报错
if (currentIndex + 1 < json.length()) {
char cc = json.charAt(++currentIndex);
// 判断包含浮点型,整型,科学技术法
while (cc == '.' || (cc >= '0' && cc <= '9') || cc == 'e' || cc == 'E' || cc == '+' || cc == '-') {
value.append(cc);
if (cc == '.') {
isFloat = true;
}
cc = json.charAt(++currentIndex);
}
}
if (isFloat) {
//浮点数
tokens.add(new Token("value", "float", Float.parseFloat(value.toString())));
} else {
//整型
tokens.add(new Token("value", "long", Long.parseLong(value.toString())));
}
} else if ((c == 'n') && json.startsWith("null", currentIndex)) {
tokens.add(new Token("value", "null", null));
currentIndex += 3;
} else if ((c == 't') &&
json.startsWith("true", currentIndex)) {
tokens.add(new Token("value", "bool", true));
currentIndex += 3;
} else if ((c == 'f') &&
json.startsWith("false", currentIndex)) {
tokens.add(new Token("value", "bool", false));
//false是5个字符因此需要移动4位
currentIndex += 4;
}
++currentIndex;
}
//将当前位置重置
currentIndex = 0;
return tokens;
}
}
代码
完整代码请参考项目https://github.com/wls1036/tiny-json-parser的0x01分支