expat 概要

ライセンスMIT
URLhttp://expat.sourceforge.net/

XMLパーサライブラリです。
かなり定番のライブラリで、あちこちで使われています。

コンパイル

Windows版のコンパイル方法は、expatのソースを展開するとwin32というフォルダがあり、
この中のReadme.txtに詳しく書かれています。

VC2008ならば、expat.dswをプロジェクトのインポートで変換できるとあります。
インポートして、7つのプロジェクトが現れたら成功です。
ビルドも難なく成功します。

プロジェクト名に”_w”がついているプロジェクトはUTF-16版だそうです。

なお、サンプルプロジェクトのelements.exeはUTF-8のXMLファイル用です。
テスト実行するときはUTF-8のXMLファイルを用意しましょう。
実行するにはDOSプロンプトから以下のようにします。

> elements < "ファイル名"

使用法

elements.exe、outline.exeを見ると大体の様子がわかりそうです。

まずパーサオブジェクトを作成、XMLの要素、属性等に対するコールバック関数を登録し、
ファイルから順次読み込んで解析していくという流れのようです。

パーサオブジェクトの作成は、以下のようにします。

XML_Parser parser = XML_ParserCreate( NULL );

引数には文字コードを指定することができるそうで、この場合はドキュメントに定義されている
文字コードを無視するそうです。

使い終わったらパーサオブジェクトを開放します。

XML_ParserFree( parser );

コールバックの書式はXMLの構造種別ごとに決まっていて、
それぞれのコールバックを登録する関数があります。

例えば、XML要素開始タグに対するコールバックは以下のように定義します。

static void XMLCALL element_start
( void *userData, const XML_Char *name, const XML_Char **attrs )
{
  // ここで、
  // nameはタグ名、
  // attrsは属性・値・属性・値、、、と続く
  // attrsの終わりにはNULLがある
}

コールバックを登録する関数はXML_Set***Handlerという名前の関数群です。
例えば上記のXML要素開始タグを登録するには以下のようにします。

XML_SetStartElementHandler( parser, element_star );

ちなみに、要素開始・終了のコールバックを同時に登録する
XML_SetElementHandler()関数もあります。

要素の文字列を取得するコールバックは以下のように定義します。

static void XMLCALL char_handler
( void *userData, const XML_Char *s, int len )
{
  int i;
  char buf[255];

  memcpy( buf, s, len );
  buf[len] = '';

  printf( "string = %sn", buf );
}

上記の例で、第2引数sが要素の文字列を格納している引数ですが、
文字列の終端文字は無く、第3引数lenに文字数が入っているので、
上記のように終端文字を入れる処理が必要になります。
このコールバックを登録する関数はXML_SetCharacterDataHandler()関数です。

そのほかCDATAセクション、処理命令など、
いろいろなコールバックがあるので、リファレンスを見ながら必要な関数を定義していきます。

なお、上記のコールバックの引数userDataはコールバックに渡すユーザデータで、
XML_SetUserData()関数でセットします。

例:

int nUserData;
XML_Parser parser = XML_ParserCreate( NULL );

  ・
  ・
  ・

XML_SetUserData(parser, &nUserData );

ファイルからの読み込みはfreadを使用します。
すなわち、読み込みは行単位とかではなく、用意したバッファ文を読み込んでしまって、パーサに渡します。
当然途中で切れたりするはずですが、そこはパーサが上手に処理してくれるようです。
(要素データの途中だとそうはならない、下記の注意参照)

読み込みバッファはユーザが用意してもいいですし、expatに用意してもらう方法もあります。
前者の場合はXML_Parse()でパーサへデータを渡します。
後者の場合はXML_ParseBuffer()関数で解析を依頼します。バッファの開放はパーサの解放時に行われるようです。
後者の方法での例は以下のとおりです。

char *pBuf;
FILE *pfXML;
int nLen, nDone;

  ・
  ・
  ・

// バッファ確保を依頼する
pBuf = (char *)XML_GetBuffer( parser, BUFFSIZE );

// バッファへ読み込み
nLen = (int)fread( pBuf, 1, BUFFSIZE, pfXML );
nDone = feof( pfKML );

if ( XML_ParseBuffer( parser, nLen, nDone ) == XML_STATUS_ERROR )
{
  // エラー処理
}

以下の例は、KMLファイルの点要素の座標値だけを抜き取ってCSVに保存する例です。
(いい加減な例なので注意!)

