使用 Boost 的 Property Tree 處理 xml

本文详细介绍Boost库中的PropertyTree,一种通用树状数据结构,适用于处理XML、JSON等格式。文章讲解了如何使用该库解析XML文件,介绍了ptree的数据结构,并演示了如何通过路径进行数据读写。

Boost C++ Libraries 的 PropertyTree 這個函式庫(官方文件),基本上是一種通用型的樹狀資料結構的資料結構;在這棵資料樹裡面的每一個節點,都有它自己的資料、以及下方的成員清單。他每一個節點的內部資料結構,在概念上可以看成類似下面的形式:


struct ptree
{
  data_type data;
  list< pair<key_type, ptree> > children;
};

而在使用上呢,Property Tree 除了類似 STL container 的操作方法外,也有提供以 key(索引值)組合出來的路徑(path)來做資料存取的能力,功能算是滿強大的!另外,PropertyTree 除了提供這樣的資料結構外,也提供了 XML、INI、JSON 這幾種常見的檔案標種的 parser,讓程式開發者可以簡單地透過這個函式庫,來讀取/寫入這些檔案,把他們變成結構化的資料。


這一篇文章,基本上就是來講一下,要怎麼使用 PropertyTree 這個函式庫,來處理 XML 檔案了。而當然,這邊也會提到一些 PropertyTree 的資料結構的存取概念,所以理論上之後要用在其他格式上,問題應該也不大。



XML 的讀取/寫入


Boost PropertyTree 所提供的 XML Parser 基本上是基於 RapidXML 這個 OpenSource 的 XML parser 來的;而官方文件裡也有提到,他並沒有完整地支援 XML 所有的標準(例如他無法處理 DTD、也不支援 encoding 的處理),這是在使用上要注意的地方。不過對於一般使用來說,基本上應該是夠用了。


在使用上,Boost PropertyTree 在 boost/property_tree/xml_parser.hpp 這個 header 檔裡,提供了 read_xml() write_xml() 這兩種函式,可以將 XML 檔案(或者從 input stream)讀取成 Property Tree 定義的資料結構,也可以將 Property Tree 寫入到 XML 檔案(或指定的 output stream)。


這些函式都在 boost::property_tree::xml_parser 這個 namespace 下,形式是:


void read_xml( istream &stream, ptree &pt,
          int flags );
void read_xml( const string &filename, ptree &pt,
          int flags, const std::locale &loc );
 
void write_xml( ostream &stream, const ptree &pt,
          const xml_writer_settings& settings );
void write_xml( const string &filename, const ptree &pt,
          const std::locale &loc, const xml_writer_settings& settings );

read_xml() 來說,第一個參數就是資料來源,如果給他一個 string 的話,就是代表是一個檔案名稱,如果是一個 istream 的話,他就會以標準的 input stream(參考)的方法、來讀取資料;而讀取完成的資料,就會儲存在第二個參數、也就是型別為 ptree 的變數中。


ptree 這個型別是被定義在 <boost/property_tree/ptree.hpp> 這個 header 檔裡,他的 namespace 是 boost::property_tree,實際上的型別是 basic_ptree<string, string>,也就是每一個節點的索引和值的型別都是 string 的通用型的標準節點;如果有需要的話,應該也是可以使用額外的型別,不過這邊就不提了。(註 1)


實際使用呢,很簡單,就是:


std::string sFilename = "a.xml";
boost::property_tree::ptree bPTree;
boost::property_tree::xml_parser::read_xml( sFilename, bPTree );

這樣就可以把 a.xml 的內容,讀取到 bPTree 裡了!


而如果 XML 的資料來源不是檔案的話,只要可以轉換成 input stream 的形式,也都可以用這個 parser 來處理~例如 XML 已經是字串的話,就可以透過 STL 的 string stream 這樣寫:


stringstream ss;
ss << "<?xml ?><root><test /></root>";
boost::property_tree::ptree bPTree;
boost::property_tree::xml_parser::read_xml( ss, bPTree );

