Libxml2 を使う

XML はあらゆる分野における基礎技術となって利用が進みつつあるが、GNOME 環境においても例外ではない。GNOME 2 においては Libxml2 が XML を扱うための基本ライブラリとして採用され、あらゆる局面で活用されている。本稿ではこの Libxml2 を使った簡単なプログラミング例を紹介しよう。

GNOME と XML

GNOME の多くのアプリケーションは設定に関する情報を GConf というフレームワークを利用して保存しているが、この GConf は標準では XML 形式のテキストファイルを利用して情報を保存している。GNOME を利用しているなら、ホームディレクトリにある ~/.gconf/ というディレクトリの中を覗いてみよう。そこの中には %gconf.xml というファイルがいくつも存在しているが、これが GConf によって書き出されたユーザ固有の設定情報である。

GNOME 標準のブラウザである Epiphany も設定情報の多くは GConf を利用して保存しているが、履歴やブックマークなどの GConf 向きではない情報は自分自身で XML 形式を使って保存している。これらはいずれも Libxml2 を使っている。Epiphany を利用しているならば、~/.gnome2/epiphany/ 以下にいくつかの .xml という拡張子をもったファイルができているのが見えるだろう。

Libxml「2」 というからには Libxml1 というのも以前は存在していて、それもまた GNOME 1 において使われていたが、西欧圏の文字、具体的には ISO-8859-1 以外の文字エンコーディング以外はうまく扱えず、日本語の利用者などでは問題があった。Libxml2 になってこの問題は解消されているので、Libxml1 を使用する理由はもはや無いといってもよい。

Libxml2 は C で書かれ、C 言語から扱いやすい高速なライブラリだ。Libxml2 のページ には xmlbench を利用したベンチマークの結果も掲載されている。Libxml2 は XML の単純な読み書きだけでなく、XPath や XInclude などの処理、DTD や RelaxNG による XML の検証も可能だ。また、XSLT 処理のための libxslt や、XML のデジタル署名を扱う xmlsec などの周辺ライブラリも揃っている。C 以外の言語用のバインディングも存在しており、スクリプト言語などから Libxml2 の特徴を亨受することもできる。Libxml2 は多機能であることもあってサイズは大きいが、必要に応じて不要な機能を削り、コンパクトにすることもできる。

XmlReader API

XML を扱う API としては DOM と SAX の 2 大 API が有名だ。Libxml2 は標準では DOM の完全な実装を含んでいないが、似たようなツリー構造を生成する API が用意されている。周辺ライブラリにより DOM の実装も利用できる。また、SAX の API も提供されている。

DOM

DOM の API は処理対象の XML と同等のツリーをメモリ上に展開した上で操作を行う。そのため、XML に対応するデータは、状況によるが、空白なども含めすべて一度はメモリ上に存在することになり、ごく一部分にしか要のない場合である場合など、そのほとんどは無駄になってしまう。

SAX

もう一つの API である、SAX はイベントベースの API である。XML を構成する各要素ごとに「イベント」を発生させ、その「イベント」に対応する「ハンドラ」が呼ばれる、というものだ。SAX を利用したアプリケーションは、ハンドラを用意し、必要な処理をそこで行う。必要なデータでなければ捨ててしまえばいいこともあり、必要とするメモリは少なくてすむ。XML パーサがイベントをアプリケーションの側に「プッシュする」という形態を取ることから、このようなパーサはプッシュパーサと呼ばれる。

SAX はメモリの消費量が少ないのはいいのだが、パーサ側に主導権を取られてしまうため、アプリケーションによってはコードが煩雑になってしまう。

XmlReader API

ここで単純なテキストファイルの処理を考えてみよう。行指向のものであれば、たとえば、

  ファイルを開く (fopen)
    行単位で読み出す (fgets)
    読み出した行を処理する
    ...を繰り返す
  終わったらファイルを閉じる (fclose)

これをエラー処理などを省いてコードを書くと、

  FILE *f;

  f = fopen(file, "r");
  while (fgets(buffer, sizeof(buffer), f) != NULL) {
      /* bufffer を処理 */
  }
  fclose(f);

といった単純なものになるだろう。ループを回すのはあくまでもアプリケーションの側だ。XML に対しても同様の構成を取りたくなる時があるが、そういった場合に適しているのが本稿で紹介する XmlReader という API だ。XmlReader はアプリケーションの側がパーサからイベントを引っぱり出す (プル) ことから「プル パーサ」と呼ばれる種類のものだ。

Libxml2 の XmlReader API は、Microsoft の .NET Framework で提供されている XmlReader を参考にして作られたものだ。.NET Framework のものについては、MSDN の「XmlReader による XML の読み取り」が参考文献として良いだろう。この XmlReader はメモリ消費が少なく、かつ、利用者側が主導権を取れるため比較的扱いやすい。

XmlReader インターフェイスを使った場合の流れは以下のようになる

  XML ファイルを開いてパーサを初期化 (xmlNewTextReaderFilenamea)
    要素を読み出す (xmlTextReaderRead)
    読み出した要素を処理
    ...を繰り返す
  終わったらパーサを閉じる (xmlFreeTextReader)

これを具体的にコードで表現すると次のようになる。

  xmlTextReaderPtr reader;
  reader = xmlNewTextReaderFilename(uri);
  while (xmlTextReaderRead(reader) == 1) {
      /* xmlTextReaderNodeType() や xmlTextReaderName() などなどの関数で
         パーサが今しがた読みこんだ要素の情報を取り出し、それを処理 */
  }
  xmlFreeTextReader(reader);

RSS を読んでみよう

