XML文書の妥当性検証 (RELAX NG, NVDL)

子ページ:

(2007.12.12 ページを分割。)

(2018.8 更新.)

目次::

  1. XML文書を検証するとは
  2. いろいろなスキーマ
  3. RELAX NG, DSDL
  4. REXML (Ruby) で RELAX NG
  5. RELAX NGの書き方
  6. NVDL

@ XML文書を検証するとは

アプリケーションプログラム同士がデータをやりとりするのにXMLを使うことがあります。

XMLデータ(XML文書)が適切かどうかチェックしなければなりませんが、いちいちプログラムを手書きするのは大変です。

XML文書が従うべき文法 -- 要素名や属性名、ある要素がどの要素・属性を含むことができるか -- を定めて、汎用の検証器に掛けるようにすれば、手間が減り、チェック漏れも防げます。

アプリケーションXML文書の文法をスキーマ (スキーマ文書) といいます。XML文書の妥当性検査は、スキーマ文書を用意して、そのスキーマに照らしてXML文書が逸脱していないかを検証します。

XML文書は、スキーマ文書の組に対して,「妥当」か「妥当ではない」のいずれかとなります。

HTMLを解釈するWebブラウザのような、処理プログラムが適宜おぎなって、解釈できるところまで処理する、ということは想定されていません。エラーを含むXML文書も受け入れたいときは、そもそもスキーマの方を修正すべきです。

@ いろいろなスキーマ定義

スキーマの書き方にもいくつかありますが、メジャーなものは次の4つです。

  • DTD
  • W3C XML Schema
  • RELAX NG
  • スキマトロン (Schematron)

DTDはSGMLの頃から使われているもので、XML仕様の一部にもなっています。ただ、

  • 直接の子しか定義できない。文脈によって制約を切り替えられない
  • 属性値として指定できるデータ型が少ない。例えば、HTMLのDTDでは、データ型がみんなCDATAになっています。
など、表現力が不足しています。

RELAX NGについては後述。

対象のXML文書

同じXML文書を対象として、スキーマを書いてみます。XML Schema Part 0: Primer Second Edition にあるサンプルがペースです。

このXML文書を掛けます。

HTML/XML
[RAW]
  1. <?xml version="1.0"?>
  2. <purchaseOrder orderDate="1999-10-20">
  3. <shipTo country="US">
  4. <name>アリス=スミス</name>
  5. <street>123 Maple Street</street>
  6. <city>Mill Valley</city>
  7. <state>CA</state>
  8. <zip>90952</zip>
  9. </shipTo>
  10. <billTo country="US">
  11. <name>Robert Smith</name>
  12. <street>8 Oak Avenue</street>
  13. <city>Old Town</city>
  14. <state>PA</state>
  15. <!-- 検証に失敗させる -->
  16. <zip>95819aa</zip>
  17. </billTo>
  18. <comment>Hurry, my lawn is going wild. 急いで!</comment>
  19. <items>
  20. <item partNum="872-AA">
  21. <productName>Lawnmower</productName>
  22. <quantity>1</quantity>
  23. <USPrice>148.95</USPrice>
  24. <comment>Confirm this is electric</comment>
  25. </item>
  26. <item partNum="926-AA">
  27. <productName>Baby Monitor</productName>
  28. <quantity>1</quantity>
  29. <USPrice>39.98</USPrice>
  30. <shipDate>1999-05-21</shipDate>
  31. </item>
  32. </items>
  33. </purchaseOrder>

文章で仕様を書こうと思うと、

  1. ルート要素はpurchaseOrder
  2. purchaseOrderはshipToとbillTo、それにitemsを含む
  3. itemsはitemを複数含む
  4. itemは、productName...を含む
  5. ...
と、長々と書かなければいけませんし、条件を完全に書き下すのは困難です。

W3C XML Schema

まずは, W3C XML Schema で書いてみます.

W3C XML Schema はW3Cで開発されたものですが、複雑すぎると言われています。また、検証によってXML文書を変更してしまう問題もあります。

仕様は3部構成になっています;

以下は, XML Schema Part 0: Primer Second Edition の丸写しです。

