C言語用XMLパーサ「Expat」でXMLドキュメントを読み込む



# Expatとは

「Expat」はC言語用のストリーム型のXMLパーサです。
https://libexpat.github.io/

XMLパーサを大別すると、一度全要素のデータを読み込んでメモリ上にツリー構造を構築した上で処理するDOM型と、データを先頭から読み込んでいって要素を発見するたびに順番に処理をしていくストリーム型があります。Expatは後者にあたります。

# インストール

[Gitリポジトリ](https://github.com/libexpat/libexpat)からソースをチェックアウトしてビルドしてもいいですが、apt-getやports、Homebrewなどのパッケージ管理ツールでもインストールできます。

私の場合はMacBook ProでHomebrewを導入済みだったので、Homebrewから入れました。

## Homebrewでインストール

```shell
$ brew install expat
```

完了したら、インストールパスを調べてbinディレクトリをPATHに追加します。マ毎回設定するのは面倒なので、.bash_profile に追加しました。

```
$ echo 'export PATH="/usr/local/opt/expat/bin:$PATH"' >> ~/.bash_profile
```

## ソースコードからインストール

[Gitリポジトリ](https://github.com/libexpat/libexpat)から最新のソースコードをチェックアウトしたら、expat ディレクトリに移動して buildconf.sh を実行します。2.58以降の autoconf が必要です。

```shell
$ ./buildconf.sh
```

続いて、configure を実行します。--prefix を使ってインストールディレクトリを指定できます。

```shell
$ ./configure --prefix=/home/myhome/expat
```

終わったら make します。

```shell
$ make & make install
```

bin ディレクトリ を PATH に追加すれば完了です。

# 使い方

まず基本として、expat.h を include します。

```c
#include <expat.h>
```

パースの基本的な手順は以下の通りです。

    1. パーサオブジェクトを生成する
    2. イベントハンドラを定義する
    3. イベントハンドラを設定する
    4. パースを実行する
    5. パーサオブジェクトを破棄する

## パーサオブジェクトを生成する

パーサを表す構造体は XML_Parser として定義されており、XML_ParserCreate（）関数で生成できます。

```c
XML_Parser parser = XML_ParserCreate(NULL);
```

## イベントハンドラを定義する

expat はストリーム型のパーサなので、XMLドキュメントを先頭から読んでいって、要素や値を発見するたびにイベントハンドラが呼び出される仕組みになっています。イベントハンドラは関数に XMLCALL という修飾をつけて宣言します。

次のコードは、要素の開始タグが発見されたときと、終了タグが発見されたときに呼び出されるハンドラの例です。要素名は el に、属性は attr に格納されます。user_data
 は呼び出し元に返したい値を設定するためのポインタになります。

```c
static void XMLCALL
elementStart(void *user_data, const XML_Char *el, const XML_Char *attr[]) 
{
  printf("[ELEMENT] %s Start!\n", el);
}

static void XMLCALL
elementEnd(void *user_data, const XML_Char *el) 
{
  printf("[ELEMENT] %s End!\n", el);
}
```

また、次のコードは、要素の中のデータが発見された場合に呼び出されるハンドラを定義した例です。データの本体は data に格納され、そのサイズが data_size として渡されます。

```c
static void XMLCALL
elementData(void *user_data, const XML_Char *data, int data_size) 
{
  if (data_size > 0) {
    UT_string *str = NULL;
    utstring_new(str);
    utstring_bincpy(str, data, data_size);
    printf("[DATA] %s\n", utstring_body(str));
    utstring_free(str);
  }
}
```

なお、この例では文字列処理に utstring というライブラリを使っています。utstring の使い方については以下のエントリを参照してください。
[C言語で文字列操作するのに便利な「utstring」](https://qiita.com/zinbe/items/224e24e77755d8c54e96)


## イベントハンドラを設定する

要素を発見した場合に呼び出すハンドラは XML_SetElementHandler()関数を使って設定します。第1引数にパーサオブジェクトを、第２引数と第３引数に、開始タグと終了タグ用のハンドラをそれぞれ指定します。

```c
XML_SetElementHandler(parser, elementStart, elementEnd);
```

データ用のハンドラは XML_SetCharacterDataHandler()関数で設定します。第１引数にパーサオブジェクトを、第２引数にハンドラを指定します。

```c 
XML_SetCharacterDataHandler(parser, elementData);
```

## パースを実行する

実際のパースの実行は、XML_Parse()関数で行います。第１引数にパーサオブジェクトを、第２引数と第３引数にパース対象のデータ（文字列）とその長さを、そして第４引数には対象データがファイルの終端に達しているか否かのフラグを渡します。

```c
int status = XML_Parse(parser, data, length, eofflg);
```

## パーサオブジェクトを破棄する

すべての処理が終了したら、XML_ParserFree()関数でパーサオブジェクトを破棄します。

```c
XML_ParserFree(parser);
```

# サンプルプログラム1 要素の一覧を表示する

以上を踏まえて、すべての要素を一覧表示するプログラムを作ってみました。開始タグと終了タグを検出して、その要素名を出力します。

```c:expat_test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <expat.h>

#define BUFSIZE 102400

static void XMLCALL
elementStart(void *user_data, const XML_Char *el, const XML_Char *attr[]) 
{
  printf("[ELEMENT] %s Start!\n", el);
}

static void XMLCALL
elementEnd(void *user_data, const XML_Char *el) 
{
  printf("[ELEMENT] %s End!\n", el);
}

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

  if ((parser = XML_ParserCreate(NULL)) == NULL) {
    fprintf(stderr, "Parser Creation Error.\n");
    exit(1);
  }

  XML_SetElementHandler(parser, elementStart, elementEnd);

  do {
    size_t len = fread(buf, sizeof(char), BUFSIZE, stdin);
    if (ferror(stdin)) {
      fprintf(stderr, "File Error.\n");
      exit(1);
    }

    done = len < sizeof(buf);
    if (XML_Parse(parser, buf, (int)len, done) == XML_STATUS_ERROR) {
      fprintf(stderr, "Parse Error.\n");
      exit(1);      
    }
  } while(!done);

  XML_ParserFree(parser);
  return(0);
}
```

## コンパイル

コンパイル方法は以下の通りです。-Iと-Lで、Expadをインストールした場所にあるヘッダファイルのディレクトリ（include）とライブラリのディレクトリ（lib）を指定しておく必要があります。

```
$ cc -I/usr/local/opt/expat/include -L/usr/local/opt/expat/lib -Wall -o xpsample expat_test.c -lexpat
```

## テスト用XMLドキュメント

テスト用のXMLドキュメントとしては、適当に以下のようなものを用意しました。

```xml:sample.xml
<?xml version="1.0"?>
<booklist>
  <book>
    <title>やさしいJava</title>
    <authors>
      <author>山田太郎</author>
    </authors>
    <publisher>ABC出版</publisher>
    <categories>
      <category>programming</category>
      <category>Java</category>
    </categories>
  </book>
  <book>
    <title>Linuxがよくわかる本</title>
    <authors>
      <author>鈴木一郎</author>
    </authors>
    <publisher>XYZ出版</publisher>
    <categories>
      <category>Linux</category>
      <category>UNIX</category>
    </categories>
  </book>
  <book>
    <title>猫でもわかるC言語</title>
    <authors>
      <author>山田太郎</author>
      <author>佐藤四郎</author>
    </authors>
    <publisher>ABC出版</publisher>
    <categories>
      <category>programming</category>
      <category>C言語</category>
      <category>猫</category>
    </categories>
  </book>
</booklist>
```

## 実行結果

以下が実行結果の例です（途中省略）。出現したタグの順番にハンドラが呼び出されているのが確認できました。

```shell
$ ./expat_test < sample.xml
[ELEMENT] booklist Start!
[ELEMENT] book Start!
[ELEMENT] title Start!
[ELEMENT] title End!
[ELEMENT] authors Start!
[ELEMENT] author Start!
[ELEMENT] author End!
[ELEMENT] authors End!
[ELEMENT] publisher Start!
[ELEMENT] publisher End!
[ELEMENT] categories Start!
[ELEMENT] category Start!
[ELEMENT] category End!
[ELEMENT] category Start!
[ELEMENT] category End!
[ELEMENT] categories End!
[ELEMENT] book End!
[ELEMENT] book Start!
[ELEMENT] title Start!
[ELEMENT] title End!
[ELEMENT] authors Start!
[ELEMENT] author Start!
...
...
...
[ELEMENT] book End!
[ELEMENT] book End!
[ELEMENT] booklist End!
```

# サンプルプログラム2 データを表示する

要素名だけでなく、中身のデータも表示するようにしたのが次の例です。データが出現した際に呼び出されるハンドラを追加しています。

```c:expat_test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <expat.h>
#include "utstring.h"

#define BUFSIZE 102400

static void XMLCALL
elementStart(void *user_data, const XML_Char *el, const XML_Char *attr[]) 
{
  printf("[ELEMENT] %s Start!\n", el);
}

static void XMLCALL
elementEnd(void *user_data, const XML_Char *el) 
{
  printf("[ELEMENT] %s End!\n", el);
}

static void XMLCALL
elementData(void *user_data, const XML_Char *data, int data_size) 
{
  if (data_size > 0) {
    UT_string *str = NULL;
    utstring_new(str);
    utstring_bincpy(str, data, data_size);
    printf("[DATA] %s\n", utstring_body(str));
    utstring_free(str);
  }
}

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

  if ((parser = XML_ParserCreate(NULL)) == NULL) {
    fprintf(stderr, "Parser Creation Error.\n");
    exit(1);
  }

  XML_SetElementHandler(parser, elementStart, elementEnd);
  XML_SetCharacterDataHandler(parser, elementData);

  do {
    size_t len = fread(buf, sizeof(char), BUFSIZE, stdin);
    if (ferror(stdin)) {
      fprintf(stderr, "File Error.\n");
      exit(1);
    }

    done = len < sizeof(buf);
    if (XML_Parse(parser, buf, (int)len, done) == XML_STATUS_ERROR) {
      fprintf(stderr, "Parse Error.\n");
      exit(1);      
    }
  } while(!done);

  XML_ParserFree(parser);
  return(0);
}
```

## 実行結果

実行結果は以下の通りです（途中省略）。

```shell
$ ./expat_test < sample.xml[ELEMENT] booklist Start!
[DATA]

[DATA]
[ELEMENT] book Start!
[DATA]

[DATA]
[ELEMENT] title Start!
[DATA] やさしいJava
[ELEMENT] title End!
[DATA]

[DATA]
[ELEMENT] authors Start!
[DATA]

...
...
...

[DATA]
[ELEMENT] book End!
[DATA]

[ELEMENT] booklist End!
```

データ用のハンドラは実際に値が入っていなくても呼び出されるので、空文字列のまま表示されている部分もあります。

# サンプルプログラム３ 要素名とデータをセットで表示する

少し工夫して、要素めいとデータがセットで表示されるようにしてみます。この場合、データのハンドラが呼び出された際に、現在どの要素を読んでいるのかという状態を記憶しておく必要があります。
そこで、現在読んでいる要素の状態を表す型を enum で宣言し、要素が出現するたびに状態を記録する（書き換える）ようにハンドラを定義します。

ポイントは、現在の要素が分かれば次に出現する要素も絞り込めるので、処理をcase分けできるという点です。たとえば"booklist"の次の開きタグは"book"しか無いので、bookのみを処理すればいいはずです。また、"book"のときに閉じタグがきたら、状態はひとつ上の要素である"booklist"に戻せばいいことになります。

また、今回の例では、データを持つのは title,auther,publisher,category の4つの要素だけなので、データのハンドラではそれ以外は無視できます。

以上を踏まえて、プログラムは次のような感じになりました。

```c:expat_test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <expat.h>
#include "utstring.h"

#define BUFSIZE 102400

typedef enum {
  DOCUMENT,
  BOOKLIST, 
  BOOK, 
  TITLE, 
  AUTHORS, 
  AUTHOR, 
  PUBLISHER, 
  CATEGORIES, 
  CATEGORY
} element;

element current = DOCUMENT;

static void XMLCALL
elementStart(void *user_data, const XML_Char *el, const XML_Char *attr[]) 
{
  switch (current) {
    case DOCUMENT:
      if (strcmp(el,"booklist") == 0)
        current = BOOKLIST;
      break;
    case BOOKLIST:
      if (strcmp(el,"book") == 0)
        current = BOOK;
      break;
    case BOOK:
      if (strcmp(el,"title") == 0)
        current = TITLE;
      else if (strcmp(el,"authors") == 0)
        current = AUTHORS;
      else if (strcmp(el,"publisher") == 0)
        current = PUBLISHER;
      else if (strcmp(el,"categories") == 0)
        current = CATEGORIES;
      break;
    case AUTHORS:
      if (strcmp(el,"author") == 0)
        current = AUTHOR;
      break;
    case CATEGORIES:
      if (strcmp(el,"category") == 0)
        current = CATEGORY;
      break;
    case TITLE:
    case PUBLISHER:
    case AUTHOR:
    case CATEGORY:
      break;
    default:
      current = DOCUMENT;
      break;
  }
}

static void XMLCALL
elementEnd(void *user_data, const XML_Char *el) 
{
  switch (current) {
    case DOCUMENT:
      break;
    case BOOKLIST:
      current = DOCUMENT;
      break;
    case BOOK:
      current = BOOKLIST;
      printf("\n");
      break;
    case AUTHORS:
      current = BOOK;
      break;
    case CATEGORIES:
      current = BOOK;
      break;
    case TITLE:
      current = BOOK;
      break;
    case PUBLISHER:
      current = BOOK;
      break;
    case AUTHOR:
      current = AUTHORS;
      break;
    case CATEGORY:
      current = CATEGORIES;
      break;
    default:
      break;
  }
}

static void XMLCALL
elementData(void *user_data, const XML_Char *data, int data_size) 
{
  switch (current) {
    case TITLE:
      printf("[TITLE] ");
      break;
    case AUTHOR:
      printf("[AUTHOR] ");
      break;
    case PUBLISHER:
      printf("[PIBLISHER] ");
      break;
    case CATEGORY:
      printf("[CATEGORY] ");
      break;
    default:
      return;
  } 

  UT_string *str = NULL;
  utstring_new(str);
  utstring_bincpy(str, data, data_size);
  printf("%s\n", utstring_body(str));
  utstring_free(str);
}

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

  if ((parser = XML_ParserCreate(NULL)) == NULL) {
    fprintf(stderr, "Parser Creation Error.\n");
    exit(1);
  }

  XML_SetElementHandler(parser, elementStart, elementEnd);
  XML_SetCharacterDataHandler(parser, elementData);

  do {
    size_t len = fread(buf, sizeof(char), BUFSIZE, stdin);
    if (ferror(stdin)) {
      fprintf(stderr, "File Error.\n");
      exit(1);
    }

    done = len < sizeof(buf);
    if (XML_Parse(parser, buf, (int)len, done) == XML_STATUS_ERROR) {
      fprintf(stderr, "Parse Error.\n");
      exit(1);      
    }
  } while(!done);

  XML_ParserFree(parser);
  return(0);
}
```

## 実行結果

実行結果は次のようになりました。

```shell
$ ./expat_test < sample.xml[TITLE] やさしいJava
[AUTHOR] 山田太郎
[PIBLISHER] ABC出版
[CATEGORY] programming
[CATEGORY] Java

[TITLE] Linuxがよくわかる本
[AUTHOR] 鈴木一郎
[PIBLISHER] XYZ出版
[CATEGORY] Linux
[CATEGORY] UNIX

[TITLE] 猫でもわかるC言語
[AUTHOR] 山田太郎
[AUTHOR] 佐藤四郎
[PIBLISHER] ABC出版
[CATEGORY] programming
[CATEGORY] C言語
[CATEGORY] 猫
```

# おわりに

以上、Expat の基本的な使い方と簡単なサンプルプログラムを解説しました。[公式サイトのドキュメントページ](https://libexpat.github.io/doc/) には、より詳しい使い方や内部的な動作の仕組みなども解説されているので、そちらも読むとさまざまな形のXMLも処理できるようになると思います。