XML であることの検証
XML (Extensible Markup Language) とは何か、みなさんだいたいご存じと思いますが、タグや属性という構造を用いて、データや文書を表現することができる技術です。
XMLそのものの構造は、XML 1.0 Fifth Edition(ありがたいことに日本語版もあります)で規定されます。「タグとはこんな形で、開いたら閉じねばならない」「属性とはこんな形」といったことが書いてあります。
お手持ちのファイルがこのルールに合っているかどうかは、もちろん検証できて、コマンドラインだと xmllint が使えます(Debian 系なら libxml2-toolsパッケージにある)。一応 XML になっているものと、そもそも XML ですらないゴミを検証するとこうですね。
$ echo '<a>b</a>' > foo.xml
$ xmllint --noout foo.xml
$ echo 'gomi' > foo.xml
$ xmllint --noout foo.xml
foo.xml:1: parser error : Start tag expected, '<' not found
gomi
^
まあ、今どきはブラウザで開いてもこのレベルの検証はできますが、コマンドラインツールだと自動化ができるのがポイントです。
次のステップ:「スキーマ言語」(普通名詞)
XML でありさえすれば何でもよいわけではないでしょう。次の段階は、「ルート要素の名前は a で、その中に b, c, d をこの順に使わねばならない」というような、タグや属性の構造についてのルールが通常あるものです。そしてルールがあるなら検証を自動的にできるべきです。つまりルールを機械可読に書くわけで、その言語を一般にスキーマ言語と言います。
(XSD だけがスキーマと思っている人に「本当かよ典拠を出せ」と噛みつかれたことがあるのですが、W3C がそう言っていれば信用してくれるかな)
このスキーマ言語(普通名詞)に色々のものがあります。最初は応用分野でそれぞれ何か特定のスキーマ言語で書かれたものを押し付けられることが多いだろうと思いますが、特徴を知っておくとよいです、というのがこの文章の動機です。
DTD
一番古いのが DTD (Document Type Definition) です。XML 1.0 標準の一部になっていて必ず使えるのですが、実際にはあまり使われません。たいしたことができないからです。
かなり簡単な例を挙げます。ルートに a 要素、その中に順に b, c, d 要素を置く(d だけは複数許す)規則があったとして、それを DTD 言語で foo.dtd に書き込んで、XML 文書 foo.xml がそれに適合していることを確認しています。
$ cat foo.dtd
<!ELEMENT a (b, c, d+)>
<!ATTLIST a xmlns CDATA #REQUIRED>
<!ELEMENT b (#PCDATA)>
<!ELEMENT c (#PCDATA)>
<!ELEMENT d (#PCDATA)>
$ cat foo.xml
<a xmlns="http://example.com/"><b>1</b><c>foo</c><d>any text</d></a>
$ xmllint --dtdvalid file://`pwd`/foo.dtd -noout foo.xml
$ cat bar.xml
<a xmlns="http://example.com/"><b>1</b><c>foo</c><d>any text</d><d>any text 2</d></a>
$ xmllint --dtdvalid file://`pwd`/foo.dtd -noout bar.xml
最後の xmllint コマンドが何も出力せず正常終了したことをもって適合が確認されます(違いがあればその旨出力されて異常終了になります)。
実用の為には、選択(b 要素の代わりに x 要素が許される、など)などの機能を知るべきでしょうが、まあできることはその辺が関の山です。次のような問題があります。
- 要素内にテキストが書けることを #PCDATA で指示できますが、どんなテキストだけが許されるか(整数だけ、数値だけ、特定の名前だけ、など)を限定することはできません
- 名前空間をサポートしていません。上の例では、名前空間指定のための xmlns 属性を、まんまその名前の属性があること、と規定していますから、入力データがプレフィクスを使う形だったらパスしません。
W3C XML Schema Definition Language (通称 XSD)
上述 DTD の不足を補うために W3C (World Wide Web Consortium) が作ったのが W3C XML Schema Definition Language です。これこそが XML の Schema Language の決定版といわんばかりのネーミングで、単に XML Schema といえばたぶんこれです。まあでも、ファイルの拡張子に .xsd が用いられるので、 XSD といったほうが紛らわしくないかもしれません。標準は Part 1, Part 2 の二部構成です。
上の例を XSD に翻訳してみましょう。
$ cat foo.xsd
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://example.com/" xmlns:e="http://example.com/"
elementFormDefault="qualified">
<xsd:element name="a">
<xsd:complexType>
<xsd:sequence minOccurs="1">
<xsd:element name="b" minOccurs="1" maxOccurs="1" type="xsd:integer" />
<xsd:element name="c" minOccurs="1" maxOccurs="1" type="xsd:token" />
<xsd:element name="d" minOccurs="1" maxOccurs="unbounded" type="xsd:string" />
</xsd:sequence>
</xsd:complexType>
</xsd:element>
</xsd:schema>
$ cat foo.xml
<a xmlns="http://example.com/"><b>1</b><c>foo</c><d>any text</d></a>
$ xmllint --noout --schema foo.xsd foo.xml
foo.xml validates
$ cat bar.xml
<a xmlns="http://example.com/"><b>1</b><c>foo</c><d>any text</d><d>any text 2</d></a>
$ xmllint --noout --schema foo.xsd bar.xml
bar.xml validates
つまりこんな特徴があります。
- XML要素 xsd:element の羅列によって、要素が取らねばならない順番、包含構造を示す
- 型を使って要素の内容を integer(整数値)や token(空白・タブ・改行のない一語)のように限定できる
- 名前空間がサポートされている
この XSD は広く使われています。たとえば GML http://schemas.opengis.net/gml/3.2.1/gml.xsd
とか例に挙げたらいいかな。割とデジュレスタンダード方面で好まれています。
まあそうではあるんだけれど、新しくプロジェクトを作るのにお勧めはしませんよ。どういう悲しみがあるかというと、達人の言を見てください。
XSD の言語仕様が複雑怪奇なので、同じことを多数の書き方が可能そうにみえて、一見役に立ちそうな機能が不可解な挙動をしたり、ひどい場合にはバリデータがバグっていたりします。まあ、そういうのが楽しみたい例外的な人以外にはお勧めできない、といえば失礼にならないでしょうか。
バリデータが完全でないと言えば、かつて libxml2 の XSD バリデータが GML の XSD に対応していなかったなんて問題がありました。あたしがパッチ書いたんだよ。
追記: XSD の仕様は、規定をみても理解しきれません。人間業ではないと思っています。それでも使わなければいけないことがあり、落とし穴を避けてなんとかするための気づきに、翔泳社のXML辞典(山田祥寛著)をいつも見ています。あんまり僕の流儀ではないんですが、しょうがない。
RELAX NG
自分で技術が選択できるなら、 RELAX NG をお勧めします。リラクシングと読むんだそうです。XSD しか眼中にない大企業・官庁・国際機関の人には、これも ISO/IEC 19757-2:2008 になっていると申し上げておきます。格だってこっちのほうが上なんだ。
Debian とかでは言語仕様を作った James Clark による jing というバリデータがパッケージで手に入ると思います。
上の例を翻訳してみましょう。RELAX NG Compact Grammer と呼ばれる Bachus-Naur form に近い文法で書いた foo.rnc を、jing パッケージに含まれる trang コマンドで RELAX NG 言語のスキーマ foo.rng に変換し、それでデータ foo.xml, bar.xml のバリデーションをしています。
$ cat foo.rnc
namespace e = "http://example.com/"
start = element e:a {
element e:b { xsd:integer },
element e:c { xsd:token },
element e:d { text }+
}
$ trang foo.rnc foo.rng
$ cat foo.rng
<?xml version="1.0" encoding="UTF-8"?>
<grammar xmlns:e="http://example.com/"
xmlns="http://relaxng.org/ns/structure/1.0"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<start>
<element name="e:a">
<element name="e:b">
<data type="integer"/>
</element>
<element name="e:c">
<data type="token"/>
</element>
<oneOrMore>
<element name="e:d">
<text/>
</element>
</oneOrMore>
</element>
</start>
</grammar>
$ cat foo.xml
<a xmlns="http://example.com/"><b>1</b><c>foo</c><d>any text</d></a>
$ jing foo.rng foo.xml
$ cat bar.xml
<a xmlns="http://example.com/"><b>1</b><c>foo</c><d>any text</d><d>any text 2</d></a>
$ jing foo.rng bar.xml
RELAX NG のいいところは、ルールに名前をつけて展開できるんですね。上記の start = もその特殊な一例ですけど、 Atom Feed の形式を定義している RFC 4287 の実例 https://www.rfc-editor.org/rfc/rfc4287#appendix-B をお見せしたほうがいいでしょう。
atomAuthor = element atom:author { atomPersonConstruct }
(中略)
atomPersonConstruct =
atomCommonAttributes,
(element atom:name { text }
& element atom:uri { atomUri }?
& element atom:email { atomEmailAddress }?
& extensionElement*)
(中略)
atomCommonAttributes =
attribute xml:base { atomUri }?,
attribute xml:lang { atomLanguageTag }?,
undefinedAttribute*
こんな調子で、ドキュメントの合間に数行で構文規則のエッセンスを記載して、しかしそれは厳密な構文規則の一部で、全部集めれば本当に構文規則として使えるわけです。
判読可能な技術文書を作るためには、こういう工夫が欠かせません。
schematron
スキマトロンは、上記の3スキーマ言語を補完するようなものです。XPath 言語でルールを次々に書いていってアサーションする仕組みなので、XML の構造全体を記述するのにはあんまり向いていませんが、ごく一部の要素・属性の値に拘束条件や相互依存がかかっているとき、それを記載するのに向いています。
いまどきは xmllint でも検証できるようになりました。
- xmllint で検証できるのは Schematron 1.5 だけ。ISO Schematron は扱えない。しかし、たとえば RFC 4287 だって Schematron 1.5 で書かれているので、よほどの権威主義でなければ Schematron 1.5 には社会実装があると胸を張るべき。
- Relax NG に埋め込まれた Schematron 1.5 を抽出する XSLT スタイルシートは、もっとも簡単に書けばこのよう。 https://gist.github.com/etoyoda/1eb5c26e9383f840e157496bc6982fc0
- ソース側には最低 s:ns と s:rule が必要。XSLT がそうであるようにドキュメントの名前空間宣言を抜き出して使えれば楽なのだが、まだ方法を見つけていない。