【C++】C++库nlohmann / json的使用

前言

作为一名本科实习生,因为在实习时需要完成c++读取并处理json文件的任务,所以在网上调研,最后选择了口碑一流,使用方便直观的nlohmann/json开源C++库,用于解析json。

也特此写一篇博客将json库的基础写出来,方便自己以后查找,还有就是分享给大家。

一、nlohmann/json库简述

1. 概述

nlohmann 是德国工程师 Niels Lohmann,以其名字为工程名的 nlohmann/json 项目,同时也叫JSON for Modern C++。
🏷️ 其他开发人员

Michael Hartmann
Stefan Hagen
Steve Sperandeo
Robert Jefe Lindstädt
Steve Wagner

Github上README.md也有很多详解示例和介绍,详情见链接:下载地址.

此篇主要面向未接触过 JSON 文件的新手,介绍如何快速上手使用 nlohmann 解析处理并生成 JSON 文件。

2. 优点

网上有无数开源的JSON解析库,每个库都有其存在的理由。个人认为使用nlohmann / json库有以下的优点:

  1. 直观的语法。类似于Python,JSON等第一类解释性语言,nlohmann/json 所有的运算符也实现了同样的感觉。详情可以看看下面的例子,你就知道我的意思了。

  2. 集成。nlohmann/json的整个代码仅由一个头文件json.hpp组成,没有其他库,没有子项目,没有依赖,没有复杂的构建系统。总之,调用起来完全不需要调整编译器或项目的设置。

  3. 模板化。每个JSON对象都有一个指针(union的最大大小)和一个枚举元素(1字节)。默认泛化是使用以下c++数据类型:

     std::string表示字符串,int64_t, uint64_t或double表示数字,std::map表示对象,std::vector表示数组,bool表示布尔值。但是,可以根据自己的需要对通用类basic_json进行模板化。
    
  4. 处理速度。当然还有更快的JSON库,例如cjson, rapidjson。但是,通过添加一个头文件支持来加快开发速度,那么nlohmann / json这个库就是第一选择。

3. 配置

我是用的VScode + C++11 + Clang++来完成编译的,使用方法很简单,就是下载并添加头文件

#include "json.hpp";
using namespace nlohmann;

注意Json.hpp库需要支持 C++11的编译环境,官方提供的已知支持编译器如下:

GCC 4.8 - 11.0 (and possibly later)
Clang 3.4 - 13.0 (and possibly later)
Apple Clang 9.1 - 12.4 (and possibly later)
Intel C++ Compiler 17.0.2 (and possibly later)
Microsoft Visual C++ 2015 / Build Tools 14.0.25123.0 (and possibly later)
Microsoft Visual C++ 2017 / Build Tools 15.5.180.51428 (and possibly later)
Microsoft Visual C++ 2019 / Build Tools 16.3.1+1def00d3d (and possibly later)

二、nlohmann/json库的基本操作

1. 读取 / 存储Json文件

操作需要适用于std::fstream或std::iostream的任何子类。
例子:

A. 读取JSON文件

// 读取JSON文件
std::ifstream i("在此填写json格式的文件路径,例如 d:\\test.json");
json j;
i >> j;

如果遇到中文路径读取问题可以更改windows设置:
1. 通过控制面板—-日期时间语言区域—-语言选项来修改系统默认编码
2. 或者更改VScode的编码语言
3. 使用STL中的locale类的静态方法

B. 写入JSON文件

// 将美化后的JSON写入另一个文件
std::ofstream o("pretty.json");
o << std::setw(4) << j << std::endl;

Remark:
setw(4) 是用于打印格式好看的 json 文件
使用 j.dump(4) 也是一样的效果

\quad

2. 创建并写入json结构

A. 创建一个空的JSON结构

json test;                  //创建一个空结构
json arr = json::array();   //表示空数组[]
json obj = json::object();  //表示空对象{}

B. 写入内容

方法一:

json j;
j["pi"] = 3.141;				// 存储double型
j["happy"] = true;				// 存储Boolean型
j["name"] = "Niels";   			// 存储string型
j["nothing"] = nullptr;			// 存储空对象
j["list"] = { 1, 0, 2 };		// 存储数组类
j["object"] = { {"currency", "USD"}, {"value", 42.99} }; // 存储对象

