トップバナー

XMLメモ

counter

主なコンテンツ
トップページ
サポート BBS
かもめ日記
ブックマーク
チャット

ゲーム
やるドラ革命委員会[ 閉鎖 ]

Computer
ソフトウェア置場
PS2 Linux kit
Oracle tips
Palmの話題
OepnBlockS
XMLメモ
ソフトウェア品質管理手法

misc
3D の数学のお勉強ノート
seagull の自宅サーバ
PGP 公開鍵(鍵サーバ)
PGP 公開鍵(ファイル)

最近のお気に入り
Slashdot Japan

書いた人:seagull
E-Mail:seagull.hiroki@nifty.ne.jp
   トピック
関連文献
developperWorks XML Zone
Free XML tools and software
startkabel XML
XSLT1.0勧告
XSLT1.1標準化案
XSL 1.0の邦訳
  

§この文書について


この文書は、私 seagull が XML 関連技術の学習を進める上でメモ書きとして残しているものです。 内容の正確さ、妥当性については一切の保証はありません。

§XSLTを書く時に気を付けておくべき事


XML文書から直接最終レイアウトを生成してはいけない

途中に FO を介在させるべき、それによって、意味情報がら見た目情報への変換と、 見た目情報から低次元データフォーマットへの変換の2つに問題を分離する事ができる。

結果として、意味情報から見掛け情報に変換する XSLT がシンプルになり、 使いまわしが効くようになる。 見た目情報から低次元への変換ルールである FO の使いまわしについては説明するまでもない。

"//"は避ける

スキーマの定義が固まりきっていない時に使いたくなってしまうが、 これが必要って事はスキーマを見直した方がいいって事。

また、XSLT プロセッサのパフォーマンスを落す原因になる。例えば。


<xsl:template match='document//p'>
の代わりに、単に

<xsl:template match='p'>
と書くように癖を付ける事。前者の書き方をすると、 XSLTプロセッサはp要素を見付けた時に、 その親に遡ってdocument要素があるか調べなくてはならない。 後者のような記述も同時あった時に優先順位を決定するために大量の処理を行わなくてはならない。

"//"が便利になるのは文書構造を厳格に指定したい時。 たとえば文書の「項」を表す"sect"要素は「章」の"chapter"の子でなくてはならないのを明示したいとき、 [ chapter//sect ] と記述し、それより優先順位の低いtemplateでxsl:messageを使って Warning を出すといった感じに使えばわりと便利かもしれないが、 これは本来バリデータを使うべきでだろう。 *1

{と}を使う

たまに以下のような記述をしている参考書を見ます。


<xsl:element name='a'>
  <xsl:attribute name='href'>
    <xsl:value-of select='@url'/>
  </xsl:attribute>
  <xsl:text>
    <xsl:value-of name='text()'/>
  </xsl:text>
</xsl:element>

冗長で読みづらいです --- それにタイプ量も多い ;-)。代わりに以 下の様に書けます。


<a href='{@url}'><xsl-value-of select='text()'/></a>
*2

HTMLへの空白&nbsp;の出力方法

以外と知らない人がいるようなので、ここにも書いておく。 dW に書いてあったような気がしたけど。。

なにも考えずに &nbsp; と書いても、HTML中に空白が出力されるだけなので、 ブラウザで見ると結局無視される。 &amp;nbsp;と書いても、HTML出力の際に先頭のアンパサンドがエスケープされてしまう。 そこで


<xsl:text disable-output-escaping="yes">
  &amp;nbsp;
</xsl:text>
と書く。

デフォルトテンプレートを置き換える


  <xsl:template match='text()|@*|*'>
    <!--
    空白文字を正規化した後に、空または空白のみから成る要素を除外する
    -->
    <xsl:variable name='tmpstr' select='normalize-space()'/>
    <xsl:if test='$tmpstr != "" and $tmpstr != " "'>
      <xsl:value-of select='$tmpstr'/>*
    </xsl:if>
  </xsl:template>
