Linq to XML (XDocument) でエンティティ宣言されたものを使う

TODO いつか書き直す。.NET Coreにおける問題のIssueが解決し、.NET Core 1.2がリリースされたら記事を書き直す。ソースコードの予定場所

(現在の中身は 2014 年 6 月 4 日当時のコードを使ったもの)

(例外の画像)

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE math PUBLIC "-//W3C//DTD MathML 3.0//EN" "http://www.w3.org/Math/DTD/mathml3/mathml3.dtd">
<math xmlns="http://www.w3.org/1998/Math/MathML">
<mn>2</mn>
<mo>&times;</mo>
<mn>5</mn>
<mo>=</mo>
<mn>10</mn>
</math>

上のような XML を読み込ませると「宣言されていないエンティティ’times’への参照です。」というエラーが出る場合の対処法など。

1: XmlReader を使って、Dtd の評価をオンにして乗り切る

一番簡単に思えるのは、 XmlReader とその設定を用いた評価です。

DtdProcessingParse を指定することで、Dtd を考慮して読み込んでくれます。

速いわけではないので、必要ない人は 1 を飛ばしてください。

プログラム

1
2
3
4
5
6
7
DateTime start = DateTime.Now;
var doc = XDocument.Load(XmlReader.Create("MathMLFile.xml", new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Parse
}));
Console.WriteLine(DateTime.Now - start);
Console.WriteLine(doc.ToString());

出力

1
2
3
4
5
6
7
8
9
10
11
00:00:08.3144756

<!DOCTYPE math PUBLIC "-//W3C//DTD MathML 3.0//EN" "http://www.w3.org/Math/DTD/mathml3/mathml3.dtd"[]>
<math xmlns="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">
<mn xmlns="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">2</mn>
<mo xmlns="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">×</mo>
<mn xmlns="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">5</mn>
<mo xmlns="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">=</mo>
<mn xmlns="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">10</mn>
</math>

結果

出力の最初を見てください。個人のネットワーク環境に左右されるとはいえ、 8 秒の処理は長すぎます。原因は XML の宣言で外部(インターネット上)の定義ファイルを指定しており、評価時にダウンロードしてくる処理が(毎回)必要になるからです。

これを解決するためには、あらかじめローカルに ‘.dtd’ ファイルを保存しておくとよいです。( XML がモジュール化されている場合は、そのモジュールすべてが必要)

2: XmlReader を使って、自作の派生クラスでローカルファイルの定義を使わせる

少々面倒な処理が必要になりますが、 XML に使うフォーマットが分かっている場合は、こちらの方が速くて便利です。また、クラス化することで処理が分かれるので見やすくなる?
XmlReader.Create の設定で、 XmlResolver に自作の派生クラスのインスタンスを指定します。

プログラム

本処理
1
2
3
4
5
6
7
8
9
10
11
DateTime start = DateTime.Now;
var doc = XDocument.Load(XmlReader.Create(
new FileStream("MathMLFile.xml", FileMode.Open, FileAccess.Read, FileShare.Read),
new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Parse,
XmlResolver = new MathMLResolver()
}));

Console.WriteLine(DateTime.Now - start);
Console.WriteLine(doc.ToString());
自作の派生クラス
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MathMLResolver : System.Xml.Resolvers.XmlPreloadedResolver
{
public MathMLResolver()
: base()
{
this.Add(
new Uri("http://www.w3.org/Math/DTD/mathml3/mathml3.dtd"),
new FileStream("mathml3.dtd", FileMode.Open, FileAccess.Read, FileShare.Read));

this.Add(
new Uri("http://www.w3.org/Math/DTD/mathml3/mathml3-qname.mod"),
new FileStream("mathml3-qname.mod", FileMode.Open, FileAccess.Read, FileShare.Read));

foreach (var file in Directory.EnumerateFiles(Environment.CurrentDirectory, "*.ent")
.Select(fullPath => Path.GetFileName(fullPath)))
{
this.Add(
new Uri("http://www.w3.org/Math/DTD/mathml3/" + file),
new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read));
}
}
}

出力

1
2
3
4
5
6
7
8
9
10
00:00:00.1200068

<!DOCTYPE math PUBLIC "-//W3C//DTD MathML 3.0//EN" "http://www.w3.org/Math/DTD/mathml3/mathml3.dtd"[]>
<math xmlns="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">
<mn xmlns="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">2</mn>
<mo xmlns="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">×</mo>
<mn xmlns="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">5</mn>
<mo xmlns="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">=</mo>
<mn xmlns="http://www.w3.org/1998/Math/MathML" xmlns:xlink="http://www.w3.org/1999/xlink">10</mn>
</math>

結果

ローカルから読むことで速くなりました。

自作のクラスは、 XmlPreloadedResolver クラスから派生しています。 XmlPreloadedResolver クラスは、 XML 関係ファイルの URI と、それに対応するファイルの関係を決めることができ、今回はそれにローカルファイルを指定しています。
今回は MathML のものを用いました。関係するすべてのファイルを実行フォルダに保存し、読み込ませています。

参照記事

(ここのサイトも追加で参考になりそう。 .NET Framework 4.5.2 でXmlReaderのDTD読み込みの挙動が変わっていた - ぽにょろん