SHOEISHA iD

※旧SEメンバーシップ会員の方は、同じ登録情報(メールアドレス&パスワード)でログインいただけます

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

特集記事

高速軽量XMLパーサ:RapidXMLを触ってみた

  • X ポスト
  • このエントリーをはてなブックマークに追加

 年に数本、ちょっとしたツール作成の依頼が舞い込みます。こんなこともあろうかと、文字コードの変換やマルチスレッドなど、よく使うライブラリを1つのディレクトリにまとめた"おどうぐばこ"を用意しています。先日XMLを読み取って加工する小さなツールを頼まれました。"おどうぐばこ"にはXMLパーサの定番:Apache Xerces-Cが入ってはいるのですが、Xerces-Cは高機能な分だけライブラリが大きいために小さなツールに使うのがためらわれ、もっと軽量なXMLパーサを探して「RapidXML」に辿り着きました。

  • X ポスト
  • このエントリーをはてなブックマークに追加

 RapidXMLのページから拾ってきたversion 1.13のZIPファイルを解いて出てきたのはヘッダが4本、HTMLドキュメントとライセンス条項……これだけ? なんともそっけない構成ですがヘッダを#includeするだけで使えるみたい、軽量ライブラリの鑑です。ヘッダのタイムスタンプが2009年5月で、きょうびのコンパイラが素直に食ってくれるか少々不安でしたが、お試しコードをVisual C++ 2017が、するりと呑んでくれたので一安心。

parseのキホン

 RapidXMLでXMLをパースするのは、とっても簡単。xml_documentを用意し、メンバ関数:parse()にXML文字列を食わすだけ。

list-01 simple.cpp
#include <rapidxml.hpp>

#include <iostream>
#include <string>

using namespace std;
// 以降、RapidXMLの名前空間: rapidxml を rx と略記する
namespace rx = rapidxml;

int main() {
  string xml = R"(<function include="<cstdio>"><return type="int"/>printf</function>)" 
                "\0"; // '\0'終端を保証する
  rx::xml_document<> doc;
  doc.parse<0>(&xml[0]); // 引数は char*

  // ルート直下にある <function> ノード および その属性について
  rx::xml_node<>* node = doc.first_node("function");
  rx::xml_attribute<>* attr = node->first_attribute();

  // 名前と値を出力する
  cout << "node : " << node->name() << "=[" << node->value() << "]\n";
  cout << "attr : " << attr->name() << "=[" << attr->value() << "]\n";
  cout << endl;

  for (char ch : xml) { cout << (ch != '\0' ? ch : '#'); }
  cout << endl;
}
fig-01
fig-01

 "function"と名付けられたノード、およびそのノードに付随した"include"属性(attribute)の名前と値を出力しています。

 ちょっと不思議なふるまいを見せていますね、ノード:xml_nodeのメンバ関数:value()が、そのノードが内包する子ノードのうち、最初に見つかったテキスト・ノードの値:"printf"を返しています。

 また、parse()に食わせたXML文字列を('\0'を'#'に置き換えて)出力してみると、ところどころ'\0'を埋め込んだり"&lt;"/"&gt;"を"<"/">"に置換したりしています。 RapidXMLはパーサに与えた文字列をデータ領域として流用するんです(そのためparse()の引数はconst char*じゃなくchar*)。RapidXMLに食わせた文字列はパーサを使い終わるまで開放したり再利用したりしちゃダメです。

 ということはつまり、テキスト・ファイルに書かれたXMLをパースするときは、入力となるテキスト・ファイルのナカミを丸ごと文字領域に保持しておかにゃなりません。ヘッダ:<rapidxml_utils.hpp>にはファイルのナカミを保持してくれるヘルパ・クラス:fileが定義されています。コレを使ってXMLファイルをパースし、ルート・ノード:person直下の子ノードを列挙してみます。

list-02 parse_file.cpp
#include <rapidxml.hpp>
#include <rapidxml_utils.hpp> // rapidxml::file

#include <iostream>

using namespace std;
namespace rx = rapidxml;

int main() {
  rx::xml_document<> doc;
  rx::file<> input("trial.xml");
  doc.parse<0>(input.data());

  rx::xml_node<>* node = doc.first_node("person");
  for ( rx::xml_node<>* child = node->first_node(); 
        child != nullptr; 
        child = child->next_sibling() ) {
    cout << child->name() << " : [" << child->value() << "]\n";
  }
}
list-03 trial.xml
<?xml version="1.0" encoding="shift_jis"?>
<person gender="female" age="52">
  フネ
  <person gender="female" age="24">
    サザエ
    <person gender="male" age="4">タラ</person>
  </person>
  <person gender="male" age="11">カツオ</person>
  <person gender="female" age="9">ワカメ</person>
  <cat gender="male" age="?">タマ</cat>
