技术: rapid-json

本文是关于 rapid-json 的简单讲解和介绍.

国人自己开发的库, 全部以头文件的形式包含. 在知乎的评测也是各种好评.
其他的解析工具, 玩玩就可以了, 实际项目中, 还是推荐用 RapidJson.

引子

发现每次都会有一大堆人去说XML和JSON的优缺点, 我觉得也是挺烦的, 下面一句话带过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="utf-8" ?>
<country>
<name>中国</name>
<province>
<name>黑龙江</name>
<citys>
<city>哈尔滨</city>
<city>大庆</city>
</citys>   
</province>
<province>
<name>广东</name>
<citys>
<city>广州</city>
<city>深圳</city>
<city>珠海</city>
</citys>   
</province>
<province>
<name>台湾</name>
<citys>
 <city>台北</city>
 <city>高雄</city>
</citys> 
</province>
<province>
<name>新疆</name>
<citys>
<city>乌鲁木齐</city>
</citys>
</province>
</country>

再看json:

1
2
3
4
5
6
7
8
9
10
{
name: "中国",
provinces:
[
{ name: "黑龙江", citys: { city: ["哈尔滨", "大庆"]} },
{ name: "广东", citys: { city: ["广州", "深圳", "珠海"]} },
{ name: "台湾", citys: { city: ["台北", "高雄"]} },
{ name: "新疆", citys: { city: ["乌鲁木齐"]} }
]
}

XML的可读性稍微好一些, 但是冗余信息多, 体积大; 并且解析方便程度来说, JSON完胜.
(但是业界流行程度来说, XML业界广泛认同)

本文主要介绍RapidJson的使用以及一些心得, 如果你对它的源码也感兴趣的话, 可以参考该 链接
(补充, 有人喜欢用 JsonCpp, 不过还是推荐你用RapidJson吧)

(虽然有官方教程, 不过觉得那个教程也是非常啰嗦的)

正文

简介

rapidjson是腾讯的开源json解析框架,用c++实现。由于全部代码仅用header file实现,所以很容易集成到项目中。rapidjson的另一个特点是对json的标准符合程度是100%的(在开启了full precision选项的情况下).

最重要的: RapidJSON is a JSON parser and generator for C++ .

总之, 业界也有好评, 个人觉得比 JsonCpp 好.

安装

RapidJSON 是只有头文件的 C++ 库。只需把 include/rapidjson 目录复制至系统或项目的 include 目录中。
给出我的参考步骤:

1
2
3
$ git clone https://github.com/miloyip/rapidjson.git
$ cmake .
$ sudo make install

然后, 你的相关库就安装到了 /usr/local/include/

库文件

安装完了, 顺便可以扫一眼, 都有哪些库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
$ tree -L 2 /usr/local/include/rapidjson
/usr/local/include/rapidjson
├── allocators.h
├── document.h
├── encodedstream.h
├── encodings.h
├── error
│   ├── en.h
│   └── error.h
├── filereadstream.h
├── filewritestream.h
├── fwd.h
├── internal
│   ├── biginteger.h
│   ├── diyfp.h
│   ├── dtoa.h
│   ├── ieee754.h
│   ├── itoa.h
│   ├── meta.h
│   ├── pow10.h
│   ├── regex.h
│   ├── stack.h
│   ├── strfunc.h
│   ├── strtod.h
│   └── swap.h
├── istreamwrapper.h
├── memorybuffer.h
├── memorystream.h
├── msinttypes
│   ├── inttypes.h
│   └── stdint.h
├── ostreamwrapper.h
├── pointer.h
├── prettywriter.h
├── rapidjson.h
├── reader.h
├── schema.h
├── stream.h
├── stringbuffer.h
└── writer.h

3 directories, 35 files

案例

先来些简单的案例, 再说相关的API和机制.

此简单例子解析一个 JSON 字符串至一个 document (DOM), 对 DOM 作出简单修改, 最终把 DOM 转换(stringify) 至 JSON 字符串.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// JSON simple example
// This example does not handle errors.

#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"
#include <iostream>

using namespace rapidjson;

int main() {
// 1. Parse a JSON string into DOM.
const char* json = "{\"project\":\"rapidjson\",\"stars\":10}";
Document d;
d.Parse(json);

// 2. Modify it by DOM.
Value& s = d["stars"];
s.SetInt(s.GetInt() + 1);

// 3. Stringify the DOM
StringBuffer buffer;
Writer<StringBuffer> writer(buffer);
d.Accept(writer);

// Output {"project":"rapidjson","stars":11}
std::cout << buffer.GetString() << std::endl;
return 0;
}

注意此例子并没有处理潜在错误, 比如:

1
2
3
4
5
// 2. Modify it by DOM.   
Value& s = d["stars"];
if( s.IsInt() ) {
s.SetInt(s.GetInt() + 1);
}

编译运行:

1
2
3
$ g++ -g -Wall -O0 simpledom.cpp -o simpledom -I/usr/local/inlcude/
$ ./simpledom
{"project":"rapidjson","stars":11}

