跳到主要内容

常见的配置文件和数据交换格式

阅读量

0

阅读人次

0

从本质上来说,这是程序中数据结构的序列化和反序列化方法。

只不过作为配置文件,我们希望序列化的内容能够清晰直观富有层次感,用户可以不凭借其它工具,只用普通的文本编辑器打开就能知道有哪些内容。

而作为数据交换,常见于网络应用中,可能希望序列化的数据能够尽可能的小以节省带宽,或者也可能希望序列化的内容能够比较直观查看而便于调试开发。

当然,也有些特殊情况,比如配置文件使用二进制形式序列保存,数据交换使用 xml 格式。这里不做特殊讨论,重要的是根据经验结合实际情况使用这些工具。

常见的配置文件格式

INI

ini 文件可以说是最常见也是结构化最简单的配置文件格式了。这里以 Boost.PropertyTree 为例。

boost::property_tree::ptree ptree;
ptree.put("Monitor.Width", 1920);
ptree.put("Monitor.Height", 1080);
ptree.put("Monitor.Manufacturer", "Dell Inc.");
ptree.put("Monitor.IsOled", true);
boost::property_tree::write_ini("Monitor.ini", ptree);

ini 最多支持两级的层级关系,我们也可以这样写,效果是一样的:

boost::property_tree::ptree monitor;
monitor.put("Width", 1920);
monitor.put("Height", 1080);
monitor.put("Manufacturer", "Dell Inc.");
monitor.put("IsOled", true);

boost::property_tree::ptree ptree;
ptree.put_child("Monitor", std::move(monitor));
boost::property_tree::write_ini("Monitor.ini", ptree);

它们都会生成一个如下的 ini 文件:

[Monitor]
Width=1920
Height=1080
Manufacturer=Dell Inc.
IsOled=true

使用 Boost.PropertyTree 读取上述生成的 ini 文件。

struct Monitor {
int32_t width;
int32_t height;
std::string manufacturer;
bool isOled;
};

Monitor monitor;
boost::property_tree::ptree ptree;
boost::property_tree::read_ini("Monitor.ini", ptree);
monitor.width = ptree.get<int32_t>("Monitor.Width");
monitor.height = ptree.get<int32_t>("Monitor.Height");
monitor.manufacturer = ptree.get<std::string>("Monitor.Manufacturer");
monitor.isOled = ptree.get<bool>("Monitor.IsOled");

同样,我们也可以这样:

Monitor monitor;
boost::property_tree::ptree ptree;
boost::property_tree::read_ini("Monitor.ini", ptree);

const boost::property_tree::ptree &child = ptree.get_child("Monitor");
monitor.width = child.get<int32_t>("Width");
monitor.height = child.get<int32_t>("Height");
monitor.manufacturer = child.get<std::string>("Manufacturer");
monitor.isOled = child.get<bool>("IsOled");

ini 有一个弱点,就是它不支持数组。比如我有多个 Monitor,此时 ini 就无法表达了。

XML

XML 就很灵活了,也很强大,网上一搜就有很多介绍。这里我们直接以 Boost.PropertyTree 为例。

boost::property_tree::ptree ptree;
ptree.put("Monitor.Width", 1920);
ptree.add("Monitor.Height", 1080);
ptree.add("Monitor.Manufacturer", "Dell Inc.");
ptree.add("Monitor.IsOled", true);
boost::property_tree::write_xml("Monitor.xml", ptree);

读取 xml 的方法和 ini 基本是一样的,这得益于 Boost.PropertyTree 良好的封装抽象。输出如下:

<?xml version="1.0" encoding="utf-8"?>
<Monitor>
<Width>1920</Width>
<Height>1080</Height>
<Manufacturer>Dell Inc2.</Manufacturer>
<IsOled>true</IsOled>
</Monitor>

数组

如果我们保存数组结构数据,我们可以这样:

boost::property_tree::ptree ptree;