至於輸出的部分,其實也是類似的,只要改用 write_xml()、來源檔案變成要輸出的檔案,或是由 intput stream 變成 output stream 而已~下面就是一個簡單的例子:


boost::property_tree::ptree bPTree;
//.....
boost::property_tree::xml_parser::write_xml( "test.xml", bPTree );

 





ptree 形式的 XML 的資料形式


既然已經把 XML 的資料都塞到 ptree 這個形式的資料結構裡了,接下來,自然就是看要怎麼把他讀出來了~


基本上,ptree 這個資料結構,是代表 XML 中的單一元素(element),但是它本身不會記錄自己的名稱,而是把名稱交給自己的上一層來做紀錄。所以它基本上只會記錄自己的值,以及用一個 pair<string, ptree> list 來記錄屬於自己的其他資料,例如底下的 child element 和 attribute;其中,list 裡的每一個 pair 的第一項就是名稱、第二項則是以 ptree 形式來記錄的資料。


要注意的是,針對 XML 的資料來源,Property Tree 會有一些特別的索引值,像是所有的 attribute 都會被群組起來,放在一個名為 <xmlattr> ptree 底下;而註解的話,則會是名為 <xmlcomment> ptree 節點。


下面就是一個簡單的例子:


<root type="node">
  <element>value</element>
  <size unit="cm" scale="1">
    <width>500</width>
    <height>500</height>
  </size>
</root>

以上方的 XML 片段來說,當透過 Boost 的 Property Tree 的 XML Parser 來分析、以 ptree 的形式來做儲存的時候,它的結構大概會變成是下面這樣:



上面的示意圖裡,每一個方塊都代表一個 ptree 的節點,而上半部淺紫色的部分,是代表這個節點本身的資料(型別是 string),可以透過 data() 這個函式取得;下半部藍色的部分,則是這個節點的 child 的 list(裡面每一項的資料型別都是 pair< string, ptree>)的索引值、也就是 child 的名稱,基本上是以類似 STL container 的 iterator 的形式來做存取。


而右上方的虛線框的部分,則是代表這個節點的名稱,但是如同前面所說的,實際上個別節點並不會真的紀錄自己的名稱,名稱的部分實際上是以 pair 的第一個元素的形式,儲存在 parent 的 child list 中。也因此可以發現,實際上他會用一個沒有名稱、也沒有值的節點當作整棵樹的「根」,底下才是我們要的 XML 的資料。


 





資料讀取範例


概念講完了,實際操作是怎麼用呢?下面就是 Heresy 針對 ptree 寫的 operator<<,可以用在 STL 的 output stream 上;不過為了可以控制縮排、讓輸出比較漂亮,所以 Heresy 是有用 pair 的方法,再把 ptree 和目前是第幾層包成 pair< int, const ptree& > 的形式,第一項的 int 就是代表要縮排幾層而已。


ostream& operator<<( ostream& os, const pair<int, const ptree&>& rNode )
{
  int iNext = rNode.first + 2;
  const ptree& rPT = rNode.second;
  os << " Value: [" << rPT.data() << "]\n";
  for( auto it = rPT.begin(); it != rPT.end(); ++ it )
  {
    os.width( iNext );
    os << "";
    os << "Name: [" << it->first << "]";
    os << pair<int, const ptree&>( iNext, it->second );
  }
  return os;
}

其中綠底的部分,就是讀取 ptree 的資料的部分,而黃底的部分,則是用 iterator 的形式讀取 child list 的部分;其中,iterator 取得的每一項實際上都是型別為 pair<string, ptree> 的資料,他的 first 就是這個 child 的名稱(型別為 string), second 就是這個 child 的資料(型別為 ptree)。


而要使用的話,就是類似下面這樣:


xml_parser::read_xml( ssXML, PTree );
cout.fill( ' ' );
cout << pair<int, const ptree&>( 0, PTree ) << endl;

然後就會輸出成下面這樣的形式:


Value: []
  Name: [root] Value: []
    Name: [<xmlattr>] Value: []
      Name: [type] Value: [node]
    Name: [element] Value: [value]
    Name: [size] Value: []
      Name: [<xmlattr>] Value: []
        Name: [unit] Value: [cm]
        Name: [scale] Value: [1]
      Name: [width] Value: [500]
      Name: [height] Value: [500]

