落とし穴になる空白文字と改行文字XMLを学ぼう(10)

今回は、意外と分かっているようで分かっていない「空白文字」の問題を取り上げる。これを知らないと、妙なところで落とし穴に陥ることもある。空白文字だからといって、ばかにできないのである。

» 2001年02月28日 00時00分 公開
[川俣晶株式会社ピーデー]

空白文字・改行文字とは何か?

 最初から当たり前のことを解説するようだが、まずは読んでいただきたい。空白文字というのは、キーボードのスペースバーを押すことで入力される見えない文字のことである(ここではいわゆる半角空白を空白文字と呼び、全角空白は含まないとする)。文字と文字の間を空けたいときに挿入するものである。一見当たり前のことのように思えるが、今回はこれが当たり前ではないケースがあって、しばしば落とし穴になるという話である。

 TAB文字は、空白文字と少しだけ似ている。あらかじめ指定された位置までカーソルを前進させる機能を持つ。これは状況によって幅が変わる空白文字と考えてもよい。

 改行文字とは、行の最後に挿入して、そこで行を変えることを示す制御文字の一種である。ソフトによっては、矢印などを表示する場合があるが、通常、改行文字は目に見えない。空白文字も目には見えないが、きちんと1文字分の場所を占有するのに対して、改行文字は場所を占有しない。つまり、空白文字には文字幅が存在するが、改行文字には文字幅は存在しない。

 改行文字としては、キャリッジリターン(CR)、ラインフィード(LF)、この2つを連続したものなどが使用される。これらは、個々の環境は標準仕様に固有のもので、どれが正しいということはなく、世の中に併存している。例えば、UNIX系OSではLF、Macintosh系ではCR、Windowsやインターネット電子メールはCR+LFが標準となっている。XMLでは、どれを使ってもよいことになっているが、内部処理のためにはLFのみに統一する(正規化という)ことがXML 1.0勧告に記述されている。

 さて、ここで重大な問題がある。そもそも改行とはなんぞや、という問題である。現実に存在するコンピュータソフトの中には、1行として扱える長さに制限があるものも多い。そのため、1行は何文字以内、という制限が課せられるケースも多い。例えば、インターネットの電子メールは典型的な例といえる。RFCで、本文テキストは1行で最低1000文字扱えること、という規定があるので、1行の長さはそれ以下にしなければならない。そのため、たいていの電子メールクライアントソフトは、表示される1行(半角70文字前後)ごとに改行を入れて送信する。だが、普通のテキストエディタでは、改行は段落の終わりに入れるのが普通であって、見た目の1行ごとに入れたりしない。つまり、改行の使い方に異なる慣習がある。

図1 改行による1行の違い 図1 改行による1行の違い

 この相違は、電子メール本文として受信したテキストを、テキストエディタなどに張り付けてみると、すぐに実感できる。テキストエディタ上では、段落の終わりにだけ改行が入っていると快適に編集できるのだが、電子メール本文では表示上の1行ごとに改行が入っているので、編集を加える前に、不必要な改行を取り除く必要が生じてしまう。

 さて、この問題は、HTMLの世界では重大な問題にはなっていない。なぜなら、HTMLでは段落を表すために、<p>要素というものが存在するからだ。また、1行の長さについても、主にHTMLの送信に使用するHTTPでは特に厳しい制限は付いておらず、無理に行の長さを短くまとめる必要もない。ところが、厳しい制約がなくなったことで、厳密な動作を知らないままHTMLでオーサリングしている人もけっこう多い。HTMLでの改行の動作を理解しているかどうかを確認するために、簡単なクイズを出してみよう。

■HTML常識クイズその1

 以下のHTMLファイル(かなり簡略化してある)は2段落のテキストを表示する。表示後の見かけは段落ごとに同じか、違うか。違う場合はどこが違うか。

 <html>
 <body>
 <p>abcdef</p>
 <p>abc
 def</p>
 </body>
 </html>

 何が問われているのかピンとこなかった人は、少し考えてみよう。

 正解は、「表示後の見かけは違う」「相違点は2段落目のcとdの間に空白が挿入されている」というものだ。

 なぜ、こういう動作になるのかは、英語テキストを頭に描いてみれば分かるだろう。例えば、以下の2行があったとしよう。

 The Extensible Markup Language (XML) is a subset
 of SGML that is completely described in this document.

 1行目のsubsetと2行目のofを結合するときには、そのまま結合して"subsetof"としてはおかしくなる。ここは単語の区切りなので、空白文字を入れねばならない。つまり、HTMLでは、改行は空白文字に等しい扱いを受けているのである。ただしここで補足しておくと、日本語などの文字を区切る改行は、空白扱いしないのが現在のHTMLである。しかし、古いWebブラウザでは、日本語文字を改行で区切る場合でも、空白が入り込んでしまう。

 つまり、段落を示す<p>要素があるからといって、改行に何の機能もないと思うのは間違いだ、ということなのである。

