XML文書の妥当性検証 (RELAX NG, NVDL)
(2007.12.12 ページを分割。)
目次::
@ XML文書を検証するとは
アプリケーションプログラム同士がデータをやりとりするのにXMLを使うことがあります。
XMLデータ(XML文書)が適切かどうかチェックしなければなりませんが、いちいちプログラムを手書きするのは大変です。
XML文書が従うべき文法 -- 要素名や属性名、ある要素がどの要素・属性を含むことができるか -- を定めて、汎用の検証器に掛けるようにすれば、手間が減り、チェック漏れも防げます。
XML文書の文法をスキーマ(スキーマ文書)といいます。XML文書の妥当性検査は、スキーマ文書を用意して、そのスキーマに照らしてXML文書が逸脱していないかを検証します。
XML文書は、スキーマ文書の組に対して、「妥当」か「妥当ではない」のいずれかとなります。
HTMLを解釈するWebブラウザのような、処理プログラムが適宜おぎなって、解釈できるところまで処理する、ということは想定されていません。エラーを含むXML文書も受け入れたいときは、そもそもスキーマの方を修正すべきです。
@ いろいろなスキーマ
スキーマの書き方にもいくつかありますが、メジャーなものは次の3つです。
- DTD
- W3C XML Schema
- RELAX NG
このほか、スキマトロン (Schematron) などもあります。
DTDはSGMLの頃から使われているもので、XML仕様の一部にもなっています。ただ、
- 文法が複雑で、ツールを書くのが難しい
- 直接の子しか定義できない。文脈によって制約を切り替えられない
- 属性値として指定できるデータ型が少ない。例えば、HTMLのDTDでは、データ型がみんなCDATAになっています。
W3C XML SchemaはW3Cで開発されたものですが、複雑すぎると言われています。仕様は3部構成になっています。
- XML Schema Part 0: Primer Second Edition
- XML Schema Part 1: Structures Second Edition
- XML Schema Part 2: Datatypes Second Edition
RELAX NGについては後述。
スキーマの例
同じXML文書を対象として、スキーマを書いてみます。XML Schema Part 0: Primer Second Edition にあるサンプルです。
- <?xml version="1.0"?>
- <purchaseOrder orderDate="1999-10-20">
- <shipTo country="US">
- <name>Alice Smith</name>
- <street>123 Maple Street</street>
- <city>Mill Valley</city>
- <state>CA</state>
- <zip>90952</zip>
- </shipTo>
- <billTo country="US">
- <name>Robert Smith</name>
- <street>8 Oak Avenue</street>
- <city>Old Town</city>
- <state>PA</state>
- <zip>95819</zip>
- </billTo>
- <comment>Hurry, my lawn is going wild</comment>
- <items>
- <item partNum="872-AA">
- <productName>Lawnmower</productName>
- <quantity>1</quantity>
- <USPrice>148.95</USPrice>
- <comment>Confirm this is electric</comment>
- </item>
- <item partNum="926-AA">
- <productName>Baby Monitor</productName>
- <quantity>1</quantity>
- <USPrice>39.98</USPrice>
- <shipDate>1999-05-21</shipDate>
- </item>
- </items>
- </purchaseOrder>
文章で仕様を書こうと思うと、
- ルート要素はpurchaseOrder
- purchaseOrderはshipToとbillTo、それにitemsを含む
- itemsはitemを複数含む
- itemは、productName...を含む
- ...
まず、W3C XML Schemaの例。XML Schema Part 0: Primer Second Edition の丸写しです。
- <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
- <xsd:element name="purchaseOrder" type="PurchaseOrderType"/>
- <xsd:element name="comment" type="xsd:string"/>
- <xsd:complexType name="PurchaseOrderType">
- <xsd:sequence>
- <xsd:element name="shipTo" type="USAddress"/>
- <xsd:element name="billTo" type="USAddress"/>
- <xsd:element ref="comment" minOccurs="0"/>
- <xsd:element name="items" type="Items"/>
- </xsd:sequence>
- <xsd:attribute name="orderDate" type="xsd:date"/>
- </xsd:complexType>
- <xsd:complexType name="USAddress">
- <xsd:sequence>
- <xsd:element name="name" type="xsd:string"/>
- <xsd:element name="street" type="xsd:string"/>
- <xsd:element name="city" type="xsd:string"/>
- <xsd:element name="state" type="xsd:string"/>
- <xsd:element name="zip" type="xsd:decimal"/>
- </xsd:sequence>
- <xsd:attribute name="country" type="xsd:NMTOKEN" fixed="US"/>
- </xsd:complexType>
- <xsd:complexType name="Items">
- <xsd:sequence>
- <xsd:element name="item" minOccurs="0" maxOccurs="unbounded">
- <xsd:complexType>
- <xsd:sequence>
- <xsd:element name="productName" type="xsd:string"/>
- <xsd:element name="quantity">
- <xsd:simpleType>
- <xsd:restriction base="xsd:positiveInteger">
- <xsd:maxExclusive value="100"/>
- </xsd:restriction>
- </xsd:simpleType>
- </xsd:element>
- <xsd:element name="USPrice" type="xsd:decimal"/>
- <xsd:element ref="comment" minOccurs="0"/>
- <xsd:element name="shipDate" type="xsd:date" minOccurs="0"/>
- </xsd:sequence>
- <xsd:attribute name="partNum" type="SKU" use="required"/>
- </xsd:complexType>
- </xsd:element>
- </xsd:sequence>
- </xsd:complexType>
- <!-- Stock Keeping Unit, a code for identifying products -->
- <xsd:simpleType name="SKU">
- <xsd:restriction base="xsd:string">
- <xsd:pattern value="\d{3}-[A-Z]{2}"/>
- </xsd:restriction>
- </xsd:simpleType>
- </xsd:schema>
要素の内容を型 (complexType) として定義します。例えば、shipTo要素の内容(子要素と属性)をUSAddress型として定義します。
データ型として、string, decimalなどが用意されていて、さらにmaxExclusiveやpatternで制約を加えることができます。
要素の出現回数は、minOccursやmaxOccursで制約を付けれます。
次は、RELAX NGです。
- <grammar xmlns="http://relaxng.org/ns/structure/1.0"
- datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes" >
- <start>
- <element name="purchaseOrder">
- <attribute name="orderDate"><data type="date" /></attribute>
- <element name="shipTo"><ref name="USAddress" /></element>
- <element name="billTo"><ref name="USAddress" /></element>
- <optional><ref name="comment"/></optional>
- <element name="items">
- <oneOrMore>
- <element name="item">
- <attribute name="partNum">
- <data type="string">
- <param name="pattern">\d{3}-[A-Z]{2}</param>
- </data>
- </attribute>
- <element name="productName"><text /></element>
- <element name="quantity">
- <data type="positiveInteger">
- <param name="maxExclusive">100</param>
- </data>
- </element>
- <element name="USPrice"><data type="decimal" /></element>
- <optional><ref name="comment"/></optional>
- <optional>
- <element name="shipDate"><data type="date" /></element>
- </optional>
- </element>
- </oneOrMore>
- </element>
- </element>
- </start>
- <define name="USAddress">
- <attribute name="country"><data type="NMTOKEN" /></attribute>
- <element name="name"><text /></element>
- <element name="street"><text /></element>
- <element name="city"><text /></element>
- <element name="state"><text /></element>
- <element name="zip"><data type="decimal" /></element>
- </define>
- <define name="comment">
- <element name="comment"><text /></element>
- </define>
- </grammar>
RELAX NGは、elementの中に子要素を書いていってもいいですし、defineとrefで括りだすこともできます。
datatypeLibrary属性でXML Schemaのデータ型を引用しています。ほかのライブラリを引用することもできます。引用したデータ型ライブラリは、data、param要素で使えます。
要素の数は、oneOrMore、optional などで指定します。
RELAX NGは、XML Schemaで指定できる、デフォルト値や固定値を指定することはできません。この点は後述します。
最後に、RELAX NG Compact Syntax で書いてみます。
grammar {
start =
element purchaseOrder {
attribute orderDate { xsd:date },
element shipTo { USAddress },
element billTo { USAddress },
comment?,
element items {
element item {
attribute partNum {
xsd:string { pattern = "\d{3}-[A-Z]{2}" }
},
element productName { text },
element quantity {
xsd:positiveInteger { maxExclusive = "100" }
},
element USPrice { xsd:decimal },
comment?,
element shipDate { xsd:date }?
}+
}
}
USAddress =
attribute country { xsd:NMTOKEN },
element name { text },
element street { text },
element city { text },
element state { text },
element zip { xsd:decimal }
comment =
element comment { text }
}
非常に簡潔です。短いですが、RELAX NGと内容は同一です。
@ ボヘミアン vs 貴族
DTD, W3C XML Schemaはスキーマにデフォルト値、固定値を含めることができます。一方、RELAX NGは含められません。
例えば、XML Schemaでは、次のように書いてデフォルト値を指定します。要素の内容のデフォルト値も指定できます。
- <xsd:element name="comment" type="xsd:string" default="(comment space)"/>
- <xsd:attribute name="quantity" type="xsd:integer" default="1"/>
スキーマがデフォルト値などを持てるかどうかは、思想の問題です。
スキーマがデフォルト値などを持てる、ということは、スキーマの有無で処理すべきXML文書の値が変わることがある、ということになります。
スキーマにより付加される情報があることを明確にするため、検証後のXML文書のことをPSVI -- Post-Schema-Validation Infoset -- と言うことがあります。
XML Schemaを推進する立場は、XML文書は「常に」スキーマとセットで処理されるべきであり、データ(XML要素の内容、属性値)は型を持つべき、突き詰めると、スキーマのないXML文書は無効、整形式のXML文書は廃止すべき、です。この立場は時に「貴族」と呼ばれます。
一方、RELAX NGを推進する立場は、XML文書はスキーマと組み合わせてもいいし、XML文書単独で使ってもよい、というもので、整形式のXML文書も排除したりしません。この立場は「ボヘミアン」と呼ばれます。
適用分野によっても違うのでしょうが、Web界隈だと、XML文書をXPathなどでつまみ食いしたりすることが多く、いちいちスキーマを要求されては実用になりません。
@ RELAX NG, DSDL
歴史的には、W3C XML Schemaが開発されている(ドラフト段階)ときに、その複雑さを嫌って、RELAX (RELAX Core) とTREXが開発されました。その後、RELAX CoreとTREXを統合する形でRELAX NGは生まれました。
RELAX NGは標準化団体の一つOASISで標準化されました。現在は、Document Schema Definition Languages (DSDL) の1パートとして、ISO化されています。JIS化もされています。
2007.12現在、DSDLで標準化されているものは、
- Part 2: Regular-grammar-based validation -- RELAX NG, Amendment 1 Compact Syntax; JIS X 4177-2 文書スキーマ定義言語 (DSDL) - 第2部: 正規文法に基づく妥当性検証 - RELAX NG
- Part 3: Rule-based validation -- Schematron
- Part 4: Namespace-based Validation Dispatching Language (NVDL)
このほか、次のパートの標準化が予定されています。
- Part 1: Overview
- Part 5: Data Type Library Language - DTLL
- Part 6: Path-based integrity constraints
- Part 7: Character Repertoire Description Language - CRDL
- Part 8: Document Schema Renaming Language - DSRL
- Part 9: Datatype- and Namespace-aware DTDs
- Part 10: Validation Management
RELAX NGの仕様は、下記で参照できます。
RELAX NGは、生け垣オートマトンというモデルにもとづいて、XML文書に対するパターンをスキーマとして定義していきます。直感的には、文字列に対する正規表現パターンのようなものです。RELAX NGパターンがXML文書とマッチすれば妥当、マッチしなければ妥当でないという結果になります。
実装
Ruby 1.8に添付のXMLライブラリREXMLも、(だいぶ)制限があるものの、RELAX NGに対応しています。
このほか、
- mono-xmltool
- Fedora8でパッケージが用意されています。RELAX NG (XML構文), RELAX NG短縮構文, NVDL, XML Schema, DTDに対応しています。XSLTによるXML文書の変換も可能。
- Sun Multi-Schema XML Validator (MSV)
- RELAX NG, RELAX Namespace, RELAX Core, TREX, XML DTD, XML Schema Part 1のサブセットをサポート。
- Jing
- RELAX NG, RELAX NG Compact Syntax
- Trang
- 多フォーマットスキーマ変換。RELAX NG, RELAX NG Compact Syntax, XML DTD, W3C XML Schema
- SourceForge.net: RelaxNGCC
- RELAX NG文法からJavaソースを生成。
この辺も参考になります。
Todd Ditchendorf’s Blog. » RELAX NG
@ REXML (Ruby) で RELAX NG
Ruby 1.8.6に添付されているREXMLは、DTDとRELAX NGを(一応)扱えるようになっています。RELAX NG短縮構文には対応していません。複数のスキーマを名前空間で切り替えて検証するNVDLにも対応していません。
RELAX NGでは、次のものは実装されていません。あまり本格的とは言えません。
data param include externalRef notAllowed anyName nsName except name
ラッパ
XML文書のなかで、スキーマでテキストを書けないことになっている場所に空白文字や改行が含まれると、検証エラーになってしまいます。
不便すぎるので、簡単なラッパを作ります。
- require 'rexml/document'
- require 'rexml/validation/relaxng'
- require 'rexml/parsers/sax2parser'
- # Ruby 1.8.6のREXMLのバリデータはXML文書の空白の扱いがまずいので、簡単にラップする。
- class WrapValidator
- def initialize v
- @validator = v
- end
- def receive event
- return if event[0] == :text && event[1].strip == ""
- @validator.receive event
- end
- end
- # RELAX NGのパターンを読ませる。
- validator = REXML::Validation::RelaxNG.new <<EOF
- <element name="addressBook" xmlns="http://relaxng.org/ns/structure/1.0">
- <zeroOrMore>
- <element name="card">
- <element name="name"><text/></element>
- <element name="email"><text/></element>
- </element>
- </zeroOrMore>
- </element>
- EOF
- wrapper = WrapValidator.new validator
- # XML文書を用意する。
- source = <<EOF
- <addressBook>
- <card>
- <name>John Smith</name>
- <email>js@example.com</email>
- </card>
- <card>
- <name>Fred Bloggs</name>
- <email>fb@example.net</email>
- </card>
- </addressBook>
- EOF
- # SAX2パーサに登録してみる。
- parser = REXML::Parsers::SAX2Parser.new source
- parser.add_listener wrapper
- # エラーなくパースできればOK
- parser.parse
@ RELAX NGの書き方
RELAX NGのチュートリアルがあります。これを見ながら、書き方を確認してみましょう。
- RELAX NG Tutorial 26 March 2003
まず、RELAX NGパターン;
- <element name="addressBook" xmlns="http://relaxng.org/ns/structure/1.0">
- <zeroOrMore>
- <element name="card">
- <element name="name"><text/></element>
- <element name="email"><text/></element>
- </element>
- </zeroOrMore>
- </element>
主な要素は、
| element | 要素。nameで要素名を表す。属性、内容は子要素として書く。 |
| zeroOrMore | 0回以上 |
| oneOrMore | 1回以上 |
| optional | あるかないか |
| choice | 子としていくつかのパターンを取り、そのいずれか |
| group | 要素をグループ化。choiceの子パターンとして使用 |
| attribute | 属性。nameで属性名を表す。 |
| empty | elementの子として、空要素を明確にする。 |
| ref | 別に定義されたパターンへの参照。name属性で参照先を表す。 |
| define | refで参照される定義。 |
| data | 要素の内容、属性値のデータ型。param要素でさらに制約を加えられる。 |
| list | 空白文字で区切られた値 |
例えば、属性値を二つの値のいずれかに制約したいときは、
- <attribute name="preferredFormat">
- <choice>
- <value>html</value>
- <value>text</value>
- </choice>
- </attribute>
要素の内容が空白で区切られた1個以上の実数、という制約は、
- <element name="vector">
- <list>
- <oneOrMore>
- <data type="double"/>
- </oneOrMore>
- </list>
- </element>
RELAX NGでは、スキーマのelementの順序がそのままXML文書での要素の順序になります。どの順でもいい、というときはinterleaveで囲みます。
次のパターンは、nameとemailはどちらが先でもいいですが、ただ1回ずつ出現しなければなりません。
- <element name="card">
- <interleave>
- <element name="name"><text/></element>
- <element name="email"><text/></element>
- </interleave>
- </element>
どのような順序でも、何回でも書けるようにするには、interleaveとzeroOrMoreを組み合わせます。例えば、次のパターンは、
- <element name="head" xmlns="http://relaxng.org/ns/structure/1.0">
- <interleave>
- <element name="title"><text /></element>
- <zeroOrMore>
- <element name="meta">
- <attribute name="name"/>
- <attribute name="content"/>
- </element>
- </zeroOrMore>
- </interleave>
- </element>
次のXML文書にマッチします。zeroOrMoreで複数回書ける要素は、それぞれ出現順序がバラバラでもOK。
- <head>
- <meta name="ROBOTS" content="NOODP"/>
- <title>foobar</title>
- <meta name="description" content="テスト"/>
- </head>
@ NVDL
XMLは、SGMLと違って、名前空間によって複数のスキーマに従う文書を書けるようになっています。
最近は実際に、一つのXML文書がいくつかのスキーマに従うXML片からなることも珍しくありません。例えば、SVGの中にXHTMLがある、Atomフィード文書にXHTMLが含まれる、など。
このようなXML文書を検証するためには、XML文書を切り分けて、AtomについてはAtomのスキーマ、XHTMLについてはXHTMLのスキーマで検証しなければなりません。
このような課題に対して開発されたのがNVDL (Namespace-based Validation Dispatching Language) です。
NVDLは、XML名前空間を基に、各スキーマにXML片を配分します。
次のNVDL文書と、
- <rules xmlns="http://purl.oclc.org/dsdl/nvdl/ns/structure/1.0">
- <namespace ns="tag:hello-world">
- <validate schema="hello-world.rng" />
- </namespace>
- <namespace ns="tag:fuga">
- <validate schema="fuga.rng" />
- </namespace>
- </rules>
次の二つのRELAX NGパターンは、
- <element name="a:root" xmlns="http://relaxng.org/ns/structure/1.0"
- xmlns:a="tag:hello-world">
- <element name="a:x"><text/></element>
- </element>
- <element name="b:y" xmlns="http://relaxng.org/ns/structure/1.0"
- xmlns:b="tag:fuga">
- <text/>
- </element>
次のXML文書にマッチします。
- <root xmlns="tag:hello-world">
- <x>
- <foo:y xmlns:foo="tag:fuga">123</foo:y>
- </x>
- </root>
tag:hello-world の要素xは内容がtextになっているにも関わらず、tag:fuga の要素yが書けることに注意してください。
リンク
- International and National Standardization Status of DSDL DSDL(スキマトロン,NVDL,他)の国際および国内の標準化動向
- NVDL Tutorial
- An introduction to NVDL, ISO 19757-4
- 標準情報 TR X 0029:2000 XML正規言語記述 RELAX コア 解説