從這邊使用的例子應該可以看出來,實際上 ptree 的使用形式就是接近一般的 STL container iterator 的操作,如果對於 STL 使用算熟系的話,這邊要操作應該不會有太大的問題;而實際上,ptree 也有提供 find()push_back()erase() 這類 STL container 常見的函式,可以進行資料的操作,不過這邊就不提了,有興趣的可以自己玩看看。


 





指定路徑的資料存取


而除了這樣把整個 ptree 當作 STL container、用 iterator 的形式來掃之外,實際上 ptree 也有提供 get() 的函式,可以直接透過索引值的組合的字串(Property Tree 是把它稱為 path、也就是「路徑」)(註 2)、直接讀取 ptree 下面特定節點的資料。


string s1 = PTree.get<string>( "root.element" );
string s2 = PTree.get<string>( "root.size.<xmlattr>.unit" );

以上面的例子來說,s1 就會是 element 的值、也就是 values2 則會是 size 這個 element 的「unit」這個 attribute 的值,也就是「cm」。


而如果希望做型別轉換的話,也是可以在透過 get() 讀取時,讓他一起做的~例如:


float w = PTree.get<float>( "root.size.width" );

這樣的寫法,就會把 width 的 500 從字串轉換成浮點數,而 w 的值就會是 500.0f;如此一來,在讀取資料的同時,也就可以同時做好資料型別轉換的動作、算是非常方便的~


不過要注意的是,這種用法在找不到指定的路徑的值、或者型別無法正確轉換的時候,會丟出 exception 來,告訴你資料有問題、無法讀取。


如果想要避免 exception 的話,可以考慮加上第二個參數、來當作預設值,這樣在讀取不到資料的時候,就自動用所給的預設值來替代。


float d1 = PTree.get<float>( "root.size.depth", 500.0f );
float d2 = PTree.get<float>( "root.size.depth" );

像上面這樣的寫法,由於 XML 資料本身並不包含 depth 的資料,所以 d1 會是得到預設的 500.0f;但是 d2 的時候,則會因為找不到 depth,所以丟出 exception、中斷程式的執行。


除了這兩種方法外,其實 Property Tree 也還可以合併 Boost 的 Opetional 這個函式庫(文件),使用 get_optional() 這個函式、來讀取不一定存在的資料,不過這邊就先不提了。


而要寫入資料該怎麼辦呢?如果是要寫入 ptree 這個節點本身的資料的話,直接使用 put_value() 這個函式就可以了(基本上可以視作對應讀取用的 data());另外,他也可以像 get() 一樣,使用 put() 這個含式、直接去設定這顆樹下面某個節點的值。下面就是一把  width 的值從 500 改成 250 的例子。


PTree.put( "root.size.width", 250 );

另外要注意的是,在使用 put() 的時候,如果路徑指定的節點存在的話,他會把本來的值改掉;但是如果指定的節點不存在的話,他是會把這些本來不存在的節點建立出來,然後再賦予它指定的值的。


而相較於此,ptree 也還有提供一個 add() 的函式,可以強制建立出新的節點;也就是說,就算路徑所指定名稱的節點本來就已經存在了,他還是會再建立一個名稱一模一樣的節點。基本上,應該是不建議這樣使用啦~有興趣的話,可以自己玩看看。


而除了這些函式之外,Property Tree 也還有像是 get_child() put_child() 這類的函式,可以取得特定路徑的節點(ptree)、而非取得該節點的值,不過在這邊就不多提了。


 





恩,Boost 的 Property Tree 這篇就先寫到這了,基本上內容已經寫的比 Heresy 預期的多不少了…而整個寫完,也又發現不少 Heresy 在用的時候,沒有注意到的地方。


整體來說,Heresy 覺得 Boost 的 Property Tree 對於樹狀結構的資料存取來說,應該算是一個相當強大的資料結構,尤其是根據路徑來做存取的功能,在很多地方應該都算是滿實用的~而如果要拿來做 JSON、INI、XML 或是自訂格式的檔案之間的轉換、應該也會是滿實用的東西。


