連載
» 2000年09月23日 00時00分 UPDATE

XMLを学ぼう(5):DTDを読んでみよう

現在のところ、XMLの入門にはどうしてもDTDの解説を避けて通ることができない。しかし、多くの人にとってDTDは記述することよりも読解することのほうが圧倒的に多いだろう。そこで、今回はW3Cで利用されている実際のDTDを例にとり、それを解読してみる。

[川俣晶,株式会社ピーデー]

DTD解読にチャレンジ

 前回はDTDは何のために存在するかを説明した。主に理由を説明したため、構文の詳細については、深く説明ができなかった。そこで、今回は、DTDの構文について説明しよう。

■サンプルとして仕様書DTDを使う

 今回はサンプルとして、W3Cの“仕様書DTD”(仕様書を記述するために作成されたDTD)を取り上げて、これを読めるようにしてみよう。簡単なDTDの書き方を説明しようかとも思ったが、実際にDTDを書く機会はそれほど多くはない。しかし、XMLを利用していると、DTDを読まなければならない状況は、比較的頻繁に発生する。そこで、今回は、実際に実用とされているDTDの例を取り上げて、この読み方を説明したいと思う。実用DTDを読みくだす能力があれば、実際にXMLを利用するさまざまな場面で役に立つだろう。

 さて、XML 1.0勧告の仕様書は、実はXMLで書かれているということをご存じだろうか。普段、皆さんが見ている仕様書は、それをHTMLに変換したものである。大本のXMLで書かれたXML仕様書は、以下にある。

 Extensible Markup Language (XML) 1.0 (XML版)(筆者注:理由は分からないが、このファイルをInternet Explorer 5.5で読みに行ったらエラーになってしまった)

<!-- ........................ -->
<!-- XML specification DTD .. -->
<!-- ........................ -->

<!--
TYPICAL INVOCATION:
# <!DOCTYPE spec PUBLIC
# "-//W3C//DTD Specification V2.1a for XML 1.0//EN"
# "http://www.w3.org/XML/1998/06/xmlspec-v21a.dtd">

