3. C++实现高性能json解析库

一、前言

json是一个非常常见的格式,目前广泛应用于web数据传输、软件配置文件等等。

但可惜C++标准库中并不存在解析json数据的库,所以想要在C++中解析json格式的数据,目前最好的办法便是使用第三方库,比如很火的nlohmann,就是一个非常优秀且好用的json格式解析库。

相比之下,Qt中的那个解析库是真的难用。

正好最近有空,我也写了一个json数据的生成与解析器,该解析器的目标:

  • 精简:只有一个头文件与一个源文件,目前只有400行左右的代码量。
  • 高效:一个2M大小的json文件(格式化后为8M),nlohmann处理需要2s左右,而我这库只需要0.5秒左右。
  • 简单:使用起来与python代码相似。

比如我要解析下面这段数据:

string str = R"(
{
	"name":"test",
	"number":10.987,
	"array":[
		1.0,
		3.45,
		9.80
		],
	"object":{
		"name":"yushi",
		"age":100
		"sex":"man"
		}
}
)";

使用我这个库的代码为:

	TJson t; //声明一个对象
	t.Parse(str); //解析

就是这么简单

想要访问或修改,那么可以这样写:

	t["object"]["age"] = 50; //修改
	cout << t["object"]; //访问

输出:

{"age": 50,"name": "yushi","sex": "man"}

看上去是不是相当的简单!

虽然看起来不错,不过目前还是有缺点的,那就是暂不支持UTF-8编码,不过如果文件数据中只有英文字母、数字、标点符号,那便无所谓文件编码了,都可以用。

亲手写一个json数据格式的解析器应该是一个不错的学习体验,所以我这里将其作为一个教程文章写出来。

二、了解json格式

json只有5个数据类型,分别为:

  1. number(数字)
  2. boolean(布尔)
  3. string(字符串)
  4. list(列表)
  5. object(对象)

那么在C++中,这五种数据类型则可以分别对应以下五种数据类型:

  1. double
  2. bool
  3. string
  4. vector
  5. map

其中stringvectormap这三个严格来说应该不算数据类型,而是C++中的容器。

对于C++中的map而言,其键可以为任意类型,但对于json中的对象,其键只能为字符串,所以我们后面还需要将其限制一下。

但如果直接暴露以上这5个数据类型给别人使用,那未免太繁琐了,所以我们需要将这5个数据类型进行封装,让其统一成为一个数据类型:TJson

Tiny json的简写,当然你可以自行将其换成其它自己想用的名字,这无所谓。

重点是我们如何才能将5个数据类型统一为一个数据类型?

三、统一数据类型:TJson

统一数据类型,实际上就是对上面五种数据类型进行封装,然后在内部用一个变量来判断当前是什么数据类型:

#include<iostream>
#include<map>
#include<vector>
using namespace std;
class TJson {
	enum class Type {
		NOT, DOUBLE, STRING, BOOL, LIST, MAP
	};

	Type m_type; //记录当前的数据类型
	
	//五种基本数据类型
	double m_double;
	string m_str;
	vector<TJson> m_list;
	map<string,TJson> m_map;
	bool m_bool;
};

注意,由于Json中数组与对象中,其值类型是不定的,所以这里要用我们这里的TJson类型作为模板参数类型。

这里的NOT枚举变量,代表当前为不确定类型,以后我们就通过m_type值来识别当前为什么数据类型。

因为我们的TJson类代表了五种任意类型,因此使用该类作为模板参数,就可以存储这五种数据类型的任意一种了。

完成了上面这一步,我们还需要初始化,也就是写构造函数:

public:
	explicit TJson() 
		: m_double(0), m_bool(false), m_type(Type::NOT) 
	{
	}
	explicit TJson(double number) 
		:TJson()
	{
		m_double = number;
		m_type = Type::DOUBLE;
	}
	explicit TJson(bool boolean)
		:TJson()
	{
		m_bool = boolean;
		m_type = Type::BOOL;
	}
	explicit TJson(const string& str)
		:TJson()
	{
		m_str = str;
		m_type = Type::STRING;
	}
	explicit TJson(const vector<TJson>& list) 
		:TJson()
	{
		m_list = list;
		m_type = Type::LIST;
	}
	explicit TJson(const map<string, TJson>& map) 
		:TJson()
	{
		m_map = map;
		m_type = Type::MAP;
	}

过程很简单,就是分别进行赋值,并通过m_type 来区分当前代表什么类型。

注意这里初始化列表的写法,如果不理解为什么这样写,可以查看另一篇文章:初始化列表

还有就是前面的那个explicit 关键字,很重要,这是为了避免自动隐式转换的。

比如,由于写了这个类的bool类型的构造函数,那么当一个函数需要TJson作为参数时,你可以直接填bool

void Test(TJson tj) {
}
Test(true); //如果没有explicit关键字,则可以正常调用

因为传入true时,会自动调用TJson的布尔构造函数,构造出一个TJson之后再传入。

