简介
这章将讲述JSON文件的读写,使用的解析库是RapidJSON
这个库的评价和性能还是蛮好的,官方网站也有很详细的教程和文档,这里只记录一些比较基础的
第一个例子,对本专栏第一篇文章中讲到的英雄联盟数据进行简单的读写操作。
第二个例子,进行稍微复杂的读取操作,从JSON文件中读取俄罗斯方块的方块数据。
相关知识
首先非常清晰的明白个概念
JSON节点只有Object和Array两种类型。Object是一个键值对,key=>value, key必须是字符串,value可以是布尔值、整型等各种类型。
RapidJSON中的节点只有rapidjson::Value类型,泛指Object和Array,如果你换成JAVA,JS等其它语言,大多数分开的JSONObject或者JSONArray,这一点差异可能会带来一定的理解障碍,很多人觉得这点很奇怪,我觉得正好相反。
环境
下载
安装
因为RapidJSON只有头文件,用的时候用到include头文件
在Visual Studio里面项目属性如下图配置
准备数据
原文件非常复杂的,简单起见,这里只提取出部分数据,包含几种基本的数据类型
文件:./json/Riven.json
{
"id": "Riven",
"key": "92",
"name": "放逐之刃",
"title": "锐雯",
"skins": [ 1, 2, 3, 4, 5 ],
"stats": {
"hp": 560,
"mp": 0,
"movespeed": 340,
"armor": 33,
"spellblock": 32.1,
"attackrange": 125,
"crit": 0,
"attackdamage": 64,
"attackspeed": 0.625
}
}
我的电脑(中文Windows 10)上面创建.txt
时,默认编码是GB2312
,需要另存为UTF-8
基本步骤
- 从文件中读取数据
- 使用RapidJSON解析库进行解析,
- 将字符串转换成
rapidjson::Document
- 从
rapidjson::Document
查询变量
- 将字符串转换成
读(ASCLL)
用到的头文件
这里我们直接使用上一章从文件读取字符串的函数
#include "BaseFileIO.h"
//Read
#include "rapidjson/document.h"
//Write
#include "rapidjson/stringbuffer.h"
#include "rapidjson/writer.h"
#include "rapidjson/prettywriter.h"
为了方便解析,在读取文件之后我们直接交给Rapid进行解析,然后返回rapidjson::Document
以便后面获取变量。
rapidjson::Document GetDocument(const char* fileName) {
rapidjson::Document doc;
char* ptr = GetCharsV(fileName);
doc.Parse(ptr);
delete ptr;
return doc;
}
测试用例
void ReadJSON() {
//1.从文件中读取数据,将字符串转换成`rapidjson::Document`
const char* fileName = "./json/Riven.json";
rapidjson::Document doc = GetDocument(fileName);
if (!doc.IsObject()) {
cout << "Faild to valid JSON";
return;
}
//2.从`rapidjson::Document`获取变量
//拷贝到变量中使用
string id = doc["id"].GetString();
cout << "id:" << id << " characters size:" << id.size() << " id[0]:" << id[0] << endl;
cout << "key:" << doc["key"].GetString() << endl;
string name = doc["name"].GetString();
cout << "name:" << name << " characters size:" << name.size() << " key[0]:" << name[0] << endl;
cout << "title:" << doc["title"].GetString() << endl;
//遍历数组
cout << "skins:";
for (auto& kv : doc["skins"].GetArray()) {
cout << kv.GetInt() << ",";
}
cout << endl;
//深层变量
rapidjson::Value& vStats = doc["stats"];
rapidjson::Value& vHp = vStats["hp"];
rapidjson::Value& vSpellblock = vStats["spellblock"];
cout << "hp:" << vHp.GetInt() << endl;
cout << "spellblock:" << vSpellblock.GetDouble() << endl;
}
输出
MBaseIO::GetCharsV:
gcount:308 size:325 strlen:308 vec.size(): 326
{
"id": "Riven",
"key": "92",
"name": "鏀鹃€愪箣鍒?,
"title": "閿愰洴",
"skins": [ 1, 2, 3, 4, 5 ],
"stats": {
"hp": 560,
"mp": 0,
"movespeed": 340,
"armor": 33,
"spellblock": 32.1,
"attackrange": 125,
"crit": 0,
"attackdamage": 64,
"attackspeed": 0.625
}
}
id:Riven characters size:5 id[0]:R
key:92
title:閿愰洴箣鍒?characters size:12 key[0]:?
skins:1,2,3,4,5,
hp:560
spellblock:32.1
数值和ASCLL正常获取,中文显然又开始乱码了,要解决这个问题,不是简单的将GetChars换成GetWChars就行了。rapidjson默认的Document和Value是使用char作为解析单元的。
读(UTF-8)
这个RapidJSON有个“诡异”的地方,首先来看一下它Document和Value的定义
//! GenericValue with UTF8 encoding
typedef GenericValue<UTF8<> > Value;
typedef GenericDocument<UTF8<> > Document;
///
// UTF8
//! UTF-8 encoding.
/*! http://en.wikipedia.org/wiki/UTF-8
http://tools.ietf.org/html/rfc3629
\tparam CharType Code unit for storing 8-bit UTF-8 data. Default is char.
\note implements Encoding concept
*/
template<typename CharType = char>
struct UTF8;
Document和Value默认解码方式为UTF8,而UTF8又是使用char来存放。
我们都知道一个UTF-8码点的宽度是1到4个字节,char很显然不能满足,而wchar_t能够满足这个条件
为什么RaidJSON不用wchar_t而是char呢?
RapidJSON:Encoding
Character Type
As shown in the declaration, each encoding has a CharType template parameter. Actually, it may be a little bit confusing, but each CharType stores a code unit, not a character (code point). As mentioned in previous section, a code point may be encoded to 1–4 code units for UTF-8.
大概意思
每一个编码都有一个Character Type模板参数,实际上Character Type存储的是一个编码单位,而不是一个字符,在UTF8中一个字符可能会编码成1到4个char。
也就是说在rapidjson中,char 是UTF-8的编码单位,把char用作存储单位无可厚非,因为对数据的整体结构没有影响,解析库根据逗号,引号,分号这些来“断句”
关键问题是在查询变量的时候,GetString只能获得一个char类型的编码单位,而我们想要取出来的是完整的字符,而不是三分之一的字符,所以这个地方,如果任然使用rapidjson默认的UTF8的Character Type就会导致这种情况的发生。
我的解决方法就是,任然使用UTF-8进行解码,但是将存储单位换成wchar_t,最终将GetDocument升级成GetWDocument
typedef rapidjson::GenericDocument<rapidjson::UTF8<wchar_t> > WDocument;
typedef rapidjson::GenericValue<rapidjson::UTF8<wchar_t> > WValue;
WDocument GetWDocument(const char* fileName) {
WDocument doc;
wchar_t* ptr = GetWCharsV(fileName);
doc.Parse(ptr);
delete ptr;
return doc;
}
新的测试用例
不一定全部都要用wcout,忘记改了
void ReadWJSON() {
const char* fileName = "./json/Riven.json";
WDocument doc = GetWDocument(fileName);
if (!doc.IsObject()) {
cout << "Faild to valid JSON";
return;
}
wstring id = doc[L"id"].GetString();
wcout << L"id:" << id << "\tid.size()" << id.size() << "\tid[0]:" << id[0] << L"\n";
wcout << L"key:" << doc[L"key"].GetString() << L"\n";
wstring name = doc[L"name"].GetString();
wcout << L"name:" << name << L"\tname.size():" << name.size() << L"\tname[0]:" << name[0] << L"\n";
wcout << L"title:" << doc[L"title"].GetString() << L"\n";
cout << "skins:";
for (auto& kv : doc[L"skins"].GetArray()) {
cout << kv.GetInt() << ",";
}
cout << endl;
WValue& vStats = doc[L"stats"];
WValue& vHp = vStats[L"hp"];
WValue& vSpellblock = vStats[L"spellblock"];
cout << "hp:" << vHp.GetInt() << endl;
cout << "spellblock:" << vSpellblock.GetDouble() << endl;
//更改之后保存
const char* newName = "./json/Riven-Update.json";
vHp.SetInt(100000);
SaveWRapidJson(doc, newName);
}
输出
MBaseIO::GetWCharsV:
gcount:296 size:325 wcslen: 296 vec.size(): 326
{
"id": "Riven",
"key": "92",
"name": "放逐之刃",
"title": "锐雯",
"skins": [ 1, 2, 3, 4, 5 ],
"stats": {
"hp": 560,
"mp": 0,
"movespeed": 340,
"armor": 33,
"spellblock": 32.1,
"attackrange": 125,
"crit": 0,
"attackdamage": 64,
"attackspeed": 0.625
}
}
id:Riven id.size()5 id[0]:R
key:92
name:放逐之刃 name.size():4 name[0]:放
title:锐雯
skins:1,2,3,4,5,
hp:560
spellblock:32.1
MBaseIO::OverWriteUTF8:
wcslen(content):393
写(ASCLL)
这个过程也是比较简单的,就是把Document先转换成char然后在调用上一章里面保存char的函数去进行保存
bool SaveRapidJson(rapidjson::Document& doc, const char* fileName) {
//1.Document=>char*
rapidjson::StringBuffer buffer;
rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
doc.Accept(writer);
const char* output = buffer.GetString();
//2.char*=>file
OverWriteFile(fileName, output);
return true;
}
StringBuffer类似于vector
在rapidjson中,PrettyWriter和Writer都可以用来保存数据
Writer是一个handle,accept将自己交给writer处理,writer将doc的字符串输入给buffer
最后将buffer中的字符串保存到文件,完成JSON的写出
Writer输出的是没有缩进和换行的字符串,适合于程序内部和网络传输,具有同样功能的PrettyWriter可以输出缩进和换行,但看起来有些冗长
测试用例
//从零构建一个JSON DOM并写入文件
void WriteJSON() {
//1.初始化Document
rapidjson::Document doc;
doc.SetObject();
//2.添加内容
doc.AddMember("key1", "string", doc.GetAllocator());
//Object
rapidjson::Value key2(rapidjson::kObjectType);
key2.AddMember("key2.1", 123, doc.GetAllocator());
key2.AddMember("key2.2", true, doc.GetAllocator());
doc.AddMember("key2", key2, doc.GetAllocator());
doc.AddMember("key3", 1.23, doc.GetAllocator());
//Array
rapidjson::Value key4(rapidjson::kArrayType);
for (int i = 1; i < 9; i++) {
key4.PushBack(i, doc.GetAllocator());
}
doc.AddMember("key4", key4, doc.GetAllocator());
//3.保存到文件
SaveRapidJson(doc, "./json/WriteJSON.json");
}
Document和Value使用默认构造函数构造,类型都是Null,需要SetXXX()或者直接赋值设置类型
输出
{
"key1": "string",
"key2": {
"key2.1": 123,
"key2.2": true
},
"key3": 1.23,
"key4": [
1,
2,
3,
4,
5,
6,
7,
8
]
}
写(UTF-8)
这个地方和之前读的时候的思路是一样的,既然把Document都换成了以wchar_t为编码单位的WDocument,那么要想对其进行保存,必然要将所有涉及到的类都进行响应修改
typedef rapidjson::GenericStringBuffer<rapidjson::UTF8<wchar_t> > WStringBuffer;
typedef rapidjson::PrettyWriter<WStringBuffer, rapidjson::UTF8<wchar_t>, rapidjson::UTF8<wchar_t>> WPrettyWriter;
bool SaveWRapidJson(WDocument& doc, const char* fileName) {
WStringBuffer buffer;
WPrettyWriter writer(buffer);
doc.Accept(writer);
const wchar_t* output = buffer.GetString();
OverWriteUTF8(fileName, output);
return true;
}
测试用例
在上面读取UTF-8文件的测试用例中你可能已经发现了,最下面有一段代码
//更改HP后之后保存
const char* newName = "./json/Riven-Update.json";
vHp.SetInt(100000);
SaveWRapidJson(doc, newName);
输出
这地方用RapidJson自带的格式化输出方法还是有些诡异,数组全部都给变成单列的了,我暂时没有去找怎么改这个。
{
"id": "Riven",
"key": "92",
"name": "放逐之刃",
"title": "锐雯",
"skins": [
1,
2,
3,
4,
5
],
"stats": {
"hp": 100000,
"mp": 0,
"movespeed": 340,
"armor": 33,
"spellblock": 32.1,
"attackrange": 125,
"crit": 0,
"attackdamage": 64,
"attackspeed": 0.625
}
}
RapidJson还有很多东西,这里就不一一列举了,查看官方文档是最佳选择。
综合案例
俄罗斯方块大家都玩过,甚至也自己制作过,网上有很多的代码,包括我自己也写过,大多数都是将方块数据在代码中硬编码,这是可行的,但是在现代游戏中,很多游戏数据都是存储在外存,当游戏需要时动态进行加载和卸载。
下面我们就单纯的从JSON文件中,加载俄罗斯方块的方块数据。
准备数据
{
"Blocks": [
[
[
0,0,0,0,
0,1,0,0,
0,1,0,0,
0,1,1,0
],
[
0,0,0,0,
0,0,0,0,
0,0,1,0,
1,1,1,0
],
[
0,0,0,0,
0,1,1,0,
0,0,1,0,
0,0,1,0
],
[
0,0,0,0,
0,0,0,0,
1,1,1,0,
1,0,0,0
]
],
[
[
0,0,0,0,
0,0,1,0,
0,1,1,0,
0,0,1,0
],
[
0,0,0,0,
0,0,0,0,
1,1,1,0,
0,1,0,0
],
[
0,0,0,0,
0,1,0,0,
0,1,1,0,
0,1,0,0
],
[
0,0,0,0,
0,0,0,0,
0,1,0,0,
1,1,1,0
]
],
[
[
0,0,0,0,
0,0,0,0,
0,1,1,0,
1,1,0,0
],
[
0,0,0,0,
0,1,0,0,
0,1,1,0,
0,0,1,0
]
],
[
[
0,0,0,0,
0,0,0,0,
0,1,1,0,
0,1,1,0
]
],
[
[
0,1,0,0,
0,1,0,0,
0,1,0,0,
0,1,0,0
],
[
0,0,0,0,
0,0,0,0,
0,0,0,0,
1,1,1,1
]
]
]
}
请注意,在Blocks内部,第一层是形状的数量,第二层是方块的元素,0表示无,1表示有。
方块是4*4的矩阵,JSON文件中使用了一维数组表示二维数组,在程序中是一个三维的数组,存储若干个二维矩阵。为什么不分形状进行存储,这一点以后有时间会说。
过程基本上都在注释里面了,解析的过程可能有点复杂
//4*4方块矩阵
const int BLOCK_LENGTH = 4;
void ReadBlocks() {
const char* fileName = "./json/Blocks.json";
rapidjson::Document doc = GetDocument(fileName);
if (!doc.IsObject()) {
cout << "Faild to valid JSON";
return;
}
//获取节点
rapidjson::Value& vBlocks = doc["Blocks"];
//形状数量
const int shapeNum = vBlocks.Size();
//形状的方块数量
int* blockNum = new int[shapeNum];
//形状范围
int* sumNum = new int[shapeNum];
//初始化二维数组
for (int i = 0; i < shapeNum; i++) {
blockNum[i] = 0;
sumNum[i] = 0;
}
//方块数量
int count = 0;
for (int i = 0; i < shapeNum; i++) {
//每个形状对应的角度数量
blockNum[i] = vBlocks[i].Size();
sumNum[i] += 0;
count += blockNum[i];
}
//初始化三维数组[13][BLOCK_LENGTH][BLOCK_LENGTH]
int*** blocks = new int** [count];
for (int i = 0; i < count; i++) {
blocks[i] = new int* [BLOCK_LENGTH];
for (int j = 0; j < BLOCK_LENGTH; j++) {
blocks[i][j] = new int[BLOCK_LENGTH];
for (int k = 0; k < BLOCK_LENGTH; k++) {
blocks[i][j][k] = 0;
}
}
}
//将文件中的数组读取到三维数组
int n = 0;
for (int i = 0; i < shapeNum; i++) {
for (int j = 0; j < blockNum[i]; j++) {
rapidjson::Value matrix = vBlocks[i][j].GetArray();
for (int k = 0; k < static_cast<int>(matrix.Size()); k++) {
int row = static_cast<int>(floor(k / BLOCK_LENGTH));
int col = k % BLOCK_LENGTH;
blocks[n][row][col] = matrix[k].GetInt();
}
n++;
}
}
//查看现在三维数组中的数据
for (int i = 0; i < count; i++) {
for (int j = 0; j < BLOCK_LENGTH; j++) {
for (int k = 0; k < BLOCK_LENGTH; k++) {
cout << blocks[i][j][k] << " ";
}
cout << std::endl;
}
cout << std::endl;
}
//释放
for (int i = 0; i < count; i++) {
for (int j = 0; j < BLOCK_LENGTH; j++) {
delete[] blocks[i][j];
}
delete[] blocks[i];
}
delete[] blocks;
delete[] blockNum;
delete[] sumNum;
}
输出
0 0 0 0
0 1 0 0
0 1 0 0
0 1 1 0
0 0 0 0
0 0 0 0
0 0 1 0
1 1 1 0
0 0 0 0
0 1 1 0
0 0 1 0
0 0 1 0
0 0 0 0
0 0 0 0
1 1 1 0
1 0 0 0
0 0 0 0
0 0 1 0
0 1 1 0
0 0 1 0
0 0 0 0
0 0 0 0
1 1 1 0
0 1 0 0
0 0 0 0
0 1 0 0
0 1 1 0
0 1 0 0
0 0 0 0
0 0 0 0
0 1 0 0
1 1 1 0
0 0 0 0
0 0 0 0
0 1 1 0
1 1 0 0
0 0 0 0
0 1 0 0
0 1 1 0
0 0 1 0
0 0 0 0
0 0 0 0
0 1 1 0
0 1 1 0
0 1 0 0
0 1 0 0
0 1 0 0
0 1 0 0
0 0 0 0
0 0 0 0
0 0 0 0
1 1 1 1
OK,会使用JSON读写文件之后,基本上已经能搞定很多数据存储问题了。