C MAGAZINE Linux programming Tips
第8回 XMLプログラミング
■はじめに
今回は、最近、何かと話題のXMLに関するLinux上でのプログラミングについて紹介します。XML(Extensible Markup Language)は、 SGML(Standard Generalized Markup Language)のサブセットで、アプリケーション間での(文章)データの交換を容易にするために作られた規格です。 SGMLに関しては馴染みの少ない方も多いかと思いますが、ワープロや表計算などの多くのアプリケーション間の文章データ交換を容易にするために考案されたものです。 SGMLのような規格は最初から提案されたのでなく、多くのソフトウエアで、同じような目的のものでもメーカ間で全然データの互換性がなかったり、 最悪のケース、同じメーカのソフトウエア間でもバージョンが違うだけで互換性を失ったりという問題を解決するために提案されました。筆者自身、 SGMLの現状について詳しくはしりませんが、大いなる野望を持って提案されたSGMLですが、DTP関連のソフトウエアが出力形式として扱っているぐらいで、 一般のソフトウエア(ワープロソフトウエアなど)はSGMLの出力ができるものは少ないようです。こう言ってしまうとまるでSGMLは失敗のようですが、 XMLもSGMLのサブセットなので、"良くないのでは "と思うかも知れません。でもXMLを実際に使ってみると、曖昧な言い方ですが、"XMLは扱いやすい"です。 XMLの基本的な発想が"なんで昔からなかったのだろう "と思うくらいです。
XMLは、言語(Language)の一種なのですが、C言語のようなプログラミング言語ではありません。もちろん言語なので何らかの役目を果たします。前述のように、 XMLの役目は、アプリケーション間でのデータ交換を容易にすることです。XML形式の文章だけあってもデータ交換は容易になりません。 C言語のソースコードがあって、C言語のコンパイラーが無いような状態です。例えば、C言語では、CコンパイラーがC言語で記述された文章を実行形式にします。 XMLでは、XML文章を解析してプログラムが理解可能なデータ構造にしたり、逆にプログラムが生成したデータ構造をXML文章にする処理系が必要になります。 XMLの処理系は、XMLの規格
http://www.w3.org/TR/REC-xml に従いライブラリーとして提供されており、 C言語にかかわらず多くの他のプログラミング(perl,python,tclなど)で実装されています。
Linuxにおいて、C言語から扱えるXMLライブラリーとしては、何種類か存在していますが、現在、 標準的なものはGNOMEプロジェクトの中で開発されたlibxmlです。もちろん、 XMLの規格
http://www.w3.org/TR/REC-xml を参考にしてXMLライブラリーの実装にトライしてみてもよいかもしれません。
■libxmlを使う前に
XMLライブラリーは、標準のCライブラリーから供給されるものではなく(そうあってほしいけど)、別にXMLライブラリーがインストールされている必要があります。
今回は、GNOMEがインストールされていてlibxmlの開発環境が利用できることを前提に話しを進めます。Linuxディストリビューションをインストールする時に、 インストールタイプによってlibxml開発環境がインストールされていない場合がありますが、その場合は、RPMベースのディストリビューションでは、 以下のようにして確かめてください。
もしインストールされていないようで、あればCDROMやFTPサイト等から libxml-devel パッケージをとって、インストールしてください。
正常にlibxmlの開発環境がインストールされている場合、GNOME環境化では、普通、ディレクトリー /usr/lib/gnome-xml/以下にヘッダーファイル、 ディレクトリー/usr/lib/以下に、libxmlのライブラリー群が存在します。ただ環境によっては、 ヘッダーファイルの位置やコンパイラーに渡すオプション等が変化するので、 ライブラリーが提供する コマンド xml-configを使ってコンパイル情報をコンパイル時に得ることをお勧めします。ちなみに、 コマンド gnome-configを使っても同様な情報を取得することができます。
$ xml-config --cflags
-I/usr/include/gnome-xml
$ xml-config --libs
-L/usr/lib -lxml -lz |
今回の例題でコンパイル時に使用した libxmlのバージョンは、1.8.10です。 バージョンによって構造体のメンバーやライブラリー関数が異なる点に注意してください。特に、libxml2-(2.2.x)をインストールしている場合は、 例題の若干の手直しが必要になるでしょう。
またXML1.0では、多くの事が規定されています。もちろんlibxmlもそれに従って実装されていますが、今回は、 libxmlをあくまで共通の文章形式を扱える便利なライブラリーとして最低限必要な関数とその動作についてのみ説明します。
libxmlに関する情報は、
http://xmlsoft.org/から得ることができます。
■ではlibxmlを使ってみよう
libxmlの開発環境を確認したところで、早速、 以下の整形式(well-formed な) XML文章 hello.xml を読み込むプログラム(リスト1)をコンパイル&実行してみてlibxmlに慣れてみましょう。 この時点でコンパイルできない場合は、libxml の開発環境をチェックしてみましょう。
コンパイル&実行
$ cc -Wall `xml-config --cflags` hello.c `xml-config --libs` -o hello
$ ./hello hello.xml
'Hello, world!' |
XML文章: hello.xml
< xml version="1.0" >
<!-- comment -->
<greeting>Hello, world!</greeting> |
リスト1: hello.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <gnome-xml/parser.h>
static int read_xml(const char *fn)
{
xmlDocPtr doc;
xmlNodePtr cur;
doc = xmlParseFile(fn);
if(doc == NULL) {
printf("It seems that %s is not a xml data.\n", fn);
return 1;
}
cur = doc->root;
if (cur == NULL) {
printf("empty document\n");
xmlFreeDoc(doc);
return 1;
}
while( cur ) {
if( strcmp( cur->name, "greeting")==0 ) {
printf("'%s'\n",cur->childs->content);
}
cur = cur->next;
}
xmlFreeDoc(doc);
return 0;
}
int main(int argc, char *argv[])
{
if( argc<2 ) {
printf("%s xmldata\n",argv[0]);
return 1;
}
return read_xml(argv[1]);
} |
このサンプルプログラムは、XMLドキュメントを読み込み、タグ <greeting> </greeting> で囲まれている文字列を出力するだけの単純なものです。 この例のように一度に全部のファイルを読み込みメモリーに格納するタイプのものは、DOM(Document Object Model) のインターフェースと呼ばれ、 一般的な方法です。
基本的に、xmlで始まる関数または構造体がlibxmlで宣言されているものです。
リスト1では、libxmlの関数として、xmlParseFile(), xmlFreeDoc()が使用されています。関数 xmlParseFile()でXML文章をパースして、 xmlDocPtr で指し示す場所に内容を格納し、それを返す。関数 xmlFreeDoc()で、xmlDocPtrが指し示す内容を消去するといった一般的な流れです。 また関数 xmlParseFile()と似たような働きをするものに、関数 xmlParseMemory()があります。これはファイルからでなくメモリーからXML文章を読み込む関数です。
libxml基本関数1
#include <gnome-xml/parser.h>
ファイルからのXML文章読み込み
xmlDocPtr xmlParseFile (const char *filename);
構造体の内容の削除
void xmlFreeDoc (xmlDocPtr cur);
メモリーからのXML文章読み込み
xmlDocPtr xmlParseMemory (char *buffer,int size); |
XML文章を読み込んだ後は、xmlNodePtr型のポインター変数を操作することで文章の内容を辿ります。以下に、 xmlNodePtr型の宣言を示します。この構造体の中で良く使われるメンバー変数は、name, content, type, next, childsで、 nameはタグの名前、contentはその名の通り内容(テキストデータ)、typeはエレメントのタイプ(xmlElementTypeで宣言)、nextは次のノードへのポインター、 childsは子のノードへのポインターになります。ちなみに、childsは、最近の実装(libxml2)では、childrenになっています。なぜかは想像がつくと思います。
ノード構造体 : xmlNode
typedef struct _xmlNode xmlNode;
typedef xmlNode *xmlNodePtr;
struct _xmlNode {
void *_private;
void *vepv;
xmlElementType type;
struct _xmlDoc *doc;
struct _xmlNode *parent;
struct _xmlNode *next;
struct _xmlNode *prev;
struct _xmlNode *childs;
struct _xmlNode *last;
struct _xmlAttr *properties;
const xmlChar *name;
xmlNs *ns;
xmlNs *nsDef;
xmlChar *content;
}; |
エレメントタイプ:xmlElementType
typedef enum {
XML_ELEMENT_NODE= 1,
XML_ATTRIBUTE_NODE= 2,
XML_TEXT_NODE= 3,
XML_CDATA_SECTION_NODE= 4,
XML_ENTITY_REF_NODE= 5,
XML_ENTITY_NODE= 6,
XML_PI_NODE= 7,
XML_COMMENT_NODE= 8,
XML_DOCUMENT_NODE= 9,
XML_DOCUMENT_TYPE_NODE= 10,
XML_DOCUMENT_FRAG_NODE= 11,
XML_NOTATION_NODE= 12,
XML_HTML_DOCUMENT_NODE= 13
} xmlElementType; |
DOM型のインターフェイスでXML文章を扱うとき、 XML文章がどのようなツリーに展開されているか正確に把握することがプログラミングをうまく行うコツになると思います。参考のために、 hello.xml を読み込んだ後のツリーのイメージを図1に描いてみました。図1では省略していますが全てのノードがdocへのポインターを持っています。 またコメントも一つのノードになっています。
今回、DTD(Document Type Definition)については触れませんが、DTDはこのようなXML文章のツリー構造を、 正規表現のようなものを使って定義するものだと思ってください。XML文章の中でも、定義できます。先程、唐突に、 整形式(well-formed な)XML文章 という表現を使いましたが、XML文章には2つのタイプがあり、DTDを持たない、 タグのネストだけをチェックした文章を整形式(well-formedな)XML文章と呼びます。一方、DTDを持って、 DTDでの宣言どおりの文法をもつものを検証済みXML文章と呼びます。
■ちょっとだけ実用的な例
次にもう少し実用的な例を紹介します。XMLの文章では、HTML文章同様にプロパティを指定することができます。HTMLでは、 フォントや色などを指定をするときにプロパティを使用します。XML文章 hello.xmlを拡張して、言語ごとのHelloを記述したものが hello2.xmlです。実は、 このサンプルのエンコーディングは間違いです。ASCII 7bitの文字と日本語EUCのみなので、EUC-JPとしていますが、 正確に書きたい場合は、UTF-8を使うべきでしょう。
XML文章 hello2.xmlでは、新しく2つの要素が登場します。
リスト2 の read_xml()は、hello2.xml用に処理が拡張されています。まずは、 リスト1の read_xml()をリスト2 の read_xml()で置き換えて動作を確認してみてください。
コンパイル&実行
$ cc -Wall `xml-config --cflags` hello2.c `xml-config --libs` -o hello2
$ ./hello2 hello2.xml
language=English : Hello!
language=Spanish : Hola!
language=Japanese : こんにちは! |
XML文章: hello2.xml
< xml version="1.0" encoding="EUC-JP" >
<greetings>
<greeting language="English">Hello!</greeting>
<greeting language="Spanish">Hola!</greeting>
<greeting language="Japanese">こんにちは!</greeting>
<greetings>
<!-- encoding="EUC-JP"は、正しくありません。これはあくまでサンプルで
す。latin系の文字と日本語を組み合わせるには、UTF-8を使うべき。ちなみに
, Holaの前には !をひっくり返した文字が必要。
--> |
リスト2: read_xml()
static int read_xml(const char *fn)
{
xmlDocPtr doc;
xmlNodePtr cur;
xmlNodePtr children;
doc = xmlParseFile(fn);
if(doc == NULL) {
printf("It seems that %s is not a xml data.\n", fn);
return 1;
}
cur = doc->root;
if (cur == NULL) {
printf("empty document\n");
xmlFreeDoc(doc);
return 1;
}
while( cur ) {
if( strcmp( cur->name, "greetings")==0 ) {
children = cur->childs;
while( children ) {
if( strcmp( children->name, "greeting")==0 ) {
printf("language=%-8s : %s\n",
xmlGetProp(children,"language"),
xmlNodeGetContent(children) );
}
children = children->next;
}
}
cur = cur->next;
}
xmlFreeDoc(doc);
return 0;
} |
リスト2を見ていただければ分かりますが、子ノードの操作は、単純にメンバー childs 以下のツリーを操作することで実現します。またプロパティの取得は、 関数 xmlGetProp() で行えます。コンテンツの取得に関して、リスト1では cur->childs->content で参照していたのに、リスト2 では、 関数 xmlNodeGetContent()で取得するようにかわったに気づいたでしょうか?実は、libxmlの中には、 いくつかツリー操作をまとめたいくつかの便利な関数が存在しています。
libxml基本関数2
#include <gnome-xml/parser.h>
プロパティーの取得
xmlChar * xmlGetProp (xmlNodePtr node,
const xmlChar *name);
ノードのコンテンツの取得
xmlChar * xmlNodeGetContent (xmlNodePtr cur) |
関数 xmlGetProp()を使わないでプロパティーを取得しようとして場合、リスト3のようになります。 構造体 xmlAttrの宣言と照らし会わせてみれば分かりますが、libxmlではプロパティも一つのノードとして管理しています。
リスト3: 関数 xmlGetProp()の処理内容
char* _getprop(xmlNodePtr cur, const char *str)
{
char *ret=NULL;
xmlAttrPtr prop = cur->properties;
while( prop ) {
if( strcmp( prop->name, str ) == 0 ) {
ret = prop->val->content;
break;
}
prop = prop->next;
}
return ret;
} |
アトリビュート構造体 : xmlAttr
typedef struct _xmlAttr xmlAttr;
typedef xmlAttr *xmlAttrPtr;
struct _xmlAttr {
(一部省略)
xmlElementType type;
struct _xmlNode *node;
struct _xmlAttr *next;
const xmlChar *name;
struct _xmlNode *val;
xmlNs *ns;
}; |
これまでに説明したプログラミング要素(ライブラリ関数)の組合せでだいたいのXML文章の読み込みプログラムは記述できると思います。 色々なパターンのXML文章を作成してみて読み込みプログラムの実装にチャレンジしてみましょう。
■XML文章の生成
すでに出来上がったXML文章の取扱を説明しましたが、次にアプリケーション(プログラム)内でXML文章(の構造)を生成しファイルに保存する方法を説明します。
リスト4は、XML文章の生成のサンプルプログラムで、指定したディレクトリ以下をスキャンして、ファイル、 ディレクトリーの情報をXML文章で保存します。では実際にコンパイル、実行して動作を確認してみましょう。
コンパイル&実行
$ cc -Wall `xml-config --cflags` xmldir.c `xml-config --libs` -o xmldir
$ ./xmldir /etc/wu-ftpd/ dir.xml |
出力結果例 : dir.xml
< xml version="1.0" >
<dirent>
<dir mode="40755">
<name>/etc/wu-ftpd</name>
<file mode="40755">ftpaccess</file>
<file mode="40755">ftpconversions</file>
<file mode="40755">ftpgroups</file>
<file mode="40755">ftphosts</file>
<file mode="40755">ftpusers</file>
</dir>
</dirent> |
リスト4: xmldir.c
/* ------------------------------------------------------------
This program is free software; you can redistribute it and/or modify
it under ther terms of GNU General Public License, version 2.
written by Kazutomo Yoshii <kazutomo@turbolinux.co.jp>
------------------------------------------------------------ */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <dirent.h>
#include <sys/stat.h>
#include <unistd.h>
#include <gnome-xml/parser.h>
static int readdir_to_xml(const char *dirname, xmlNodePtr tree )
{
DIR *dir;
struct dirent *ent;
xmlNodePtr subtree, subsubtree;
struct stat sbuf;
char mode_buf[BUFSIZ];
dir = opendir(dirname);
if( dir==NULL ) return 0;
subtree = xmlNewChild(tree, NULL, "dir", NULL);
xmlNewChild(subtree, NULL, "name", dirname);
stat( dirname, &sbuf);
sprintf(mode_buf, "%o", sbuf.st_mode);
xmlSetProp(subtree, "mode", mode_buf);
while( (ent=readdir(dir))!=NULL ) {
if( !(strcmp(ent->d_name,".")==0 ||
strcmp(ent->d_name,"..")==0) ) {
char tmp[BUFSIZ];
sprintf(tmp, "%s/%s", dirname, ent->d_name);
if( readdir_to_xml(tmp, subtree)==0 ) {
subsubtree = xmlNewChild(subtree, NULL, "file", ent->d_name );
stat( ent->d_name, &sbuf);
sprintf(mode_buf, "%o", sbuf.st_mode);
xmlSetProp(subsubtree, "mode", mode_buf);
}
}
}
closedir( dir );
return 1;
}
int main(int argc, char *argv[])
{
xmlDocPtr doc;
char dirname[BUFSIZ];
if( argc < 3 ) {
printf("%s DIR XMLFILE\n", argv[0]);
return 1;
}
strcpy( dirname, argv[1]);
/* eliminate the '/' character in the last */
if( strlen(dirname)>1 && dirname[strlen(dirname)-1] == (char)'/' ) {
dirname[strlen(dirname)-1] = (char)NULL;
}
doc = xmlNewDoc("1.0");
doc->root = xmlNewDocNode(doc, NULL, "dirent", NULL);
if( readdir_to_xml(dirname, doc->root) == 0 ) {
printf("'%s' is not a directly\n", dirname);
} else {
xmlSaveFile(argv[2], doc);
}
xmlFreeDoc(doc);
return 0;
}
/*
* Local variables:
* compile-command: " cc -Wall `gnome-config --cflags xml`
xmldir.c `gnome-config --libs xml` -o xmldir"
* c-indent-level: 4
* c-basic-offset: 4
* tab-width: 4
* End:
*/
|
簡単にリスト4のプログラムの説明をします。まず、このプログラムは、XMLライブラリー関数を除くと ディレクトリーを再帰的にスキャンし、 ファイルまたはディレクトリーのパーミッションを取得するものです。コマンド tree(1)のような動作をします。
リスト4は、ディレクトリーをスキャンした結果をXML文章としてファイルに保存するための実装です。実用的には、 このようなプログラムが必要かどうか分かりませんが、ディレクトリーもXML文章の構造もどちらもツリー構造なので、 相性が良いので例題にしてみました。
libxml基本関数3に、リスト4で使われているXMLライブラリー関数に列挙します。
リスト4の大まかな流れは、関数xmlNewDoc(), xmlNewDocNode()でXML文章を生成し、ディレクトリー、ファイルを発見するごとに、 関数 xmlNewChild()によって子ノードを作成、XML文章に追加し、最後に関数xmlSaveFile()でファイルに保存します。関数xmlNewDoc()で指定している文字列 "1.0"は、 XMLのバージョンを示しており、その他の値を指定できますが、現状では"1.0"のみになります。
libxml基本関数3
#include <gnome-xml/parser.h>
XML文章(の構造)の生成
xmlDocPtr xmlNewDoc (const xmlChar *version);
ドキュメントノードの生成
xmlNodePtr xmlNewDocNode (xmlDocPtr doc,
xmlNsPtr ns,
const xmlChar *name,
const xmlChar *content);
子ノードの生成
xmlNodePtr xmlNewChild (xmlNodePtr parent,
xmlNsPtr ns,
const xmlChar *name,
const xmlChar *content);
プロパティーの設定
xmlAttrPtr xmlSetProp (xmlNodePtr node,
const xmlChar *name,
const xmlChar *value);
XML文章(の構造)をファイルへ保存
int xmlSaveFile (const char *filename,
xmlDocPtr cur);
|
■さいごに
今回は、libxmlの代表的な関数とそれを利用したプログラムを紹介しました。
libxmlを便利な汎用文章フォーマットを扱うライブラリとして考えた場合、今回説明した範囲で十分ではないかと思います。その他、 必要な処理として、ノードやプロパティーの書き換えや削除などがありますが、 ヘッダーファイルを見ながらそれ風の名前のついた関数を使ってみると以外に実装できてしまうと思います。
オープンソースの利点を利用して、ライブラリーのソースコードを眺めてみるのも良いかと思います。