但这种行为必须要进行避免,因为后面我们将重载等于运算符,如果不对这种行为进行禁止,那么使用t["num"] = 50; 这样的语句时,会自动将50构造成一个TJson对象,导致其本该调用的等号重载函数不会得到调用。

因为=右边的不是数字,而是一个对象,参数不符

这样我们的类就封装好了,只是缺少两个最重要的功能:解析数据访问数据

四、解析json数据

首先来看看如何解析json数据。

解析这一步很重要,也很繁琐,首先需要注意的一个点是:JSON格式的数据是可以嵌套的。

比如一个列表中的元素,可以继续为一个列表,或者一个对象类型。

基于嵌套的这一点,我们首先可以想到的就是递归,即:让函数可以自己调自己,已解决这种嵌套数据的解析。

嵌套之前,我们需要先写好基本数据类型的解析函数,比如字符串数字布尔这三个类型。

为了方便,我们需要一个存放原始数据的缓存区,以及该缓存区的长度。

比如:

	char* m_data; //the data
	int m_len; //the data's size

现在假设,m_data已经指向了原始json数据缓存区,而m_len则代表这个缓存区的长度。

可以直接它们直接放在类里面,作为私有变量,后面的单个解析函数同理作为私有函数,只需要为用户放出一个parse的统一解析函数即可。

首先是字符串:

TJson ParseString(int& beg) {
	int i = beg;
	while (i != m_len && m_data[i] != '\"') i++; //find "
	i++; //remove "
	int tmp = i; //the string's begin
	for (; i < m_len; i++) {
		if (m_data[i] != '\"') continue;
		//the string's end
		beg = i;
		return TJson(string(m_data + tmp, m_data + i));
	}
	throw "parse string error";
}

注意这里的参数与返回值,参数为引用数据类型,其目的是传入我们这个函数应该从m_data缓存区中的哪里开始进行解析,并在解析结束后,将本函数解析的终点,再通过这个参数传出去,而返回值则是我们的解析结果,包含stringTJson类。

因为json中的字符串是在两个引号之间,所以我们要做的,就是找到两个引号,然后取出其中的内容即可。

过程很简单,看代码应该也能看明白,而且有注释。

只是注意最后的一句:

throw "parse string error";

如果函数执行到了这里,就说明解析失败了,我们就直接抛出异常,这里抛出的是字符串类型。

所以可以通过以下代码来处理这个异常:

	try
	{
		int begin = 0;
		ParseString(begin);
	}
	catch (const char* errStr)
	{
		cout << errStr;
	}

然后是解析布尔类型:

TJson ParseBoolean(int& beg) {
	int i = beg;
	while (i != m_len && m_data[i] != 't' && m_data[i] != 'f') i++;
	if (i + 4 > m_len) throw "parse boolean error";
	if (m_data[i] == 't') {
		if (m_data[i + 1] == 'r' && m_data[i + 2] == 'u' && m_data[i + 3] == 'e') {
			beg = i + 3;
			return TJson(true);
		}
		throw "parse boolean error";
	}
	else {
		if (i + 5 > m_len) throw "parse boolean error";
		if (m_data[i + 1] == 'a' && m_data[i + 2] == 'l' && m_data[i + 3] == 's' && m_data[i + 4] == 'e') {
			beg = i + 4;
			return TJson(false);
		}
		throw "parse boolean error";
	}
}

这里的代码就要长一些了,因为json中的布尔类型是通过truefalse这两个字符串来表示的。

所以我们这个函数的目的就很明确了,通过在beg处开始找这两个字符串,如果找到就构造TJson并返回,否则抛出异常,处理方法与上面相同。

还有解析数字:

TJson ParseNumber(int& beg) {
	int i = beg;
	while (i != m_len && m_data[i] != '+' && m_data[i] != '-' && !isdigit(m_data[i])) i++;
	bool sign = true;
	if (m_data[i] == '+') {
		i++;
	}
	else if (m_data[i] == '-') {
		sign = false;
		i++;
	}
	int j = i;
	while (isdigit(m_data[j]) || m_data[j] == '.') j++;
	beg = j;
	if (!sign) return TJson(-atof(m_data + i));
	else return TJson(atof(m_data + i));
}

逻辑就是找符号(+或-)以及数字作为开始解析的地方,并通过找不是数字与.的位置作为终点,使用函数atof进行转换,并将终点位置传回。

isdigit函数可以判断这个字符是否为数字,这是C标准库里面的函数,可以直接用。

解决了上面三个基础的解析,下面就可以来看看我们重头戏了:解析数组与对象。

两个函数的形式与上面是一致的:

TJson ParseArray(int& ben);
TJson ParseObject(int& beg);

不同之处在于,因为数组与对象中的内容可以仍为数组与对象,所以这两个函数就需要开始进行递归了。

大部分json数据,都是只有一个数组或对象,只不过其内部继续嵌套了数组与对象,才导致看起来很复杂。

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux