libxml2 で XMLTextReader を使ってみる

XML文書をパースする機能が必要になったので、libxml2 を使ってみた。
今までオイラは XML のパースは軽量言語(Perl、PHPなど)でしかやったことがなかった。libxml2をそのまんま使うとものすごく面倒なんだろうなぁと若干恐れおののいていたのだが、いざ使ってみると案外そうでもない。(少なくともフォーマットが分かっているXMLから必要な項目を取り出すだけなら)


ということで、XML文書から商品名と価格を取り出すという簡単なサンプルを作ってみた。

(例によってサンプルコード全体は github にあげてあります)


サンプルデータ(sample.xml)はこんな感じ。

<?xml version="1.0" encoding="UTF-8"?>
<fruits>
    <fruit>
        <item>りんご</item>
        <price>150</price>
    </fruit>
    <fruit>
        <item>みかん</item>
        <price>250</price>
    </fruit>
    <fruit>
        <item>メロン</item>
        <price>1000</price>
    </fruit>
</fruits>


で、これを処理してみる。プログラム側のざっくりとした流れは以下のようになる。

    xmlTextReaderPtr reader;
    int ret;

    //  Readerの作成
    reader = xmlNewTextReaderFilename("./sample.xml");

    //  次のノードに移動 
    ret = xmlTextReaderRead(reader);
    while (ret == 1) {
        //  現在のノードを処理(processNodeは別途定義)
        processNode(reader);

        //  次のノードに移動
        ret = xmlTextReaderRead(reader);
    }

    //  Reader のすべてのリソースを開放
    xmlFreeTextReader(reader);


ここで言う「ノード」とは、開始タグ・終了タグやテキストなどのこと。例えば

<item>りんご</item>

このようなXMLは、

  1. <item>
  2. りんご
  3. </item>


の3つのノードとして認識される。

    //  Readerの作成
    reader = xmlNewTextReaderFilename("./sample.xml");

xmlNewTextReaderFilename() は、ある XML リソースを読み込むためのReaderを作成する関数。引数に指定するのは、処理対象のリソースURI。今回はファイル名を指定したが、http://〜 で始まる文字列を指定すればネット上からリソースを取得することも可能。

    //  次のノードに移動 
    ret = xmlTextReaderRead(reader);
    while (ret == 1) {
        //  現在のノードを処理(processNodeは別途定義)
        processNode(reader);

        //  次のノードに移動
        ret = xmlTextReaderRead(reader);
    }

xmlTextReaderRead() は、XMLストリーム上の次のノードへ読み進める関数(初回は最初のノード)。戻り値は

  • 1: 正常終了
  • 0: 読み進めるべきノードがない
  • -1: エラー

よって、1 を返している間は読み進めたノードの処理を繰り返す。


処理本体は processNode() という別関数で定義した。

//  1つのノードを処理する
void processNode(xmlTextReaderPtr reader)
{
 …


現在 reader がポイントしているノードのさまざまな情報を取得する関数が用意されている。例えば以下のようなものがある。

項目名 取得用関数 概要
NodeType xmlTextReaderNodeType() ノードの種類
Name xmlTextReaderName() ノード名
Value xmlTextReaderValue() Textノードの場合、そのText
Depth xmlTextReaderDepth() XMLツリー内の階層の深さ(root=0)

NodeType は整数の値で表される。libxml/xmlreader.h では以下の18種類の値が定義されている。

typedef enum {
    XML_READER_TYPE_NONE = 0,
    XML_READER_TYPE_ELEMENT = 1,
    XML_READER_TYPE_ATTRIBUTE = 2,
    XML_READER_TYPE_TEXT = 3,
    XML_READER_TYPE_CDATA = 4,
    XML_READER_TYPE_ENTITY_REFERENCE = 5,
    XML_READER_TYPE_ENTITY = 6,
    XML_READER_TYPE_PROCESSING_INSTRUCTION = 7,
    XML_READER_TYPE_COMMENT = 8,
    XML_READER_TYPE_DOCUMENT = 9,
    XML_READER_TYPE_DOCUMENT_TYPE = 10,
    XML_READER_TYPE_DOCUMENT_FRAGMENT = 11,
    XML_READER_TYPE_NOTATION = 12,
    XML_READER_TYPE_WHITESPACE = 13,
    XML_READER_TYPE_SIGNIFICANT_WHITESPACE = 14,
    XML_READER_TYPE_END_ELEMENT = 15,
    XML_READER_TYPE_END_ENTITY = 16,
    XML_READER_TYPE_XML_DECLARATION = 17
} xmlReaderTypes;

今回のサンプルでは、1 = 開始タグ、3 = テキスト、15 = 終了タグ の3つのみ使用する。


さて、さっそく現在処理中のノードタイプとノード名を取得してみる。

    xmlElementType nodeType;
    xmlChar *name, *value;

    //  ノード情報の取得
    nodeType = xmlTextReaderNodeType(reader);       //  ノードタイプ
    name = xmlTextReaderName(reader);               //  ノード名
    if (!name)
        name = xmlStrdup(BAD_CAST "---");

name は常に取得可能とは限らず、NULL が返る場合もある。今回は取得出来なかった場合 "---" という文字列を代わりに代入することにした。BAD_CAST というのは、libxml2 で定義されているマクロで、通常の文字列を xmlChar* 型にキャストしても安全と分かっている場合のキャスト用に使用する。

    if (nodeType == XML_READER_TYPE_ELEMENT) {              //  開始
        if ( xmlStrcmp(name, BAD_CAST "item") == 0 ) {
            state = STATE_ITEM;

        } else if ( xmlStrcmp(name, BAD_CAST "price") == 0 ) {
            state = STATE_PRICE;
        }

ノードが開始タグである場合は、ノード名を調べる。"item" または "price" というノードなら変数 state を変更して、現在それらのノードの中にいることが分かるようにする。

    } else if (nodeType == XML_READER_TYPE_END_ELEMENT) {   //  終了
        state = STATE_NONE;

ノードが終了タグである場合は、"item" または "price" タグから抜けたということを表すため state を変更する。"item" "price" 以外のタグから抜けた場合でも無駄に処理されるが気にしない。w

    } else if (nodeType == XML_READER_TYPE_TEXT) {          //  テキスト
        //  テキストを取得する
        value = xmlTextReaderValue(reader);

        if (!value)
            value = xmlStrdup(BAD_CAST "---");

        if ( state == STATE_ITEM ) {
            printf("品名: %s\n", value);

        } else if ( state == STATE_PRICE ) {
            printf("価格: %s\n", value);
        }

ノードがテキストノードだった場合は、現在どのノード内にいるかを調べ、適切な文字列を表示する。テキストノードの値を取得するには xmlTextReaderValue() を使用する。

        xmlFree(value);
    }

    xmlFree(name);
}  

xmlChar* 型の変数を宣言し、かつ xmlTextReader〜() 関数を使って文字列を取得できた場合は、使用後に xmlFree() で適切に開放しなければならない。

    //  Reader のすべてのリソースを開放
    xmlFreeTextReader(reader);

すべての XML パース処理が終わったら、Reader のリソースをすべて開放する。


コンパイルは以下のとおり行う。ライブラリやインクルードファイルのパス指定は、xml2-config を使うと楽。

gcc -Wall -o xmlread xmlread.c `xml2-config --cflags --libs`


実行結果

-----------------------
品名: りんご
価格: 150円
-----------------------
品名: みかん
価格: 250円
-----------------------
品名: メロン
価格: 1000円
-----------------------