HTML/XML
[RAW]
  1. <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  2. <!-- type で子要素を参照する -->
  3. <xsd:element name="purchaseOrder" type="PurchaseOrderType"/>
  4. <xsd:element name="comment" type="xsd:string"/>
  5. <xsd:complexType name="PurchaseOrderType">
  6. <xsd:sequence>
  7. <xsd:element name="shipTo" type="USAddress"/>
  8. <xsd:element name="billTo" type="USAddress"/>
  9. <!-- ref で定義を参照する -->
  10. <xsd:element ref="comment" minOccurs="0"/>
  11. <xsd:element name="items" type="Items"/>
  12. </xsd:sequence>
  13. <xsd:attribute name="orderDate" type="xsd:date"/>
  14. </xsd:complexType>
  15. <xsd:complexType name="USAddress">
  16. <xsd:sequence>
  17. <xsd:element name="name" type="xsd:string"/>
  18. <xsd:element name="street" type="xsd:string"/>
  19. <xsd:element name="city" type="xsd:string"/>
  20. <xsd:element name="state" type="xsd:string"/>
  21. <xsd:element name="zip" type="xsd:decimal"/>
  22. </xsd:sequence>
  23. <xsd:attribute name="country" type="xsd:NMTOKEN" fixed="US"/>
  24. </xsd:complexType>
  25. <xsd:complexType name="Items">
  26. <xsd:sequence>
  27. <xsd:element name="item" minOccurs="0" maxOccurs="unbounded">
  28. <xsd:complexType>
  29. <xsd:sequence>
  30. <xsd:element name="productName" type="xsd:string"/>
  31. <xsd:element name="quantity">
  32. <xsd:simpleType>
  33. <xsd:restriction base="xsd:positiveInteger">
  34. <xsd:maxExclusive value="100"/>
  35. </xsd:restriction>
  36. </xsd:simpleType>
  37. </xsd:element>
  38. <xsd:element name="USPrice" type="xsd:decimal"/>
  39. <xsd:element ref="comment" minOccurs="0"/>
  40. <xsd:element name="shipDate" type="xsd:date" minOccurs="0"/>
  41. </xsd:sequence>
  42. <xsd:attribute name="partNum" type="SKU" use="required"/>
  43. </xsd:complexType>
  44. </xsd:element>
  45. </xsd:sequence>
  46. </xsd:complexType>
  47. <!-- Stock Keeping Unit, a code for identifying products -->
  48. <xsd:simpleType name="SKU">
  49. <xsd:restriction base="xsd:string">
  50. <xsd:pattern value="\d{3}-[A-Z]{2}"/>
  51. </xsd:restriction>
  52. </xsd:simpleType>
  53. </xsd:schema>

要素の内容を型 (complexType) として定義します。例えば、shipTo要素の内容(子要素と属性)をUSAddress型として定義します。

データ型として、string, decimal などが用意されていて、さらにmaxExclusiveやpatternで制約を加えることができます。

要素の出現回数は、minOccursmaxOccurs で制約を付けれます。

検証器に掛けます;

$ xmllint --schema schema.xsd doc1.xml > /dev/null 
doc1.xml:16: element zip: Schemas validity error : Element 'zip': '95819aa' is not a valid value of the atomic type 'xs:decimal'.
doc1.xml fails to validate
$ msv schema.xsd doc1.xml
スキーマを読み込んでいます...
検証しています: doc1.xml
エラー 16行目23文字目 : file:doc1.xml
  decimal型の値ではありません:95819aa

文書に違反がみつかりました
$ jing schema.xsd doc1.xml
doc1.xml:16:23: error: http://www.w3.org/TR/xml-schema-1#cvc-datatype-valid.1.2.1?95819aa&decimal
doc1.xml:16:23: error: http://www.w3.org/TR/xml-schema-1#cvc-type.3.1.3?zip&95819aa

RELAX NG

次は、RELAX NGです。

HTML/XML
[RAW]
  1. <grammar xmlns="http://relaxng.org/ns/structure/1.0"
  2. datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes" >
  3. <!-- top level を宣言 -->
  4. <start>
  5. <element name="purchaseOrder">
  6. <attribute name="orderDate"><data type="date" /></attribute>
  7. <element name="shipTo"><ref name="USAddress" /></element>
  8. <element name="billTo"><ref name="USAddress" /></element>
  9. <!-- ref で別の定義を参照 -->
  10. <optional><ref name="comment"/></optional>
  11. <element name="items">
  12. <oneOrMore>
  13. <element name="item">
  14. <attribute name="partNum">
  15. <data type="string">
  16. <param name="pattern">\d{3}-[A-Z]{2}</param>
  17. </data>
  18. </attribute>
  19. <element name="productName"><text /></element>
  20. <element name="quantity">
  21. <data type="positiveInteger">
  22. <param name="maxExclusive">100</param>
  23. </data>
  24. </element>
  25. <element name="USPrice"><data type="decimal" /></element>
  26. <optional><ref name="comment"/></optional>
  27. <optional>
  28. <element name="shipDate"><data type="date" /></element>
  29. </optional>
  30. </element>
  31. </oneOrMore>
  32. </element>
  33. </element>
  34. </start>
  35. <define name="USAddress">
  36. <attribute name="country"><data type="NMTOKEN" /></attribute>
  37. <element name="name"><text /></element>
  38. <element name="street"><text /></element>
  39. <element name="city"><text /></element>
  40. <element name="state"><text /></element>
  41. <element name="zip"><data type="decimal" /></element>
  42. </define>
  43. <define name="comment">
  44. <element name="comment"><text /></element>
  45. </define>
  46. </grammar>

