サイトマップ
お知らせ、メモ
案内板
うちのヘッドライン
 




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] = '\0';

  printf( "string = %s\n", 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] = '\0';

  if ( g_bCoord )
  {
    fprintf( *(FILE **)userData, "%s\n", 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%s\n",
        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さんの記事にサンプルがあります。 まずはこちらを参考に特訓です。


クリエイティブ・コモンズ・ライセンス
This documents by Yamate,N is licensed under a Creative Commons 表示 - 継承 3.0 非移植 License.
login