脚本与程序交互推荐方式:json解析

DinS          Written on 2018/4/18

本文将介绍一种脚本与程序交互的推荐方式:json。因为这是使用angelscript解决一个复杂问题,所以读者最好充分理解了AS相关知识,可参考《AngelScript框架介绍》等一些列文章。

如何做到脚本与程序有效交互是一个大问题,我个人的经验认为使用纯文本在脚本和程序间交互是最保险的方式,就相当于网络上的两台机器那样。如果想到这里自然会想到xml和json,当然json更适用于此处的情况。json自身的优越性不需要论证了,互联网上一大部分数据都是用json传输的,因此使用json格式以string的方式进行脚本和程序交互是一个非常好的方法。

另外你会发现dictionary实际上就是json格式的,key是string,value可以是任意基本类型,比json强的地方是可以放handle。这是天然相性。

于是结论就是使用string来交互,而string又是json格式

一来string是作者自己写的,还是std:string,兼容性、移植性、安全都有保障。
二来string完全可以承担json格式,传递任意数据。
三来这种方式让脚本与程序的互动清晰化和解耦。

接下来的任务就是如何从string按照json格式解析得到dictionary,以及如何将dictionary变成json格式的字符串,解决了这两个问题脚本与程序互动就解决了。
至于程序方面的json,推荐使用nlohmann::json,可见《nlohmann::json概述与基础用法》。

遗憾的是目前还没有AS中的js,于是得自己写了。
解析json并不是一件容易的事情,所以这里仅仅提供一个dirty implementation,同时增加扩展的余地。

一、准备工作

在开始前,先要说一下AS里的string。[]返回的不是char,而是uint,使用起来并不方便,所以在scriptstdstring.cpp中我额外注册了一个成员函数用于获取单个的char,如下(实际上是substr的另一种写法):

都是仿照substr的写法,只注册了native的。于是在脚本中可以这样写:

类似于strJson[0],c表示char。

另外为了方便增加replace,沿用std的使用方法:

另外解析json时要用到AS提供的另一个东西:any,这个类型可以装载任意类型。

二、整体思路

使用OO方法来解析json。

what?什么意思?
我想直接从json字符串中构造出合适的类型,这样就可以直接使用了。不仅限于基本类型,还包括自定义的类。
这可能吗?可以做到,使用OO的方式。不过既然是OO,效率肯定没有那么高了。效率问题交给硬件去解决吧。
最终达到的结果是一行代码,如下:

返回的是一个dictionary,拥有前面dictionary的全部功能,因此既可以装基本类型又可以装自定义类,得到这个jsData后就可以该怎么用就怎么用了。

采用一种自上而下的方法来解析,先定义几个概念:
一对key:value称为一个unit。
value是基本类型的场合unit称为简单unit,比如”ABC”:3.45 或者 “String”:”Hello”
value里包含了json节点或数组的称为复杂unit,比如”More”:{“fisrt”:1,”second”:”2″} 或者 “arr”:[1,2,3]
一个只包含了简单unit的json字符串称为简单json串;
一个包含了复杂unit的json字符串称为复杂json串。

另外对json的key做一个额外的强限制:key的格式是:类型名_节点名
比如说”d_ABC”:3.45 或者 “arrint_Num”:[-1,0,1]
为什么要这样做?因为我们想从字符串直接构造类,这样的话必须知道类型的信息,于是就利用key捎带value的类型。

有了这些就可以开始介绍核心算法了。
(1)将传入的json串转变为简单json串,将复杂unit隐藏掉
(2)将简单json串分解为若干简单unit
(3)对每一个unit提取key中的类型,根据类型构造相应的parser,然后将简单unit再展开成复杂unit,解析value
(4)解析成功后得到对应的类型,加入dictionary中

看起来挺简单的,但是实现起来需要一些技巧,特别是第三步。让我们看看OO在哪里:

定义一个基类JsonParser,派生类可以分成两大类:基本类型parser和自定义类型parser。使用工厂模式依据key中的type构造适当的派生类。
既然类型已知,Parse()就可以很简单的书写了。对于不断嵌套的node也没有问题,可以不断地调用工厂函数直到基本类型,然后解析,然后层层构造自定义类。

三、算法层级代码

这部分比较简单,直接看代码:

大图点这里

基本上算是自然语言描述了,成功掩盖了复杂性,接下来让我们详细看一看内部是如何实现的。

Preprocessing & Expand是一对函数,效果正好相反。

