スキーマによってXML文書を補う (PSVI)

(2018.8 新規作成, C#サンプル.)

@ ボヘミアン vs 貴族

DTD, W3C XML Schemaはスキーマにデフォルト値、固定値を含めることができます。一方、RELAX NGは含められません。

例えば、XML Schemaでは、次のように書いてデフォルト値を指定します。要素の内容のデフォルト値も指定できます。

HTML/XML
[RAW]
  1. <xsd:element name="comment" type="xsd:string" default="(comment space)" />
  2. <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などでつまみ食いしたりすることが多く、いちいちスキーマを要求されては実用になりません。

DTD

DTDのエンティティ宣言は, マクロです。単なるテキストだけでなく、タグなど構造すら生成できます。

次のXML文書を読み込んでみます。

HTML/XML
[RAW]
  1. <?xml version="1.0" encoding="UTF-16" ?>
  2. <?xml-stylesheet href="doc.xsl"
  3. type="text/xsl" ?>
  4. <!DOCTYPE foo [
  5. <!ENTITY x "foobar">
  6. <!ENTITY e "<p>text</p>">
  7. ]>
  8. <foo attr="&x;">
  9. &e;<![CDATA[<greeting>Hello, world!</greeting>]]>
  10. </foo>
  11. <!-- comment <head> <body> -->

C#で簡単に作ります. 構造を表示するため、ストリームパーサを使います。XmlNodeType.DocumentType では, DTD全体が得られます。

C++
[RAW]
  1. using System;
  2. using System.Collections.Generic;
  3. //using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.Xml;
  7. namespace dtd_entity
  8. {
  9. class Program
  10. {
  11. // 2. ストリーム
  12. // 制限事項: unparsed entity-ref が致命的エラーになり、それ以上読めない.
  13. static void xml_stream_test(string doc_filename, XmlReaderSettings docSettings)
  14. {
  15. // fast, forward-only, non-cached access.
  16. XmlReader docReader;
  17. try {
  18. // XmlReader では, unparsed entity-ref が、どのようにしても、
  19. // 致命的エラー.
  20. docReader = XmlReader.Create(doc_filename, docSettings);
  21. // XmlTextReader class は非推奨.
  22. // unparsed entity-ref が許容できるように見えるが、やはり致命的エラー
  23. //docReader = new XmlTextReader(doc_filename);
  24. }
  25. catch (System.IO.FileNotFoundException ex) {
  26. Console.WriteLine(ex.Message);
  27. return;
  28. }
  29. while ( true ) { // Read() の戻り値は bool
  30. // XmlReader では, unparsed entity-ref は, ここで XmlException
  31. // が発生し, 以降 Read() が false を返すため, リカバリできない.
  32. bool cont = docReader.Read();
  33. if (!cont)
  34. break;
  35. // Attribute, Document, DocumentFragment は来ない.
  36. switch (docReader.NodeType)
  37. {
  38. // 宣言部
  39. case XmlNodeType.XmlDeclaration:
  40. Console.WriteLine("<?xml " + docReader.Value + ">");
  41. break;
  42. case XmlNodeType.DocumentType:
  43. Console.WriteLine("doctype: <!DOCTYPE {0} [{1}]>",
  44. docReader.Name, docReader.Value);
  45. break;
  46. case XmlNodeType.Whitespace:
  47. break;
  48. // document-type-end がないので、来ない.
  49. //case XmlNodeType.Entity:
  50. //case XmlNodeType.Notation:
  51. // 内容
  52. // [43] content ::= CharData? ((element | Reference | CDSect | PI | Comment) CharData?)*
  53. case XmlNodeType.Element: // 開始タグまたは空要素タグ
  54. Console.Write("stag: <{0}", docReader.Name);
  55. if (docReader.HasAttributes) {
  56. while (docReader.MoveToNextAttribute()) {
  57. Console.Write(" {0}=\"{1}\"",
  58. docReader.Name, docReader.Value);
  59. }
  60. // Move the reader back to the element node.
  61. docReader.MoveToElement();
  62. }
  63. if (docReader.IsEmptyElement )
  64. Console.WriteLine(" />");
  65. else
  66. Console.WriteLine(">");
  67. break;
  68. case XmlNodeType.Text:
  69. Console.WriteLine("text: " + docReader.Value);
  70. break;
  71. case XmlNodeType.CDATA:
  72. Console.WriteLine("cdata: <![CDATA[{0}]]>", docReader.Value);
  73. break;
  74. /* 展開されるので、来ない
  75. case XmlNodeType.EntityReference:
  76. // XmlTextReader なら, ここで catch できる.
  77. try {
  78. docReader.ResolveEntity();
  79. // 成功したときは, 次回の Read() で、展開した文字列を
  80. // 読み込む.
  81. }
  82. catch (XmlException ) {
  83. Console.Write("unparsed entity-ref: &" + docReader.Name + ";");
  84. }
  85. break;
  86. */
  87. case XmlNodeType.EndElement: // 終了タグ
  88. Console.WriteLine("etag: </{0}>", docReader.Name);
  89. break;
  90. /* 来ない
  91. case XmlNodeType.EndEntity:
  92. break; // 単に捨てる.
  93. */
  94. // 共通
  95. case XmlNodeType.ProcessingInstruction:
  96. Console.WriteLine("pi: <?{0} {1}?>", docReader.Name, docReader.Value);
  97. break;
  98. case XmlNodeType.Comment:
  99. Console.WriteLine("comment: <!--{0}-->", docReader.Value);
  100. break;
  101. default:
  102. Console.WriteLine("unexpected node!! " +
  103. docReader.NodeType.ToString());
  104. break;
  105. }
  106. } // end while
  107. docReader.Close();
  108. }
  109. static void Main(string[] args)
  110. {
  111. // 2. ストリーム
  112. XmlReaderSettings docSettings = new XmlReaderSettings();
  113. // ignore でも, "宣言されていないエンティティ 'x' への参照" エラーになる.
  114. docSettings.DtdProcessing = DtdProcessing.Parse;
  115. docSettings.IgnoreWhitespace = false;
  116. xml_stream_test("entity1.xml", docSettings);
  117. }
  118. }
  119. } // namespace dtd_entity

.NET の XmlReader は、非常に厳しい (厳密な) パーサです。

  • DTDを解釈し、エンティティ宣言を認識できる.
  • エンティティ参照をマクロとして展開できる.
  • 宣言されていないエンティティ参照は、致命的エラーになる。厳しすぎ。
  • ストリームパーサなのに, タグの対応のチェックも行われる。やはり致命的エラーになる。厳しすぎ.

XmlTextReaderクラスは非推奨です。エラーチェックが XmlReader より緩いのかと思って試してみましたが、一緒でした。ResolveEntity() で例外が発生した場合、次回以降の Read() が偽を返すため, 結局, エラーリカバリになりません。機能も微妙で, もはや選ぶ理由がありません。

XmlReader.Create() でインスタンス化するため, サブクラスを作って挙動を変えることもできません。どうしてこんなデザインにしたのか?

ともかく, 処理すると、次のようになります。エンティティ参照 &x; が展開された部分が、<p> 開始タグ、終了タグとして認識されています。

<?xml version="1.0" encoding="UTF-16">
pi: <?xml-stylesheet href="doc.xsl"
                 type="text/xsl"   ?>
doctype: <!DOCTYPE foo [ 
<!ENTITY x "foobar"> 
<!ENTITY e "<p>text</p>">
]>
stag: <foo attr="foobar">
stag: <p>
text: text
etag: </p>
cdata: <![CDATA[<greeting>Hello, world!</greeting>]]>
etag: </foo>
comment: <!-- comment <head> <body> -->

W3C XML Schema

次のXML文書に対して,

HTML/XML
[RAW]
  1. <foo>
  2. <!-- 存在して、かつ空の場合だけ適用 -->
  3. <elm />
  4. <e2 />
  5. <comment />
  6. </foo>

次のスキーマで検証します。要素、属性の両方について、デフォルト値、固定値を設定してみます。

HTML/XML
[RAW]
  1. <?xml version="1.0" encoding="UTF-16" ?>
  2. <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  3. <xsd:element name="foo">
  4. <xsd:complexType>
  5. <xsd:sequence>
  6. <xsd:element name="elm" type="e1type" />
  7. <!-- default, fixed の内容は, テキストのみ. 複合型と両立しない. -->
  8. <xsd:element name="e2" type="xsd:string" default="hoge" />
  9. <xsd:element name="comment" type="xsd:string" fixed="here is comment" />
  10. </xsd:sequence>
  11. </xsd:complexType>
  12. </xsd:element>
  13. <xsd:complexType name="e1type">
  14. <xsd:attribute name="at1" type="xsd:integer" default="123" />
  15. <xsd:attribute name="at2" type="xsd:string" fixed="abc" />
  16. </xsd:complexType>
  17. </xsd:schema>

こちらも簡単に, C# で書きます。XmlReaderSettings インスタンスのSchemasプロパティに、スキーマ文書を足します。

C++
[RAW]
  1. using System;
  2. using System.Collections.Generic;
  3. //using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.Xml;
  7. using System.Xml.Schema;
  8. namespace ConsoleApp1
  9. {
  10. class Program
  11. {
  12. static XmlReaderSettings validation_settings(string schema_filename)
  13. {
  14. // XML文書の読み込み設定
  15. XmlReaderSettings docSettings = new XmlReaderSettings();
  16. // XmlTextReader class は非推奨.
  17. XmlReader schemaReader = XmlReader.Create(schema_filename);
  18. // スキーマ文書自体の妥当性検証するとき.
  19. // System.IO.Stream, System.IO.TextReader, System.Xml.XmlReader
  20. XmlSchema myschema = XmlSchema.Read(schemaReader, ValidationCallback);
  21. // スキーマ文書の妥当性検証をしないときは, XmlReader インスタンスを
  22. // 渡せばよい.
  23. //docSettings.Schemas.Add(null, schemaReader);
  24. docSettings.Schemas.Add(myschema);
  25. // XML文書の妥当性検証するように設定
  26. // The default is no data validation.
  27. docSettings.ValidationType = ValidationType.Schema;
  28. docSettings.ValidationEventHandler += ValidationCallback;
  29. // The default is Prohibit: XmlException を投げる.
  30. //docSettings.DtdProcessing = DtdProcessing.Parse;
  31. // By default, ProcessIdentityConstraints and AllowXmlAttributes.
  32. docSettings.ValidationFlags |=
  33. XmlSchemaValidationFlags.ReportValidationWarnings;
  34. foreach (XmlSchema schema in docSettings.Schemas.Schemas()) {
  35. schema.Write(Console.Out); // DEBUG
  36. }
  37. Console.WriteLine("");
  38. return docSettings;
  39. }
  40. static void Main(string[] args)
  41. {
  42. // 1. DOM読み込み時に妥当性検証する
  43. string schema_filename = @"schema1.xsd";
  44. XmlReaderSettings docSettings = validation_settings(schema_filename);
  45. XmlReader docReader = XmlReader.Create(@"doc1.xml", docSettings);
  46. XmlDocument doc = new XmlDocument();
  47. doc.Load(docReader);
  48. docReader.Close();
  49. Console.WriteLine(doc.OuterXml); // DEBUG
  50. foreach (XmlAttribute a in doc.ChildNodes[0].ChildNodes[1].Attributes) {
  51. Console.WriteLine("{0}={1}", a.Name, a.Value);
  52. }
  53. }
  54. // for ValidationEventHandler
  55. static void ValidationCallback(object sender, ValidationEventArgs args)
  56. {
  57. if (args.Severity == XmlSeverityType.Warning) {
  58. Console.Write("WARNING: ");
  59. Console.WriteLine(args.Message);
  60. }
  61. else if (args.Severity == XmlSeverityType.Error) {
  62. Console.Write("ERROR: ");
  63. Console.WriteLine(args.Message);
  64. }
  65. }
  66. }
  67. } // namespace ConsoleApp1

処理すると, 次のようになります。e2 要素と comment 要素の内容が補われていることに注目。

属性のほうは、OuterXml では表示されないですが、Attributes 経由で見ると、補われていることが分かります。

<?xml version="1.0" encoding="shift_jis"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <xsd:element name="foo">
    <xsd:complexType>
      <xsd:sequence>
        <xsd:element name="elm" type="e1type" />
        <xsd:element default="hoge" name="e2" type="xsd:string" />
        <xsd:element fixed="here is comment" name="comment" type="xsd:string" />
      </xsd:sequence>
    </xsd:complexType>
  </xsd:element>
  <xsd:complexType name="e1type">
    <xsd:attribute default="123" name="at1" type="xsd:integer" />
    <xsd:attribute fixed="abc" name="at2" type="xsd:string" />
  </xsd:complexType>
</xsd:schema>
<foo><!-- 存在して、かつ空の場合だけ適用 --><elm /><e2>hoge</e2><comment>here is comment</comment></foo>
at1=123
at2=abc