第19回 落とし穴が潜む属性値の正規化

XML 1.0は、1998年にW3Cから勧告として公開された。当然中身は英語で、しかもEBNFと呼ばれる式によって重要な部分が記述してある。この連載では、XML 1.0を深く理解するために、そのXML 1.0勧告の最新版「Extensible Markup Language (XML) 1.0 (Third Edition)」をだれでも分かるように、やさしく読み解きながら解説していくことを目指している。(編集局)

川俣 晶
株式会社ピーデー
2004/3/6



属性値の正規化とは?
今回の主な内容
属性値の正規化とは?
XML 1.1とXML 1.0 Third Editionに関する補記
属性値の正規化の概要
属性値は正規化しなければならない
正規化を行うアルゴリズム
正規化の処理の続き
正規化の例

 前回は、「3.3.2 Attribute Defaults(3.3.2 属性のデフォルト)」について読んだ。これは、属性の省略を許すか否か、省略した場合のデフォルト値をどうするかについて記述された部分である。属性のデフォルト値を活用すると、DTDを使った処理と使わない処理では結果が異なる状況が容易に発生する。これは、「貴族とボヘミアン」の問題などとも関係が深く、XMLをどのように進化させていくかについての論点の1つだった。

 さて今回は、「3.3.3 Attribute-Value Normalization(3.3.3 属性値の正規化)」を読んでいく。ここは、知らないと落とし穴になる可能性のある重要な個所である。例えば、要素の内容として記述した文字列と、属性値として記述した文字列の扱いは常に同じというわけではない。XMLによって作られた言語では、属性で記述しても要素で記述しても同じ、と見なして扱う場合もあるのだが、すべてのケースでそれがうまくいくわけではないことに注意が必要である。また、属性の型を変更するだけで、同じXML文書から取得される属性値が変わってしまうケースもある。このような落とし穴になり得る属性値の正規化について、しっかりと読んでおこう。

編集注:この連載では、XML 1.0勧告であるW3Cの「Extensible Markup Language (XML) 1.0 (Third Edition)」(英語)を参照し、その日本語訳として、日本工業規格 JIS X 4159:2002(Second Edition相当。リンク先は該当規格の原案ですが、最終版とほぼ同等の内容です)と追補1として出版予定の原稿(Third Edition対応)を参照しています。本文中のピンクの地の部分は、XML 1.0勧告の原文を示しています。

XML 1.1とXML 1.0 Third Editionに関する補記

 本題に入る前に、2004年2月4日にXML 1.1とXML 1.0 Third Editionという2つの新しい勧告が登場したことで、この連載を今後どのような方針で進めていくかについて述べておく。関心のある方は下記のリンクから参照してほしい。

  XML 1.1とXML 1.0 Third Editionについて
  (別ページにジャンプします)

属性値の正規化の概要

 ここからは、属性値の正規化についての説明を読んでいく。すでに「2.11 End-of-Line Handling(2.11 行末の取扱い)」(第12回 XMLにおける空白、行末、言語識別)で、行末の文字が正規化されることは説明した。つまり、改行文字に関しては、書かれたとおりの文字がありのまま応用プログラムに渡されない場合がある。属性値には、そのほかにも正規化されるルールがある。よく、ある値を要素で表現するか属性で表現するか悩むことがあるが、ここで説明される属性値の正規化という規定があるため、両者は完全に同一にならないことに注意が必要である。例えば、以下の2つの例があるとしよう。

<element>……</element>

<element attribute="……" />

 ここで記述された「……」の部分に同じ文字列が書かれていたとしても、応用プログラムから取得した場合に両者が一致しないこともあり得る。どちらの書式でも通るように配慮して設計された言語もあるのだが、この2つは完全に一致しないことに注意が必要である。

 余談だが、この2つが同じ結果になると思い込んでいる人も多いので、ここを読んで知識を付けておくと、少しだけライバルに差をつけられるかもしれない。

属性値は正規化しなければならない

 さて、具体的にどう違うのか、これから読んでいくことにしよう。まず、属性値を正規化しなければならないことを示す文章から始まる。

