Entry

Expat を使ってみよう(その2)

2006年10月12日

Expat を検索してこのサイトにいらっしゃる方が多いにもかかわらず,前回のコンテンツが中途半端に終わっているので,続きを書いておこうと思います(参照:qune: Expat を使ってみよう(その1))。おそらく,Expat をそのまま使う方は,あまりいないんでしょうけれど,ストリーム型のパーサを扱うノウハウを見る分には勉強になると思います。

まず初めに,ストリーム型パーサがどんなものなのか確認しておきます。ストリーム型のパーサというのは,XML を次々と読み込んでいって,要素等々が見付かる度にそれに応じた処理をしていくタイプのパーサです。パーサは XML を読んでいる最中に要素が見付かると,「要素が見付かった!」というイベントを受け付けて,それに対応する「要素を扱うルーチン」に処理を移します。このイベントに対応するルーチンのことを,「ハンドラ」と言います。Win32 API とか GTK+ なんかをいぢったことがある向きには,もはや常識なんだと思いますけど,考え方が独特なので慣れないとなかなか理解できないかもしれません。

ストリーム型のパーサは,DOM のように構文木をメモリ内に作らないので,軽いプログラムが作れます(入力用のバッファを用意しておくだけでいい)。一方,ストリームはどんどん流れていってメモリ内に溜まらないので,XML の中をうろつくには,かなり面倒な処理が必要になってきます。

で,前回はというと,Expat を使ったプログラムをコンパイルする方法について紹介したのでした。まったく中身について触れていないもんで,検索でやってきた方には申し訳ない限りです。今回は,Expat の中身を少し見てみることにします。Expat を使うには,大体以下のような手順を踏むことになります。

  1. パーサを作る
  2. ハンドラを登録する
  3. パース元のテキストを読み込んでパーサに渡す
  4. バースが終わったらパーサを片付ける

ここでの説明も,おおむねこの順番通りに説明していこうと思います。

今回のお題(使い回し)

ここでは,前回の例を使って(※ 手抜き),「要素が見付かった!」というイベントを受け取って,要素の名前を出力するプログラムを作ってみましょう。単純すぎるプログラムなので,実用性はナッシングです。前回のコードは以下の通り(※ 少しコメントを修正しています)。

#include <stdio.h>
#include <stdlib.h>

#include <expat.h>

#define BUFSIZE 1024  

void
element_start(void *userData, const XML_Char *name, const XML_Char *atts[])
{
    printf("[ELEMENT] %s Start!\n", name);
}

void
element_end(void *userData, const XML_Char *name)
{
    printf("[ELEMENT] %s End!\n", name);
}

