当前位置: 首页 > article >正文

Rapidjson 实战

Rapidjson 是一款 C++ 的 json 库. 支持处理 json 格式的文档. 其设计风格是头文件库, 包含头文件即可使用, 小巧轻便并且性能强悍. 本文结合样例来介绍 Rapidjson 一些常见的用法.

环境要求

有如何的几种方法可以将 Rapidjson 集成到您的项目中.

  1. Vcpkg安装: 使用 vcpkg install rapidjson即可. 如果不熟悉 vcpkg 请参考我的文章: [C++包管理工具-Vcpkg 简介]({{< relref “2024-07-29-cpp-package-management.md” >}}).

  2. CMakeFetchContent_Declare方法.

    # 引入 FetchContent 模块
    include(FetchContent)
    
    # 设置 Rapidjson 编译选项
    set(RAPIDJSON_BUILD_TESTS OFF CACHE INTERNAL "")
    set(RAPIDJSON_BUILD_DOC OFF CACHE INTERNAL "")
    set(RAPIDJSON_BUILD_EXAMPLES OFF CACHE INTERNAL "")
    set(RAPIDJSON_BUILD_CXX20 ON CACHE INTERNAL "")
    
    FetchContent_Declare(
        rapidjson
        URL https://github.com/Tencent/rapidjson/archive/refs/tags/v1.1.0.zip
    )
    FetchContent_MakeAvailable(rapidjson)
    
  3. 源码安装: 下载源码并将其路径加入include目录列表中: gcc -I /path/to/rapidjson

基础用法

解析 json

auto input = R"({"name": "华安", "id": 9527})";
rapidjson::Document doc;

doc.Parse(input);
if (doc.HasParseError()) {
  return -1;
}

访问元素

检查并获取

HasMember 查询 key 是否存在, 然后使用Is方法来判断类型是否兼容, 最后用Get方法来获取对应的值.

if (doc.HasMember("name") && doc["name"].IsString()) {
  std::string name = doc["name"].GetString();
  std::cout << "name is: " << name << std::endl;
}
if (doc.HasMember("id") && doc["id"].IsInt()) {
  int id = doc["id"].GetInt();
  std::cout << "id is: " << id << std::endl;
}
使用FindMember减少查询开销

上述示例中, doc["name"]被使用了两次, 相当于创建了两个临时变量. 使用FindMember方法则可以减少这种额外开销.

if (auto it = doc.FindMember("name");
    it != doc.MemberEnd() && it->value.IsString()) {
  std::string name = it->value.GetString();
  std::cout << "name is: " << name << std::endl;
}

if (auto it = doc.FindMember("id");
    it != doc.MemberEnd() && it->value.IsInt()) {
  auto id = it->value.GetInt();
  std::cout << "id is: " << id << std::endl;
}
访问对象(Object)

查询方法与前面的基础类型相似. 需要注意的是, GetObject()方法返回的是一个const引用.
Rapidjson 为了提高效率, 接口的设计上避免使用对象拷贝.

auto response =
    R"({"code":200,"data":{"total":200,"curr":[12345,23456,34564]}})";

rapidjson::Document doc;

if (doc.Parse(response).HasParseError()) {
  return -1;
}

if (auto it = doc.FindMember("data");
    it != doc.MemberEnd() && it->value.IsObject()) {
  const auto& data = it->value.GetObject();
  // ...
}
访问数组(Array)

我们用IsArray()GetArray()来判断和获取对应的数据.

需要注意的是: json 中的数组是允许多个不同类型的, 如下是一个合法的 json:

{
  "array": ["string", true, null, [], {}, 123]
}

但是 C++ 的数组或者容器vector仅支持相同的元素, 所以我们在获取数组元素时需要注意判断元素类型.

for (auto it = curr.Begin(); it != curr.End(); ++it) {
  if (it->IsInt()) {
    std::cout << it->GetInt() << std::endl;
  }
}

由于 rapidjson 支持range based for, 我们可以这样写:

for (const auto& item : curr) {
  if (item.IsInt()) {
    std::cout << item.GetInt() << std::endl;
  }
}

生成 json 对象

基础类型

