介绍
前面两节我们介绍了分词和分词清洗,经过清洗后的token列表已经可以用来生产语法树,本节我们将实现语法树的生成,语法树的目的就是将数据结构化,什么叫结构化,比如字符串表达式(1+(2*3))
要写个程序计算这个结果还是有点难的,因为这个字符串只是一个字符串,我们需要去解析,那么这个字符串就不是一个结构化的数据,如果给你的是解析后的数据,那么就很好处理,解析后的数据就是结构化的数据,你可能会说那不就是分词吗,其实不是的,也是以上面那个表达式为例,分词都是平铺结构,没有层级关系,但表达式有优先级,嵌套等层级关系,这个就是语法树要做的事。
实现
难点
生成语法树的难点在于,token数组是平铺结构,但语法树是树形结构,有层级关系,我们需要将将平铺结构变成树形结构,当token数组中包含object和array时语法树就会有新的分支,因此重点就是要处理好object和array,这两个处理方式本质上是一致的,可以简化为找括号问题,什么意思呢,比如一个表达式(1+(2+(3+4)+5)+6)
如何找到成对的括号,一种简单的方法是如果碰到(
就进入递归,直到碰到)
退出,这样最后一次递归会先匹配第一个)
和我们想要的结果相符,有点类似栈,后进先出,就以(1+(2+(3+4)+5)+6)
为例,总共17个字符,括号的匹配情况应该是
0匹配16
3匹配13
6匹配10
代码实现
public static void test(String text) {
int begin = 0;
int end = 0;
while (index < text.length()) {
int c = text.charAt(index);
if (c == '(') {
begin = index;
char cc = text.charAt(++index);
while (cc != ')') {
test(text);
cc = text.charAt(index);
}
end = index;
System.out.println(String.format("%d匹配%d", begin, end));
} else if (c == ')') {
break;
}
++index;
}
}
输出
6匹配10
3匹配13
0匹配16
因为是最里面括号开始匹配所以先输出6匹配10,结果是符合预期的,理解了这个就很容易理解语法树的生成过程
实现
首先定义抽象语法树数据结构,如下
public class Ast {
//节点类型
private String type;
//如果是object则是字段列表
//如果是array则是值列表
private List items;
//字段名(可为空)
private String name;
//字段值
private Object value;
....
}
语法树构建步骤如下
- 如果是object和array,那么就按照递归的方式进行遍历
- 如果是key,那么根据value的类型进行处理,如果是object和array,那么按照递归的方式进行遍历,如果是基本类型,直接处理
- 如果是value,直接处理
代码如下:
/**
* 生成抽象语法树
*
* @return
*/
public List generateAST() {
List items = new ArrayList<>();
//currentIndex作为当前读取token的位置
//取出对象则currentIndex需要往前移动
Token token = jsonTokens.get(currentIndex);
if ("object".equals(token.getType()) && "{".equals(token.getValue())) {
Ast item = new Ast();
item.setType(token.getType());
//如果json语法错误currentIndex有可能超过范围,这里先不考虑语法错误问题
Token tt = jsonTokens.get(++currentIndex);
while (!"object".equals(tt.getType()) || ("object".equals(tt.getType()) && !"}".equals(tt.getValue()))) {
item.getItems().addAll(generateAST());
tt = jsonTokens.get(currentIndex);
}
items.add(item);
++currentIndex;
} else if ("array".equals(token.getType()) && "[".equals(token.getValue())) {
Ast item = new Ast();
item.setType(token.getType());
Token tt = jsonTokens.get(++currentIndex);
while (!"array".equals(tt.getType()) || ("array".equals(tt.getType()) && !"]".equals(tt.getValue()))) {
item.getItems().addAll(generateAST());
tt = jsonTokens.get(currentIndex);
}
items.add(item);
++currentIndex;
} else if ("key".equals(token.getType())) {
// key和value是成对出现,如果只有key没有value说明json语法错误,这里暂时不考虑语法错误
Token value = jsonTokens.get(++currentIndex);
if ("object".equals(value.getType()) || "array".equals(value.getType())) {
//对象和数组需要递归获取语法树
List tts = generateAST();
//如果是key-value结构必须设置key的名称
tts.get(0).setName((String) token.getValue());
return tts;
} else if ("value".equals(value.getType())) {
Ast item = new Ast();
item.setValue(value.getValue());
item.setType(value.getType());
item.setName((String) token.getValue());
items.add(item);
++currentIndex;
}
} else if ("value".equals(token.getType())) {
//只有value没有key的情况,比如["1",2,true,null]
Ast item = new Ast();
item.setValue(token.getValue());
item.setType(token.getType());
items.add(item);
++currentIndex;
}
return items;
}
代码看着挺长,但就是做上面说的三件事,里面的逻辑并不复杂
测试
语法树结构不容易打印出来,我们用fastjson转化为json进行打印
public class Main {
public static void main(String[] args) throws Exception {
InputStream fin = Main.class.getResourceAsStream("/example.json");
byte[] buf = new byte[fin.available()];
fin.read(buf);
fin.close();
String json = new String(buf, "utf-8");
JSONParser parser = new JSONParser();
List tokens = parser.tokenizer(json);
tokens = parser.tokenClean(tokens);
List astItems = parser.generateAST();
Ast ast = astItems.get(0);
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("-------------------------------------------");
System.out.println(JSON.toJSONString(ast, true));
}
}
结果:
{
"items":[
{
"name":"name",
"type":"value",
"value":"asan"
},
{
"name":"age",
"type":"value",
"value":32
},
{
"name":"married",
"type":"value",
"value":true
},
{
"name":"birthday",
"type":"value",
"value":"1992-02-08"
},
{
"name":"salary",
"type":"value",
"value":1234.56
},
{
"name":"description",
"type":"value",
"value":"a \"hudsom\" man"
},
{
"items":[
{
"type":"value",
"value":"coder"
},
{
"type":"value",
"value":"mid-age"
}
],
"name":"tags",
"type":"array"
},
{
"items":[
{
"name":"province",
"type":"value",
"value":"福建省"
},
{
"name":"city",
"type":"value",
"value":"泉州市"
},
{
"name":"area",
"type":"value",
"value":"晋江市"
}
],
"name":"location",
"type":"object"
},
{
"items":[
{
"items":[
{
"name":"relation",
"type":"value",
"value":"couple"
},
{
"name":"name",
"type":"value",
"value":"Helen"
}
],
"type":"object"
},
{
"items":[
{
"name":"relation",
"type":"value",
"value":"daughter"
},
{
"name":"name",
"type":"value",
"value":"XiaoMan"
}
],
"type":"object"
}
],
"name":"family",
"type":"array"
}
],
"type":"object"
}
这样我们成功构建了一棵语法树
代码
完整代码请参考项目https://github.com/wls1036/tiny-json-parser的0x03分支