RELAX NGは, element の中に子要素を書いていってもいいですし、defineとrefで括りだすこともできます。

grammarタグ datatypeLibrary 属性でXML Schemaのデータ型を引用しています。ほかのライブラリを引用することもできます。引用したデータ型ライブラリは、data、param要素で使えます。

要素の数は oneOrMore, optional などで指定します。

RELAX NGは、XML Schemaで指定できる、デフォルト値や固定値を指定することはできません。この点は後述します。

$ jing grammar.rng doc1.xml
doc1.xml:16:23: error: character content of element "zip" invalid; must be a decimal number
$ msv grammar.rng doc1.xml
スキーマを読み込んでいます...
検証しています: doc1.xml
エラー 16行目23文字目 : file:doc1.xml
  decimal型の値ではありません:95819aa

文書に違反がみつかりました
$ xmllint --relaxng grammar.rng doc1.xml > /dev/null
doc1.xml:16: element zip: Relax-NG validity error : Type decimal doesn't allow value '95819aa'
doc1.xml:16: element zip: Relax-NG validity error : Error validating datatype decimal
doc1.xml:16: element zip: Relax-NG validity error : Element zip failed to validate content
doc1.xml fails to validate

RELAX NG Compact Syntax

最後に、RELAX NG Compact Syntax で書いてみます。Compact Syntax は, 表現力は RELAX NG と同じですが, XMLではありません。

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と内容は同一です。

$ jing -c grammar.rnc doc1.xml
doc1.xml:16:23: error: character content of element "zip" invalid; must be a decimal number

@ 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化もされています。

2018年8月現在, ISO/IEC 19757 DSDL は, 次が発行されています;

  • Part 2: Regular-grammar-based validation -- RELAX NG
  • Part 3: Rule-based validation -- Schematron [2016が最新。現在、改訂版が開発中。]
  • Part 4: Namespace-based Validation Dispatching Language (NVDL)
  • Part 5: Extensible Datatypes
  • Part 7: Character Repertoire Description Language (CREPDL)
  • Part 8: Document Semantics Renaming Language (DSRL)
  • Part 9: Namespace and datatype declaration in Document Type Definitions (DTDs)
  • Part 11: Schema association

RELAX NG, Schematron, NVDL が、現実に使われています。

おそらく、予定されていた次のパートは, 標準化されなかったのだろうと思います;

  • Part 1: Overview
  • Part 6: Path-based integrity constraints
  • 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文書のなかで、スキーマでテキストを書けないことになっている場所に空白文字や改行が含まれると、検証エラーになってしまいます。

不便すぎるので、簡単なラッパを作ります。

Ruby
[RAW]
  1. require 'rexml/document'
  2. require 'rexml/validation/relaxng'
  3. require 'rexml/parsers/sax2parser'
  4. # Ruby 1.8.6のREXMLのバリデータはXML文書の空白の扱いがまずいので、簡単にラップする。
  5. class WrapValidator
  6. def initialize v
  7. @validator = v
  8. end
  9. def receive event
  10. return if event[0] == :text && event[1].strip == ""
  11. @validator.receive event
  12. end
  13. end
  14. # RELAX NGのパターンを読ませる。
  15. validator = REXML::Validation::RelaxNG.new <<EOF
  16. <element name="addressBook" xmlns="http://relaxng.org/ns/structure/1.0">
  17. <zeroOrMore>
  18. <element name="card">
  19. <element name="name"><text/></element>
  20. <element name="email"><text/></element>
  21. </element>
  22. </zeroOrMore>
  23. </element>
  24. EOF
  25. wrapper = WrapValidator.new validator
  26. # XML文書を用意する。
  27. source = <<EOF
  28. <addressBook>
  29. <card>
  30. <name>John Smith</name>
  31. <email>js@example.com</email>
  32. </card>
  33. <card>
  34. <name>Fred Bloggs</name>
  35. <email>fb@example.net</email>
  36. </card>
  37. </addressBook>
  38. EOF
  39. # SAX2パーサに登録してみる。
  40. parser = REXML::Parsers::SAX2Parser.new source
  41. parser.add_listener wrapper
  42. # エラーなくパースできればOK
  43. parser.parse