#include "expat.h"
#include <stdio.h>
#include <string.h>

#define BUFFSIZE  512

bool g_bPoint = false;
bool g_bCoord = false;

static void XMLCALL
startElement(void *userData, const char *name, const char **attrs)
{
  if ( !_strcmpi( name, "Point" ) )
  {
    g_bPoint = true;
    return;
  }

  if ( g_bPoint )
  {
    if ( !_strcmpi( name, "coordinates" ) )
    {
      g_bCoord = true;
    }
  }
}


static void XMLCALL
endElement(void *userData, const char *name)
{
  if ( !_strcmpi( name, "Point" ) )
  {
    g_bPoint = false;
  }
  else if ( !_strcmpi( name, "coordinates" ) )
  {
    g_bCoord = false;
  }
}

static void XMLCALL
value_handler( void *userData, const XML_Char *s, int len )
{
  char buf[100];

  memcpy( buf, s, len );
  buf[len] = '';

  if ( g_bCoord )
  {
    fprintf( *(FILE **)userData, "%sn", buf );
  }
}

int main( int argc, char *argv[] )
{
  int nDone, nLen;
  FILE *pfKML, *pfCSV;
  XML_Parser parser = XML_ParserCreate( NULL );
  char *pBuf;

  if ( !parser )
  {
    exit( 1 );
  }

  if ( fopen_s( &pfKML, argv[1], "r" ) )
  {
    exit( 1 );
  }

  if ( fopen_s( &pfCSV, argv[2], "w" ) )
  {
    exit( 1 );
  }

  pBuf = (char *)XML_GetBuffer( parser, BUFFSIZE );

  XML_SetElementHandler( parser, startElement, endElement );
  XML_SetCharacterDataHandler( parser, value_handler );
  XML_SetUserData( parser, &pfCSV );

  while ( 1 )
  {
    nLen = (int)fread( pBuf, 1, BUFFSIZE, pfKML );
    nDone = feof( pfKML );

    if ( XML_ParseBuffer( parser, nLen, nDone ) == XML_STATUS_ERROR )
    {
      fprintf(stderr, "Parse error at line %u:n%sn",
        XML_GetCurrentLineNumber( parser ),
        XML_ErrorString(XML_GetErrorCode( parser ) ) );

      fclose( pfKML );
      fclose( pfCSV );
      XML_ParserFree( parser );
      exit( 1 );
    }

    if ( nDone )
      break;
  }

  XML_ParserFree( parser );

  fclose( pfKML );
  fclose( pfCSV );

  return 0;
}

注意!

ファイルから読み込んだバッファが要素データ文字列の途中で切れている場合、
XML_SetCharacterDataHandler()関数で指定したコールバックに渡される文字列は
その切れたところまでが渡されます。
したがって、XML_SetCharacterDataHandler()関数で指定したコールバックで
この文字列を数値に変換などの処理をしてしまうと、
切れる前の数値とあとの数値と2回処理してしまいます。

上の例ではその問題が起きる可能性があります。
対策としては、XML_SetCharacterDataHandler()関数で指定したコールバックではとりあえず一時的に文字列を保存する
だけにしておいて、タグ終了時に呼ばれる、XML_SetEndElementHandler()関数で指定するコールバック内で
文字列を処理するようにする必要があります。

SHIFT-JISのXMLは?

expatがサポートする文字コードはUTF-8、UTF-16、ISO-8859-1、US-ASCIIだけだそうです。
それ以外の文字コードは読めません。
そんな場合は、XML_SetUnknownEncodingHandler()関数でその文字コードに対する
処理を指定するのですが、これは大変です。

ですが、ありがたいことに蛭子屋本舗さんで、
SHIFT-JISコードのハンドラを公開してくださっています。
このハンドラをXML_SetUnknownEncodingHandler()関数に指定すれば、
SHIFT-JISのXMLでも解析することができます。

参考

expatはパーサに渡された文字データを全てUTF-8(expatwならUTF-16)に変換しようと試みます。
なので、例えば上記のハンドラを使ってSHIFT-JISの日本語文字列をパーサに渡すと、
内部でUTF-8に変換してXML_SetCharacterDataHandler()関数で指定したコールバックに
帰ってきたりします。

参考サイト

quneさんの記事にサンプルがあります。
まずはこちらを参考に特訓です。

アーカイブ