// 并可以在对象里嵌入对象
json inside = {
	{"pi", 3.141},
	{"happy", true},
  	{"name", "Niels"},
  	{"nothing", nullptr},
  	{"list", {1, 0, 2}}, 
  	{"object", {{"currency", "USD"},{"value", 42.99}}
};
j["object"].push_back(inside);								

\quad
方法二:

// 从字符串字面量来创建对象
json j1 = "{ \"happy\": true, \"pi\": 3.141 }"_json;
// 需要注意这种方法不会解析实际的对象,而只是存储整体的字符串

或者

auto j2 = R"(
  {
    "happy": true,
    "pi": 3.141
  }
)"_json;

三、nlohmann/json库的常用函数

1. 容器类函数

函数作用
push_back添加数据(构造和复制)
emplace_back添加数据(仅构造)
size得到json结构体的大小
empty检查json结构体是否为null
type得到json结构体的种类 (object/array)
clear将json结构体数据清空

示例:

json j;
j.push_back("foo");
j.push_back(1);
j.push_back(true); 		// 使用push_back创建一个数组

j.emplace_back(1.78);  // 也使用emplace_back

j.size();     // 4
j.empty();    // false
j.type();     // json::value_t::array
j.clear();    // 数组再次清空

2. 遍历/查找/修改等函数,算法

A. 遍历并打印json结构体:

for (json::iterator it = j.begin(); it != j.end(); ++it) {
  std::cout << *it << '\n';
  // 或者
  std::cout << it.key() << " : " << it.value() << "\n";
}

B. 从json结构体取数据或更改数据:

const auto tmp = j[0].get<std::string>();
j[1] = 42;
bool foo = j.at(2);

C. 类型检查:

j.is_null();
j.is_boolean();
j.is_number();
j.is_object();
j.is_array();
j.is_string();

D. 查找json结构体是否含有特定的条目

// 查找一个条目
if (o.contains("foo")) {
}

// 使用迭代查找一个条目
if (o.find("foo") != o.end()) {
}

// 简单计算特定条目的数量
int foo_present = o.count("foo"); 
int fob_present = o.count("fob");

// 删除一个条目
o.erase("foo");

E. 使用指针和补丁更改json结构体数据

// 	创建一个json
json j_original = R"({
  "num": ["one", "two", "three"],
  "test": "for_test"
})"_json;

// 	使用JSON指针访问成员
j_original["/baz/0"_json_pointer];            // 得到结果one
j_original["/baz/1"_json_pointer];            // 得到结果two
j_original["/test"_json_pointer];             // 得到结果for_test

// 	创建一个JSON补丁
json j_patch = R"([
  { "op": "replace", "path": "/num", "value": "empty" },
  { "op": "add", "path": "/hello", "value": ["world"] },
  { "op": "remove", "path": "/test"}
])"_json;

// 	应用这个补丁
json j_result = j_original.patch(j_patch);
// j_result:
// {
//    "num": "empty",
//    "hello": ["world"]
// }

//	比较两个json结构体,生成从参数1到参数2的转换补丁
json::diff(j_result, j_original);
// [
//   { "op":" replace", "path": "/num", "value": ["one", "two", "three"] },
//   { "op": "remove","path": "/hello" },
//   { "op": "add", "path": "/test", "value": "for_test" }
// ]

F. json结构体和类对象相互转换

// 创建一个类对象
struct Info_of_People{
    string name;
    string country;
    string age;
};

// 重载json的转换函数
void to_json(json& j, const Info_of_People& p) {
        j = json{
        		 {"name", p.name}, 
        		 {"country", p.country}, 
        		 {"age", p.age}
       	};
};

void from_json(const json& j, Info_of_People& p) {
        j.at("name").get_to(p.name);
        j.at("country").get_to(p.country);
        j.at("age").get_to(p.age);
};

// 转换示例如下:
auto j = R"([
	{"name": "小明","country": China,"age": 7},
	{"name": "Ton","country": American,"age": 9},
	{"name": "小姜","country": China,"age": 8}
])"_json;
std::vector people = j.get<:vector>>();

四、一些实用示例

1. 解决json库存储数据自动按照首字母顺序排列的问题

nlohmann / json生成的json结构体默认是按照首字母排序,但是我需要的是按照插入顺序来存储,也能便利之后的输出。

我这里是选用一个fifo_map的头文件,下载地址

fifo_map:   一个fifo顺序的c++关联容器

fifo_map包含:
   	一个std::unordered_map对象来存储键顺序,
 	一个指向Compare的对象指针。

使用方法:

#include "json.hpp"
#include "fifo_map.hpp"

using namespace std;
using namespace nlohmann;

// 使用fifo_map作为map的解决方案
template<class Key, class Value, class dummy_compare, class trans>
using my_workaround_fifo_map = fifo_map<Key, Value, fifo_map_compare<Key>, trans>;
using my_json = basic_json<my_workaround_fifo_map>;
using Json = my_json; 

//之后使用Json作为数据格式来声明就行
Json output;
output["projectname"] = "doc";
output["compiler version"] = "0.1.0";
output["showplot"] = 1;
output["displaytime"] = 1;

2. Json函数使用示例

Json同时可作为函数来输出Json格式的参数,做一些特定修改,示例:

Json seq_Export(int cseq){
            Json component_output;
            component_output = {
                {"type", type},
                {"name", name},
                {"seq", cseq}   
            };           
            return component_output;
};

3. 存储中文示例

注意这个库只支持UTF-8。当你在库中存储不同编码的字符串时,调用dump()可能会抛出异常,除非json::error_handler_t::replace或json::error_handler_t::ignore被用作错误处理程序。

对于中文字符的处理,可以使用更改系统的编码语言,但是这样就不具有普适性。所以我这里的处理方式,是先检测路径的的字符串是否含有中文,然后将GBK转为UTF8的格式,就能实现对中文的兼容了。

使用到的头文件和函数:

encode.cpp

#include "encode.h"
#define WINDOWS_OS __WIN32
#if WINDOWS_OS
#include <windows.h>
#include <wchar.h>

std::string GBKtoUTF8(const std::string& str)
{
    std::string strout = "";
    WCHAR * strGBK;
    int len = MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, NULL, 0);
    strGBK = new WCHAR[len];
    MultiByteToWideChar(CP_ACP, 0, str.c_str(), -1, strGBK, len);

    len = WideCharToMultiByte(CP_UTF8, 0, strGBK, -1, NULL, 0, NULL, NULL);
    char * strUTF8 = new char[len];
    WideCharToMultiByte(CP_UTF8, 0, strGBK, -1, strUTF8, len, NULL, NULL);

    //memcpy(str, strUTF8, strlen(strUTF8));

    strout = strUTF8;

    delete[] strGBK;
    strGBK = NULL;
    delete[] strUTF8;
    strUTF8 = NULL;

    return strout;
};

std::string UTF8toGBK(const std::string& strint)
{
    std::string strout = "";
    int len = MultiByteToWideChar(CP_UTF8, 0, strint.c_str(), -1, NULL, 0);
    unsigned short * wszGBK = new unsigned short[len + 1];
    memset(wszGBK, 0, len * 2 + 2);
    MultiByteToWideChar(CP_UTF8, 0, strint.c_str(), -1, (LPWSTR)wszGBK, len);

    len = WideCharToMultiByte(CP_ACP, 0, (LPWSTR)wszGBK, -1, NULL, 0, NULL, NULL);
    char *szGBK = new char[len + 1];
    memset(szGBK, 0, len + 1);
    WideCharToMultiByte(CP_ACP,0, (LPWSTR)wszGBK, -1, szGBK, len, NULL, NULL);
    //strUTF8 = szGBK;
    //memcpy(strout, szGBK, strlen(szGBK));

    strout = szGBK;

    delete[]szGBK;
    delete[]wszGBK;

    return strout;
};

#elif LINUX_OS_OS
#endif

encode.h

#ifndef ENCODE_H
#define ENCODE_H
#include "stdlib.h"
#include <iostream>
#include <string>

std::string GBKtoUTF8(const std::string &str);
std::string UTF8toGBK(const std::string &strint);

#endif //EMTPZ_CL_ENCODING_ISSUE_H

main.cpp

#include "encode.h"
#include "string.h"
#include "json.hpp"
#include <iomanip>
#include <fstream>
#include <iostream>

using namespace nlohmann;
using namespace std;

int IncludeChinese(char *str){
    char c;
    while(1){
        c=*str++;
        if (c==0) break; //如果到字符串尾则说明该字符串没有中文字符
        if (c&0x80) //如果字符高位为1且下一字符高位也是1则有中文字符
        if (*str & 0x80) return 1;
    }
    return 0;
}

int main(int argc, char *argv[]){
    //1.  read a JSON file
    json jsons;
    string path = argv[1];

    if(IncludeChinese(path.data())){
        path = GBKtoUTF8(path);
    };

    jsons["path"] = path;

    ofstream o1("test.json");
    o1 << setw(4) << jsons << endl;

    return 0;
}

总结

第一次写文章, 文章里面可能出现口水话或者错误, 但这都属于正常现象, 毕竟我也仅仅是想分享保存自己的项目经历和感受。愿读者们多多包涵,也希望可以一起交流学习!!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值