サンプルアプリケーションとして、単純な RSS リーダーを書いてみよう。RSS とはサイトの更新情報やメタ情報を XML 形式で表現したものだ。歴史的事情などにより、複数のバージョンが乱立していたりする混乱はあるものの、多くのニュースサイトや日記・blogサイトが RSS による情報提供を行なっている。本稿を掲載している opentechpress.jp にも RSS News Feed というのがある (左側のサイドバーを見てみよう)。RSS の規格そのほかの情報については検索エンジンで RSS と入力すればいろんな情報が得られるので、ここでは省略する。

ここで示すサンプルは RSS のファイルを指定し、パースし、タイトルと URL を表示する、というものだ。具体的には item 要素内の title 要素と url 要素の内容を抜き出して表示するものだ。

RSSでは一つ一つの「ニュース」を item 要素が表わし、それらの item の表題を title 要素が、その item へアクセスするための URL を link 要素が表わしている。

まずは中心となる部分を示す。

typedef enum {
        STATE_START = 0,
        STATE_ITEM,
        STATE_ITEM_TITLE,
        STATE_ITEM_LINK
} RssState;

void parse_node(xmlTextReaderPtr reader, RssState *state);
void parse_rss(char* uri);

int
main(int arg, char* argv[])
{
        parse_rss(argv[1]);
        return 0;
}

void
parse_rss(char* uri)
{
	xmlTextReaderPtr reader;
	RssState state = STATE_START;

	reader = xmlNewTextReaderFilename(uri);
	while (xmlTextReaderRead(reader) == 1) {
		parse_node(reader, &:state);
	}
	xmlFreeTextReader(reader);
	xmlCleanupParser();
}

ここで parse_rss() 関数の引数、uri は、RSS ファイルを格納したファイルのファイル名だ。state は RSS 処理ルーチンの状態を表わす列挙型の変数で、基本状態、item 要素内、title 要素内、link 要素内、を表現する。この state の内容に合わせて処理を変える状態遷移機械が処理のメインである parse_node() 内で実装されている。

void
parse_node(xmlTextReaderPtr reader, RssState *state)
{
        xmlElementType node_type;

        node_type = xmlTextReaderNodeType(reader);
        if (node_type == XML_READER_TYPE_ELEMENT) {
                /* 要素の開始タグの場合 */
                xmlChar *element_name;
                element_name = xmlTextReaderName(reader);
                if (*state == STATE_START) {
                        if (xmlStrcmp(element_name, "item") == 0) {
                                *state = STATE_ITEM;
                        }
                } else if (*state == STATE_ITEM) {
                        if (xmlStrcmp(element_name, "title") == 0) {
                                *state = STATE_ITEM_TITLE;
                        } else if (xmlStrcmp(element_name, "link") == 0) {
                                *state = STATE_ITEM_LINK;
                        }
                }
                xmlFree(element_name);
        } else if (node_type == XML_READER_TYPE_END_ELEMENT) {
                /* 要素の終了タグの場合 */
                if (*state == STATE_ITEM) {
                        *state = STATE_START;
                        return;
                } else if (*state == STATE_ITEM_TITLE) {
                        *state = STATE_ITEM;
                } else if (*state == STATE_ITEM_LINK) {
                        *state = STATE_ITEM;
                }
        } else if (node_type == XML_READER_TYPE_TEXT) {
                /* テキスト要素の場合 */
                if (*state == STATE_ITEM_TITLE) {
                        /* item 要素の中身を表示 */
                        printf("** %s\n", xmlTextReaderValue(reader));
                } else if (*state == STATE_ITEM_LINK) {
                        /* link 要素の中身を表示 */
                        printf("  %s\n", xmlTextReaderValue(reader));
                }
        }
}

これが parse_node() 関数だ。state 変数に格納されている値 (状態) により、ひたすら処理を分岐し、処理し、状態を更新しているだけのものである。

まず、parse_node() の冒頭で xmlTextReaderNodeType(reader) 関数を使って、読みこんだ要素の「型」を調べる。この関数により返る値は「要素の開始」「要素の終了」「テキスト要素」などによって変わってくる。

たとえば、「<item>アイテム</item>」という XML を処理する場合、「<item>」を読んだところで XML_READER_TYPE_ELEMENT が返る。そして、「アイテム」を読みこんだところで XML_READER_TYPE_TEXT が、「</item>」を読んだところで XML_READER_TYPE_END_ELEMENT が返ってくる。

xmlTextReaderName(reader) は要素名を取り出す関数で、xmlTextReaderValue() は要素の値、テキストノードであれば、そのテキストそのものを返す関数だ。これによりタグの名前や、テキストの値を取り出しているのである。

RSS 処理コードは以上で全てである。これを parse.c として保存しておき、

cc `xml2-config --cflags`   -c -o parse.o parse.c
cc -o parse parse.o `xml2-config --libs`

とすればコンパイルできる。あとは wget などで RSS ファイルを取得し、その RSS ファイルを指定して parse を実行する。

ところで、xmlNewTextReaderFilename(uri); によって xmlTextReader のパーサを初期化しているが、この uri はローカルにあるファイル名しか受けつけないのではない。http で始まる URL を指定すれば、ネットワーク経由で RSS を取得してパースしてくれる。libxml2 は簡易 HTTP クライアント (nano HTTP) を内蔵しているのだ。

たとえば、

  ./parse http://opentechpress.jp/japanlinuxcom.rdf

とすれば、opentechpress.jp の最新記事が 10 本表示される。なお、文字コードは UTF-8 で出力されるので、lv などの UTF-8 対応のページャを使うなどしてもらいたい。

さて、XmlReader については Libxml2 XmlTextReader Interface tutorial が参考になるだろう。また、今回紹介した RSS パーサにいくらか手を加えて locale のエンコーディングで表示されるようにしたものを 私のページ に置いておいた。

MSDNの価格