Preprocessing就是预处理,用意是把传入的json串转变为简单json串。原理很简单了,就是括号匹配。只要存在{}和[]就提取出来,换成一个指定的token值,然后把字符串存到一个dictionary里。
Expand刚好相反,根据token值去dictionary查字符串,替换掉token值。具体写法就不展示了。
当然这里有一个小问题:{}里还可以套{}和[]
所以会发生多次替换的情况,但是这个不是问题,只要展开也多次展开即可。
给个实例:
{“Name”:”DinS”,”No.”:123,”array”:[1,2,3],”node”:{“first”:4.5,”second”:”666″,”vector”:[4,5,6]}}
会处理成
{“Name”:”DinS”,”No.”:123,”array”:<arr0>,”node”:<node0>}

之所以要这么做是为了算法第二步split做准备。我们需要把json切分成多个unit,最自然的方法就是按照,切分,但是只要存在复杂unit就无法做到,所以才需要转换。

四、JsonParser类体系-基本类型

这个是重点,解析全部靠他。

这里先展示几个基本类型的parser:

大图点这里

大图点这里

这里实际上体现了OO的优越性:既然我们知道传进来的字符串应该是什么样子的,直接解析即可。
再看一眼工厂函数。

大图点这里

根据key的类型信息,构造派生类parser,返回基类handle,这样就实现了多态。

不过这里还有一个小问题。
成员函数Parse返回的是any对象。固然可以直接把any插入dict,但是这样一来获取dict值的时候写法会很复杂,这是因为从any里取出值本身就有一些风险。于是还要写一个函数包装这个过程,将any变成真正的类型,然后插入dictionary。之所以要返回any是因为json可能返回任何类型,为了保证接口统一必须用any。

大图点这里

这是局部,you get the idea,看起来写法比较繁琐,但是这就是使用any必须付出的代价。

有了以上的内容,就可以解析基本类型json了,比如我们把这个json传入:

执行这段脚本:

大图点这里

运行结果:

成功!

五、JsonParser类体系-自定义类型

为了展示自定义类型,让我们先定义一个类:

大图点这里

注意,我们既然定义了类,就可以定义出该类对应的json串格式,解析也就是按照这个形式来做。

大图点这里

这是一个完整的解析派生类的代码块。
似曾相识,跟ParseJson非常像,这不奇怪,因为Person本身就是一个json 对象。但是还是有不一样的地方,一个是我们知道该类内部的成员,另一个是Parse返回any对象,而不是dictionary。
这两个因素促使我们使用一个不同的方式来处理,而不是递归调用。工厂函数和解析函数都是一样的,但是下一步不一样。我们把any放到了一个dictionary里,注意是直接把类型any的变量放入,然后写一个私有成员函数从dictionary里将any转换成我们需要的类型(这是可能的因为我们事先知道类型)。
看起来是boilerplate,但是这个最直观的方式,也最不容易出错。最后直接构造Person对象并返回。

如果类内部还有json对象,不要紧,使用Preprocessing和Expand,这是一种OO形式下的递归调用,只要调用了Parser的Parse后,保证返回对应的类实例,以供上层类构造使用。

通过OO我们可以掌握局部,更容易理解,而且更加容易改变,这是递归无法做到的。以上的代码就是解析三个基本类型,构造出一个person,然后返回。
从这个例子也可以看出一个模式:只要新增加了,都可以照着这个方式仿写,一定可以完成任务。

当然不要忘记更新工厂函数和插值函数:

大图点这里

大图点这里

运行看效果,传入这个json串:

相比之前多了一个person值,脚本里这么写:

大图点这里

结果:

成功!
这样一来扩展性就是无限的了,以后只要增加自定义类型,就在上面三处有more to add…的地方增加对应代码,即可解析。也就是说我们搭建了一个解析json的框架。

至此我建议再看一遍ParseJson这个函数,理解OO如何进行了json解析。

六、JsonParser类体系-数组

如果理解了上面的内容,解析数组应该不是问题。直接上代码,顺便可以看一下如何扩展这个框架:

大图点这里

array不是基本类型,所以也算class。又因为是泛型,所以每一种泛型都算是一个独立的class。于是加起来可能略显繁琐,但是不是大问题。
由于我们知道是int数组,就不需要费劲调用OO了,可以直接解析。这也是OO的优点:视情况随意变化。
加完一个类之后,别忘了去工厂函数和插值函数中增加对应内容。

就OK了,传入下列json:

脚本如下:

大图点这里

return里输出arrRoll[3]的值:

Perfect!

至此我们只完成了脚本中解析程序传入的json串,还需要做的是返回给程序加工过的合法json串,见《脚本与程序交互推荐方式:json去格式化》。