Hello, Boost.PropertyTree!

追記 (2010/03/08)

find()の戻り値で、見つかったかどうかの判定をit != pt.end()ではなくit != pt.not_found()と書くようになり、find()の戻り値の型もiteratorからassoc_iteratorに変更されています。この辺りは、Boost 1.42.0時点ではドキュメントが古いままなので注意してください。

Boost.PropertyTreeでXML読み込み - Faith and Brave - C++で遊ぼう

上記のように,find() のインターフェースに変更があったようです.早とちりしてしまったのですが,iterator → assoc_iterator, end() → not_found() になった訳ではなく,find() の戻り値に関してのみ assoc_iterator や not_found() を利用する形になったようです.

サンプルコードも変更のあった部分はコメントアウトして修正しました.

本文

まだリリースされていないようですが,Boost C++ Libraries の次期バージョン (ver. 1.41.0) に,XML, JSON, Windows レジストリなどのデータを扱うための PropertyTree と言うライブラリが追加されるそうです.面白そうだったので,cvs からダウンロードしてきて一足先に使ってみる事にしました.

今回は,サンプルプログラムとして RapidXML - Life like a clown で作成したものを PropertyTree を使用して書き変えてみる事にしました.やっている事は,XML ファイルを読み込んで,タブ文字などで整形したものを標準出力に再出力すると言うものです.

#include <cstdlib>
#include <iostream>
#include <string>
#include <limits> // インクルードしないとコンパイラに怒られた
#include <exception>

/* ------------------------------------------------------------------------- */
/*
 *  gcc (cygwin) に wstring が存在しないため暫定処理.
 *  最終的には,BOOST_NO_CWCHAR を定義すれば良くなる模様.
 *  wstring が定義されているコンパイラの場合は,コメントアウト.
 */
/* ------------------------------------------------------------------------- */
namespace std {
    typedef basic_string<wchar_t> wstring;
}

#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>

/* ------------------------------------------------------------------------- */
//  child_count
/* ------------------------------------------------------------------------- */
template <class PTree>
size_t child_count(const PTree& x) {
    return x.size() - x.count("<xmlattr>");
}

/* ------------------------------------------------------------------------- */
//  ptree_format
/* ------------------------------------------------------------------------- */
class ptree_format {
public:
    typedef std::string string_type;
    typedef boost::property_tree::ptree ptree_type;
    typedef ptree_type::key_type key_type;
    typedef ptree_type::value_type value_type;
    
    explicit ptree_format() : level_(0) {}
    
    void run(ptree_type::const_iterator pos) {
        string_type indent;
        for (int i = 0; i < level_; ++i) indent += '\t';
        const ptree_type& node =  pos->second;
        
        // 1. タグ名の出力.
        std::cout << indent << '<' << pos->first;
        
        // 2. 属性 (attribute) の出力.
        //if (node.find("<xmlattr>") != node.end()) { // ver. 1.42.0 で変更!
        if (node.find("<xmlattr>") != node.not_found()) {
            this->attributes(node.find("<xmlattr>"));
        }
        
        // 3. テキスト,または子ノードの出力.
        if (child_count(node) == 0 && node.data().empty()) {
            std::cout << " />" << std::endl;
        }
        else if (child_count(node) == 0) {
            std::cout << ">" << node.data() << "</" << pos->first << ">" << std::endl;
        }
        else {
            std::cout << '>' << std::endl;
            for (ptree_type::const_iterator child = node.begin(); child != node.end(); ++child) {
                if (child->first == "<xmlattr>") continue;
                ++level_;
                this->run(child);
                --level_;
            }
            std::cout << indent << "</" << pos->first << '>' << std::endl;
        }
    }
    
private:
    int level_;
    
    //void attributes(ptree_type::const_iterator pos) { // ver. 1.42.0 で変更!
    void attributes(ptree_type::const_assoc_iterator pos) {
        const ptree_type& node = pos->second;
        for (ptree_type::const_iterator attr = node.begin(); attr != node.end(); ++attr) {
            std::cout << ' ' << attr->first;
            if (!attr->second.data().empty()) {
                std::cout << "=\"" << attr->second.data() << '"';
            }
        }
    }
};