■HTML常識クイズその2

 もう1個クイズを出しておこう。今度は、空白文字に関するクイズである。

 以下のHTMLファイル(かなり簡略化してある)は4段落のテキストを表示する。表示後の段落ごとの見かけは同じか、違うか。違う場合はどこが違うか。ただし、単語間の空白はすべて半角空白とする。

 <html>
 <body>
 <p>abc def</p>
 <p>abc  def</p>
 <p>abc   def</p>
 <p>abc    def</p>
 </body>
 </html>

 正解は、「実装によっては表示後の見かけは4段落とも同じ」というものだ。HTMLでは、単語間の空白は、何個の空白文字で表現されているかに関係なく、自動的に調整してもよく,実際にInternet Explorerなどでは自動調整される。

まったく挙動が変わるXML

 ここまで長々とXMLと関係ない話を書いてきた理由は、XMLの挙動がこれらのほかの例とまったく違っていることを明らかにしたいためだ。何がどう違っているかを明らかにするには、XML以外の技術での空白や改行の扱いがどうなっているのか知らねばならない。だが、理解していない人もいるので、迷子にならないために説明してみた。

 では、XMLでの空白文字と改行文字は、どう扱われるのか。まず以下の例を見てみよう。

 <映画>
   <名前>Avalon</名前>
   <監督>押井守</監督>
   <主演>マウゴジャータ・フォレムニャック</主演>
   <助演>T-72</助演>
 </映画>

 よくあるXML文書の一例である。このXML文書を、以下のように書いてしまう人がいる。

 <映画>
   <名前>
     Avalon
   </名前>
   <監督>
     押井守
   </監督>
   <主演>
     マウゴジャータ・フォレムニャック
   </主演>
   <助演>
     T-72
   </助演>
 </映画>

 このように書きたくなる意図は明らかだ。つまり、XML文書を人間が見ても見やすくしたい。その意図を持って、空白文字や改行を挿入して、見やすく手直ししたものだ。ところが、このような書き換えは、HTMLでは大した問題を引き起こさないのに対して、XMLではしばしば致命的なトラブルを引き起こす。

 そのメカニズムを知るために、XMLパーサがこのXML文書をどのように認識しているかを調べると分かりやすい。ここでは、Xerces 1.3.0に付属しているDocumentTracerというサンプルプログラムを使って、調べてみることにする。このプログラムは、SAXパーサが何を認識したのかを逐一コンソールに表示するというものである。これで、上記のサンプルを処理してみよう。

 startDocument()
  startElement(element={,,映画},attributes={})
  characters(text="\n  ")
  startElement(element={,,名前},attributes={})
  characters(text="\n    Avalon\n  ")
  endElement(element={,,名前}})
  characters(text="\n  ")
  startElement(element={,,監督},attributes={})
  characters(text="\n    押井守\n  ")
  endElement(element={,,監督}})
  characters(text="\n  ")
  startElement(element={,,主演},attributes={})
  characters(text="\n    マウゴジャータ・フォレムニャック\n  ")
  endElement(element={,,主演}})
  characters(text="\n  ")
  startElement(element={,,助演},attributes={})
  characters(text="\n    T-72\n  ")
  endElement(element={,,助演}})
  characters(text="\n")
  endElement(element={,,映画}})
 endDocument()

 上記の出力の読み方を説明しよう。startElementとは要素が開始されたことを示す。endElementは要素の終わりだ。charactersは文字情報を検出したことを示す。文字情報はtext=のあとで内容が具体的に表示される。\nと書かれているのは改行のことである。

 さて、要素の内容となる文字列を見てみよう。例えば17行目を見てほしい。ここで意図しているのは、"T-72"という4文字をデータとして扱うことである。ところが、17行目で文字データとして認識されているのは、驚くなかれ、[改行][空白][空白][空白][空白]T-72[改行][空白][空白]という計12文字である(改行がCR+LFなら14文字)。

 これこそが、どこに改行や空白を入れても、割と破綻することが少ないHTMLとの決定的な違いである。つまりXMLでは、書かれた空白文字、TAB文字、改行文字はすべてあるがままに認識される。複数の空白文字が同じ1個の空白になったり、改行が空白文字に化けたりすることはない。

 上記の問題を回避するには、余計な空白や改行は入れないようにするのが最も容易である。<助演>T-72</助演>と改行や空白文字を入れずに書いておけば、文字データはcharacters(text="T-72")と認識され、ちゃんと4文字になる。通常は、これで問題ない。

 ただし、以下の例でいうと、1行目と2行目の間に存在する改行文字と空白文字は、あるがままに認識される点にも注意が必要だ。

 <映画>
   <名前>Avalon</名前>
   <監督>押井守</監督>
   <主演>マウゴジャータ・フォレムニャック</主演>
   <助演>T-72</助演>
 </映画>

 つまり上記のXML文書で、最初に出現する文字データは、"Avalon"ではなく、1行目の最後の改行文字と2行目の先頭の空白文字になる、ということである。このことを忘れ、うっかりデータ本体以外の文字データがくるはずがないという前提でプログラムを書くと、バグを紛れ込ませてしまう危険がある。