Before the value of an attribute is passed to the application or checked for validity, the XML processor MUST normalize the attribute value by applying the algorithm below, or by using some other method such that the value passed to the application is the same as that produced by the algorithm.

属性値を応用プログラムに渡す前、または妥当性を検証する前に、XMLプロセサは、次に示すアルゴリズムを適用することによって、属性値を正規化しなければならない。ただし、このアルゴリズムと同じ結果を応用プログラムに渡すなら、ほかの方法によって正規化してもよい。

 この文章はやや長い。orの前後で分けて読んでみよう。

 前半は、この文章が「しなければならない(MUST)」であることに注意しよう。属性値の正規化は任意ではなく、必ず行わねばならないものである。次に、正規化を行うタイミングについて注意してみよう。ここでは、「属性値を応用プログラムに渡す前」と「妥当性を検証する前」という2つのタイミングが示されている。ここで2つのタイミングが示されている理由は、少し考えると分かるだろう。もし、妥当性を検証しない場合は、「妥当性を検証する前」というタイミングはあり得ず、「属性値を応用プログラムに渡す前」というタイミングで正規化を行う必要がある。しかし、妥当性を検証する際に属性値が調べられる場合があるので、正規化がそれより後に行われると不都合が生じる。妥当であると確認した属性値がさらに変更されてはまずいのである。それ故に、2つのタイミングを明示する必要がある。

 次に後半を見ていこう。ここでは、結果が同じになれば、ほかの方法でもよい、といっている。これは、この後の説明に対応する正規化の手順を実行した結果と、同じ結果が出ればよいという意味である。この文章の趣旨を前半に書かれたタイミングを例に考えてみよう。前半の文章では2つのタイミングで正規化を行うことが示されている。しかし、プログラマの読者なら、XML文書を読み込んだタイミングで正規化してしまえば、妥当性の検証を行っても行わなくても、1つのタイミングで正規化を行うだけで同等の結果が得られると思ったかもしれない。そのような方法を選択してもよい、ということを示しているのが、後半の文章である。

正規化を行うアルゴリズム

 さて、次は具体的に正規化を行うアルゴリズムの説明である。技術者ではない読者には、アルゴリズムという言葉は難解に思えるかもしれない。しかし、アルゴリズムというのは、「演算手続きを指示する規則」というほどの意味で、ここでは文字を置き換える手順を指す。内容はさほど難しくない。

  1. All line breaks MUST have been normalized on input to #xA as described in 2.11 End-of-Line Handling, so the rest of this algorithm operates on text normalized in this way.

  2. Begin with a normalized value consisting of the empty string.

  3. For each character, entity reference, or character reference in the unnormalized attribute value, beginning with the first and continuing to the last, do the following:

    • For a character reference, append the referenced character to the normalized value.

    • For an entity reference, recursively apply step 3 of this algorithm to the replacement text of the entity.

    • For a white space character (#x20, #xD, #xA, #x9), append a space character (#x20) to the normalized value.

    • For another character, append the character to the normalized value

  1. すべての改行は入力の際に、「2.11 行末の取扱い」で記述されるとおりに#xAに正規化されなければならず、このアルゴリズムの残りの操作はこの方法で正規化されたテキストに対して行う。

  2. 正規化された値は、空文字列であるとして処理を始める。

  3. 正規化前の属性値にある、文字、実体参照および文字参照に対して、先頭から最後まで次の処理を行う。

    • 文字参照については、正規化された値に参照された文字を追加する。

    • 実体参照については、このアルゴリズムのステップ3を実体の置換テキストに再帰的に適用する。

    • 空白文字(#x20、#xD、#xA、#x9)については、正規化された値にスペース文字(#x20)を追加する。

    • そのほかの文字については、その文字を正規化された値に追加する。

 項目の頭に振られた項番の表記が、W3C勧告とJIS版では異なっている。W3C版では1、2、3となっている部分が、JIS版ではa)、b)、c)となっていて、その下の項目はW3Cでは○が付いているだけだが、JIS版では1)、2)、3)、4)と番号が振られている。この相違は、JISという規格が要求する項番表記のルールに従うことが要求されたという編集上の問題であり、特に意識する必要はない。