目前 Heresy 這邊應該是會先把它當作 XML 的 Paser 來用吧~之後可能也會再看看還有沒有什麼其他地方用的到, :)




附註:




  1. 除了使用 string 當作主要型別的 ptree 外,Property Tree 也還有使用 wstring wptree 可以用;另外,也還有無視大小寫的 iptree 以及 iwptree




  2. Property Tree 的路徑實際上也是另一種特殊型別 ptree::path_type,不過可以自動從 string 轉過去;他預設是用「.」來做索引值間的連接,如果需要特別的連接字元的話,就需要使用直接使用 path_type 的建構子來建立所需要的路徑。像下面的倆的 get() 的範例,實際上就是一樣的,只是第二行的例子是強制把索引值之間的連接字元設定為「/」。


    PTree.get<float>( "root.size.opt.depth" );
    PTree.get<float>( ptree::path_type( "root/size/opt/depth", '/' ) );



  3. ptree 的所有函式列表請參考官方文件(連結)。


### Boost Property Tree 的基本概念与用法 Boost.PropertyTree 是一个用于处理分层数据结构的库,支持多种格式的数据解析和存储,例如 INI、JSON 和 XML 文件。它提供了一种简单的方式来读取和写入配置文件中的键值对。 #### 创建和访问属性树 以下是创建并操作 `boost::property_tree` 的基础方法: ```cpp #include <boost/property_tree/ptree.hpp> #include <boost/property_tree/json_parser.hpp> #include <iostream> namespace pt = boost::property_tree; void createAndAccessPropertyTree() { // 创建一个空的 property tree 对象 pt::ptree tree; // 向 property tree 中添加子节点 tree.put("database.host", "localhost"); tree.put("database.port", 3306); tree.add("servers.server.name", "server1"); tree.add("servers.server.name", "server2"); // 访问 property tree 中的内容 std::string host = tree.get<std::string>("database.host"); // 获取字符串类型的值 int port = tree.get<int>("database.port"); // 获取整数类型的值 // 输出结果 std::cout << "Host: " << host << ", Port: " << port << "\n"; // 遍历 servers 节点下的所有 server 子节点 for (const auto& item : tree.get_child("servers.server")) { std::cout << "Server Name: " << item.second.get<std::string>("name") << "\n"; } } ``` 上述代码展示了如何向 `property_tree` 添加键值对以及如何遍历其内容[^5]。 --- #### 解析 JSON 或其他格式文件 Boost.PropertyTree 支持从不同格式的文件中加载数据,并将其转换为内部表示形式以便进一步处理。 以下是一个简单的例子,展示如何从 JSON 文件中读取数据: ```cpp #include <fstream> #include <boost/property_tree/json_parser.hpp> void readJsonFile(const std::string& filename) { using boost::property_tree::ptree; ptree tree; try { // 将 JSON 数据载入到 property tree std::ifstream json_file(filename); read_json(json_file, tree); // 提取消息字段 std::string message = tree.get<std::string>("message"); std::cout << "Message: " << message << "\n"; // 处理嵌套对象 double value = tree.get<double>("data.value"); std::cout << "Data Value: " << value << "\n"; } catch (std::exception& e) { std::cerr << "Error parsing JSON file: " << e.what() << "\n"; } } ``` 此函数演示了如何通过 `read_json` 函数将外部 JSON 文件导入到程序中[^6]。 --- #### 常见问题及其解决办法 1. **无法找到头文件** 如果编译器提示找不到 `boost/property_tree/ptree.hpp`,则可能是因为未正确设置 Boost 库路径。可以按照引用说明安装依赖项[^3]。 2. **优化选项冲突** 当启用某些特定优化选项时可能会遇到错误(如 `/ZI` 和 `/Ob1` 不兼容)。可以通过调整项目设置来避免此类问题[^4]。 3. **性能瓶颈** 使用静态链接可能导致较大的二进制文件大小;考虑动态链接以减少最终产品的体积。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值