libxml2 の SAX インタフェースで XML をパースする

思うところあって LL からではなく C で libxml2 を使ってみました*1

普通にメモリ上やファイル上の XML ドキュメントを SAX インタフェースでパースするだけであれば,最上位 API[http://xmlsoft.org/html/libxml-parser.html#xmlSAXUserParseFile:title=xmlSAXUserParseFile()][http://xmlsoft.org/html/libxml-parser.html#xmlSAXUserParseMemory:title=xmlSAXUserParseMemory()] を使えばいいのですが*2,自力ストリーミング I/O と一緒に使う場合どうすればいいのかがわからなかったのでコードを書いてみました。

I/O callback 編

まずは read や close に使う callback 関数を登録しておいて,ドキュメントをパースする場合のサンプルです。

手順は

  1. [http://xmlsoft.org/html/libxml-parser.html#xmlCreateIOParserCtxt:title=xmlCreateIOParserCtxt()] で callback を登録した IO パーサコンテキストを生成
  2. [http://xmlsoft.org/html/libxml-parser.html#xmlParseDocument:title=xmlParseDocument()] でドキュメント全体をどーんとパース

になります。

[http://xmlsoft.org/html/libxml-parser.html#xmlCtxtReadIO:title=xmlCtxtReadIO()] といった似た名前の API もありますが,こちらだと SAX インタフェース用の構造体を渡せないので,おそらく DOM インタフェース用じゃないかと思います。

#include <stdio.h>
#include <stdlib.h>
#include <libxml/xmlerror.h>
#include <libxml/parser.h>
#include <libxml/SAX2.h>

static void
start_element_ns_callback(void *ctx, const xmlChar *localname,
                          const xmlChar *prefix, const xmlChar *URI,
                          int nb_namespaces, const xmlChar **namespaces,
                          int nb_attributes, int nb_defaulted,
                          const xmlChar **attributes)
{
    fprintf(stdout, "<%s>\n", localname);
}

static int
read_callback(void *context, char *buffer, int len)
{
    FILE *fp = (FILE *) context;
    fprintf(stderr, "*** read_callback()\n");
    return fread(buffer, 1, len, fp);
}

static int
close_callback(void *context)
{
    FILE *fp = (FILE *) context;
    fprintf(stderr, "*** close_callback()\n");
    return fclose(fp);
}

int
main(int argc, char *argv[])
{
    xmlParserCtxtPtr xmlctx;
    xmlSAXHandler saxh;
    FILE *fp;

    LIBXML_TEST_VERSION

    xmlInitParser();

    xmlDefaultSAXHandlerInit();

    xmlSAX2InitDefaultSAXHandler(&saxh, 1);
    xmlSAXVersion(&saxh, 2);

    saxh.startElementNs = start_element_ns_callback;

    fp = fopen(argv[1], "r");
    if (! fp) {
        fprintf(stderr, "open failed\n");
    }

    xmlctx = xmlCreateIOParserCtxt(&saxh, NULL, read_callback, close_callback,
                                   fp, XML_CHAR_ENCODING_NONE);

    /* パース開始 */
    if (xmlParseDocument(xmlctx)) {
        xmlParserError(xmlctx, "xmlParseDocument");
        exit(1);
    }

    /* ほんとは別に必要ないけど */
    xmlClearParserCtxt(xmlctx);

    xmlCleanupParser();

    return 0;
}

SAX ハンドラのイニシャライズ部分がちょっとわけありなので解説します。

    xmlDefaultSAXHandlerInit();

    xmlSAX2InitDefaultSAXHandler(&saxh, 1);
    xmlSAXVersion(&saxh, 2);

[http://xmlsoft.org/html/libxml-tree.html#xmlSAXHandler:title=xmlSAXHandler] 構造体のイニシャライズが面倒だったので [http://xmlsoft.org/html/libxml-SAX2.html#xmlSAX2InitDefaultSAXHandler:title=xmlSAX2InitDefaultSAXHandler()] を使っています。ただこのままだと startElementNs のような SAX2 用 callback を呼び返してくれないので,[http://xmlsoft.org/html/libxml-SAX2.html#xmlSAXVersion:title=xmlSAXVersion()] で SAX version 2 を指定しています。しかしこの API は libxml2 組み込みのハンドラを登録してしまうので,そのハンドラが使うメモリやポインタが初期化されないままなんですね。なので,実行するとセグフォってしまいます。そのため [http://xmlsoft.org/html/libxml-SAX2.html#xmlDefaultSAXHandlerInit:title=xmlDefaultSAXHandlerInit()] を実行して内部領域を実行してあるわけです。

おそらくこのデフォルトハンドラは DOM ツリーを構築してしまうのでメモリを浪費します。最終的に DOM ツリーが必要ないのであれば,[http://xmlsoft.org/html/libxml-tree.html#xmlSAXHandler:title=xmlSAXHandler] 構造体をゼロクリアして,必要なハンドラだけ登録すればメモリの消費量をおさえることができます。今回は最初に書いたようにサンプルですし初期化が面倒だったので,ある意味礼儀正しくコードを書いたのでした。ふぅ。

さて。実行結果は,

*** read_callback()
<html>
<body>
<div>
<p>
*** read_callback()
<div>
<span>
<span>
<ul>
<li>
*** close_callback()

のようになります。startElementNs だけ登録して endElementNs を登録していないのでアンバランスですが,うまくパースできていることはわかると思います。

かたや stdout に結果を出力かたや stderr にステータスを出力しているのでタイミングは参考程度ですが,パースしながら read_callback()close_callback() が呼び出されているのがわかります。

chunk push 編

ドキュメントの read 時に callback される関数を登録する,というのはストリーミングの状況によってはうれしくない仕様の場合もあります。このため,パーサにドキュメントの chunk をどんどん push していって都度都度パースしてもらう,という API もあります。

手順は

  1. [http://xmlsoft.org/html/libxml-parser.html#xmlCreatePushParserCtxt:title=xmlCreatePushParserCtxt()] で push 型パーサのコンテキスト生成
  2. [http://xmlsoft.org/html/libxml-parser.html#xmlParseChunk:title=xmlParseChunk()] でちまちまと chunk を渡しつつパース

になります。

#include <stdio.h>
#include <stdlib.h>
#include <libxml/xmlerror.h>
#include <libxml/parser.h>
#include <libxml/SAX2.h>

static void
start_element_ns_callback(void *ctx, const xmlChar *localname,
                          const xmlChar *prefix, const xmlChar *URI,
                          int nb_namespaces, const xmlChar **namespaces,
                          int nb_attributes, int nb_defaulted,
                          const xmlChar **attributes)
{
    fprintf(stdout, "<%s>\n", localname);
}

int
main(int argc, char *argv[])
{
    xmlParserCtxtPtr xmlctx;
    xmlSAXHandler saxh;
    FILE *fp;

    LIBXML_TEST_VERSION

    xmlInitParser();

    xmlDefaultSAXHandlerInit();

    xmlSAX2InitDefaultSAXHandler(&saxh, 1);
    xmlSAXVersion(&saxh, 2);

    saxh.startElementNs = start_element_ns_callback;

    xmlctx = xmlCreatePushParserCtxt(&saxh, NULL, NULL, 0, NULL);

    fp = fopen(argv[1], "r");
    if (! fp) {
        fprintf(stderr, "open failed\n");
        exit(1);
    }

    /* ちょっとずつパースする */
    for (;;) {
        char buf[32];
        int len;

        len = fread(buf, 1, 32, fp);
        fprintf(stderr, "*** fread()\n");

        if (xmlParseChunk(xmlctx, buf, len, feof(fp))) {
            xmlParserError(xmlctx, "xmlParseChunk");
            exit(1);
        }

        if (feof(fp))
            break;
    }

    fclose(fp);

    xmlClearParserCtxt(xmlctx);

    xmlCleanupParser();

    return 0;
}
*** fread()
<html>
<body>
*** fread()
<div>
<p>
<div>
*** fread()
<span>
<span>
<ul>
*** fread()
<li>

書いてから気づいたんですが,Libxml2 set of examples[http://xmlsoft.org/examples/index.html#parse4.c:title=parse4.c] がまったく同じことやってるんですな。まぁ,先ほどのコードとの違いを見ていただければ。

上記のコードでは feof() でドキュメント終端を検出して xmlParseChunk()terminate フラグとして渡していますが,libxml2 のサイトのサンプルによると xmlParseChunk(dummy, 0, 1); みたいにゼロレングス文字列として渡してもよいみたいです。

感想

  • 今回つかった SAX インタフェースだけでなく DOM インタフェースもある
    • LL から使うなら DOM のほうが使いやすいと思います
  • 今回あんまり使ってないですが XML namespace をサポートしている
    • なんちゃってパーサだとサポートしてないか自力で attributes を見たりしなきゃいけない
  • もちろん DTDRELAX NG でのバリデーションもできる
  • XInclude をサポートしている
  • XPath をサポートしている
  • 簡易 FTP, HTTP クライアントコードも実装してある(外部リソース・スキーマ等の取り込み用?)
  • ほかいろいろの features

これだけサポートして MIT ライセンス。おとくですねー。

*1:たいていの LL には libxml2 のバインディングがあるので通常それを使えばいいです。

*2:上位 API を使う場合,An example of SAX2 parser using libxml2覚え書き:libxml2のSAXの使い方 | A7Mの日記 | スラド が参考になります