ステップ1

 ステップ1は、操作の内容というよりは、タイミングの説明である。「2.11 End-of-Line Handling(2.11 行末の取扱い)」(第12回 XMLにおける空白、行末、言語識別)で説明した行末の正規化は当然行わねばならないが、その操作を行った後で、属性の正規化の操作を行うということである。ちなみに、この文章も「しなければならない(MUST)」である。

ステップ2

 次のステップ2にある「空文字列であるとして処理を始める」という表現は唐突で面食らうかもしれない。これを理解するには、ここで説明されているものが、正規化の手順であるということを把握する必要がある。つまり、正規化の作業を始める前に、結果を入れておく箱を空にしておきましょう、という意図を表現したものである。この後の手順で、文字をこの空き箱に入れていくことになる。

ステップ3

 次のステップ3で確認しておきたいことは、正規化前の属性値に含まれるすべての文字などについて、この後に説明される処理を行うということである。これらの処理の結果を空き箱に入れていくことで、正規化された文字列が箱の中に残るわけである。

 さて、ここで行う処理は4つある。それぞれを見ていこう。

 最初は、&#x1234;のような文字参照は、それが示す文字を空き箱に追加するという意味である。「正規化された値に追加する」とは空き箱に追加する、つまり、文字参照で記述された文字はその文字として扱われ、それ以上何かの処理をされることはない。その点で、この後に記述された処理が行われるケースとは明らかに結果が異なる。正規化の対象にしたくない文字は、文字参照として書いておくというテクニックがあり得るだろう。

 2番目の「再帰的」という言葉が分かりにくいかもしれないが、その対象にも同じ手順を繰り返すという意味である。この2番目は、ステップ3の手順の一部だが、それ自身がもう一度ステップ3を繰り返すように求めている。これが再帰的と呼ばれる形態である。この手順により、実体参照はすべてその内容となる文字列に置き換えられて空き箱に追加されることになる。この規定から分かることは、属性値に実体参照を書いたとしても、応用プログラムは実体参照が書かれていたという事実を知る手だてがないということである。この処理によって、実体参照は展開された文字列に置き換えられてしまう。

 3番目の「正規化された値に追加する」とは空き箱に追加するということであるから、#x20、#xD、#xA、#x9の場合は#x20を空き箱に追加するという意図が読み取れる。#x20は半角空白(スペース文字)、#xDが改行、#xAは復帰、#x9はタブ文字である。この規定によって、属性値に書き込まれた改行、復帰、タブはすべて半角空白に置き換えられてしまい、これらを使い分けて記述した文字列を応用プログラムが受け取れないことを意味する。例えば、属性値にタブを書き込む場合と、半角空白を書き込む場合で処理を変えるような応用プログラムを書くことはできない。なお、ここでは#x20のように文字参照的な書式で書かれているが、もちろんこれは文字をコードで表現するために取られた表記であって、文字参照を対象とした規定ではない。文字参照を対象とした規定は最初の規定である。

 4番目は、1〜3番目に該当しなかった文字は、その文字をそのまま空き箱に追加するということである。

正規化の処理の続き

属性の型がCDATAでない場合

 さて、これでリストは終わりだが、正規化の処理はこれで終わりではない。さらに処理の規定は続く。