int
main (int argc, char *argv[])
{
    char buf[BUFSIZE];
    int eofflag;
    size_t len;
    XML_Parser parser;

    /* (1) create XML parser */
    if ((parser = XML_ParserCreate(NULL)) == NULL) {
        fprintf(stderr, "parser creation error\n");
        exit(-1);
    }

    /* (2) register element handler */
    XML_SetElementHandler(parser, element_start, element_end);

    /* (3) read and parse */
    do {
        len = fread(buf, sizeof(char), BUFSIZE, stdin);
        if (ferror(stdin)) {
            fprintf(stderr, "file error\n");
            exit(-1);
        }
        eofflag = feof(stdin);
        /* XML parse */
        if ((XML_Parse(parser, buf, (int)len, eofflag)) == 0) {
            fprintf(stderr, "parser error\n");
            exit(-1);            
        }
    } while (!eofflag);

    fprintf(stderr, "done.\n");

    return 0;

パーサを作る

main() ルーチンを見てください。まず,パーサを作ります。パーサを作るには XML_Parser 型のオブジェクト(parser)を作って,それを XML_ParserCreate() で初期化します。こんな感じ。

   XML_Parser parser;

    /* (1) create XML parser */
    if ((parser = XML_ParserCreate(NULL)) == NULL) {
        fprintf(stderr, "parser creation error\n");
        exit(-1);
    }

簡単ですね。もっとも,引数の意味が謎なので XML_ParserCreate() のプロトタイプを見てみることにしましょう。引数にはエンコーディングを指定します。

XML_Parser XML_ParserCreate(const XML_Char *encoding)

引数にエンコーディングを指定すると,それに従ったパーサを作ってくれます。もっとも,扱える文字コードはそれほど多くなくて,以下のエンコーディングが使えるだけです。日本語を扱うなら,とりあえず UTF-8 か UTF-16 を選ぶことになりますね。

  • US-ASCII
  • UTF-8
  • UTF-16
  • ISO-8859-1

なお,今回の例では,引数に NULL を指定しているわけですけれど,以上の文字列以外の文字列を与えると,UnknownEncodingHandler なるハンドラが呼ばれることになります。つまり,「エンコーディングが分からなかったよ!」というイベントに対応したルーチンに処理を移すということですね。EUC-JP や Shift_JIS を扱う時は,ここで何らかの処理をすることになります。UnknownEncodingHandler は,XML_SetUnknownEncodingHandler()で登録するんですけれど,今回の例では登録してないので,「エンコーディングが分からなかったよ!」なイベントはスルーされることになります。

もうひとつ。XML_CreateParser() は,失敗すると NULL が返るので,ちゃんとエラー処理をしておきましょう。

ハンドラを作る

さて,無事にパーサができたら,今度はイベントに対応した各ルーチンを作ることにします。

今回は,要素が来たら要素名を表示するプログラムを作りたいわけですから,「要素が見付かったよ!」というイベントを受けとるハンドラを用意することになります。もう少し厳密に言うと,XML の要素は「開始タグ」と「終了タグ」からできていることから,「要素が見付かったよ!」というイベントは,「開始タグが見付かったよ!」というイベントと「終了タグが見付かったよ!」というイベントの2つから構成されることになります。つまり,ハンドラを2つ作りましょう,ということです。

開始タグのハンドラを作る

まずは開始タグのハンドラから。

void
element_start(void *userData, const XML_Char *name, const XML_Char *atts[])
{
    printf("[ELEMENT] %s Start!\n", name);
}

ハンドラの名前はどんな名前を付けても構いません。ここでは,element_start() という名前にしました。引数の名前についても特に決まりは無いんですけれど,引数の型や個数は決まっているので,疑問の余地は作らずにオマジナイとして書く方が混乱が少ないと思います。

引数を説明すると,まず第1引数である userData は,ユーザが受け渡しするデータです。好きなデータを入れることができて,型も決まってないので(void *),必要なときにキャストして使うことになります。次に,第2引数である name は,見付かった要素の名前が入ります。これは割と使うんじゃないでしょうか。今回も使います。最後に attr には,属性(attributes)がベクタの形で収められます。main() ルーチンの argv と同じ要領で扱うことになります。

で,しつこいですけど,今回はタグの要素名をプリントするプログラムなので,このルーチンでは name を標準出力にしています。

ハンドラの仕組みをちょっとだけのぞく(※余談)

とりあえず,開始タグのハンドラは引数だけを踏まえておけばいいんですけれど,一応,どんな風に定義してあるのか気になる方のために,紹介だけしておきます。

typedef void
(*XML_StartElementHandler)(void *userData,
                           const XML_Char *name,
                           const XML_Char **atts);

見てお分かりの通り,XML_StartElementHandler() という関数に対するポインタとして typedef されているんです。オブジェクト志向なストリームパーサの場合は,パーサやハンドラのクラスを上書き(オーバーライド)して登録するのが普通なんですけど,C にはそういう仕組みがないので,こういう方法を取ることになります。ハンドラを扱う C プログラムではよくあることなんですけど,分からなければ「こういうもんだ」と思ってスルーしても大丈夫です。

終了タグのハンドラを作る

次に,終了タグのハンドラを作ります。

void
element_end(void *userData, const XML_Char *name)
{
    printf("[ELEMENT] %s End!\n", name);
}

開始タグと似たような形で,違うところは attr がないことだけ。終了タグには属性がないから当たり前ですね。一応,こちらも定義を紹介しておきます。説明は開始タグと同じです。

typedef void
(*XML_EndElementHandler)(void *userData,
                         const XML_Char *name);

ハンドラを登録する

ここまでで,ハンドラを作ることはできました。もっとも,ここではまだハンドラを定義しただけなので,パーサは「開始/終了タグが見付かったよ!」なイベントに対して,どのルーチンに処理を移せばいいのか分かっていません。そこで,パーサに「開始/終了タグが見付かったよ!」なイベントがあったら,さっき作ったルーチンに処理を移すんだよ,と教えておく必要があります。これが,ハンドラの登録です。この処理はは main() ルーチンで行っています。

    /* (2) register element handler */
    XML_SetElementHandler(parser, element_start, element_end);

要素が見付かったときのハンドラを登録するには,XML_SetElementHandler() という関数を使います。

第1引数に,教えてあげるパーサのオブジェクト,第2・第3引数に開始タグと終了タグのハンドラ名をそれぞれ登録します。ハンドラ名(関数名)を変数みたいに使っているのは,先述の通り,(*XML_StartElementHandler)() と (*XML_EndElementHandler)() が,それぞれ typedef されているからです。

XML_SetElementHandler() のプロトタイプを紹介して,次に進むことにします。

XML_SetElementHandler(XML_Parser p,
                      XML_StartElementHandler start,
                      XML_EndElementHandler end);

パース元のテキストを読み込んでパーサに渡す

Expat の処理は,以上で8割方終了です。あとは,普通のテキスト処理と同じように,お好みのソースをパーサに食べさせていけばいいだけ。ここでは,こんな感じで読むことにしました。

    /* (3) read and parse */
    do {
        len = fread(buf, sizeof(char), BUFSIZE, stdin);
        if (ferror(stdin)) {
            fprintf(stderr, "file error\n");
            exit(-1);
        }
        eofflag = feof(stdin);
        /* XML parse */
        if ((XML_Parse(parser, buf, (int)len, eofflag)) == 0) {
            fprintf(stderr, "parser error\n");
            exit(-1);            
        }
    } while (!eofflag);

パーサに食べさせるには,XML_Parse() という関数を使います。以下にプロトタイプを示します。

int XML_Parse(XML_Parser p, const char *s, int len, int isFinal) 

第1引数に食べるパーサのオブジェクト(p),第2・第3引数に読んだ文字列(s)とその長さ(len)をそれぞれ指定します。

ちょっと重要なのは第4引数で,終了フラグ(isFinal)に,まだ読み途中(non-zero)なのか,全部読み終わったのか(0)を指定することです。大きなバッファを用意しておけば,ループも1回で済むわけですけれど,読み切れないときは何度かに分けて読む必要があります。isFinal があれば,読み途中で尻切れトンボになっても,残りを次のループで読んでくれるというわけです。

全部読み終わったのかをチェックするのに,ここでの例では feof() マクロを使って丁寧に終端チェックしているんですけれど,面倒な場合は文字列の長さ(len)を与えても同じ結果になるはずです。fread() の返値は読んだバイト数ですから,終端に達した後 fread() が失敗したら,いずれにしろ len には 0 が入るというわけ(パーサのループは1回多く回るけど)。

後は何も考えずにどんどこ食べさせていったら,先に作ったハンドラがよろしく処理していってくれるはずです。

パーサを片付ける

今回の例ではやっていないんですけれど,パーサを使い終わったら片付けておくと,お行儀のいいプログラムになります。パーサを片付けるには,XML_ParserFree() 関数を使います。プロトタイプは以下の通り。引数には使っていたパーサを指定するだけでオッケーです。

void XML_ParserFree(XML_Parser p)

この関数を呼ぶと,パーサ・オブジェクトが解放されるのはもちろん,パーサが保持していた userData(各ハンドラに引数として渡される値)も解放されます。後に処理を続けるようなら,呼んでおいた方がいいかもしれません。

最後に

今回は非常に簡単な例(というか使い回し)を使って,簡単なパースをしてみました。まだ,ストリームパーサの醍醐味云々なんかには程遠いところにいるわけですけど,大体の概要は紹介できたと思います。コンパイルの仕方は,前回のエントリを参考になさってください。

次回があるとしたら,もう少し実践的な例を使って説明できたらなぁ……と思ったり,思わなかったり(ゴニョゴニョ)。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN