我的json parser/generator
我的json parser/generator
json parser是用来解析json的工具,按照规则将符合标准json文本转换成相应的数据结构(类)。
- 使用语言 : C++(11)。
- 项目规模 :实现部分700行左右,测试部分400行左右。
- 耗时 :大约一周。
- 功能简介:
[1] 解析器部分:
能解析NULL,False,True,Number(只支持double),String,array,object六种类型。对于常见的出现错误定义了相应的错误码。
[2] 生成器部分:
能够将解析好的数据再转换成为json格式的文本。能够对现有的数据进行修改,实现了添加,修改两部分的功能。对于删除部分尚未实现(to do)。
[3] 测试部分:
对所有类型的解析/生成均做了相应的测试。
对重要API做了相应测试。
- 部分实现:
由于篇幅和精力所限制,我只选择阐述部分较为重要的实现,具体细节可以看源码。
[1]整体框架
我们创建一个叫做json_value的类来存储不同的类型和值,创建json_tree这个类用来解析相应json文本并保存在json_value里面。
(1)json_value的结构
type+(null,false,true,number,string,array,object)
为了节约空间,我们选择使用一个union将这六种类型的变量全部封装起来,但是由于string,vector,map等类型不是简单类型,所以我们必须自己创建相应的构造函数,拷贝构造函数,赋值运算符,析构函数来保证相应的操作的正确执行。
同时我们不直接使 用vector,map
(2)json_tree执行具体的解析
json_tree留给外部使用的接口是lept_parse(const string&)
这个函数负责最外层的判断以及部分处理错误的功能,我们首先写好lept_parse_whitespace()这个函数用来去掉分割用的空格。然后将剩下的工作交给另一个函数lpet_parse_value:检测遇到的第一个字符,根据
(n,f,t,",[,{,其他(数字或者无效值))
这六种首字母的情况,我们可以很容易的确定需要解析的是哪一种。这里我们分别实现对应功能的函数。
[2] Literal(null,false,true):
literal是指普通的字面值类型,一共三种 null,false,true
由于这三种类型非常相似,所以我选择一个函数lept_parse_literal来解析这三种类型,在这个函数里面,我们只需要判断它是三种类型里面的哪一种,并且指定好相应的类型即可。
此外,还需要处理可能发生的错误,比如nul,fuck这种无效的字符。这一块的难度不大,细心即可顺利的完成。
注意,我们还要处理类似 “null x”这样的不正确输入,并且返回的错误类型应该是NOT_SINGULAR_ROOT(该节点不止一个值)。
[3] Number的解析
也许这是整个项目最困难的部分,但是我选择一定程度的回避^_^
[1]首先我们了解number的格式以及错误输入:
(1)*json的number不支持+xxx*的形式,只支持-xxxx/xxxx,所以首先要处理这一块可能的错误格式。
(2)其次不支持0xxx的格式,只支持0.xxxx,所以0123是错误的输入。
(3)对于小数点后面的位数,至少要有一位数字。所以
1.,0.这样都是不正确的。
(4)支持xxxxE(e)+yyyy,xxxxE(e)-yyyy这样的格式,所以1.234E+12是正确的输入。
(5)还可能出现超过0-9范围之外的字符,这同样是不正确的输入。
[2]解析:
如果手动来序列话浮点数其实解决方案会相当的麻烦,由于只是一个用来练习的项目,所以我选择使用stod这个函数来进行转换。注意,这里有一个坑^[1]:
如果我们选择strtod这个函数会发现它不能很好的进行边界值的判断,比如1E-1000其实是应该解析为0,但是这里会被认为指数太大而越界。而stod这个函数则会对超过浮点数最值的文本都转换为一个HUGE_VAL|-HUGE_VAL,这样我们就能精确的判定是不是范围越界了。然而stod接受的参数是char*,所以这里要来回转换会使得实现起来比较不舒服。
我们首先需要判断前面提到的可能的错误,因为stod函数并不会帮我们进行判断,它只会在第一个不满足格式的字符前停止解析,同时它支持的一些格式json不支持。所以必须要手动判断。
判断完成之后我们需要确认当前的字符,如果是
, [ { 空格 \t \f \r \s \n
我们可以暂时不报错,因为这些可能是分割符号,至于这些符号是否满足正确的格式,交给调用lept_parse_number的外层函数来判断。如果是其他字符,那么可以判定当前的解析数字不成功,返回相应的错误。
最后我们还要处理可能的越界错误。
这一部分的解析相当麻烦,虽然代码量看似不大,但是要考虑的问题较多,一不小心就会出错,我在这个地方花费来一下午才通过所有测试。
[4] string的解析:
这一部分的解析包含两个内容,第一个是普通的字符和转义字符,第二个是unicode的转义字符。
[1]普通\转义 字符的解析:
首先认识到string里面的所有需要转义的字符都应该多加一个\来表示,因为json文本本身是被包含在一对双引号之内的。
对于普通的字符,没有什么好说的,只需要直接翻译就好了,但是注意对于ASCII码在1-31之间的普通字符是非法的。
对于转义字符,我们一共有如下这些可能:
\n \b \f\ \r \t \" \\ \/ \u
在switch语句里面,针对每一种类型,我们都需要翻译成真正的转义字符,这里相当于做了一个简化版本的C语言对字面值的parse工作。由于这些工作的相似度(除了\u)很高,所以我们可以用一个宏来完成这些类似的工作,注意宏后面记得加上break,我在这里浪费了不少时间检查。
[2] unicode字符的解析:
这一部分比上面要困难一些,主要在于需要真正搞清楚unicode和utf-8之间的转换关系,以及可能遇见的错误格式。
[1] 码点和代理对
面对unicode字符,我们需要将它转换成utf-8的格式,unicode采用一个整数码点来映射到字符集。
首先假设输入合法:
面对一个\uxxxx,它表示U+0000到U+FFFF,我们需要将这个十六进制解析成为一个整数码点。同时由于字符串是通过UTF-8来存储的,所以我们也要把这个码点编码成UTF-8.
但是注意到这个十六进制的数是不能表示完所有的码点的。其实Json字符对于超过U+FFFF范围以外的码点,选择采用代码对来表示,如果第一个码点是U+DC00到U+DBFF,那么我们就知道它应该和后面紧跟的另外一个码点共同代表一个码点。具体的转换就不仔细说了。