对于基础类型(整型, 布尔值, 浮点数)我们可以直接使用AddMember添加, 需要注意的是接口中需要指定一个Allocator.

rapidjson::Document doc(rapidjson::kObjectType);
doc.AddMember("name", "华安", doc.GetAllocator());
doc.AddMember("id", 9527, doc.GetAllocator());
doc.AddMember("is_intern", true, doc.GetAllocator());

此时doc的内容为:

{ "name": "华安", "id": 9527, "is_intern": true }

为了减少对GetAllocator()的调用, 可以使用一个变量保存该结果, 见后续代码.

添加对象

一个 Object 对象可以用rapidjson::Value表示. 其添加成员的方法是AddMember(rapidjson::Documentrapidjson::Value的衍生类).

对于特殊值null, 我们可以使用SetNull()方法或者在构造函数中指定rapidjson::kNullType来实现.

rapidjson::Value contact(rapidjson::kObjectType);

rapidjson::Value email;
email.SetNull();  // 设置为null
contact.AddMember("email", email, allocator);

contact.AddMember("twitter", rapidjson::Value(rapidjson::kNullType),
                  allocator);

doc.AddMember("contact", contact, allocator);

此时的doc为:

{
  "name": "华安",
  "id": 9527,
  "is_intern": true,
  "contact": { "email": null, "twitter": null }
}

添加数组

Array 类型的创建和添加如下所示.

auto& allocator = doc.GetAllocator();

rapidjson::Value friends(rapidjson::kArrayType);
friends.PushBack("祝枝山", allocator);
friends.PushBack("文征明", allocator);
friends.PushBack("徐祯卿", allocator);

doc.AddMember("friends", friends, allocator);

此时doc为:

{
  "name": "华安",
  "id": 9527,
  "is_intern": true,
  "contact": { "email": null, "twitter": null },
  "friends": ["祝枝山", "文征明", "徐祯卿"]
}

序列化 json 对象

#include <rapidjson/document.h>
#include <rapidjson/filewritestream.h>
#include <rapidjson/writer.h>

#include <iostream>

void print(rapidjson::Value& value) {
  rapidjson::StringBuffer buffer;
  rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
  value.Accept(writer);
  std::cout << buffer.GetString() << std::endl;
}

进阶用法

使用函数模板简化解析

从前面解析的例子我们可以看到, 对每一个字段都要解析代码, 这样会存在很多的代码冗余.

可以通过模板函数来实现一个解析代码. 我们用std::variant来存储不同的解析类型, 比如:int*, double*, std::string*等.

接着我们用std::visit来访问std::variant, 针对不同类型做不同的解析, 对目前尚不支持的类型则报错.

template <typename... T>
bool Parse(rapidjson::Value& data, const char* name,
           std::variant<T...>& target) {
  auto it = data.FindMember(name);
  if (it == data.MemberEnd()) {
    std::cerr << "key not found: " << name << std::endl;
    return false;  // 字段不存在
  }

  // 使用 std::visit 处理 std::variant
  return std::visit(
      [&](auto value) {
        using ValueType = std::remove_pointer_t<decltype(value)>;

        if constexpr (std::is_same_v<ValueType, std::string>) {
          if (!it->value.IsString()) {
            std::cerr << "string not match: " << name << std::endl;
            return false;  // 类型不匹配
          }
          *value = std::string(it->value.GetString(), it->value.GetStringLength());
        } else if constexpr (std::is_integral_v<ValueType> ||
                             std::is_floating_point_v<ValueType>) {
          if (!it->value.Is<ValueType>()) {
            std::cerr << "integer not found: " << name << std::endl;

            return false;  // 类型不匹配
          }
          *value = it->value.Get<ValueType>();
        } else {
          std::cerr << "unsupported type\n";
          return false;  // 不支持的类型
        }

        return true;  // 解析成功
      },
      target);
}

如何使用呢? 此处以解析一个结构体为例:

struct Person {
  bool married = false;
  int id = 0;
  int age = 0;
  double point = 0;
  std::string name;
  std::string email;
};

