一个Json设计和实现分析库
- 一个Json设计和实现分析库
-
- 设计思路
- 实现方法
-
- 1. 预处理(去除注释)
- 2. 词法分析
- 3. 语法分析
- 4. 树型优化
- 5. Json树构建
- 6. 后端处理
- 整体架构
一个Json设计和实现分析库
设计思路
当前很多Json库基于状态机的思想Json所有元素都在同一状态机中,Json文件的内容逐字流入状态机,促进状态流通。在这台大状态机中,每台机器Json元素设计为子状态机。具体表现为:为每一个Json元素在Json设计入口函数的文件。从这个入口函数开始,调用每个入口函数Json每个元素的分析函数Json根据状态流转调用其他元素的分析函数Json元素分析函数。
将Json所有元素都放在同一状态机器中,优点是直观,细化到每个字符可以找到不同的状态流通。Json元素不多,格式简单,状态机的设计不会很复杂。根据分割的子状态机是每个子状态机Json单独实现元素分析函数,允许相互递归调用减少代码量。
但这种实现是对的Json定制很复杂。对于某些场景,需要定制特殊语法Json文件(此时Json严格意义上的文件不再严格Json),例如,添加注释、数值、引用等。随着定制元素的增加,状态机的设计变得越来越复杂。由于Json通过对类似编译的分析,可以实现从字符串到编译的理念Json树、Json编译前端和编译后端的编译前端和定制元素相对直观,易于修改。
以下是编译理念设计的Json库流程图。 前端部分:
后端部分:
将字符串转换为前端Json树型结构,后端部分Json树型结构转换为字符串。
实现方法
每个子模块分别实现特定功能,然后将结果传输到下一个模块。
1. 预处理(去除注释)
这一步类似于C编译器的预处理器。此步骤删除注释(可选转换字符\n \t \r \u …字符串表示转换为UTF8编码,删除部分空间等。).使用状态机实现去除注释的过程,并依次将每个字符输入状态机。当状态为PCOMMENT、LINE_COMMENT、LEAVE_COMMENT跳过字符:
预处理后,文件仍以字符串的形式流入字法分析器
2. 词法分析
Json数据结构不是很复杂,大致如下表所示:
元素名称 | 形式 | 说明 |
---|---|---|
字符串 | “xxxx” | |
数字 | -0.1E 8 | |
对象 | {xxx: xxx} | 无序的 名字:值 对集合 |
数组 | [xxx, xxx] | |
布尔 | true, false | |
Null |
为了方便描述,以下是Node指名字:值对Field指名称,Value指值Json格式描述可见,Json文件中的基本词法单元可定义为:(见仁见智)
词法单元 | 形式 | 说明 |
---|---|---|
字符串 | “xxxx” | |
数字 | -0.1E 8 | |
域Field | xxx: | 只存在于对象中 |
布尔 | true, false | |
对象入口 | { | |
对象出口 | } | |
数组入口 | [ | |
数组出口 | ] | |
分隔符 | , | |
Null |
每个词法单元分别实现其判定函数,每个判定函数的输入为json文件字符串的输出范围[start, end], 表示词法单元在整个Json文件的开始和结束offset。文件的第一个字符json[0]开始,将json字符串输入到每个单词单元的解析函数中,只有一个解析函数可以成功返回单词单元的范围[start, end],然后跳过这个词法单元json end 1为起始将json将字符串输入到每个单元的分析函数中,重复上述过程,直至json字符串末尾或所有分析函数都失败了。若所有分析函数都失败,则意味着词法错误,停止后续过程。
下面是string本法单元的判定过程:
参见数值判定过程Json标准委员会ECMA-404_2nd文档流程图为以下状态流通:
布尔等词法单元的判定过程此处不再赘述。 词法分析完成后,生成了一个以{type, [start, end]}为单元的数组,每个数组元素均代表一个词法单元,标识出词法类型和在全文中的范围。该数组送入语法分析模块内。
3. 语法分析
词法分析生成的数组为一维形式,仅标识了单词。单词和单词间的合法性和关系由语法分析去判定和构建。本程序拟采用LR分析法,由于Json元素关系相对简单,文法的表示不长:
定义x为对象,N为Node列表,n为Node,0为Null,f为Field,y为Value,s为字符串,z为数组,num为数组,bool为布尔,NY为数组成员列表; x->{N} N->N,n N->n N->0 n->fy y->x y->z y->s y->num y->bool z->[NY] NY->NY,y NY->y NY->0
该文法需要根据LR分析过程做局部调整避免二义性。将空数组和空对象直接表示为: x->{} z->[] 同时删除N->0和NY->0两个文法。
修改后的文法如下, 其中V为起始符(根),$为终结符。
文法编号 | 文法 |
---|---|
0 | V->x$ |
1 | x->{N} |
2 | x->{ } |
3 | N->N,n |
4 | N->n |
5 | n->fy |
6 | y->x |
7 | y->z |
8 | y->s |
9 | y->num |
10 | y->bool |
11 | z->[NY] |
12 | z->[ ] |
13 | NY->NY,y |
14 | NY->y |
根据文法确定闭包和闭包内的文法,绘制闭包跳转图(LR(1)状态图).最后依据LR状态图得到LR分析表:
状态 | { | } | ] | num | , | f | s | [ | bool | $ | NY | z | y | n | N | x |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | s2 | g1 | ||||||||||||||
1 | END | |||||||||||||||
2 | s4 | s6 | g5 | g3 | ||||||||||||
3 | s7 | s8 | ||||||||||||||
4 | r2 | |||||||||||||||
5 | r4 | r4 | ||||||||||||||
6 | s15 | s13 | s12 | s16 | s14 | g11 | g9 | g10 | ||||||||
7 | r1 | |||||||||||||||
8 | s6 | g17 | ||||||||||||||
9 | r5 | r5 | ||||||||||||||
10 | r6 | r6 | ||||||||||||||
11 | r7 | r7 | ||||||||||||||
12 | r8 | r8 | ||||||||||||||
13 | r9 | r9 | ||||||||||||||
14 | r10 | r10 | ||||||||||||||
15 | s19 | s6 | g5 | g18 | ||||||||||||
16 | s28 | s21 | s26 | s25 | s29 | s27 | g20 | g24 | g22 | g23 | ||||||
17 | r3 | r3 | ||||||||||||||
18 | s30 | s8 | ||||||||||||||
19 | r2 | r2 | ||||||||||||||
20 | s31 | s32 | ||||||||||||||
21 | r12 | r12 | ||||||||||||||
22 | r14 | r14 | ||||||||||||||
23 | r6 | r6 | ||||||||||||||
24 | r7 | r7 | ||||||||||||||
25 | r8 | r8 | ||||||||||||||
26 | r9 | r9 | ||||||||||||||
27 | r10 | r10 | ||||||||||||||
28 | s34 | s6 | g5 | g18 | ||||||||||||
29 | s28 | s36 | s26 | s25 | s29 | s27 | g35 | g24 | g22 | g23 | ||||||
30 | r1 | r1 | ||||||||||||||
31 | r11 | r11 | ||||||||||||||
32 | s28 | s26 | s25 | s29 | s27 | g24 | g37 | g23 | ||||||||
33 | s38 | |||||||||||||||
34 | r2 | r2 | ||||||||||||||
35 | s39 | s32 | ||||||||||||||
36 | r12 | r12 | ||||||||||||||
37 | r13 | r13 | ||||||||||||||
38 | r1 | r1 | ||||||||||||||
39 | r11 | r11 |
准备状态栈,起始状态0入栈,依次输入词法单元,每输入一个词法单元,则对应坐标为 [栈顶状态, 词法单元] 的单元格,这些单元格存在四种表示:
- 出现sN标识:接受该输入,跳转到N状态,即状态N入栈;每个sN对应入栈一个状态,栈中的每个状态对应一个词法单元。
- 出现rX标识:接受该输入,并按照文法X归约;归约出栈若干状态,同时归约结果为一个非终结符Y(Y->xxx),归约后以Y作为输入, 查找单元格 [归约后栈顶的状态, Y] 。
- 出现gU标识:表示切换到状态U;此时接受非终结符Y,入栈状态U。
- 空:表示语法错误。
生成文法列表后,依据LR分析过程用栈回溯方式依次匹配各文法,归约生成树型结构,直至访问END单元格为止,此时栈顶为状态1,对应x,且匹配文法0:V->x.$。
4. 树型优化
语法分析生成的二叉语法树,存在大量与Json内容无关的节点,这些节点仅用于表示Json元素的关系,并且受限于二叉树结构,存在大量表示层级关系的节点。需要进一步删除冗余节点,将二叉树变为树。树型优化的工作是找到一个对象、数组的所有成员节点(存在Json元素含义的),将这些成员节点直接连入对象、数组下面,删除对象、数组下面表示元素关系、层级关系(不含Json元素含义的)的节点。这种优化过程如下图所示:
存在Json元素含义的节点种类为:
- Node 文法中表示为n
- 字符串 文法中表示为s
- 数值 文法中表示为num
- 布尔 文法中表示为bool
- Field 文法中表示为f
- 对象Object 文法中表示为s
- 数组Array 文法中表示为z
该优化的具体实现过程如下,采用先序遍历二叉语法树。从根节点开始,依次访问各子节点,如果找到存在Json元素含义的节点,如Node,则停止Node的向下遍历,将Node接入根节点中,再从Node的兄弟节点开始继续向下遍历,重复上述过程。当根节点遍历完成后,根节点下的所有存在Json元素含义的节点均直接连入根节点下面。再从根节点的子节点出发(此时的子节点全部为存在Json元素含义的节点),对于每个对象Object和数组Array类型的节点,将这些节点作为子树的根,重复上述过程,依次完成所有对象Object、数组Array的子节点的连入。重复上述过程直至整个语法树的每个节点全部访问完为止。
5. Json树构建
当树型优化完成后,该树型即为用户所需的Json树型,但这个树中的节点没有保存数值,仅给出了{type,[start,end]} 词法类型和单词范围。一般情况下,语法树树节点的数据类型也不是用户所需的Json对象类型。Json树构建需要完成两个工作:
- 从语法树节点生成Json对象,即类型转换,此时维持树型不变;
- 对于每个Json对象,依据{type,[start,end]}将单词字符串解析为数值。
第一条相对简单,仅依据节点的Node、字符串、数值、布尔、Field等类型依次创建Json对象类型,每创建一个Json对象,替换语法树中相应的节点即可。
第二条仍需要借助状态机,将字符串转换为数值。Json中存在的转换有如下情况:
转换 | 说明 |
---|---|
字符串->数值 | 整型、浮点型、精度 |
字符串->布尔 | |
字符串->字符串 | 转义字符处理 |
这些从字符串到数值的处理过程在ECMA-404_2nd文档中都有详细的状态机模型,参见其实现即可。由于词法分析已经判定了字符串的合法性,此处的解析省略合法性判定,认为其一定是合法的。
Json树构建完成后,从字符串到Json对象的解析过程就全部完成了。该过程仅为Json库的核心部分。实际使用中,用户总不能直接对树进行遍历查找元素吧。需要设计一套用户接口,实现如create、add、iterator、delete、find等接口,在接口内访问Json树并进行边界检查。这些接口此处不再赘述。
6. 后端处理
对于一个给定的Json树,将其转换为字符串的过程相对简单,思路是按照先序方式遍历Json树,第一次进入Object节点则输出’{‘字符,离开Object节点则输出’}‘,遍历子节点的依次将数值转换为字符串输出,每个子节点间输出’,'字符。Array节点与之类似。
在将字符串写入文件中时,需要注意转义字符的问题,对于字符串中的换行\n \r字符,需要输出两个字符"\\n" “\\r”,防止直接输出\n \r字符到文件导致Json格式错乱。
在将数值输出至文件时,要分别处理整型和浮点型,浮点型兼顾精度和科学计数法等表现形式。
整体架构
Json库的整体结构如下图:
该图不包含用户接口,即核心组件分为三部分:
- 读组件,完成前端处理,从字符串到Json树;
- 写组件,完成后端处理,从Json树到字符串;
- 转换组件,完成字符串转义字符处理,完成数值转换和精度处理。
具体实现参见: gitee AJson工程
采用C++实现,代码量在1600行,模块化设计便于阅读。
参考文献 [1]. ECMA-404_2nd_edition_december_2017Json.pdf [2]. 知乎-从零开始的 JSON 库教程 https://zhuanlan.zhihu.com/json-tutorial [3]. 现代编译原理-C语言描述(修订版)