■改行文字も生きるXML

 特に注意すべき点は、XMLでは改行文字も生きている、ということだろう。HTMLを使い込んでいると、つい、改行文字はないもの、あるいは、空白文字扱いされると思い込んでしまいがちである。しかし、XMLでは、改行文字は改行文字として、あるがままに扱われるのが基本である。

 そのため、XML文書に書き込まれた改行文字は改行文字として、アプリケーションプログラムに渡される。これを活用するのも、無視するのも、アプリケーションプログラム次第である。

 ちなみに、HTMLでも、<pre>要素を使う場合は、改行文字が改行として機能するので、HTMLにまったくない機能というわけでもない。しかし、XMLでは、要素の名前によってルールが変わるようなことはない。ルールを変えたいときは、アプリケーションプログラムが条件を判断して挙動を変えるしかない。

xml:space属性によるコントロール

 XML 1.0勧告において、XMLプロセッサは全ての空白文字(TAB文字や改行文字も含む)をアプリケーションソフトに渡すと規定している。しかし、渡された空白文字の解釈については、2通り規定されている。1つは、全ての空白文字は有効な文字であると認識する方法。もう1つは、アプリケーションソフト側のデフォルト判断基準により、意味のある空白文字と、そうではない空白文字を区別する方法である。

 この2つの方法は、xml:space属性を用いることで明示的に指定することができる。

 xml:space属性は、コロン記号(:)が中間に入っていることから、名前空間を使用しているように見えるが、XML 1.0勧告を単体で使用する場合は「xml:space」という1つの名前を構成し、xml:は名前空間接頭辞ではない。名前空間を併用する場合は、xml:という名前空間接頭辞はhttp://www.w3.org/XML/1998/namespace という名前空間に結びつけられていて、xml:は宣言することなく利用できる。

 ただし、DTDを使う場合には、DTDにもxml:space属性を使用することを書いておく必要がある。xml:space属性の値には、defaultとpreserveのどちらかを指定することができる。defaultは、アプリケーションソフトのデフォルトを示し、preserveはすべての空白が有効であると示す。xml:space属性で指定した設定はその要素と子孫要素に対して有効である。しかし、子孫が再びxml:space属性を記述して設定を上書きすることは可能である。

 ルート要素でxml:space属性を指定しない場合は、どちらの方法を使うか、明示的に示していないと見なされる。そのため、間違いなくすべての空白文字が処理されてほしい場合は、xml:space="preserve"を必ずルート要素に指定する。DTDで、ルート要素のxml:space属性のデフォルト値をpreserveと規定して、XML文書本体に書かないという方法もあり得るが、整形式(well-formed)の場合は使えない。

属性の正規化

 ここまでの話は、主に要素の内容として書かれた空白文字の話題である。タグ内部では、話はやや違ってくる。例えば、属性と属性の間に入れる空白文字は、何文字入れてもよいが、文字の個数はまったく何の意味も持たないし、アプリケーションソフトに伝達されることもない。

 属性の値に関しては、少しややこしい「正規化」と呼ばれる文字列の修正プロセスが行われる。この詳細は、XML 1.0 2nd Editionになって説明が追加され、より分かりやすくなった。正規化の手順は複雑なので、詳細はXML 1.0 2nd Editionの3.3.3 Attribute-Value Normalizationを見ていただくとして、概要を説明したい。

 まず、正規化のルールは2つある。DTDで指定された属性のデータ型がCDATAかそれ以外かによって、正規化ルールが変わる。ただし、整形式として処理される場合は、すべてCDATAとして扱われる。

 改行やTAB文字を含む空白文字は、すべて#x20;に置き換えられる。これは半角空白文字を意味する文字参照である。CDATAの場合はこれで終わりだが、それ以外の場合は、トークン前後の空白文字が取り除かれ、トークン間の空白文字は複数あっても1個に集約される。つまり、a=" abc "という属性は、CDATAの場合はそのまま処理されるが、CDATA以外の場合は、前後の空白文字が捨てられて、a="abc"と扱われることになる。CDATA以外のデータ型は、何らかの名前を扱うことになるので、名前を構成する文字になれない空白を取り除いてしまうことは問題ない処理なのである。

 なお、属性値の記述に参照が使用されるとルールがややこしくなるのだが、ここでは詳細の説明を割愛しておく。

次回は言語識別に関する機能について

 空白文字や改行文字は、当たり前の存在のように見えて、悩まされることも多い存在である。これらを正しく理解するかどうかは、XMLを活用するうえで重要なことである。特に、既存の見た目が似ている技術と挙動が違う点が要注意である。しかし、すべての空白文字が保存されるという規定は、アプリケーションソフト側で利用するのも無視するのも自由ということを示す。選択の幅が広いという意味では、XMLの仕様は正しいものといえる。しかし、裏を返せば、利用者側が空白文字をどう扱うか、明確に選択する必要があることを示す。新しいXMLによる言語を作ったり、応用システムを開発する場合は、この点に注意して設計するように心がけよう。

 次回は、XML 1.0勧告の中で規定されている言語識別に関する機能について解説しよう。

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

■更新履歴
2002/2/18 「xml:space属性によるコントロール」の内容を、読者からの指摘により変更しました。
2002/11/15 「xml:space属性によるコントロール」の表現を、より適切な内容に変更しました。


Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

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

メールマガジン登録

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