boost::property_tree讲解

转自:http://viml.nchc.org.tw/blog/paper_info.php?CLASS_ID=1&SUB_ID=1&PAPER_ID=315


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

[cpp]  view plain copy
  1. struct ptree  
  2. {  
  3.   data_type data;  
  4.   list< pair > children;  
  5. };  
而在使用上呢,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 下,形式是:

[cpp]  view plain copy
  1. void read_xml( istream &stream, ptree &pt, int flags );  
  2. void read_xml( const string &filename, ptree &pt, int flags, const std::locale &loc );  
  3.    
  4. void write_xml( ostream &stream, const ptree &pt, const xml_writer_settings& settings );  
  5. 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 這個型別是被定義在  這個 header 檔裡,他的 namespace 是boost::property_tree,實際上的型別是 basic_ptree,也就是每一個節點的索引和值的型別都是 string的通用型的標準節點;如果有需要的話,應該也是可以使用額外的型別,不過這邊就不提了。(註 1)


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

[cpp]  view plain copy
  1. std::string sFilename = "a.xml";  
  2. boost::property_tree::ptree bPTree;  
  3. boost::property_tree::xml_parser::read_xml( sFilename, bPTree );  

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

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


[cpp]  view plain copy
  1. stringstream ss;  
  2. ss << "";  
  3. boost::property_tree::ptree bPTree;  
  4. boost::property_tree::xml_parser::read_xml( ss, bPTree );  

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

[cpp]  view plain copy
  1. boost::property_tree::ptree bPTree;  
  2. //.....  
  3. boost::property_tree::xml_parser::write_xml( "test.xml", bPTree );  

ptree 形式的 XML 的資料形式

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


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


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


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

[html]  view plain copy
  1. <root type="node">  
  2.   <element>valueelement>  
  3.   <size unit="cm" scale="1">  
  4.     <width>500width>  
  5.     <height>500height>  
  6.   size>  
  7. 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  的資料,他的  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: [] Value: []
      Name: [type] Value: [node]
    Name: [element] Value: [value]
    Name: [size] Value: []
      Name: [] 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( "root.element" );
string s2 = PTree.get( "root.size..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 的所有函式列表請參考官方文件(連結)。


你可能感兴趣的:(c++,boost)