If the attribute type is not CDATA, then the XML processor MUST further process the normalized attribute value by discarding any leading and trailing space (#x20) characters, and by replacing sequences of space (#x20) characters by a single space (#x20) character.

属性の型がCDATAでない場合は、XMLプロセサは正規化された属性値に対して、さらに次の処理をしなければならない。まず、先頭または末尾にあるスペース文字(#x20)をすべて取り除く。次に、連続するスペース文字(#x20)を1つのスペース文字(#x20)に置き換える。

 この文章は2つに分けて読んでいこう。

 前半は、この規定の発動条件について述べられている。DTDを使わない場合、属性はCDATA型として扱うことが望ましいので、これは主にDTDで属性の型を記述している場合に意味を持つ規定であるといえる。

 そして、後半の文章も「しなければならない(MUST)」である。さて、この規定は、CDATA型ではない場合、つまり、ID型やNMTOKENS型などで必要とされる処理である。例えば、ID型の属性idがあるとき、

id="(スペース)ID01234(スペース)"

と記述された属性の値を応用プログラムから取得すると、前後のスペース文字が取り除かれ、"ID01234"という文字列が得られることを意味する(なお、ここでは"(スペース)"がスペース文字を示すとする)。

 また、NMTOKENS型の属性nmtokensがあるとき、

nmtokens="(スペース)ALPHA(スペース)(スペース)(スペース)BETA(スペース)(スペース)GAMMA(スペース)"

という属性の値を応用プログラムから取得すると、"ALPHA(スペース)BETA(スペース)GAMMA"という値が取得される。これにより、容易に文字列を分解して、個々のNMTOKEN型の値を取り出せるわけだ。逆にいえば、CDATA型以外の型を指定した属性値で、スペース文字の数の違いによって処理を変えることはできない。つまり、

"A(スペース)B"

"A(スペース)(スペース)B"

の記述の違いに意味を持たせることができない。意味を持たせたい場合は、CDATA型にする必要がある。とはいえ、NMTOKENS型などの意図からすれば、これらのスペース文字の違いに意味がないのは当然といえる。

空白文字の正規化と文字参照

 次は、空白文字の正規化と文字参照との関係について書かれている。

Note that if the unnormalized attribute value contains a character reference to a white space character other than space (#x20), the normalized value contains the referenced character itself (#xD, #xA or #x9). This contrasts with the case where the unnormalized value contains a white space character (not a reference), which is replaced with a space character (#x20) in the normalized value and also contrasts with the case where the unnormalized value contains an entity reference whose replacement text contains a white space character; being recursively processed, the white space character is replaced with a space character (#x20) in the normalized value.

正規化前の属性値が、スペース(#x20)以外の空白文字への文字参照を含んでいるなら、正規化された値は参照された文字(#xD、#xAまたは#x9)をそのまま含む。正規化前の値が、空白文字(参照ではなく)を含んでいるときは異なる結果になる。この場合、各空白文字は、正規化された値では、スペース文字(#x20)に置き換えられる。置換テキストがスペース文字を含む実体が、正規化前の値で参照されているときも異なる結果になる。この場合、正規化された値では、再帰的な処理によって、空白文字はスペース文字(#x20)に置き換えられる。

 最初の文章は、すでに説明した、文字参照は正規化の対象にならないという規定を思い出せば、当然のこととして理解できるだろう。つまり、改行、復帰、タブ文字を含む属性値は、文字参照を使って記述することができる。

 次の文章は長いので、分割しながら読んでいこう。まず「正規化前の値が……」の部分は、これまでの説明からすると当然のことといえる。次の文もすでに説明していることである。最後の2つの文は、実体参照で参照される値も、すでに読んだとおり「再帰的に」処理するという規定により、処理の対象となることを述べている。

妥当性を検証しないプロセッサ

 次は、妥当性を検証しないプロセッサでの属性の型についての規定である。

All attributes for which no declaration has been read SHOULD be treated by a non-validating processor as if declared CDATA.

妥当性を検証しないプロセサは、宣言が見つからない属性は、すべて、CDATAと宣言しているとして扱うことが望ましい。

 ここでは「でなければならない(MUST)」ではなく「望ましい(SHUOLD)」であるため、絶対にCDATAとして扱うと決まったわけではないが、ほとんどの場合はCDATA型として扱うことになるだろう。

宣言を読み込んでいない実体への参照

 次は、宣言を読み込んでいない実体への参照を含む場合についての規定である。

It is an error if an attribute value contains a reference to an entity for which no declaration has been read.

属性値が、宣言を読み込んでいない実体への参照を含むときは、誤りとする。

 XML文書を処理する場合、宣言が読み込まれていない実体への参照というものがあり得る。要素内容で出現する場合などは、不明の実体への参照を応用プログラムに渡すケースもあり得るが、属性に関しては同じことができない。属性値は必ず実体参照を展開して文字列として渡すことになるが、宣言が読み込まれていない実体への参照を文字列に展開することはできないので、実行できない。つまり、誤りとするしかないわけである。これも、要素内容と属性値が同じ結果にならない事例の1つである。

正規化の例

 次は、正規化の例である。

Following are examples of attribute normalization. Given the following declarations:

<!ENTITY d "&#xD;">
<!ENTITY a "&#xA;">
<!ENTITY da "&#xD;&#xA;">

次に、属性の正規化についての例を示す。次の宣言がある場合を考える。

<!ENTITY d "&#xD;">
<!ENTITY a "&#xA;">
<!ENTITY da "&#xD;&#xA;">

 ここでは、例として3つの実体が宣言されている。これは、この後の表で参照して使う実体の宣言であって、これ自体が正規化の例というわけではない。

 次が、実際の例である。

the attribute specifications in the left column below would be normalized to the character sequences of the middle column if the attribute a is declared NMTOKENS and to those of the right columns if a is declared CDATA.

(編集部注:ここに入るべき表は別ページに表示しています)

Note that the last example is invalid (but well-formed) if a is declared to be of type NMTOKENS.

属性aがNMTOKENSとして宣言されていれば、表1の左欄の属性指定は、中欄の文字の並びに正規化され、属性aがCDATAとして宣言されていれば、右欄の内容に正規化される。

(編集部注:ここに入るべき表は別ページに表示しています)

aがNMTOKENS型として宣言された場合、最後の例は、整形式ではあるが妥当ではないことに注意。

 最初に表の説明が記述されている。つまり、この表は、1つの表記の属性値が、属性の型の相違によって、どう異なる値に正規化されるかを示しているわけである。

 表の中で分かりにくいのは、Attribute specificationの最も上の項目だろう。これはかなり微妙な内容で、仕様書自身のファイルを転送したりファイルに保存した時点で改行コードが書き換わっている可能性もあるのだが、筆者の手元のファイルでは、以下のようになっている。ここで(復帰)は#d、(改行)は#aとする。

a="(復帰)(改行)(復帰)(改行)xyz"

 この属性値は、まず「2.11 End-of-Line Handling(2.11 行末の取扱い)」(第12回 XMLにおける空白、行末、言語識別)のルールに従い、(復帰)(改行)が1個の(改行)に置き換えられる。この時点での内容は以下のようになる。

"(改行)(改行)xyz"

 そして、改行文字はスペース文字に置き換える属性の正規化が行われることにより、以下のような内容に置き換えられる。

"(スペース文字)(スペース文字)xyz"

 もし、属性の型がCDATAならこれが結果になる。それが、結果として記述された"#x20 #x20 x y z"という文字列に対応する。

 もし、属性の型がNMTOKENSなら、前後のスペース文字は取り除かれるで、結果は"xyz"となる。

 ほかの例も、じっくり追いかけて、確かにそのような結果になることを確認してみてほしい。

 さて、表の後に付加されたNoteがある。この文章の意味を理解するには、生成規則[8] Nmtokensが、表中の"#xD #xD A #xA #xA B #xD #xA"という文字列を許すかどうか、見てみると分かるだろう。生成規則[8] Nmtokensは、スペース文字で区切られた生成規則[7] Nmtokenの繰り返しを意味しており、そこに#xDや#xAの出番はなく、それ故に妥当ではないという結論になる。

 さて、以上で属性値の正規化の説明は終わりである。次回は、「3.4 Conditional Sections(3.4 条件付きセクション)」について説明する。これは、条件によりDTDの内容を変更するという、興味深い機能である。これはプログラムのソースコードでも行われることだが、DTDでは少し面白いトリックによって実現している。請うご期待である。

 

連載 やさしく読む「XML 1.0勧告」


XML & SOA フォーラム 新着記事
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

HTML5+UX 記事ランキング

本日月間