资讯动态

C++项目用Boost.PropertyTree读写修改JSON的完整指南

发布时间:2026/10/17 4:13:25 来源:尧图企业网站定制
做C项目最常遇到的一个需求就是把JSON配置文件读进来、改一改、再写回去。我以前的第一反应是手写解析函数写过几回之后发现纯粹是给自己挖坑。后来系统用了Boost库里的PropertyTree才把这一摊事彻底理顺。这篇文章就讲一件事怎么用Boost.PropertyTree完成JSON的读、写、改以及哪些环节最容易让人绕弯路。它适合刚接触C、又不想为一个JSON功能引入太多依赖的朋友也适合用Boost已经有一段时间、但没怎么碰过json_parser的老手当作快速参考。1. 为什么在C项目里处理JSON我绕不开Boost.PropertyTree1.1 手写JSON字符串拼接的惨痛教训我最早处理JSON配置的方式是老老实实拼字符串。需求很简单也就是把几个配置字段拼成一个对象std::string buildConfig(const std::string host, int port) { return {\host\:\ host \,\port\: std::to_string(port) }; }这段代码在只有两个字段的时候还能看。等字段多起来嵌套对象一多代码就变成了转义地狱。引号、反斜杠、逗号的位置错一个整个文件就完了。而且你还没法马上发现得等下游程序或者目标系统解析失败才报错。最让人难受的是修改如果配置项的顺序调整或者中间多了一个数组字符串代码要改的地方非常多稍微漏掉一个逗号写出来的JSON就是非法的。后来我把这个做法形容成“碰运气式开发”本地调试能跑换一台机器、换一个编码环境就崩。所以当时我下定决心要么自己写一个解析器要么找一个成熟库。自己写解析器的成本其实很高因为.JSON语法看着简单真正要处理的坑包括字符串转义、Unicode编码、嵌套数组、数字和布尔值的区分、重复键名的处理。真要完整实现工作量不比项目本身小。1.2 Boost.PropertyTree能解决什么不能解决什么Boost.PropertyTree并不是一个专门为JSON设计的解析器它的定位更接近“通用属性树”。你可以把任意层次的数据组织成一棵树然后通过统一的读写接口输出成JSON、XML、INI这些格式。这一招在面对配置文件时特别好用因为配置项天然就是树状结构。它解决的核心问题有三个不用手动处理转义和缩进构造和输出都交给库内部完成提供路径式访问也就是可以用类似server.port的写法直接读到深层字段模板化的getType()接口能直接把字符串转成int、double、bool等类型。但它也有非常明显的边界。最重要的一点是它内部所有节点值实际上都存成字符串JSON里的数字和布尔在解析时不会保留“原生类型”的概念。也就是说读进来再写出去数字可能会被加上双引号这在某些对类型敏感的接口对接场景里是不能接受的。其次它也不擅长处理超大JSON毕竟底层是一棵带有字符串值的通用树解析性能和内存占用都比专门的数据结构库差一些。我的推荐是内部配置、中小体积的数据交换、快速原型用它很顺手但如果做严格意义上的接口对接协议特别是对外提供API还是要考虑更专业的JSON库。2. 写代码之前先弄明白这些东西能省一大半调试时间2.1 ptree到底是一棵什么样的树PropertyTree的核心类是boost::property_tree::ptree它的定义其实是basic_ptreestd::string, std::string。这行定义说了两件事键是std::string值也是std::string。这一点极其重要因为它决定了后面所有行为。你往节点里put一个int它会把整数转成字符串存进去读的时候再调用getint()把字符串解析成整数。正是因为这样JSON数组和嵌套对象在树里并不是“子值”而是“子节点”。每个ptree成员由一对组成first是键名second是另一个ptree。顶层对象的键名就是字段名数组元素则是一群键名为空的子节点。理解这个模型之后很多坑你就能提前避开。2.2 路径语法和三个最常用的接口路径用点号分隔例如config.server.host指的是顶层config节点的server子节点下的host节点。这个设计很直观但也会带来一个隐患如果字段名本身包含点号就会出现歧义我在第5部分会细聊。最常用的接口是这三个操作函数行为写入tree.put(a.b.c, value)沿路径创建节点并把值转为字符串存进去读取tree.getT(a.b.c, default)读取并转换为类型T失败时返回默认值取子节点tree.get_child(a.b.c)返回该路径下的子树用于遍历对象或数组这三个接口覆盖了日常90%的需求。在此基础上还有add_child用于挂子树erase用于删除节点push_back用于添加数组元素。2.3 环境准备其实很简单Boost.PropertyTree的JSON读写部分基本是Header-Only的使用的时候只需要包含头文件#include boost/property_tree/ptree.hpp #include boost/property_tree/json_parser.hpp如果你想用Qt、网络库之类的东西那是另一回事但单单读写JSON确实不需要链接额外的Boost库文件。安装Boost本身可以通过系统包管理器完成也可以用官方安装包只要编译器能找到头文件路径即可。我第一次用的时候只加了两行include编译一次性通过印象很深。3. 读入JSON从文件读、从字符串读、异常怎么排查3.1 从文件读取的标准姿势从文件读取JSON是配置文件场景的标配。基本代码长这样#include boost/property_tree/ptree.hpp #include boost/property_tree/json_parser.hpp #include iostream int main() { boost::property_tree::ptree tree; try { boost::property_tree::read_json(config.json, tree); } catch (const boost::property_tree::json_parser_error e) { std::cerr JSON解析失败: e.what() std::endl; std::cerr 文件: e.filename() , 行: e.line() std::endl; return 1; } std::string host tree.getstd::string(server.host, 127.0.0.1); int port tree.getint(server.port, 8080); std::cout host host std::endl; std::cout port port std::endl; return 0; }注意read_json的第一个参数可以是文件路径也可以是一个输入流。如果你需要先对文件内容做一些预处理比如替换某些模板变量那更建议走字符串入口先把文件读进std::string再解析。3.2 从字符串读取适合接口消息和内存数据项目里经常遇到的情况是JSON内容不是文件而是网络接口返回的一段字符串。这时候需要先把字符串放进std::stringstream再交给read_json#include sstream std::string jsonText R({ status: ok, data: { count: 42, tags: [cpp, boost] } }); std::stringstream ss(jsonText); boost::property_tree::ptree tree; boost::property_tree::read_json(ss, tree); std::string status tree.getstd::string(status); int count tree.getint(data.count); // 遍历数组 for (const auto tag : tree.get_child(data.tags)) { std::cout tag.second.get_valuestd::string() std::endl; }这里有个细节数组节点的first是空字符串所以遍历的时候不能依赖键名直接看second就行。这段代码在C11及以上都能编译。3.3 解析失败时别懵先看异常信息我见过很多人解析失败后第一反应是“我代码哪里写错了”其实大多数情况下是JSON文本本身有问题。可能是多了个逗号可能是字符串少了引号也可能是文件编码里带了不可见字符。json_parser_error这个异常里包含了文件、行号以及错误消息出现问题时第一步就是打出来看catch (const boost::property_tree::json_parser_error e) { std::cout e.what() std::endl; }如果是文件读取失败还需要检查文件是否真的存在、路径是否正确。有一个很偷懒但好用的办法先单独用浏览器或者命令行工具把JSON格式化一遍把格式化后的内容重新喂给程序。绝大多数手写JSON的错误在这个步骤里都会暴露出来。4. 写出与修改JSON构造树、更新数据、数组处理一条龙4.1 从零构造一个JSON并写出构造JSON比解析更简单核心就是put和add_child。假设要生成下面这个结构{ name: demo, version: 1.0, server: { host: 127.0.0.1, port: 8080 }, modules: [log, cache] }对应代码#include boost/property_tree/ptree.hpp #include boost/property_tree/json_parser.hpp #include iostream int main() { boost::property_tree::ptree tree; tree.put(name, demo); tree.put(version, 1.0); tree.put(server.host, 127.0.0.1); tree.put(server.port, 8080); boost::property_tree::ptree modules; for (const std::string m : {log, cache}) { boost::property_tree::ptree item; item.put(, m); // 数组元素的键为空值放这里 modules.push_back(std::make_pair(, item)); } tree.add_child(modules, modules); boost::property_tree::write_json(std::cout, tree); return 0; }这里特别说明一下数组的构造方式。PropertyTree没有“数组类型”它的实现方式就是往某个节点下面挂多个键为空的子节点。用push_back(std::make_pair(, item))把这些子节点按顺序放进去写出来的JSON就是数组。顺序由插入顺序决定所以不用担心乱序问题。4.2 修改已有JSON改值、删字段、新增子节点修改配置最常见的是改值。假设配置文件读进来之后发现端口需要从8080改到9090直接覆盖写入就行boost::property_tree::read_json(config.json, tree); tree.put(server.port, 9090); boost::property_tree::write_json(config.json, tree);put遇到路径不存在的节点时会自动创建路径上的所有节点。这很方便但需要注意如果路径中间某一段是数组节点这时可能覆盖掉原有的数组内容导致数据丢失。所以修改前最好用get_child_optional确认一下路径结构。删除字段用erasetree.erase(server.host);新增子节点和添加数组元素一样也是构造好子树之后add_child挂上去。这一套组合拳足够应付绝大多数“改配置”的需求。4.3 写JSON时最容易踩的坑数字被写成字符串回到2.1节强调的那个底层问题——ptree的值是std::string。这意味着write_json输出的时候它并不知道9090这个值原本是数字还是字符串于是会统一当成字符串处理。实际操作中你会看到这种输出{ server: { port: 9090 } }注意port的值带了双引号类型已经从数字变成了字符串。如果你写的JSON只给自己程序读那没事因为tree.getint(server.port)仍然会正确解析但如果这份JSON要交给别的语言、别的服务消费对方就可能因为字段类型不符合预期而拒绝。处理办法有几种最省事的做法是把字段约定成字符串下游也统一按字符串接或者干脆对类型敏感的字段做二次序列化也就是自己控制这部分输出。我自己的习惯是凡是配置类JSON统一约定为字符串业务层再做转换凡是跨语言协议我不用PropertyTree写而是交给更专业的JSON库去处理。了解这个限制之后你就能判断它是否适合你的项目。5. 我在实际项目里踩过的五个坑按排名排序5.1 数字精度丢失问题其实出在字符串转换第一次用put保存浮点数我写的是tree.put(server.weight, 0.1);读回来发现变成0.10000000000000001但更多时候是变成精度更差的字符串。原因不在PropertyTree本身而在于它内部用的转换方式。put会把值交给标准字符串转换处理默认的流精度通常只保留6位有效数字导致浮点数的有效数字截断。解决办法是自己控制格式化。比如你要保留足够精度就先用std::ostringstream和std::setprecision格式化好再放进去std::ostringstream oss; oss std::setprecision(17) value; tree.put(server.weight, oss.str());整数不受影响但浮点数一定要养成“先格式化再写入”的意识。5.2 中文被转义成\uXXXX文件读起来很痛苦用write_json输出包含中文的节点时输出文件里看到的是一串\uXXXX转义序列。比如{ message: \u4f60\u597d }转义后的内容在程序读回时没有任何问题解码出来的依然是中文但直接人工维护这种文件就很痛苦尤其是配置文件要经常手改。当时我在项目里遇到这个情况第一反应是找库的配置开关结果发现json_parser默认就是这样设计的没有提供友好的“不转义”选项。后来我把这部分逻辑绕过了不需要人工维护中文的接口数据直接保持转义没问题需要人工维护的配置文件我尽量只用ASCII字符中文内容单独放。从实际体验看这个方案最稳妥不建议自己去写反转义很容易处理出非法JSON。5.3 字段不存在时空指针念头默认值是好东西PropertyTree没有空指针但报异常的方式类似。用getT读取一个不存在的路径时会抛出ptree_bad_path路径存在但类型转换失败时会抛出ptree_bad_data。我以前写过这样的代码int port tree.getint(server.port);一旦配置文件里漏了server.port程序直接崩溃。后来我养成了两个习惯一个是对可选配置项全部给默认值另一个是先用get_child_optional判断节点是否存在auto child tree.get_child_optional(server.port); if (child) { // 存在才处理 }对程序健壮性来说这一步价值非常大。配置文件的字段齐全性是不可控的尤其是用户手动改过的配置文件。5.4 键名里带点号路径语法会打架路径用点号分隔那字段名本身如果叫a.b怎么办tree.get(a.b)会被拆成两个层级解析出来完全不是你想要的。这个问题在解析某些外部系统的JSON时偶尔会遇到它们的字段名里可能带有特殊字符。一个简单做法是走迭代器而不是路径语法直接拿到当前节点的所有子节点再逐个判断first是否等于想要的键名bool found false; for (const auto kv : tree) { if (kv.first a.b) { std::string value kv.second.get_valuestd::string(); found true; } }如果你的场景经常出现这类怪字段名可以在拿到原始JSON后先做个字段名映射把它们替换成合法的键再交给PropertyTree处理。5.5 修改数组元素别指望用路径一步到位数组在ptree里是一组键为空的子节点所以put(modules.0, x)这种写法是走不通的。要改数组里的某一个元素最直接的办法是清掉整个数组重新构造一遍或者通过迭代器定位到对应位置再改auto arr tree.get_child(modules); int index 0; for (auto it arr.begin(); it ! arr.end(); it, index) { if (index targetIndex) { it-second.put(, newValue); break; } }数组顺序很重要而PropertyTree内部保存的是插入顺序所以如果你在一个已经解析好的数组上做大量插入删除可能会出现顺序错乱的心智负担。我的建议很简单数组需要频繁增删时宁可整组重建也别试图在原有节点上做局部修补。这一点和手写JSON时的经验是一致的。6. 一个顺手的小Demo把配置文件读写、修改整个串起来为了把上面的内容融到一起我拿一个很常见的场景举例某跨平台桌面应用的配置文件管理。配置文件app_config.json初始内容如下{ client: { name: desktop-client, version: 2.0, serverAddr: 127.0.0.1, serverPort: 8080 }, features: { enableLog: true, logLevel: info }, plugins: [plugin_a, plugin_b] }程序启动时要检查版本号如果版本过低就自动把端口改成9090并追加一个插件然后写回文件#include boost/property_tree/ptree.hpp #include boost/property_tree/json_parser.hpp #include iostream #include string int main() { namespace pt boost::property_tree; pt::ptree tree; try { pt::read_json(app_config.json, tree); } catch (const pt::json_parser_error e) { std::cerr 读取配置失败: e.what() std::endl; return 1; } std::string version tree.getstd::string(client.version, 1.0); if (version 2.0) { std::cout 版本正常不做修改 std::endl; } else { tree.put(client.serverPort, 9090); std::cout 版本过低端口已改为9090 std::endl; } // 判断插件列表是否存在存在则追加一个插件 if (auto plugins tree.get_child_optional(plugins)) { pt::ptree newPlugin; newPlugin.put(, plugin_c); plugins-push_back(std::make_pair(, newPlugin)); } pt::write_json(app_config.json, tree); std::cout 配置文件已更新 std::endl; return 0; }这个例子基本覆盖了读、判断、修改、追加、写回这一整条链路。实际跑一遍就会发现整个过程比手拼字符串要稳得多尤其是处理数组和嵌套对象时代码量能省下一大半。我在自己的项目里把这一套基本固定下来了配置文件的字段全部按字符串约定读出来之后在业务层转成需要的类型配置里不直接放中文内容避免人工维护时看到一长串Unicode转义数组需要改动时优先整组重建。用Boost.PropertyTree这几年最深的体会就是它很适合做“程序的配置文件管家”别把它当成完整的JSON协议库去用。选择工具之前先搞清楚它的数据模型比硬背接口重要得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