json parser是用来解析json的工具,按照规则将符合标准json文本转换成相应的数据结构(类)。
能解析NULL,False,True,Number(只支持double),String,array,object六种类型。对于常见的出现错误定义了相应的错误码。
能够将解析好的数据再转换成为json格式的文本。能够对现有的数据进行修改,实现了添加,修改两部分的功能。对于删除部分尚未实现(to do)。
对所有类型的解析/生成均做了相应的测试。
对重要API做了相应测试。
由于篇幅和精力所限制,我只选择阐述部分较为重要的实现,具体细节可以看源码。
我们创建一个叫做json_value的类来存储不同的类型和值,创建json_tree这个类用来解析相应json文本并保存在json_value里面。
type+(null,false,true,number,string,array,object)
为了节约空间,我们选择使用一个union将这六种类型的变量全部封装起来,但是由于string,vector,map等类型不是简单类型,所以我们必须自己创建相应的构造函数,拷贝构造函数,赋值运算符,析构函数来保证相应的操作的正确执行。
同时我们不直接使 用vector,map
json_tree留给外部使用的接口是lept_parse(const string&)
这个函数负责最外层的判断以及部分处理错误的功能,我们首先写好lept_parse_whitespace()这个函数用来去掉分割用的空格。然后将剩下的工作交给另一个函数lpet_parse_value:检测遇到的第一个字符,根据
(n,f,t,",[,{,其他(数字或者无效值))
这六种首字母的情况,我们可以很容易的确定需要解析的是哪一种。这里我们分别实现对应功能的函数。
literal是指普通的字面值类型,一共三种 null,false,true
由于这三种类型非常相似,所以我选择一个函数lept_parse_literal来解析这三种类型,在这个函数里面,我们只需要判断它是三种类型里面的哪一种,并且指定好相应的类型即可。
此外,还需要处理可能发生的错误,比如nul,fuck这种无效的字符。这一块的难度不大,细心即可顺利的完成。
注意,我们还要处理类似 “null x”这样的不正确输入,并且返回的错误类型应该是NOT_SINGULAR_ROOT(该节点不止一个值)。
也许这是整个项目最困难的部分,但是我选择一定程度的回避^_^
(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范围之外的字符,这同样是不正确的输入。
如果手动来序列话浮点数其实解决方案会相当的麻烦,由于只是一个用来练习的项目,所以我选择使用stod这个函数来进行转换。注意,这里有一个坑^[1]:
如果我们选择strtod这个函数会发现它不能很好的进行边界值的判断,比如1E-1000其实是应该解析为0,但是这里会被认为指数太大而越界。而stod这个函数则会对超过浮点数最值的文本都转换为一个HUGE_VAL|-HUGE_VAL,这样我们就能精确的判定是不是范围越界了。然而stod接受的参数是char*,所以这里要来回转换会使得实现起来比较不舒服。
我们首先需要判断前面提到的可能的错误,因为stod函数并不会帮我们进行判断,它只会在第一个不满足格式的字符前停止解析,同时它支持的一些格式json不支持。所以必须要手动判断。
判断完成之后我们需要确认当前的字符,如果是
, [ { 空格 \t \f \r \s \n
我们可以暂时不报错,因为这些可能是分割符号,至于这些符号是否满足正确的格式,交给调用lept_parse_number的外层函数来判断。如果是其他字符,那么可以判定当前的解析数字不成功,返回相应的错误。
最后我们还要处理可能的越界错误。
这一部分的解析相当麻烦,虽然代码量看似不大,但是要考虑的问题较多,一不小心就会出错,我在这个地方花费来一下午才通过所有测试。
这一部分的解析包含两个内容,第一个是普通的字符和转义字符,第二个是unicode的转义字符。
首先认识到string里面的所有需要转义的字符都应该多加一个\来表示,因为json文本本身是被包含在一对双引号之内的。
对于普通的字符,没有什么好说的,只需要直接翻译就好了,但是注意对于ASCII码在1-31之间的普通字符是非法的。
对于转义字符,我们一共有如下这些可能:
\n \b \f\ \r \t \" \\ \/ \u
在switch语句里面,针对每一种类型,我们都需要翻译成真正的转义字符,这里相当于做了一个简化版本的C语言对字面值的parse工作。由于这些工作的相似度(除了\u)很高,所以我们可以用一个宏来完成这些类似的工作,注意宏后面记得加上break,我在这里浪费了不少时间检查。
这一部分比上面要困难一些,主要在于需要真正搞清楚unicode和utf-8之间的转换关系,以及可能遇见的错误格式。
面对unicode字符,我们需要将它转换成utf-8的格式,unicode采用一个整数码点来映射到字符集。
首先假设输入合法:
面对一个\uxxxx,它表示U+0000到U+FFFF,我们需要将这个十六进制解析成为一个整数码点。同时由于字符串是通过UTF-8来存储的,所以我们也要把这个码点编码成UTF-8.
但是注意到这个十六进制的数是不能表示完所有的码点的。其实Json字符对于超过U+FFFF范围以外的码点,选择采用代码对来表示,如果第一个码点是U+DC00到U+DBFF,那么我们就知道它应该和后面紧跟的另外一个码点共同代表一个码点。具体的转换就不仔细说了。
而UTF-8是一种存储码点的格式,它选择将码点存储为一到多个存储单元,其中每一个单元是一个字节,所以每个ASCII字符只需要一个字节去存储。我们的json parser只支持UTF-8的格式。
得到来真正的码点(整数),我们就需要把它按照UTF-8的格式来存储,UTF-8把二进制的码点拆分为1-4个字节。这个编码方式和ASCII码编码是兼容的,而这个范围内的unicode字符和ASCII字符相同。
具体的编码方式也不仔细说了,总之我们要按照码点的大小来进行拆分,涉及到一些位运算。
这个解析思路相对较繁琐,首先将json里面的unicode转义字符变成码点,这里涉及到符到整数的转换,注意还要处理可能的错误格式,然后判断是否还需要解析下一个码点作为低位的代理项.所以我们可能会有2种错误:
(1)无效的转义字符
(2)缺少低位的代理项或者低位代理项不正确
接下来我们要将码点变成1到四个字节,这个分成4种情况来处理,根据相应的规则进行位运算,可能需要仔细思考一下才能做对.分别将每一个字节存储字符串里面.
当我们从字面值”xxxx”里面提构造string的时候,如果里面含有\0,”“在转换位string时候会被截断.但是如果我们直接向string里面添加\0,那么这个\0就只是一个普通的字符而已.所以在存在\u0000的情况下,我们得到的结果就包含了\u0000后面的字符,因为push_back \0并不会造成string被截断.这样得到的结果是不正确的.所以我们必须要在转义处理完毕之后进行一次处理,截断掉\0后面的部分.比如hello\u0000fuck得到应该是hello而不应该是hellofuck!
所以整个过程分成两个步骤,第一是将json里面的unicode转义字符变成码点,然后是将码点拆分成1到4个字节。
这一部分的内容同样不简单,涉及到字符到整数的转换(包含无效字符等不正确格式的处理),整数之间的位运算等内容。当然由于规则比较明显,只要按照标准一步一步的走下去,没有什么太大的难度。
数组是一个复合结构,很明显这里必须将数据结构涉及为树状的。我们选择
vector
来表示一个数组。>
数组实例:[1,[2,3,[4,"567"],false,true],null]
首先在解析数组的时候有这样几种可能的错误
(1)数组本省某个元素解析错误
(2)数组缺少 逗号(,)
(3)数组缺少 ]
(4)特殊情况 空数组
这几种错误相对来说不难处理,当然需要注意调用lept_parse_whitespace()来去掉中间用来分割的空白字符。
考虑一下有两个,,的时候会发生什么?
~^_^~
这一部分也比较简单,当我们要解析一个元素时,首先生成一个json_value的对象,并且将这个接下来的解析的结果都存放在这个新对象上面.只要调用lept_parse_value()这个函数就可以了,它会帮我们解析元素的成分,当它解析成功之后,我们只需要将新的shared_ptr
添加到vector里面就可以了。
object就是对象,它由{key:value}来表示,同样是一个符合结构,不过它比数组要复杂一点.
有如下几种可能的错误:
(1)缺少 :
(2)缺少 ,
(3) 缺少 }
(4)key或者value的解析不正确.
(5)特殊情况 空对象
这几种错误同样算不上很麻烦,但是实现起来还是需要细心,我们需要把逻辑理清楚,一点一点的判断即可.
由于json没有规定只能有一个key,所以我们必须选择multimap,同时考虑到查找的效率,unordered_multimap应该是比较好的选择.
同样我们只需要把key先解析出来,然后解析掉可能的空格以及分割符,最后调用lept_parse_value()来解析value即可.最后把{key,shared_ptr
插入到unordered_multimap里面即可.过程类似于array部分.
对已有的数据生成json文本
这一部分的内容相对要简单,我们需要做的是根据当前的json_value的类型来生成json文本.模仿parser的部分,我们留给外界的接口就是lept_stringify(),然后把具体的生成任务交给lept_sringify_value(),再分别实现相应的函数.
对于普通的字面值来说生成太简单,下面我们来看看其他的类型.
如果要直接手动生成的话难度是比较大的,所以我们可以选择利用to_string这个函数来做.
这个部分相比较要麻烦一点,首先我们需要添加一个引号,然后调用生成string的函数,在调用结束以后再添加一个引号.其次要处理各种转义字符.举个例子:当我们遇到\n,我们知道它在json里面是\n,当我们遇到,它在json里面是\,注意,那么我们push的时候push的是
\\\\才得到 \\ 这个比较容易弄错.
接着要处理普通的字符,如果这个字符的值是小于等于31,那么它应该是\u00xx转义过来的,我们就需要进行位运算来还原.这里还原的方式可以先构造一个简单的常量数组{'0','1','2'...'A','B','C',..'F'}
然后分别求出字符值的第5-8位代表的值和0-3位代表的值,然后选择数组里面对应的字符.
如果字符的值大于31,就必须要解析utf-8了,这个工作并不容易,在这个项目里先不做要求.
我们解析完以后应该能够有合理安全的方式访问内部的数据.
我设计了下面的API
get方法是一个模板方法,它有多种重载的类型.
get ()
:这个方法返回当前json_tree内部的json_value的数据,并且转换为T类型.因为只有json_value才知道它是什么类型并且应该返回什么数据.所以我们需要把这个任务交给json_value来决定.
在此,我实现了关于json_value到double,bool,string,vector,unordered_map的一系列类型转换操作符,所以只需要在get函数里面直接返回json_value即可,它会自动转换成相应的数据.get的任务就是进行检查,确保当前的类型不是NULL.
get(const std::string& path)
:这个方法接受一个参数path,path的形式应该是a.b.c.d,代表路径,不如 animal.tiger.number就应该是
{animal:{tiger:{ number:xxxx}}}.
为了完成这个方法,需要为json_value实现一些函数用来取得相应的节点.所以我实现了一个叫 get_objet_element
的方法,用来执行真正的递归查找的操作.这里需要注意的几点,
(1)我们必须要处理查找不到相应路径的情况.这时候应该报错!
(2)如果路径中不存在 . 的情况
(3)路径为空
get(initializer_list)
:这个方法用来取得当前对象是数组类型时候的某一个子节点的值.
比如{0,0,1,2}代表[[[2,[1,2,3]]]]中的3.用一个循环就可以实现,同样要检测边界条件.
get_number等一系列函数
:这些函数是为了将json_tree和json_value解耦设计的,不让json_tree知道json_value内部的具体成员,而是通过get_xxx等一系列方法调用.同时还可以在这些方法内部检测具体的类型是否正确.
set方法是用来修改数据的API.同样具有几种重载的模式
set(T v)
:这个方法用来修改当前节点的值,把这个任务交给json_value就好了,所以为json_value同样设计了一个相同类型的函数,直接调用它.而json_value.set本身则通过重载的方式来判断应该set哪一个值并作出相应的处理.
首先注意int->bool的优先级别是高于int->double的!所以我们在重载的时候必须添加一个int的类型并且强制转换后再调用一次set.
其次”“->bool的优先级高于”“->string!!!这个真是难以预料.所以我们还要特别重载一个const char[]的类型来强制转换后并转发.
最后我们要注意,由于json_value里面含有union,所以我们在set的时候必须执行释放原来数据的内存的操作.因此我们实现了一个叫做set_null()的函数专用来释放内存.
set(const std::string& path,const T& v)
:这个API类似于get那一个,专用来修改指定的节点的数据,但是现在有一个问题,如果指定的节点不存在,我们是选择为它生成这样的节点,还是选择直接报错呢?我个人觉得应该交给用户一个选择权,所以先实现了较为复杂的情况:生成指定路径上的节点.剩下的一种比较简单,可以利用前面的get函数实现.
path同样应该是a.b.c.d的模式,但是如果是animal.bird.number,那么如果存在则直接覆盖掉原来的值即可.
若不存在,就应该生成{animal:{bird:{number:v}}}
这样的节点,所以我们必须获取到倒数第二个层次的节点,然后insert({number,v})这样一个节点.或者也可以选择将v当作参数传入json_value的create_object_element函数,然后在这个函数里面直接修改.具体的实现哪个更好我也没想太清楚,我选择了第一种.
add_child和append_child
方法除了修改,还应该可以添加 ~^__^~
set方法无论哪一种模式都会覆盖掉原来的值,也就是说不能够创建多个具有同一个key的节点,所以我设计了另外一个方法add_child(path,const json_tree&t),这个方法则用来给指定的位置添加一个节点,如果中间指定的路径不存在怎么办?在添加这个逻辑下,我个人倾向于直接创建相应路径的节点.所以还是可以调用json_value的相应方法,然后insert这个相应的子节点.
如果我们只是想添加一个节点到list上面,那么我们就需要使用append_child()这个方法,这个方法的不同在于最后的子节点是被添加到某个list上面的,所以不需要取倒数第二个节点.比如animal.dog.number这样的路径,得到的结果就应该是{animal:{dog:{number:[….,child]}}},这里面是有较大差别的,需要细细思考.
无论那种方法,都必须要考虑多种情况,比如是否有path,path是否为空,path是否含有 . 等,这些细节决定成败
比较奇怪的功能,但实现起来并不容易.
判断两个json_value是不是相同的并不是一件容易做到的事情.首先它包含的很多子节点是智能指针,而两个指针的比较在这里没有意义,所以这就杜绝了我们直接用相应的容器的算法,从而必须要自己实现,因为容器的比较算法一定是比较key和value,而这里的value是指针.
假如它们只是普通的树结构,那么判断两棵树相等也并不复杂.但是问题的关键在于它有可能一个unordered的树,只要它包含了object的对象,这将问题的难度大大提高了.我们为了时间复杂度和空间的节约没有考虑了shared_ptr和ordered的map,这为实现==比较运算带来了麻烦.
首先我们看一下普通的unordered_multimap是怎么实现==比较的.
根据Cpp reference,我们可以看到它主要执行两个操作,首先判断size,然后对每一个不同的key都执行equal_range,得到两组迭代器范围,(b1,e1),(b2,e2).通过std::is_permuation就可以确定是否这两组范围指定的对象是否属于同一个排列的.只要每一个不同的可以对应的范围都满足这个条件,那么就可以确定这两个unordered_multimap是相等的.
现在问题关键在于is_permuation是怎么实现的呢?这里给出一种可能的实现方式:
template<class ForwardIt1, class ForwardIt2>
bool is_permutation(ForwardIt1 first, ForwardIt1 last,
ForwardIt2 d_first)
{
// skip common prefix
std::tie(first, d_first) = std::mismatch(first, last, d_first);
// iterate over the rest, counting how many times each element
// from [first, last) appears in [d_first, d_last)
if (first != last) {
ForwardIt2 d_last = d_first;
std::advance(d_last, std::distance(first, last));
for (ForwardIt1 i = first; i != last; ++i) {
if (i != std::find(first, i, *i)) continue; // already counted this *i
auto m = std::count(d_first, d_last, *i);
if (m==0 || std::count(i, last, *i) != m) {
return false;
}
}
}
return true;
}
根据上面这个思路,我们发现实现is_permutation又涉及到value本身是否相等了,所以这就变成了一个递归的调用.而递归必须要有一个出口,我们容易发现当当前的json_value不是复合结构的时候,就可以直接比较了.所以这个递归的调用是合乎逻辑的.
从而我们可以根据普通的unordered_multimap的比较来实现我们自己的比较.当然我们还要处理一些额外的情况.最简单的一种实现如下:
bool operator== (const json_value& t1,const json_value& t2){
CHECK_CONDITION(t1.get_type() == t2.get_type());// IF size1!=size2,return false
switch(t1.get_type())
{
case LEPT_ARRAY:return test_json_array(t1,t2);
case LEPT_OBJECT:
{
auto o1 = t1.get_object();
auto o2 = t2.get_object();
CHECK_CONDITION(o1.size()==o2.size());// IF size1!=size2,return false
std::unordered_multiset< std::string> unique_keys;
for( auto& pair:o1)
{
std::string key = pair.first;
if(unique_keys.find(key) ==unique_keys.end())
{
unique_keys.insert(key);
auto range1 = o1.equal_range(pair.first);
auto range2 = o2.equal_range(pair.first);
CHECK_CONDITION \
( std::distance(range1.first,range1.second)==\
std::distance(range2.first,range2.second));//IF range1!=range2,return false
/****compare two group****/
std::unordered_set group1,group2;
INSERT_JSON_VALUE(range1.first,range1.second,group1);
INSERT_JSON_VALUE( range2.first,range2.second,group2);
CHECK_CONDITION(group1==group2);// if group1!group2, return false
}
}
return true;
}
case LEPT_NUMBER:
return (t1.get_number()==t2.get_number());
case LEPT_STRING:
return (t1.get_str()==t2.get_str());
default:
return true;
}
}
通过实现hash function,我们可以利用unordered_multiset来间接实现is_permutation的比较,这样做比较投机取巧,效率也很低,但是可以先验证我们算法的正确性.然后再真正的实现is_permuation函数.
测试利用了很多的宏,减少了重复的代码.同时我们测试生成器的功能时会遇到困难,因为生成的符号标准的json文本可以有多种形式:比如{“a”:1,”b”:2}和{“b”:2,”a”:1},更不要说中间的那些空格了.另外1和1.000000也是相等的,但是再字符串里不一样.
所以可行的办法就是我们用生成的json再次解析一遍,这样得到的结果可以和原来的解析结果进行比较.这也是我们为什么要实现==的一个重要原因之一.
另外每一个测试都包含了对解析结果类型以及是否成功的检查.
这个项目还有很多不足,首先没有做优化,所以效率低.其次应该把json_value直接并入json_tree里面,让json_tree自己就当作节点,这样可以减少很多代码转发的工作.少了一个连接层.要知道当你重载三个函数类型的时候,如果存在一个层屏障,那么你还得再写三个重载的函数做转发…这是比较痛苦的.所以抽象也会带来复杂,在这个例子里面,我认为对json_value的抽象是没有必要的.以后有时间可以重构一下.但是这在一开始确又不是那么容易分辨清楚的….