この記事は、月刊『JavaWorld』誌(IDGジャパン)連載「XMLボキャブラリ の理論と実践」第30回、31回、33回に付属のコラム(本文外)をまとめて、わ ずかに変更したものである。
前々回の記事において、空白問題とその周辺事情に関 し紹介し、前回記事では空白処理に関してより詳しく分析した。今回は、 それらをふまえて一応の解決策を示そう。前回/前々回 回の復習も織り交ぜながら話を進める。
これから以下に示す方法は、空白問題に対する1つのアプローチにすぎない。これが唯 一の解だとか最良の解だとか主張する気は毛頭ない。だが、何もしないで手を こまねいているよりは、状況が改善されるとは思う。この対処案の応用として、 最初の記事で触れた“奇妙な内容モデル”に正当な地位を与えることにする。 つまり、“奇妙な内容モデル”でも何の困難もなく扱えることを示す。
空白問題とは、「空白が文字データなのか、それとも区切り記号/整形用詰 め物なのか、判断ができない」ことに起因するさまざまな困難を指している。 この問題に関係して次のような問題も引き起こされる。
例えば、「要素<numberOfItem>の内容データ型がpositiveInteger(正の整数) である」とスキーマで指定されているとき、インスタンスの正しさは空白の解 釈に依存する。次の3つの事例の正しさ(内容がpositiveIntegerであるか)は、 そこに含まれる空白の取り扱いを決めない限りは判断できない。
<numberOfItems> 307</numberOfItems>
<numberOfItems> 307 </numberOfItems>
<numberOfItems> 3 0 7 </numberOfItems>
次の2つの内容モデル(DTD構文で示す)は、直感的には納得できるものだが、 実際には禁止されている。その理由は、文字データ(#PCDATAの部分)に空白 を含めて良いかどうかが曖昧なためである。
<!ELEMENT 姓 (#PCDATA) > <!ELEMENT 名 (#PCDATA) > <!ELEMENT 名前 (#PCDATA | (姓, 名)) >
<!ELEMENT 段落 (#PCDATA) > <!ELEMENT 節 (#PCDATA, 段落*) >
もし、出現する空白がデータなのか無視すべき空白なのかを判断できれば、上の ような内容モデルにも合理的な意味が与えられるはずである。
ここで、後の説明のために、「テキストチャンク」という概念を導入しよう。 テキストチャンクとは、XML文書中で、マークアップを含まない極大な(それ 以上は伸ばせない)文字列範囲のことである。正規化されたDOMツリーで考え れば、1個のテキストノードがテキストチャンクを表すと考えてよい。ただし、 正規化されてないDOMツリーでは、複数のテキストノードが連続したり、文字 を含まないテキストノードが出現するので、「テキストノード=テキストチャ ンク」とは限らない -- この点は注意する必要がある。なお、文字参照/実体 参照/CDATAセクションは前もって展開しておくことにする。
以上の仮定のもとでは、どんなXML文書も、マークアップとテキストチャンク の列に、一意的に分解できることになる。主なマークアップは、開始タグと終了タグ である。空内容タグは、連続した開始タグ、終了タグのペアに直せるし、コメ ントとPIは無視してよい。XML宣言やDOCTYPE宣言も考慮からはずそう。つまり、 話を単純化するため、XML文書を「開始タグ、終了タグ、テキストチャンクが 混ざったもの」と解釈する。
さて、このように単純化した設定で、空白問題をもっと追いつめよう。まず、 開始タグと終了タグでは空白問題は生じないことが分かる。なぜなら、タグの なかの空白は区切り記号だし、属性値内の空白は属性値正規化(DOMの正規化 とは別物)で処理されるからだ。よって、テキストチャンク内の空白にだけ注 目すればよい。理論上は、テキストチャンクに対する空白処理は無限の多様性 があるが、経験上は次の4種に分類される。
上から順に、空白保持、空白トリミング、空白圧縮、アプリケーション固有 処理と呼ぶことにしよう。特別な知識やルールを仮定せずに行えるのは、空白 保持、空白トリミング、空白圧縮の3種となる(*注1)。
テキストチャンクを行に分割して、行ごとにリーディング空白、トレーリン グ空白、行の内部に出現する空白の処理をそれぞれ指定し、さらに行の連結方 法なども指定すれば、細かな空白制御が行える。ここでは、話を単純にするた めに、そのような細かな処理は、アプリケーション固有処理に分類している。
第30回の本コラムで、DOMツリーをテキストストリーム(キャラクタ列)に書 き出すとき、要素ごとの整形ルールに基づき空白を追加することがあると述べ た。そうであるならば、テキストストリームから読み込むときに、追加した余 分な空白を削除すべきだろう。その削除(場合によっては何も削除しない)処 理は、要素ごとに指定される。
例えば、要素<numberOfItems>にトリミング処理を行えば次のようになる。
<numberOfItems> 307</numberOfItems> ⇒ <numberOfItems>307</numberOfItems>
<numberOfItems> 307 </numberOfItems> ⇒ <numberOfItems>307</numberOfItems>
<numberOfItems> 3 0 7 </numberOfItems> ⇒ <numberOfItems>3 0 7</numberOfItems>
この例(要素<numberOfItems>)においては、空白処理をした後で、内容文字 列がpositiveIntegerの構文パターンと一致するかどうかをチェックすれば、 直感に一致した結果が得られる。
テキストストリームとしてのXML文書は、かなりの雑音が含まれる。“雑音” とは、どうでもいい非本質的な情報である。区切りや整形のための空白は典型 的な雑音なのだが、困ったことに、雑音であるかどうか判定できない状態にある。 目の前の空白をいくら眺めても何の印もついてないのだから、判定は絶対に不 可能である。そうであるなら、メタ情報にたよるしかない。
とはいえ、空白処理のメタ情報はスキーマ定義から与えられる必要はない(もち ろん、スキーマ定義に由来してもいいが)。例えば、書き出しのときに空白を追加 するルールがあるなら、それに対応して、読み込みのときに空白を削除するルール が必要になる。これは、単にラウンドトリップ性を保つ対処であり、特に構文 構造を制約するものではない。
スキーマが存在する場合でも、まず空白処理のルールを与え、スキーマとの 照合はその空白処理ルールの適用後に行うほうが合理的である。そのようにす れば、「奇妙な内容モデル」問題は解消する。
このことを、先に挙げた例で確認してみよう。
<!ELEMENT 姓 (#PCDATA) > <!ELEMENT 名 (#PCDATA) > <!ELEMENT 名前 (#PCDATA | (姓, 名)) >ここに登場する要素、<姓>、<名>、<名前>に対して、その内容に出現するテキ ストチャンクをトリミングすることにする。例えば、次のように処理される (空白だけのテキストチャンクは取り除かれる)。
<名前> <姓> 板東 <姓> <名> トン吉 <名> </名前> ⇒ <名前><姓>板東<姓><名>トン吉<名></名前>
<名前> 板東 トン吉 </名前> ⇒ <名前>板東 トン吉</名前>
空白処理が既に終わっているという前提だと、バリデータは非常に単純な動 作をすればよい。要素<名前>の内容の最初が文字であれば(その文字がなんで あっても)、内容モデル(#PCDATA)を選択すればよいし、そうでなければ、(姓, 名)という内容モデルでチェックすればよい。
実は、DTD構文の「#PCDATA」は、無視可能な空白も含む文字データの意味で、 もともとが曖昧なのである。空白処理の後に残った文字は、すべてデータ文字 (無視できない文字)となる。いまここで、データ文字を#CHARと書くことに しよう。#CHARを使って、要素<名前>を再定義すれば次のようになるだろう。
<!ELEMENT 姓 (#CHAR+) > <!ELEMENT 名 (#CHAR+) > <!ELEMENT 名前 (#CHAR+ | (姓, 名)) >
もうひとつの“奇妙な内容モデル”も書き直してみると次のようになる。
<!ELEMENT 段落 (#CHAR+) > <!-- 中身が空な段落は認めない --> <!ELEMENT 節 (#CHAR*, 段落*) >
段落と節において、無用な空白は取り除いてあると仮定すれば、この内容モ デルはもはや奇妙でも何でもない。何の問題もなく照合(パターンマッチング) できる。
空白問題に対処するには、バリーデーションとは独立に空白処理を考えるべ きなのだ。バリデーションをする場合でも、それに先だって空白処理が済んで いるほうが話がスッキリする(実際、“奇妙な内容モデル”はこれでスッキリ した)。また、何にでも通用するような普遍的な空白処理を期待してはいけな い。ありもしない理想の方法を求めても徒労に終わる。空白処理は、テキストストリー ムへの書き出し/テキストストリームからの読み込みのルールに依存して決ま る。つまり、応用領域ごとに個別に考えるしかない。
ところで、今回の考察を進めるうえでいくつかの単純化をしている。例えば、 「コメントとPIは無視してよい」と書いているが、「ほんとに無視可能なのか」 「どうやって無視するか」までは踏み込んでない。実のところ、これは微妙な 問題を含む。無視するには削除してしまえばいいのだが、単純に完全削除する か、コメントやPIを単一の間隔文字で置き換えるかによって結果が変わってく る。今回、大筋を述べたアイディアの細部まで詰めるのはそれなりの作業が必 要になる。だが、この方針で十分に実用的な空白処理が得られると筆者は考え ている。
空白問題のように、一見構文の問題に見えるものでも、構文の議論だけでは まったく解決できないことがある。結局、適切な解は構文の使用状況に依存す るのである。繰り返し強調するが、XML文書の同値性は応用領域に依存する。 空白問題が解けないのは、個々の応用領域を無視して、普遍的/絶対的な処理 を求めたからだったのだ。応用領域ごとの空白処理(同値性定義の一部)に基 づけば、そこにもはやミステリーはない。