こんな感じのテンプレートを優先順位の一番低い所に書いておく。

すると、


<xsl:apply-template select='text()'/>
<xsl:apply-template select='@mailaddress'/>
<xsl:apply-template select='autor'/>
見たいなやつが、すべてこのテンプレートに引っかかる様になります。 全てってのが気にいらないのなら、mode を併用するといろいろできる。

これを使うと、全ての出力に同一のフィルターを噛ませる事ができる。 あ、俺もう xsl:value-of 使うのやーめた。

-----------
*1 : XMLの検証するツールを知らないもので、、、
*2 : 誤植なおしました。なに喰ったかのメモさん、ありがとうございます。 いや〜。使い込んでますね。私も見習わないと。とりあえず、 yggdrasillはダメって事で。。

§複数の入力/出力文書を扱う


元になる XML 文書を整形出力した結果の中に別の XML 文書を埋め込み たい、逆に1つのXML文書から複数の文書を出力したいという事があります。

documet()関数

XPath の [ document() ] 関数を使う方法です。

File: foo.xslt

<xsl:template match='include-xml'>
  <xsl:apply-templates select='document(@href)'/>
</xsl:template>
          
        
当然、 [ select ] の所を、

  <xsl:apply-templates select='document(@href)/foo/bar'/>
          
        
として、部分的に取り込む事もできます。 欠点は、XSLTの保守が面倒になるという事。 この方式はレンダリングのルールに過ぎないので、XML文書同士には直接のリンク関係はありません。 このため、ドキュメント同士のリンクでなく、 最終的な出力の中に固定的に他の文書を埋め込むのによく使います。 *3

ただ、マスタ - ディテール の解決をするには便利です。

File: XSLT

<xsl:template match='社員情報'>
  <xsl:variable name='ID' select='@社員ID'/>
  <xsl:apply-templates select='document("master.xml")/社員一覧/社員[@ID = $ID]'
                       mode='@type'/>
</xsl:template match='社員情報'>
<xsl:template match='/社員一覧/社員' mode='詳細'>
              :
</xsl:template match='/社員一覧/社員'>
<xsl:template match='/社員一覧/社員' mode='簡易'>
              :
</xsl:template match='/社員一覧/社員'>
とかしておくと、

<社員情報 type='詳細' 社員ID='012345'>
<社員情報 type='簡易' 社員ID='012345'>
という感じで使える。

XInclude

XIncludeは、XML文書の中に他のXML文書を取り込む方法です。 元のXML中に取り込みの指定を行うため、文書間の依存関係が XML 同士だけで完結します。 *4

File: sample.xml

<?xml version='1.0'?>
<foo xmlns:xi='http://www.w3.org/2001/XInclude'>
  <xi:include href='foo.xml' parse='xml' encoding='EUC-JP'/>
</foo>

@xmlns:xi 要素
XInclude を使う時には必須です。 xi:include エレメントの親のどこかで指定する必要があります。
xi:include エレメント (必須)
他の文書を取り込むための指定です。
xi:include@href 属性 (必須)
取り込む文書の URL です。
xi:include@parse 属性 (省略可能)
取り込む文書が XML('xml') か プレーンテキスト('text')かを指定します。 省略時は 'xml' です。
xi:include/@encoding 属性 (省略可能)
取り込む文書のエンコーディングを指示します。 XML文書の場合は文書内に記述されているはずなので、 テキスト文書を取り込む時に必要になるのでしょうか。。。 省略時は。。良く判りませんが、多分自文書と同じになるのかな?

XLink + XPointer

勉強CHU!。「取り込む」のではなく、リンク関係を記述する規格らしい。

xsl:document

これまでと逆に、1つのXML文書から複数の出力を得たい時には、 xsltproc XSLT1.1から入る(予定?)の xsl:documentが使えます。