@ RELAX NGの書き方

RELAX NGのチュートリアルがあります。これを見ながら、書き方を確認してみましょう。

まず、RELAX NGパターン;

HTML/XML
[RAW]
  1. <element name="addressBook" xmlns="http://relaxng.org/ns/structure/1.0">
  2. <zeroOrMore>
  3. <element name="card">
  4. <element name="name"><text/></element>
  5. <element name="email"><text/></element>
  6. </element>
  7. </zeroOrMore>
  8. </element>

主な要素は、

element 要素。nameで要素名を表す。属性、内容は子要素として書く。
zeroOrMore0回以上
oneOrMore1回以上
optionalあるかないか
choice 子としていくつかのパターンを取り、そのいずれか
group 要素をグループ化。choiceの子パターンとして使用
attribute属性。nameで属性名を表す。
empty elementの子として、空要素を明確にする。
ref 別に定義されたパターンへの参照。name属性で参照先を表す。
define refで参照される定義。
data 要素の内容、属性値のデータ型。param要素でさらに制約を加えられる。
list 空白文字で区切られた値

例えば、属性値を二つの値のいずれかに制約したいときは、

HTML/XML
[RAW]
  1. <attribute name="preferredFormat">
  2. <choice>
  3. <value>html</value>
  4. <value>text</value>
  5. </choice>
  6. </attribute>

要素の内容が空白で区切られた1個以上の実数、という制約は、

HTML/XML
[RAW]
  1. <element name="vector">
  2. <list>
  3. <oneOrMore>
  4. <data type="double"/>
  5. </oneOrMore>
  6. </list>
  7. </element>

RELAX NGでは、スキーマのelementの順序がそのままXML文書での要素の順序になります。どの順でもいい、というときはinterleaveで囲みます。

次のパターンは、nameとemailはどちらが先でもいいですが、ただ1回ずつ出現しなければなりません。

HTML/XML
[RAW]
  1. <element name="card">
  2. <interleave>
  3. <element name="name"><text/></element>
  4. <element name="email"><text/></element>
  5. </interleave>
  6. </element>

どのような順序でも、何回でも書けるようにするには、interleaveとzeroOrMoreを組み合わせます。例えば、次のパターンは、

HTML/XML
[RAW]
  1. <element name="head" xmlns="http://relaxng.org/ns/structure/1.0">
  2. <interleave>
  3. <element name="title"><text /></element>
  4. <zeroOrMore>
  5. <element name="meta">
  6. <attribute name="name"/>
  7. <attribute name="content"/>
  8. </element>
  9. </zeroOrMore>
  10. </interleave>
  11. </element>

次のXML文書にマッチします。zeroOrMoreで複数回書ける要素は、それぞれ出現順序がバラバラでもOK。

HTML/XML
[RAW]
  1. <head>
  2. <meta name="ROBOTS" content="NOODP"/>
  3. <title>foobar</title>
  4. <meta name="description" content="テスト"/>
  5. </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文書と、

HTML/XML
[RAW]
  1. <rules xmlns="http://purl.oclc.org/dsdl/nvdl/ns/structure/1.0">
  2. <namespace ns="tag:hello-world">
  3. <validate schema="hello-world.rng" />
  4. </namespace>
  5. <namespace ns="tag:fuga">
  6. <validate schema="fuga.rng" />
  7. </namespace>
  8. </rules>

次の二つのRELAX NGパターンは、

HTML/XML
[RAW]
  1. <element name="a:root" xmlns="http://relaxng.org/ns/structure/1.0"
  2. xmlns:a="tag:hello-world">
  3. <element name="a:x"><text/></element>
  4. </element>
HTML/XML
[RAW]
  1. <element name="b:y" xmlns="http://relaxng.org/ns/structure/1.0"
  2. xmlns:b="tag:fuga">
  3. <text/>
  4. </element>

次のXML文書にマッチします。

HTML/XML
[RAW]
  1. <root xmlns="tag:hello-world">
  2. <x>
  3. <foo:y xmlns:foo="tag:fuga">123</foo:y>
  4. </x>
  5. </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 コア 解説