boost::property_tree::ptree screen;
screen.put("Width", 1920);
screen.add("Height", 1080);
screen.add("Manufacturer", "Dell Inc.");
screen.put("IsOled", true);
ptree.put_child("Computer.Screen", screen);

screen.put("Width", 1280);
screen.put("Height", 720);
screen.put("Manufacturer", "HP Inc.");
screen.put("IsOled", false);
ptree.add_child("Computer.Screen", screen);

boost::property_tree::xml_writer_settings<std::string> settings('\t', 1); // 格式化 xml 文件,为每个层级加上 \t 空白,便于查看
boost::property_tree::write_xml("Computer.xml", ptree, std::locale(), settings);

生成如下:

<?xml version="1.0" encoding="utf-8"?>
<Computer>
<Screen>
<Width>1920</Width>
<Height>1080</Height>
<Manufacturer>Dell Inc.</Manufacturer>
<IsOled>true</IsOled>
</Screen>
<Screen>
<Width>1280</Width>
<Height>720</Height>
<Manufacturer>HP Inc.</Manufacturer>
<IsOled>false</IsOled>
</Screen>
</Computer>

在上面的示例代码中,我们有用到(为方便表达,参数忽略):

boost::property_tree::ptree::put();
boost::property_tree::ptree::put_child();

boost::property_tree::ptree::add();
boost::property_tree::ptree::add_child();

其中 put 前缀的函数是 ptree 中如果不存在该 key,则创建,否则覆盖。add 前缀的函数是 ptree 中如果不存在该 key,则创建,否则则添加成为其他拥有相同 key 的值的同胞,也即该 key 将成为数组。

严格来说其实 XML 没有数组,只有层级关系。但是在程序开发中应当注意拥有一个结构良好易读的 XML 结构。

下面示例读取上述生成包含数组结构的 XML 文件:

boost::property_tree::ptree ptree;
boost::property_tree::read_xml("Computer.xml", ptree);
std::vector<Screen> screens;
for (auto &child : ptree.get_child("Computer")) {
if (child.first != "Screen") continue;
Screen screen;
screen.width = child.second.get<int32_t>("Width");
screen.height = child.second.get<int32_t>("Height");
screen.manufacturer = child.second.get<std::string>("Manufacturer");
screen.isOled = child.second.get<bool>("IsOled");
screens.push_back(screen);
}

attributes

另外,XML还支持属性设置,例如 <a href="https:/amass.fun"></a>href 就是 a 的一个属性。同样 Boost.PropertyTree 也支持

使用 <xmlattr> 可以添加属性,如果想生成注释,可以使用 <xmlcomment>

boost::property_tree::ptree ptree;

boost::property_tree::ptree screen;
screen.put("", "Display 1");
screen.put("<xmlattr>.Width", 1920);
screen.put("<xmlattr>.Height", 1080);
screen.put("<xmlattr>.Manufacturer", "Dell Inc.");
screen.put("<xmlattr>.IsOled", true);
ptree.put_child("Computer.Screen", screen);

screen.put("", "Display 2");
screen.put("<xmlattr>.Width", 1280);
screen.put("<xmlattr>.Height", 720);
screen.put("<xmlattr>.Manufacturer", "HP Inc.");
screen.put("<xmlattr>.IsOled", false);
screen.put("<xmlcomment>", "这是第二个显示器");
ptree.add_child("Computer.Screen", screen);

boost::property_tree::xml_writer_settings<std::string> settings('\t', 1); // 格式化 xml 文件,为每个层级加上 \t 空白,便于查看
boost::property_tree::write_xml("Computer.xml", ptree, std::locale(), settings);

会生成如下 XML 内容:

<?xml version="1.0" encoding="utf-8"?>
<Computer>
<Screen Width="1920" Height="1080" Manufacturer="Dell Inc." IsOled="true">Display 1</Screen>
<Screen Width="1280" Height="720" Manufacturer="HP Inc." IsOled="false">
Display 2
<!--这是第二个显示器-->
</Screen>
</Computer>