<xsl:template match='/'>
出力文書に出力する内容1
<xsl:document href='foo.xml'>
foo.xmlに出力する内容。
</xsl:document>
出力文書に出力する内容2
</xsl:template>

ちなみに動作確認したxsltprocのバージョンはこんな感じ。


Using libxml 20416, libxslt 10006 and libexslt 600
xsltproc was compiled against libxml 20408, libxslt 10006 and libexslt 600
libxslt 10006 was compiled against libxml 20408
libexslt 600 was compiled against libxml 20408

-----------
*3 : Seagull's working room では、目次の部分などに使っています。
*4 : Seagull's working room では、TODOリストとプログラムソースの表示などに使っています。

§XML文書を部分的に更新する


例えば、、、

XML文書をアプリケーションのデータストアの1形態として見た時、ア プリケーションの機能として、当然

レコードを新規に追加する(文書を新規に作成する)
レコードを更新する(文書の1部分を変更し、上書き保存する)
レコードを削除する(文書を破棄する)
レコードを検索する(複数の文書の中から条件に合致するものを 抜き出す)
という要求が自動的に存在します。この内、新規と削除はまぁいいでしょ う。単にテキストファイルの作成/削除/移動ができればそれで済みます。

さて、最大の問題は更新ですたとえば、社員マスタを考えてみた場合。


<社員マスタ>
  <社員 ID='0001' BRANCH='北埼玉工場' FNAME='磯野' LNAME='かつお'/>
  <社員 ID='0002' BRANCH='首都圏営業本部' FNAME='磯野' LNAME='波平'/>
</社員マスタ>
この XML 文書の場合にはマスタデータですので、レコードの新 規作成も、削除も XML 文書のレベルでいえば「更新」操作となります。

さて、ここで磯野かつおさんが青森営業所に異動になったとします。 この時、


update document('社員マスタ')/社員マスタ[/社員@ID="0001"] set BRANCH = '青森営業所'
        
のようにかければ便利です。XUpdate をつかえば似たような事はできる ようですが、いまいち実装系が無くて。。少なくとも今のところはこ れを一般的な手段で解決できません。 また、 XML ネイティブDBと名乗る連中は大抵が「なんちゃってデータ ベース」ですので *5、 こんな本当に欲しい機能は持っていません。

しばし思案

「入力も出力もXMLなんだからXSLTでなんとかならない?」

ん〜、もっともな御意見。スマートではないが、現在の所は最も現実 的な要望でしょう。少なくともそう見えます。

ここで目を付けるのは、「出力の大部分は入力と同一」という事。つ まり、「入力文書 + 差分の文書 = 出力文書」という事です。しかも、 差分文書を単なるプロトコルととらえればよいわけです。 す。

んで、ちょっとこのプロトコルについて考えてみる。 *6


<xsl:template match='社員マスタ更新[@action='update]'>
  <xsl:variable name='TARGET' select='@ID'/>
  <社員マスタ>
    <xsl:for-each select='document("社員マスタ.xml")/社員'>
      <社員 ID='{$TARGET}'>
          BRANCH の更新が指定されてれば新しいBRANCH属性を出力。
          指定されていない場合は元の BRANCH 属性を出力。
                    :
              (他の属性と子要素について繰り返す)
                    :
      </社員>
    </xsl:for-each>
  </社員マスタ>
</xsl:template>
問題は、更新しない所をどうやってそのまま出力するかという点だな。。。 そこさえクリアになれば、1回テンプレートを用意すれば全ての文書 に適用できるのだが。。。いずれにしても、こういった事はアプリケー ションでやる事でなくて、バックエンドストレージサービスが対応す べき事だな。。。。

-----------
*5 : 例えば、某 「yggなんとか」
*6 : うまく行けば、XFormsと組み合わせて楽できるなぁ。

§ツール


ファイルの一覧を XML 化

perl スクリプトをダウンロード
File: genfilelist.pl

#! /usr/bin/perl

use strict;

my $timezone = "JST";
my $dir = shift @ARGV;