</person>
fig-02
fig-02

 テキストの前後に余計な空白がくっついたままですが、これを取り除くこともできます。parse()のテンプレート引数でパース・オプションを指定できて、parse<rapidxml::parse_trim_whitespace>(input.data());すると、このとおり。

fig-03
fig-03

 パース・オプションは1ダースほど定義されています(マニュアルをご一読ください)。

 RapidXMLは文字コードの変換はやってくれません。このサンプルでは食わせたtrial.xmlがshift-jisなので(Windowsでは)問題ありませんが、XMLはUTF-8で書かれることが多いので適宜文字コードの変換が必要になります。ためしにUTF-8のファイルを食わせてみたら、当然ながらバケバケでした。

fig-04
fig-04

 さらに、パース時にエラーを検出するとparse_error例外がthrowされます。parse_errorからはエラーの理由:what()と、その位置:where()が得られます。where()が返すのは、文字列上でエラーと判定された位置(ポインタ)です。

list-04 parse_error.cpp
#include <rapidxml.hpp>

#include <iostream>
#include <string>

using namespace std;
namespace rx = rapidxml;

int main() {
  //                               ↓ココに"..."がない!
  string xml = R"(<function include=><return type="int"/>printf</function>)" "\0";
  rx::xml_document<char> doc;
  try {
    doc.parse<0>(&xml[0]);
  } catch (const rx::parse_error& ex) {
    cerr << ex.what() << " @ [" << ex.where<char>() << "]\n";
  }
}
fig-05
fig-05

traverse:ノード間を移動する

 XMLはtree構造を表現するものですから、treeを構成する節(ノード)の間を渡り歩くメンバ関数が用意されています。

  • xml_base::parent() : 親ノード (xml_baseはxml_node/xml_attributeのbase-class)
  • xml_node::first_node() : 最初の子(長子)ノード
  • xml_node::last_node() : 最後の子(末子)ノード
  • xml_node::next_sibling() : 兄弟関係にある次(弟)ノード
  • xml_node::previous_sibling() : 兄弟関係にある前(兄)ノード

 従って、特定のノードに対し、その子ノードを順方向/逆方向に列挙するなら:

list-05
// 純方向
xml_node<>* node = ...
for ( xml_node<>* child = node->first_node();
      child != nullptr;
      child = child->next_sibling() ) {
  // childに対してなにかする
}

// 逆方向
xml_node<>* node = ...
for ( xml_node<>* child = node->last_node();
      child != nullptr;
      child = child->previous_sibling() ) {
  // childに対してなにかする
}

 属性(xml_attribute)も同様に、

  • 最初の属性 : xml_node::first_attribute()
  • 最後の属性 : xml_node::first_attribute()
  • 次の属性 : xml_attribute::next_sibling()
  • 前の属性 : xml_attribute::previous_sibling()

 これらxml_node/xml_attributeを渡り歩くメンバ関数群は引数に文字列を与えられると、その文字列とノード名/属性名が一致するものに限定することができます。

list-06
#include <rapidxml.hpp>
#include <rapidxml_utils.hpp>

#include <iostream>

using namespace std;
namespace rx = rapidxml;

// nameを名前に持つノードを再帰的に列挙する
void walk(rx::xml_node<>* node, const char* name) {
  for ( rx::xml_node<>* child = node->first_node(name); 
        child != nullptr; 
        child = child->next_sibling(name) ) {
    cout << child->name() << " : [" << child->value() << "]\n";
    walk(child, name);
  }
}

int main() {
  rx::xml_document<> doc;
  rx::file<> input("trial.xml");
  doc.parse<rx::parse_trim_whitespace>(input.data());
  walk(&doc, "person"); // xml_document は xml_node の派生クラス
}
fig-06
fig-06

 ノード名/属性名を指定しない順方向の列挙であればヘッダ:<rapidxml_iterators.hpp>に定義されたイテレータ:node_iterator/attribute_iteratorを使うと、ちょっと楽になります。

list-07 iterator_cpp
#include <rapidxml.hpp>
#include <rapidxml_utils.hpp>
#include <rapidxml_iterators.hpp>

#include <iostream>
#include <algorithm>

using namespace std;
namespace rx = rapidxml;

// ノードを再帰的に列挙する
template<typename Function>
void walk(rx::xml_node<>* node, Function fun) {
  for_each( rx::node_iterator<char>(node), rx::node_iterator<char>(), 
            [&](rx::xml_node<>& child) { 
              fun(child); 
              walk(&child, fun); 
            });
}

int main() {
  rx::xml_document<> doc;
  rx::file<> input("trial.xml");
  doc.parse<rx::parse_trim_whitespace>(input.data());
  walk(&doc, 
       [](rx::xml_node<>& node) {
         // エレメント・ノードであれば、名前と値および属性名/属性値を出力する
         if ( node.type() == rx::node_element ) {
           cout << node.name() << " : [" << node.value() << "] ";
           for_each(rx::attribute_iterator<char>(&node), rx::attribute_iterator<char>(),
                    [](const rx::xml_attribute<>& attr) { 
                      cout << attr.name() << "=" << attr.value() << " "; 
                    });
           cout << endl;
         }
       });
}

 node_iterator/attribute_itteratorを返すメンバ関数begin()/end()を持ったヘルパ・クラス、あるいはグローバル関数std::begin()/std::end()を定義しておけばrange-based forが使えますね:

list-08 range_based_for.cpp
#include <rapidxml.hpp>
#include <rapidxml_utils.hpp>
#include <rapidxml_iterators.hpp>

template<typename Ch = char>
class children {
  typedef rapidxml::node_iterator<Ch> iterator_type;
public:
  explicit children(rapidxml::xml_node<Ch>* node) : node_(node) {}
  iterator_type begin() const { return iterator_type(node_); }
  iterator_type end() const { return iterator_type(); }
private:
  rapidxml::xml_node<Ch>* node_;
};

template<typename Ch = char>
class attributes {
  typedef rapidxml::attribute_iterator<Ch> iterator_type;
public:
  explicit attributes(rapidxml::xml_node<Ch>* node) : node_(node) {}
  iterator_type begin() const { return iterator_type(node_); }
  iterator_type end() const { return iterator_type(); }
private:
  rapidxml::xml_node<Ch>* node_;
};

namespace std {
  template<typename Ch> inline rapidxml::node_iterator<Ch> 
    begin(rapidxml::node_iterator<Ch> iter) { return iter; };

  template<typename Ch> inline rapidxml::node_iterator<Ch> 
    end(rapidxml::node_iterator<Ch> iter) { return rapidxml::node_iterator<Ch>(); };

}

namespace std {
  template<typename Ch> inline rapidxml::attribute_iterator<Ch> 
    begin(rapidxml::attribute_iterator<Ch> iter) { return iter; };

  template<typename Ch> inline rapidxml::attribute_iterator<Ch> 
    end(rapidxml::attribute_iterator<Ch> iter) { return rapidxml::attribute_iterator<Ch>(); };

}

#include <iostream>

using namespace std;
namespace rx = rapidxml;

// ノードを再帰的に列挙する
template<typename Function>
void walk(rx::xml_node<>* node, Function fun) {
  for ( rx::xml_node<>& child : children<>(node) ) {
    fun(child);
    walk(&child, fun);
  }
}

int main() {
  rx::xml_document<> doc;
  rx::file<> input("trial.xml");
  doc.parse<rx::parse_trim_whitespace>(input.data());
  walk(&doc,
       [](rx::xml_node<>& node) {
         // エレメント・ノードであれば、名前と値および属性名/属性値を出力する
         if (node.type() == rx::node_element) {
           cout << node.name() << " : [" << node.value() << "] ";
           for (const rx::xml_attribute<>& attr : rx::attribute_iterator<char>(&node)) {
             cout << attr.name() << "=" << attr.value() << " "; 
           }
           cout << endl;
         }
       });
}
fig-07
fig-07

会員登録無料すると、続きをお読みいただけます

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

次のページ
documentを出力する

この記事は参考になりましたか?

  • X ポスト
  • このエントリーをはてなブックマークに追加
特集記事連載記事一覧

もっと読む

この記事の著者

επιστημη(エピステーメー)

C++に首まで浸かったプログラマ。Microsoft MVP, Visual C++ (2004.01~2018.06) "だった"りわんくま同盟でたまにセッションスピーカやったり中国茶淹れてにわか茶...

※プロフィールは、執筆時点、または直近の記事の寄稿時点での内容です

この記事は参考になりましたか?

この記事をシェア

  • X ポスト
  • このエントリーをはてなブックマークに追加
CodeZine(コードジン)
https://codezine.jp/article/detail/10235 2017/07/26 14:00

おすすめ

アクセスランキング

アクセスランキング

イベント

CodeZine編集部では、現場で活躍するデベロッパーをスターにするためのカンファレンス「Developers Summit」や、エンジニアの生きざまをブーストするためのイベント「Developers Boost」など、さまざまなカンファレンスを企画・運営しています。

新規会員登録無料のご案内

  • ・全ての過去記事が閲覧できます
  • ・会員限定メルマガを受信できます

メールバックナンバー

アクセスランキング

アクセスランキング