/* ------------------------------------------------------------------------- */
//  main
/* ------------------------------------------------------------------------- */
int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "ptree_format filename" << std::endl;
        std::exit(-1);
    }
    
    boost::property_tree::ptree root;
    boost::property_tree::read_xml(argv[1], root);
    
    // 全要素を出力.
    ptree_format fmt;
    fmt.run(root.begin());
    
    return 0;
}

サンプルファイルとして http://kaalus.atspace.com/ptree/doc/index.html に掲載されてある XML を読み込んだ結果は以下の通りです.

[example]$ g++ -Wall ptree_format.cpp
[example]$ ./a foo.xml 
<debug>
        <filename>debug.log</filename>
        <modules>
                <module>Finance</module>
                <module>Admin</module>
                <module>HR</module>
        </modules>
        <level>2</level>
</debug>

ptree は,あるキー (key_type) に対応するデータ (data_type) と子ノードを保持しています.子ノードは,キーと ptree のペア (value_type) であり,begin(), find() などのメソッドはこのペア (value_type) への iterator を返します.

PropertyTree では,入力元のデータを(ライブラリ内で)共通の木構造に展開した結果をユーザに渡すため,ユーザは元のデータ形式が何であるかと言う事をあまり深く考えることなく利用できるようになっています.例えば,上記のサンプルプログラムを「INI ファイル整形プログラム」として使おうとした場合,以下の 2行を修正するだけで完了します.

[example]$ diff ptree_format.cpp ptree_format_ini.cpp 
19c19
< #include <boost/property_tree/xml_parser.hpp>
---
> #include <boost/property_tree/ini_parser.hpp>
97c97
<       boost::property_tree::read_xml(argv[1], root);
---
>       boost::property_tree::read_ini(argv[1], root);

XML の属性の扱い

PropertyTree では,「キー」,「データ」,「子ノード」の 3要素のみを保持します.したがって,このままでは XML の属性 (attribute) を扱うことができません.そこで,PropertyTree は "" と言う特別なキーを定義することによって,XML の属性を扱えるようにしています.

The adopted solution is to add special keys in property tree that hold the extra information. In case of XML, attributes of each key are stored as subkeys of a special key called (the angle brackets are necessary to avoid collision with normal XML keys). For example this XML:

<a href="index.html"><b>Index</b></a>

will be stored as the following tree:

a                          ;'a' key (no data)
{
    <xmlattr>		   ;special <xmlattr> subkey (no data)
    {
        href "index.html"  ;'href' attribute and its value
    }
    b "Index"              ;'b' key and its data
}

Of course, different ways of storing extra information are possible. This one has been chosen because it allows easy access and iteration on the data. For example, to retrieve href attribute from key a, one would just write

get<type>("a.<xmlattr>.href").
http://kaalus.atspace.com/ptree/doc/index.html

これに関連する注意事項として,PropertyTree ではあるキーが持つ XML の属性 (attribute) 全体を一つの子ノードと見なして展開します.そのため,あるノードが子ノードを持つかどうかを単純に empty() メソッドや size() メソッドを用いて判断すると問題になる場合があります.あるノードが(XML 上での)子ノードを持つかどうかを判断するためには,以下のように記述する必要があります.

template <class PTree>
bool has_children(const PTree& x) {
    return (x.size() - x.count("<xmlattr>") > 0);
}

ある特定のキーに対応するデータを取得する方法

PropertyTree では,ある特定のキー(パス)に対するデータを取得する方法として以下の 3通りが提供されています.

float v = pt.get<float>("a.path.to.float.value");
float v = pt.get("a.path.to.float.value", -1.f);
boost::optional<float> v = pt.get_optional<float>("a.path.to.float.value");

これらの違いは,引数に指定したパスが存在しないときの挙動で,最初の方法では例外が投げられます.それに対して,2, 3番目の方法では,それぞれデフォルト値(第2引数),初期化されていない boost::optional の値が返ります(※と思ったのですが,boost::optional の方法でも例外が投げられているような気が・・・ 私の勘違いでした).get の使い方については,公式ページに掲載されているサンプルプログラムも参考にして下さい.

ちなみに,XML のパーサはデフォルトでは RapidXml が用いられるようです.尚,現在の cvs にコミットされているバージョンだと Lurker - Database message source pull failure で指摘されている個所が修正されておらず,コンパイルエラーが発生します(gcc version 4.3.2 の場合).コンパイルを通すには,上記にしたがって boost/property_tree/detail/xml_parser_read_rapidxml.hpp を修正する必要があります.