opendir DIR, $dir;

print <<_EOF;
<?xml version="1.0" ?>
<filelist>
_EOF

while ($_ = readdir(DIR)) {
  next if ($_ eq '..');

  my @stat = stat("$dir/$_");
  my $fname = $_;
  my $realname = $_;

  my @tms = map do {
    my @lt = localtime($stat[$_]);
    sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
           $lt[5] + 1900, $lt[4] + 1, $lt[3],
           $lt[2], $lt[1], $lt[0],
           $timezone);
  }, (7, 8, 9);
  
  my $mode = $stat[2];
  my $is_directory = ($mode & 0040000)? "yes" : "no";  # directory?
  my $md5sum = "";
  if (-f "$dir/$fname" and -r "$dir/$fname") {   # get MD5 sum if readable file
    my $path = "$dir/$fname";
    $path =~ s/'/\\'/g;
    my $cmd = "md5sum '" . $path . "'";
    `$cmd` =~ /^([^ ]*)[ ]/;
    $md5sum = "$1";
  }
  chomp $md5sum;

  $mode = sprintf("%o", ($mode & ~0140000));
  print <<_EOF;
  <file name="$fname" directory="$is_directory"
        mode="$mode"  size="$stat[7]"
        md5sum="$md5sum"
        atime="$tms[0]" ctime="$tms[2]" />
_EOF
}
closedir DIR;

print "</filelist>\n";

目次ファイルから Makefile を生成する

XSLT をダウンロード目次ファイルをダウンロード
File: Makefile.xslt

<?xml version='1.0' encoding='EUC-JP'?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="text" encoding="EUC-JP"/>

<xsl:template match='/table-of-contents'>
##
## this file was generated from Makefile.xslt and toc.xml autometicaly.
##  DO NOT TOUCH THIS FILE
##


.SUFFIXES:
.SUFFIXES: .xml .html

XSLTPROC=xsltproc --xinclude
BASE_XSLT=XSLT/to-web.xslt
DESTDIR=../
HTMLDIR=$(DESTDIR)/homepage
CGIDIR=$(DESTDIR)/cgi-bin

TARGETS= \
<xsl:for-each select='(toc-group/toc[@document != ""] | project[@document != ""])'>
  <xsl:value-of select='concat(" ", @document)'/>
</xsl:for-each>


.xml.html:
	$(XSLTPROC) -o xsltproc.tmp $(BASE_XSLT) $&lt;
	if diff xsltproc.tmp $@ >/dev/null; then true; else \
		cp xsltproc.tmp $@; \
	fi
	rm xsltproc.tmp

all: $(TARGETS)

clean:
	rm -f $(TARGETS)
	rm -f filelist.xml stamp-filelist

distclean: clean
	rm -f Makefile
	rm -f *~ *.bak

stamp-filelist:
filelist.xml: stamp-filelist
	./genfilelist.pl $(HTMLDIR)/archive &gt;$@
	touch stamp-filelist

Makefile: Makefile.xslt toc.xml
	./autogen.sh
	${MAKE}


<xsl:for-each select='(toc-group/toc | project)[@document != ""]'>
      <xsl:value-of select='@document'/>: <xsl:value-of select='@source'/> \
      $(BASE_XSLT) toc.xml site-config.xml \
      <xsl:for-each select='depend'>
        <xsl:value-of select='concat(" ", @file)'/>
      </xsl:for-each>
      <xsl:if test='boolean(document(@source)//download-files)'>
        <xsl:text> filelist.xml </xsl:text>
      </xsl:if>
<xsl:text>
</xsl:text>
</xsl:for-each>

</xsl:template>

</xsl:stylesheet>
$Id: to-web.xslt,v 1.1.1.1 2002/04/25 00:48:38 seagull Exp $, Copyright (C) 2001 seagull.
Browse SOURCE XML or XSLT. | Get my gpg public key. | Contact to webmaster seagull