bool ParsePerson(Person* person, rapidjson::Value& json) {
  std::vector<
      std::tuple<const char*, std::variant<int*, std::string*, double*, bool*>>>
      list = {
          {"id", &person->id},           {"age", &person->age},
          {"name", &person->name},       {"email", &person->email},
          {"married", &person->married}, {"point", &person->point},
      };

  for (auto& [name, variant] : list) {
    if (!Parse(json, name, variant)) {
      return false;  // 解析失败
    }
  }

  return true;  // 解析成功
}

完整示例请参考仓库代码: parse.cpp

处理多重嵌套

在工作中我们有时候会遇到嵌套很深的 json 文档. 比如给定这样一个 json 文档, 现在我们要获取/data/avatar/image/thumbnail如何操作?

{
  "code": 200,
  "data": {
    "avatar": {
      "image": {
        "medium": "https://image.com/hua.an.jpg",
        "thumbnail": "https://image.com/hua.an-thumbnail.jpg"
      }
    }
  }
}

如果按照之前的写法层层解析, 那么必然是个很深的嵌套. 但是现在有个更好的解决办法, 就是JSONPath, 在 rapidjson 里面就是 Pointer类, 参考如下写法:

rapidjson::Document doc;

if (doc.Parse(response).HasParseError()) {
  std::cerr << "JSON parse error!" << std::endl;
  return -1;
}

// 使用 RapidJSON 的 Pointer 解析 JSONPath
const char* jsonpath = "/data/avatar/image/thumbnail";
rapidjson::Pointer pointer(jsonpath);

// 获取 JSONPath 对应的值
if (rapidjson::Value* value = pointer.Get(doc)) {
  if (value->IsString()) {
    std::cout << "Thumbnail URL: " << value->GetString() << std::endl;
  } else {
    std::cerr << "Thumbnail is not a string!" << std::endl;
  }
} else {
  std::cerr << "Thumbnail not found!" << std::endl;
}

完整的代码请参考: jsonpath.cpp

总结

本文通过示例介绍了一些 rapidjson 的使用方法, 包括解析,生成,以及如何做代码优化. 希望能给读者带来一些帮助.

如果您觉得有用, 希望您点赞收藏关注, 感激不尽.

源码链接

  • 源码链接
  • Rapidjson 官网
  • Rapidjson Pointer

http://www.kler.cn/a/534308.html

相关文章:

  • Python-基于PyQt5,Pillow,pathilb,imageio,moviepy,sys的GIF(动图)制作工具
  • 算法随笔_39: 最多能完成排序的块_方法2
  • 【学术投稿-2025年计算机视觉研究进展与应用国际学术会议 (ACVRA 2025)】从计算机基础到HTML开发:Web开发的第一步
  • 深入浅出:频谱掩码 Spectral Masking —— 噪音消除利器
  • 【贪心算法篇】:“贪心”之旅--算法练习题中的智慧与策略(三)
  • 什么是LPU?会打破全球算力市场格局吗?
  • Spring @EventListener 注解:让应用更加模块化和可扩展
  • Java面试题基础篇2:10道基础面试题
  • Docker深度解析:容器与容器局域网
  • 5-Scene层级关系
  • 机器学习--python基础库之Matplotlib (2) 简单易懂!!!
  • 【centOS】安装docker环境,替换国内镜像
  • 分布式光伏监控解决方案-并网柜保护装置
  • gym-anytrading
  • 作业二.自定义数据集使用scikit-learn中的包实现线性回归方法对其进行拟合
  • 算法--最长回文子串
  • Github 2025-02-05 C开源项目日报 Top9
  • 堆(Heap)的原理与C++实现
  • Java 大视界 -- Java 大数据在智能安防中的应用与创新(73)
  • NacosRce到docker逃逸实战
  • vulnhub DC-3
  • 一文解释pytorch 中的 squeeze() 和 unsqueeze()函数(全网最详细版)
  • Docker基础以及单体实战
  • Node.js 与 PostgreSQL 集成:深入 pg 模块的应用与实践
  • 基于Ceph14对接openstack的Nova、Glance、Cinder服务为后端存储
  • [权限提升] Linux 提权 — 系统内核溢出漏洞提权