可以简单的得出结论, 解析JSON是围绕 rapidjson::Document, rapidjson::Value 展开的, 关键性头文件:

  • rapidjson/document.h
  • rapidjson/writer.h

再来一个实用一点儿的例子:
test.json

1
2
3
4
5
6
7
8
9
10
11
{
"dictVersion": 1,
"content":
[
{"key": "word1", "value": "单词1"} ,
{"key": "word2", "value": "单词2"} ,
{"key": "word3", "value": "单词3"} ,
{"key": "word4", "value": "单词4"} ,
{"key": "word5", "value": "单词5"}
]
}

对于这种格式化好的文件的读写, 可以采用流式读写处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// test.cpp
#include "rapidjson/document.h"
#include "rapidjson/stringbuffer.h"
#include "rapidjson/writer.h"
#include <fstream>
#include <string>
#include <cassert>
#include <iostream>

#define psln(x) std::cout << #x " = " << (x) << std::endl



void testSimpleDoc() {
using std::string;
using std::ifstream;

// read json content into string.
string stringFromStream;
ifstream in;
in.open("test.json", ifstream::in);
if (!in.is_open())
return;
string line;
while (getline(in, line)) {
stringFromStream.append(line + "\n");
}
in.close();

// ---------------------------- read json --------------------
// parse json from string.
using rapidjson::Document;
Document doc;
doc.Parse(stringFromStream.c_str());
if (doc.HasParseError()) {
rapidjson::ParseErrorCode code = doc.GetParseError();
psln(code);
return;
}

// use values in parse result.
using rapidjson::Value;
using rapidjson::Type;
using rapidjson::StringBuffer;
using rapidjson::Writer;

Value & v = doc["dictVersion"];
if (v.IsInt()) {
psln(v.GetInt());
}

Value & contents = doc["content"];
if (contents.IsArray()) {
for (size_t i = 0; i < contents.Size(); ++i) {
Value & v = contents[i];
assert(v.IsObject());
if (v.HasMember("key") && v["key"].IsString()) {
psln(v["key"].GetString());
}
if (v.HasMember("value") && v["value"].IsString()) {
psln(v["value"].GetString());
}
}
}
// ---------------------------- write json --------------------
psln("add a value into array");

Value item(Type::kObjectType);
item.AddMember("key", "word5", doc.GetAllocator());
item.AddMember("value", "单词5", doc.GetAllocator());
contents.PushBack(item, doc.GetAllocator());

// convert dom to string.
StringBuffer buffer; // in rapidjson/stringbuffer.h
Writer<StringBuffer> writer(buffer); // in rapidjson/writer.h
doc.Accept(writer);// Accept() traverses the DOM and generates Handler events.

psln(buffer.GetString());
}


int main(void)
{
testSimpleDoc();
return 0;
}

编译运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ g++ -g -Wall test.cpp -o test -I/usr/local/include
$ ./test
v.GetInt() = 1
v["key"].GetString() = word1
v["value"].GetString() = 单词1
v["key"].GetString() = word2
v["value"].GetString() = 单词2
v["key"].GetString() = word3
v["value"].GetString() = 单词3
v["key"].GetString() = word4
v["value"].GetString() = 单词4
v["key"].GetString() = word5
v["value"].GetString() = 单词5
"add a value into array" = add a value into array
buffer.GetString() = {"dictVersion":1,"content":[{"key":"word1","value":"单词1"},{"key":"word2","value":"单词2"},{"key":"word3","value":"单词3"},{"key":"word4","value":"单词4"},{"key":"word5","value":"单词5"},{"key":"word5","value":"单词5"}]}

从上面两个例子, 就可以看出, 只要你给document对象parse(const char*)一个C串, 那么它就会把解析的内容全部存储在document对象里; Value & v = doc[key]; 可以拿到单个对象或者Array(这里的array就像一个list或者数组), 结合Value进行读写, Value类含有 HasMember(), AddMemeber() 之类的方法, 以及和Document类一样重载了 operator[](), 而v["key"].GetString(), v.GetInt() 则可以拿到具体的内容. 上面例子只给出了array如何添加成员, 其实document添加成员, 也是document.AddMember("key buffer", "value buffer-object", document.GetAllocator()); , 并且 document 可以是Object, 也可以是Array, 上面这些例子都是Object.

value[“key”]得到的类型还是Value类型, 当然也可以用v.IsObject()检验:

1
2
3
4
5
6
//Document -> Value
Value & v = doc[key];
v.GetInt();

//对比: value.GetInt()
v["key"].GetString()

漂亮的输出到屏幕上(格式修正):

1
2
3
4
5
6
#include "rapidjson/prettywriter.h"

StringBuffer sb;
PrettyWriter<StringBuffer> writer(sb);
document.Accept(writer);
puts(sb.GetString());

运行结果大致是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
v.GetInt() = 1
v["key"].GetString() = word1
v["value"].GetString() = 单词1
v["key"].GetString() = word2
v["value"].GetString() = 单词2
v["key"].GetString() = word3
v["value"].GetString() = 单词3
v["key"].GetString() = word4
v["value"].GetString() = 单词4
"add a value into array" = add a value into array
buffer.GetString() = {
"dictVersion": 1,
"content": [
{
"key": "word1",
"value": "单词1"
},
{
"key": "word2",
"value": "单词2"
},
{
"key": "word3",
"value": "单词3"
},
{
"key": "word4",
"value": "单词4"
},
{
"key": "word5",
"value": "单词5"
}
]
}

之后如果你想写入文件, 那么就把 sb.GetString() 以文本格式写入文件即可; 或者借助其他的流(下面的例子从stdin和stdout作为流输入和输出):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "rapidjson/reader.h"
#include "rapidjson/prettywriter.h"
#include "rapidjson/filereadstream.h"
#include "rapidjson/filewritestream.h"
#include "rapidjson/error/en.h"

using namespace rapidjson;

int main(int, char*[]) {
// Prepare reader and input stream.
Reader reader;
char readBuffer[65536];
/*
ifstream in;
in.open("test.json", ifstream::in);
*/
FileReadStream is(stdin, readBuffer, sizeof(readBuffer));

// Prepare writer and output stream.
char writeBuffer[65536];
FileWriteStream os(stdout, writeBuffer, sizeof(writeBuffer));
PrettyWriter<FileWriteStream> writer(os);

// JSON reader parse from the input stream and let writer generate the output.
if (!reader.Parse<kParseValidateEncodingFlag>(is, writer)) {
fprintf(stderr, "\nError(%u): %s\n",
static_cast<unsigned>(reader.GetErrorOffset()),
GetParseError_En(reader.GetParseErrorCode()));
return 1;
}

return 0;
}

总之, 先把流读到字符串, 之后写解析逻辑.

深入

其实说到这里, 使用上, 基本没有问题了.

进一步的深入, 我给的建议是:

  1. 先读一下 官方教程.
  2. 然后把源码下面的 example 下面所有的 demo 全部玩一遍.

教程里面, 拷贝转移以及move()部分, 内存流&文件流部分(包装流效率不高), DOM&SAX部分都是仔细看的, 下面是我的部分笔记:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
Value可以用作表示Object, Array, 甚至简单的string
Value a(kArrayType);
Value o(Type::kObjectType);
Value s = "str";


如果Value直接写
Value v; //NUll
此时你并不知道它表示什么.


下面这种形式存储的就是指针了(浅拷贝)
Value s;
s.SetString("rapidjson"); // 可包含空字符,长度在编译萁推导
s = "rapidjson"; // 上行的缩写

直接赋值, 有时候为了安全, 需要做一下标记: `StringRef(cstr)`
const char * cstr = getenv("USER"); //注意是 const char *
size_t cstr_len = ...; // 如果有长度

Value s;
// s.SetString(cstr); // 这不能通过编译
s.SetString(StringRef(cstr)); // 可以,假设它的生命周期安全,并且是以空字符结尾的
s = StringRef(cstr); // 上行的缩写
s.SetString(StringRef(cstr, cstr_len));// 更快,可处理空字符
s = StringRef(cstr, cstr_len); // 上行的缩写


补充: 和value的直接赋值, AddMember(), PushBack() 都采用转移语义(没有分配内存)

这是深拷贝, setString时提供了allocator, 并且指定了长度.
//value.GetStringLength() 能获取UTF字符串中如果存在\u000即空字符的情况下的长度
//比strlen()强大, 当然如果你不传入长度, 那就默认使用strlen()
value.SetString(buffer, len, document.GetAllocator());


如果value之间, 想深复制, 那么有两种方式:
1. 构造中带有allocator
Document d;
Document::AllocatorType& a = d.GetAllocator();
Value v1("foo");
// Value v2(v1); // 不容许
Value v2(v1, a); // 制造一个克隆

2. 采用copyfrom
Value v2;
v2.CopyFrom(v1, a);



对于Value如果是 Object, 它的AddMemeber()比较标准的写法, 即采用的是重置的写法:
//和Document一样的AddMember(), 注Object是有key, value的pair,并且key一定是字符串
Value item(Type::kObjectType);
item.AddMember("key", "word5", doc.GetAllocator());


Value类型是Array, 注意它的PushBack是移动语义就可以了(即使带了allocator),
当然这是针对非基本类型的数据(String, Object),
即Push进去的元素可以是string类型的value, 或者Object类型的value.

push进去的是 string类型或者Object类型也存在临时变量无法赋值给非const引用的,
移动语义问题, 可以类似解决方法: move()返回一个非const引用
// 就地 Value 参数
contact.PushBack(Value("copy", document.GetAllocator()).Move(), // copy string
document.GetAllocator());
// 显式 Value 参数
Value val("key", document.GetAllocator()); // copy string
contact.PushBack(val, document.GetAllocator());

这个时候(把那个几个example玩熟), 对这个库已经很熟悉了.
再深入, 就结合他的官网以及源码慢慢琢磨吧.


尾巴

没太多可说的, 写多了就熟悉了.

文章目录
  1. 1. 引子
  2. 2. 正文
    1. 2.1. 简介
    2. 2.2. 安装
    3. 2.3. 库文件
    4. 2.4. 案例
    5. 2.5. 深入
  3. 3. 尾巴
|