同样,我们对上述生成的 XML 文件进行读取:

ParseXmlAttr.cpp
boost::property_tree::ptree ptree;
boost::property_tree::read_xml("Computer.xml", ptree);

std::vector<Screen> screens;
for (auto &child : ptree.get_child("Computer")) {
if (child.first != "Screen")
continue;
std::cout << "Screen: " << child.second.get<std::string>("") << std::endl;
Screen screen;
screen.width = child.second.get<int32_t>("<xmlattr>.Width");
screen.height = child.second.get<int32_t>("<xmlattr>.Height");
screen.manufacturer = child.second.get<std::string>("<xmlattr>.Manufacturer");
screen.isOled = child.second.get<bool>("<xmlattr>.IsOled");
screens.push_back(screen);
}

注意

你有可能这么写:

boost::property_tree::ptree ptree;

boost::property_tree::ptree screen;
screen.put("Screen", "Display 1");
screen.put("Screen.<xmlattr>.Width", 1920);
screen.put("Screen.<xmlattr>.Height", 1080);
screen.put("Screen.<xmlattr>.Manufacturer", "Dell Inc.");
screen.put("Screen.<xmlattr>.IsOled", true);
ptree.add_child("Computer", screen);

screen.put("Screen", "Display 2");
screen.put("Screen.<xmlattr>.Width", 1280);
screen.put("Screen.<xmlattr>.Height", 720);
screen.put("Screen.<xmlattr>.Manufacturer", "HP Inc.");
screen.put("Screen.<xmlattr>.IsOled", false);
ptree.add_child("Computer", screen);

boost::property_tree::xml_writer_settings<std::string> settings('\t', 1);
boost::property_tree::write_xml("Computer.xml", ptree, std::locale(), settings);

以期待生成上面同样的文件,但实际上它是这样的:

<?xml version="1.0" encoding="utf-8"?>
<Computer>
<Screen Width="1920" Height="1080" Manufacturer="Dell Inc." IsOled="true">Display 1</Screen>
</Computer>
<Computer>
<Screen Width="1280" Height="720" Manufacturer="HP Inc." IsOled="false">Display 2</Screen>
</Computer>

是不是很意外?按照之前讲述的 add() 或者 add_child()的用法,上述代码其实是两个 Computer 的同胞,所以表现会这样,这是 Boost.PropertyTree 的处理。不过,虽然它的内容结构变化了,但是,我们使用 ParseXmlAttr.cpp 的代码同样也能反序列化一样的结果。这也是 XML 结构灵活的地方。

同时,我们也应注意到 key.,正式因为对它的解析处理,才使 Boost.PropertyTree 变得更加灵活。当然,同时也让它有时更加难以理解。

常见的数据交换格式

JSON

json 常用于数据交换,当然,也有用其作为配置文件的,比如 VS Code,它对 json 做了修改,允许在里面添加注释。

Boost.PropertyTree 也提供了生成 json 文件的支持,但是它序列化的内容只适合作为配置文件使用。一个明显的问题,就是数值类型的值,在序列化的时候,它都会当成字符串来看待保存。

Boost.JSON 是 Boost 中 json 的实现库,在作为数据交换时,应当使用它而不是 Boost.PropertyTree。

cJSON 是一个轻量级的 C 库,它可以让我们在纯标准 C 环境下使用。

nlohmann/json 是一个非常现代化和易于使用的高性能 C++ 库,使用时只需引入一个头文件即可,而且性能很高,也提供了很多实用性的功能特性。

Protocol Buffers

Protobuf 是 Google 开发的一个序列化工具。

对于嵌入式纯C,还有一个 nanopb 的项目,使得我们在纯C开发下面,也能够使用 Protobuf。

BSON

bson 是 json 的一个超集,json 中的 Value 无法保持 Binary,所以 bson 就扩展了这一点。

mongo-cxx-driver 提供了对 bson 的支持,同样 nlohmann/json 也对 bson 提供了支持。