PURPOSE:
This XML DTD is for W3C specifications and other technical reports.
It is based in part on the TEI Lite and Sweb DTDs. This is a
one-off version, based on Version 2.1, that has modifications
especially for use with the XML 1.0 (first edition) specification,
unchanged from its original publication on 10 February 1998. Only
the minimum changes have been made to support this specific XML
instance; DO NOT use this DTD as the basis for further modification.
Instead, use the latest version of XMLspec on the regular development
track (http://www.w3.org/XML/1998/06/xmlspec.dtd).

…以下略…

 このXML文書から参照されているDTDは以下にある。

 このDTDを見ると、長くて複雑そうに見える(先頭から数行を右に示した)。そこで、そんな難しいことが理解できるはずがないと思った人もいるだろう。だが、量は多いが決して複雑ではない。

■コメントを取り除くと…

 まず、XMLのコメントを除外して、もう一度眺めていただきたい。XMLのコメントは、<!--で始まり、-->で終わる部分である。これらは、すべて人間の言葉で補足説明を付けているだけのものであって、機能には何の影響も与えていない。

 コメントを取り除くと、残された部分には、<!ENTITYで始まる記述、<!ELEMENTで始まる記述、<!ATTLISTで始まる記述の3種類しか存在しないのである。つまり、実用DTDといっても、このたった3種類を理解するだけでよいのである。

<!ENTITY ... >の機能

 “ENTITY”は、日本語では“実体”と呼ぶ。ある種の文字列などの内容に名前を付けて定義しておき、それ以後は名前を書くだけで容易に参照できるようにするものである。

■実体を宣言する

 最も基本的な実体の使い方は以下のようになる。まず、以下のようにDTDの中で実体を宣言する。

<!ENTITY i18n "internationalization">

 ここで規定した名前を利用して、本文中にアンパサント(&)記号で始まり、セミコロン(;)記号で終わる表記を行うことができる。例えば、以下のように記述できる。

国際化は英語では&i18n;と呼ぶ。

 このテキストは、以下のテキストであるかのように扱われる。

国際化は英語ではinternationalizationと呼ぶ。

 実体は、内容を統一しておきたい単語や、あとで変更する可能性がある単語などを定義するのに役立つ。

■パラメータ実体

 しかし、DTD内部で使用される実体は、これとは少々異なる機能を持つ。そして、実体は実体でも、「パラメータ実体」と呼ばれる。パラメータ実体は、実体を宣言するときに名前の手前にパーセント(%)記号を記述して、パラメータ実体であることを示す。そして、パラメータ実体は、本文ではなく、DTDの内部で参照される。

 以下が一例である。

<!ENTITY % Special "a|b">

 ここで規定した名前を利用して、DTD中にパーセント(%)記号で始まり、セミコロン(;)記号で終わる表記を行うことができる。例えば、以下のように記述できる。

<!ELEMENT block (%Special;|c)>

 このテキストは、以下の行が存在するかのように扱われる。<!ELEMENT ... >については、前回も解説したので説明は略す。

<!ELEMENT block (a|b|c)>

 パラメータ実体は、ただ単に文字列を置き換えているだけで、文法的な意味までコントロールしているわけではない。だから、パラメータ実体の内容が文法的に正しいとしても、パラメータ実体を置き換えた結果が文法エラーになるなら、これはエラーになる。

 パラメータ実体は、いくつかの利用方法がある。例えば、DTDで繰り返し出現する文字列を1カ所で定義しておき、手直しは1カ所で済むようにするような使い方。頻繁に書き換える部分だけをパラメータ実体として独立させ、書き換え時に関係ない場所まで書き換えないようにする、といった使い方。利用者がカスタマイズできる個所を、パラメータ実体として独立させておき、パラメータ実体の追加だけで機能追加できるようにする使い方、などがある。

複雑怪奇な&lt;の定義を解きほぐす

 今回読むサンプルの中で、実体の実際の利用例を見てみよう。

 通常の実体の利用例は多くない。しかし皆無というわけではない。まず以下の行に注目してみよう。

<!ENTITY lt "&#38;#60;">

 これは、互換のために定義することが推奨されているものである。しかし、意味が分かりにくいと思うので、説明を加えよう。

■記号を文字列で置き換える

 不等号記号(<)を記述するためには、&lt;という文字列を記述するのがXMLの決まりである。これを実体として宣言したのが上記の行である。

 まず、以下のように記述することは間違いである。

<!ENTITY lt "<">

 XMLでは、クオーテーション(")記号の内側だからといって、マークアップ用の記号を特別扱いしない。そのため、マークアップ用の記号を直接書き込むことは、単なる文法エラーの発生源にしかならない。その代わり、クオーテーション(")記号の内側でも、実体の参照などが記述できる。この例はあとで出てくる。

 では、これをXML本文と同じように参照の構文に置き換えたらどうだろうか。

<!ENTITY lt "&lt;">

 この構文には決定的な問題がある。それは、&lt;を定義するために&lt;を使ったのでは定義が循環してしまうからである。つまり、自分の靴ひもを引っ張って自分の身体を空に浮かべようというのと同じように、現実的ではない。

■XML文書中で文字コードを使う

 これを解決するためには、文字参照の構文を用いて、文字コードを直接記述することである。不等号記号(<)は、Unicodeの10進数表記で60という値になる。これをXML文書中に書く場合は、&#60;と記述する。つまり、以下の2例は、等価である。

例1: x &lt; y
例2 x &#60; y

 文字参照を使えば、&lt;を定義するのに&lt;を使ってしまうという不都合を回避できる。

 つまり、こう記述する。

<!ENTITY lt "&#60;">

 ところが、これはうまく機能しないのである。なぜかというと、この実体をXML本文中に展開すると、妙なことになるからだ。具体的な例を示そう。以下のようなXML文書(の断片)があったとする。

x &lt; y

 上記のような実体の定義がなされているとすると、実体を置き換えると以下のようになってしまう。

x < y

 つまり、マークアップ用の記号が生のままXML文書の中に出現してしまうのである。これを回避するには、実体の宣言の中に書かれたアンパサント(&)記号がマークアップとして解釈されることを防がねばならない。そのためには、アンパサント(&)記号を直接書く代わりに、これも文字参照で書く。アンパサント(&)記号をUnicodeの10進数のコードで表現すると、38になる。つまり、文字参照でアンパサント(&)記号を書く場合は、&#38;と記述することになる。そこで、アンパサント(&)記号を&#38;に置き換える。

<!ENTITY lt "&#38;#60;">

 これをXML文書の本文中で展開すると、以下のようになる。

x &#60; y

 &#60;は間違いなく不等号(<)に変換されるので、これで意図通りの結果になるわけである。

空のパラメータ実体の謎

 このDTDを眺めていくと、奇妙なパラメータ実体を見かけるだろう。例えば以下のように空のパラメータ実体が定義されているのである。

■空のパラメータ実体

<!ENTITY % local.list.class "">

 このパラメータ実体は以下の宣言で参照されている。

<!ENTITY % list.class
   "ulist|olist|slist|glist %local.list.class;">

 この行は、local.list.classという名前のパラメータ実体を参照している。つまり、%local.list.class;の部分は、そのパラメータ実体の内容に置き換えられる。ところが、この内容はその手前で空として宣言されている。つまり、このパラメータ実体を参照したからといって、何かの内容が追加されるわけではないのである。

 だが、一見無意味に見えるこのようなパラメータ実体が、このDTDの中にはたくさん含まれている。これはいったい何を意味しているのだろうか?

 これは、利用者がDTDを拡張することを意図した拡張予備なのである。

■利用者がDTDを拡張できる

 例えば、list.classというパラメータ実体は、リストを表現する要素の名前を列挙したものである。もし、このDTDを使う利用者が、どうしても、もう1種類独自のリストを使わねばならないとき、普通に考えればlist.classの宣言を書き換える。例えば、新しいmylistという要素をリストに追加するなら、以下のように書く。

<!ENTITY % list.class "ulist|olist|slist|glist|mylist">

 だが、このように宣言を直接書き換えてしまうことは、好ましいことではない。もともとのDTDと、書き換えた結果のDTDの違いが、一目見て分かりにくいからだ。更にその文書やDTDを再利用しようというユーザーが出てきたとき、どこまでがオリジナルで、どこからが独自の追加なのか、区別がつけにくい。

 そこで、あらかじめ、独自拡張用に、専用のパラメータ実体を用意しておくわけである。list.classというパラメータ実体に対しては、local.list.classlocalという名前を付け加えたパラメータ実体を用意しておく。localの付いたパラメータ実体の中身はすべて空にしておく。そして、独自拡張が必要な場合は、このlocalの付いたパラメータ実体に中身を追加する。上記の例を、この方法で記述すると、以下のようになる。

<!ENTITY % local.list.class "|mylist">
<!ENTITY % list.class
   "ulist|olist|slist|glist %local.list.class;">

 このようなルールは、DTDの仕様に含まれているものではなく、パラメータ実体という機能を利用して慣習的に行われているものである。そのため技術資料だけ見ていると分からないことがある。注意されたい。

属性を宣言する<!ATTLIST ... >

 このDTDの中で、要素の宣言(<!ELEMENTで始まる宣言)を見ていると、それに付随して、ATTLISTという宣言が一緒に記述されていることが多いのが分かるだろう。

<!ELEMENT body (div1+)>
<!ATTLIST body %common.att;>

 ATTLISTは、その要素に付けることができる属性に関する宣言を行う。

■属性を宣言する

 例えば、aという名前で任意の文字列を記述することができ、省略が許されるとき、それは以下のように表現される。

<!ATTLIST body a CDATA #IMPLIED>

 aの手前に書かれたbodyは、もちろん、その宣言を適用する要素の名前である。

 ATTLISTは、以下の3情報を記述する。属性の数だけ、この3情報が繰り返される。

  1. 属性の名前
  2. データ型
  3. 省略可能かどうかなどの情報

 データ型というのは、DTDにおいては、要素には付かない属性独自の情報だ。だが、データベースやプログラム言語のデータ型と違って、整数や浮動小数点のような数値型はない。あるのは、任意の文字列か、あるいは、いくつかあるルールによって記述された文字列だけである。指定できるバリエーションは以下の通り。

データ型 説明
CDATA 任意の文字列。XMLが許す文字であればどんな文字が並んでもよい
ID XML文書内でユニークな名前を付ける。異なるID型の属性に、同じ文字列が指定されていたらエラーになる
IDREF ほかの個所でID型の属性として指定された名前を参照する。同じ名前のID型の属性がなければエラーになる
IDREFS IDREFをリストして記述することを許す
ENTITY 実体の名前を記述することを許す
ENTITIES ENTITYをリストして記述することを許す
NMTOKEN XMLで名前として許されている文字の組み合わせの文字列
NMTOKENS NMTOKENをリストして記述することを許す
選択可能値のリスト 選択可能値のリスト 指定が許される文字列の種類が限定され列挙できるときは、ここに列挙することができる。例えば、(a|b|c)のように記述する

 最後の省略可能かどうかなどの情報として指定できるのは、以下のいずれかだ。

  • #REQUIRED
    その属性は必須である。省略したらエラーである
  • #IMPLIED
    デフォルト値は指定されない。属性は書いても書かなくてもよい
  • デフォルト値
    その属性の省略時には記述された値が指定されたかのように扱われる
  • #FIXED デフォルト値
    デフォルト値が指定されているが、それから変更することは許されないことを示す。デフォルト値と違う値を書くとエラー

 属性の宣言は3つの内容がペアで出現すると書いたが、上記の#FIXEDの場合のみ、見方によっては4つあるかのように見えてしまうことになる。

共通の属性とは何か?

 さて、ここまでの知識を元に、実際のDTDを読んでみよう。最初に登場する属性の宣言は以下のようなものだ。

<!ATTLIST head %common.att;>

 これはパラメータ実体を使っているため、具体的な内容はこの行に書かれていない。さて、ほかの属性の宣言を見ると、あれあれ? と思うだろう。このパラメータ実体%common.att;が、ほぼすべての属性の宣言に登場しているのだ。もちろん、ほかの要素では、ほかの属性の宣言も付け加えられていることがある。だが、%common.att;を付けていない属性の宣言は少数である。これはいったい、どういうことだろうか?

 その謎を解くために、%common.att;の内容をじっくり見てみよう。

<!ENTITY % common.att 'id ID #IMPLIED %role.att; %diff.att;'>

 またパラメータ実体の参照である。これでは何が書かれているのかよく分からないので、全部展開してしまおう。以下は見やすいように定義をリスト形式に書き換えてある。

属性 定義
id ID #IMPLIED
role NMTOKEN #IMPLIED
diff (chg|add|del|off) #IMPLIED

 つまり、日本語でいうと、idという名前の属性は、ID型で書いても書かなくてもよい。roleという名前の属性は、NMTOKEN型で書いても書かなくてもよい。diffという名前の属性は、chg、add、del、offのいずれかを値として記述することができ、属性そのものは書いても書かなくてもよい。

 しかし、これだけでは何を意図したものか分からない。これらの属性が意図した機能を考えなければ、物事のよし悪しは分からない。

 id属性は、いろいろな理由でXML文書を取り扱うときに便利なので、よく記述されるものである。要素に対して、id属性に名前を付けておくと、具体的に特定の要素を指し示すときに便利である。特に、XMLのリンク標準仕様の1つであるXPointerなどでXML文書の特定の場所を指し示すとき、id属性の有無は重要だ。XPointerでは何も手がかりがないときは、何番目の要素、という形で特定の要素を指し示す。しかし、これは、XML文書に要素を挿入、削除して編集してしまうと、別の要素を示すことになってしまう恐れがある。しかし、id属性を仕込んでおくと、何番目ではなく、何というid属性を持った要素、という形で特定の要素をはっきりと指名することが可能になる。これなら、挿入、削除を繰り返して要素の個数が変動しても、正しい要素を常に指し示すことができる。このように、XPointerなど、外部からの情報参照を受けることを想定すると、id属性はすべての要素に付けられるようにしておくのがベストである。

 さて、次にあるrole属性は、特別な意味を与えられていない。これは、著者が何かの独自の役割分類を要素に付け加えるために用意されている。

 最後のdiff属性は、編集の結果を示すために使われる。chgはその要素が変更されたことを示す。addは追加されたことを、delは削除されたことを、offは変更がないことを示すのだと思われる。

 ここで、この3つの属性の性格を考えてみよう。これらに共通することは、要素の種類によって、必要性が変化しないということである。そして、どの属性も、ほぼすべての要素に付ける可能性があるということだ。

 このような共通な属性は、同じ名前、同じ条件のものを、(例外を除けば)間違いなくすべての要素に付ける必要がある。そのためには、パラメータ実体を定義して、それをおのおのの要素の属性宣言から参照する方がよい。それによって、同じ内容であることが強制されるし、それに加えて、修正を加えるときには1カ所だけ書き換えればよいというメリットもある。

DTDを読むとは、知の蓄積に触れること

 前回の説明に加えて、今回の解説内容を把握すれば、実用的なDTDをかなり読み下すことが可能になるはずだ。もちろん、まだ複雑すぎると判断した一部の機能は説明していないので、すべての内容を読めるわけではないが、大筋は追えるだろう。

 DTDを読むということは、知の蓄積に触れるということであり、作成者の考え抜いた対象の本質を共有するということである。実際に、人間が知っていると思い込んでいるルールが、DTDとして書いてみると矛盾を含んでいることが分かるというのは珍しくない。そこで、物事をきちんと処理するために、対象を整理分類して、それを体系的に組み立て直す必要がある。その成果物がDTDというわけである。

 つまり、DTDを読むということは、DTDというツールを通して、人間と人間社会を見ることになるのである。これは単なる技術論で片づけられる問題ではない。それゆえに、技術者だけでDTDは作ることができない。また、DTDを作るというプロセスが、組織のルールの欠陥や矛盾を暴き出してしまうこともあるので、ただ単に部下にDTDを作れと命じれば済むという問題ではない。DTDを作るプロセスで分かった事実を、組織の改善に反映していく手順も、重要である。

 さて、DTDのだいたいの役割を解説したところで、次回はXML文書を書く場合の文字コードの問題を解説しよう。実際、XML文書を扱うとき、文字化けに遭遇することは珍しい話ではない。たとえ、複雑なことはしない初心者であっても、文字化けのリスクは大きい。そこで、次回は、特にXML初心者向けに、文字化けの発生メカニズムと対策について解説したい。

 それでは次回、また会おう。


Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。