C++项⽬:Json_parser
我的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的结构error parse new
type+(null,false,true,number,string,array,object)
为了节约空间,我们选择使⽤⼀个union将这六种类型的变量全部封装起来,但是由于string,vector,ma
p等类型不是简单类型,所以我们必须⾃⼰创建相应的构造函数,拷贝构造函数,赋值运算符,析构函数来保证相应的操作的正确执⾏。
同时我们不直接使 ⽤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,那么我们就知道它应该和后⾯紧跟的另外⼀个码点共同代表⼀个码点。具体的转换就不仔细说了。
[2] UTF-8编码
⽽UTF-8是⼀种存储码点的格式,它选择将码点存储为⼀到多个存储单元,其中每⼀个单元是⼀个字节,所以每个ASCII字符只需要⼀个字节去存储。我们的json parser只⽀持UTF-8的格式。
得到来真正的码点(整数),我们就需要把它按照UTF-8的格式来存储,UTF-8把⼆进制的码点拆分为1-4个字节。这个编码⽅式和ASCII 码编码是兼容的,⽽这个范围内的unicode字符和ASCII字符相同。
具体的编码⽅式也不仔细说了,总之我们要按照码点的⼤⼩来进⾏拆分,涉及到⼀些位运算。
[3]解析思路:
这个解析思路相对较繁琐,⾸先将json⾥⾯的unicode转义字符变成码点,这⾥涉及到符到整数的转换,
注意还要处理可能的错误格式,然后判断是否还需要解析下⼀个码点作为低位的代理项.所以我们可能会有2种错误:
(1)⽆效的转义字符
(2)缺少低位的代理项或者低位代理项不正确
接下来我们要将码点变成1到四个字节,这个分成4种情况来处理,根据相应的规则进⾏位运算,可能需要仔细思考⼀下才能做对.分别将每⼀个字节存储字符串⾥⾯.
注意这⾥存在⼀个坑[2]:
当我们从字⾯值”xxxx”⾥⾯提构造string的时候,如果⾥⾯含有\0,”“在转换位string时候会被截断.但是如果我们直接向string⾥⾯添加\0,那么这个\0就只是⼀个普通的字符⽽已.所以在存在\u0000的情况下,我们得到的结果就包含了\u0000后⾯的字符,因为push_back \0并不会造成string被截断.这样得到的结果是不正确的.所以我们必须要在转义处理完毕之后进⾏⼀次处理,截断掉\0后⾯的部分.⽐如hello\u0000fuck得到应该是hello⽽不应该是hellofuck!
[3] 总结
所以整个过程分成两个步骤,第⼀是将json⾥⾯的unicode转义字符变成码点,然后是将码点拆分成1到4个字节。
这⼀部分的内容同样不简单,涉及到字符到整数的转换(包含⽆效字符等不正确格式的处理),整数之间的位运算等内容。当然由于规则⽐较明显,只要按照标准⼀步⼀步的⾛下去,没有什么太⼤的难度。
[5]数组的解析
数组是⼀个复合结构,很明显这⾥必须将数据结构涉及为树状的。我们选择vector<shared_ptr <json_value > >来表⽰⼀个数组。
数组实例: [1,[2,3,[4,"567"],false,true],null]
[1] 错误处理
⾸先在解析数组的时候有这样⼏种可能的错误
(1)数组本省某个元素解析错误
(2)数组缺少 逗号(,)
(3)数组缺少 ]
(4)特殊情况 空数组
这⼏种错误相对来说不难处理,当然需要注意调⽤lept_parse_whitespace()来去掉中间⽤来分割的空⽩字符。
考虑⼀下有两个,,的时候会发⽣什么? ~^_^~
[2]递归解析的过程
这⼀部分也⽐较简单,当我们要解析⼀个元素时,⾸先⽣成⼀个json_value的对象,并且将这个接下来的解析的结果都存放在这个新对象上⾯.只要调⽤lept_parse_value()这个函数就可以了,它会帮我们解析元素的成分,当它解析成功之后,我们只需要将新的shared_ptr<json_value>添加到vector⾥⾯就可以了。
[6]object的解析
object就是对象,它由{key:value}来表⽰,同样是⼀个符合结构,不过它⽐数组要复杂⼀点.
[1]可能的错误:
有如下⼏种可能的错误:
(1)缺少 :
(2)缺少 ,
(3) 缺少 }
(4)key或者value的解析不正确.
(5)特殊情况 空对象
这⼏种错误同样算不上很⿇烦,但是实现起来还是需要细⼼,我们需要把逻辑理清楚,⼀点⼀点的判断即可.
[2]递归的过程
由于json没有规定只能有⼀个key,所以我们必须选择multimap,同时考虑到查的效率,unordered_multimap应该是⽐较好的选择.
同样我们只需要把key先解析出来,然后解析掉可能的空格以及分割符,最后调⽤lept_parse_value()
来解析value即可.最后
把{key,shared_ptr<json_value>}插⼊到unordered_multimap⾥⾯即可.过程类似于array部分.
[7]⽣成器
对已有的数据⽣成json⽂本
这⼀部分的内容相对要简单,我们需要做的是根据当前的json_value的类型来⽣成json⽂本.模仿parser的部分,我们留给外界的接⼝就是
lept_stringify(),然后把具体的⽣成任务交给lept_sringify_value(),再分别实现相应的函数.
对于普通的字⾯值来说⽣成太简单,下⾯我们来看看其他的类型.
[1]数字:
如果要直接⼿动⽣成的话难度是⽐较⼤的,所以我们可以选择利⽤to_string这个函数来做.
[2]字符串:
这个部分相⽐较要⿇烦⼀点,⾸先我们需要添加⼀个引号,然后调⽤⽣成string的函数,在调⽤结束以后再添加⼀个引号.其次要处理各种转义字符.举个例⼦:当我们遇到\n,我们知道它在json⾥⾯是\n,当我们遇到,它在json⾥⾯是\,注意,那么我们push的时候push的是
\\\\才得到 \\ 这个⽐较容易弄错.
接着要处理普通的字符,如果这个字符的值是⼩于等于31,那么它应该是\u00xx转义过来的,我们就需要进⾏位运算来还原.这⾥还原的⽅式可以先构造⼀个简单的常量数组{'0','1','2'...'A','B','C',..'F'}
然后分别求出字符值的第5-8位代表的值和0-3位代表的值,然后选择数组⾥⾯对应的字符.
如果字符的值⼤于31,就必须要解析utf-8了,这个⼯作并不容易,在这个项⽬⾥先不做要求.
[8]获取/修改/添加数据
我们解析完以后应该能够有合理安全的⽅式访问内部的数据.
我设计了下⾯的API
[0]
get⽅法是⼀个模板⽅法,它有多种重载的类型.
get <T>():
这个⽅法返回当前json_tree内部的json_value的数据,并且转换为T类型.因为只有json_value才知道它是什么类型并且应该返回什么数据.所以我们需要把这个任务交给json_value来决定.
在此,我实现了关于json_value到double,bool,string,vector,unordered_map的⼀系列类型转换操作符,所以只需要在get函数⾥⾯直接返回json_value即可,它会⾃动转换成相应的数据.get的任务就是进⾏检查,确保当前的类型不是NULL.
get<T>(const std::string& path) :
这个⽅法接受⼀个参数path,path的形式应该是d,代表路径,不如 animal.tiger.number就应该是
{animal:{tiger:{ number:xxxx}}}.
为了完成这个⽅法,需要为json_value实现⼀些函数⽤来取得相应的节点.所以我实现了⼀个叫 get_objet_element的⽅法,⽤来执⾏真正的递归查的操作.这⾥需要注意的⼏点,
(1)我们必须要处理查不到相应路径的情况.这时候应该报错!
(2)如果路径中不存在 . 的情况
(3)路径为空
get<T>(initializer_list<size_t>) :
这个⽅法⽤来取得当前对象是数组类型时候的某⼀个⼦节点的值.
⽐如{0,0,1,2}代表[[[2,[1,2,3]]]]中的3.⽤⼀个循环就可以实现,同样要检测边界条件.

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。