XElement を用いた C# における XML の解読

C# で XML を取り扱う方法はいくつかあるが、その中でも特に推奨されるのが LINQ to XML を用いた操作である。

ここでは特に有用であろう、読み取りに限定した操作方法を記述する。書き込みが必要な場面というのはあまり無いような気もする。

## そもそも XML とは何か

XML は、**Extensive Markup Language** のことを指し、HTML の兄弟規格として制定された、汎用的に使えるデータフォーマットである。要素の内容は自由に設定可能だが、一方で構造は重厚さがあり、堅牢に扱えるフォーマットだともいえる。

一般的なデータフォーマットとしてのシェアは人間が簡単に書ける JSON の方が高いが、それでも有用な場面が多いフォーマットである。特に XML Schema を使い、フォーマットの明確な定義ができる点は重要である。

Microsoft は XML を自社ソフトウェアのフォーマットに積極的に取り入れており、例えば C# のコード説明コメントは XML によるものだし、Office ファイル全般の実装も zip 内に各種 XML を梱包する形で実装されている。Office ファイルの規格名自体も OpenXML である。

## LINQ to XML における XML の抽象

LINQ to XML を利用すると、XML ファイルを LINQ の方法で「比較的」簡単に扱えるが、ここでいくつかの抽象化が施されている。

以下、特によく利用するであろう XML 抽象のクラス (ここでいう「抽象」は C# における`abstract`ではないので注意) を列挙する。

<dl>
    <dt>XElement</dt>
    <dd>任意のXML要素。</dd>
    <dt>XAttribute</dt>
    <dd>任意のXML要素に付与された属性。</dd>
    <dt>XName</dt>
    <dd>任意のXML要素の名前。</dd>
    <dt>XNamespace</dt>
    <dd>任意のXML要素が持っている名前空間。</dd>
</dl>

プログラマーは、LINQ to XML メソッドを使い、これらのクラスにアクセスする。

## XML 要素を準備する

XML ファイルをすでに作っているなら、ファイルを`Stream`もしくはその継承先クラスで開き、`XElement.Load(Stream)`を利用することで`XElememt`に変換することができる。Excelファイル[^1]を開く場合、

[^1]: 正確な拡張子は.xlsx。このファイルの中身を見て、シートごとに画像を抽出するプログラムを作りたかったので今回の記事の内容を調査した。

``` C#
using (var file = File.OpenRead(xlsxFilePath)) // Excelファイルを開く
using (var zip = new ZipArchive(file, ZipArchiveMode.Read)) // Excelファイルはzipなので、zipとして扱う
{
    var entry = zip.Entries.FirstOrDefault(etr =>
        etr.FullName.Equals("xl/workbook.xml", StringComparison.OrdinalIgnoreCase));
    using (var xmlFile = entry.Open())
    {
        var element = XElement.Load(xmlFile);
        // elementに対する処理
    }
}
```

## XML から欲しいものを抽出する

### 子孫要素を見つける

XML の子孫要素を探るための LINQ to XML メソッドは以下のものがある。

<dl>
    <dt><code>XElement.Element(XName)</code></dt>
    <dd>指定した名前の、当該要素<strong>直下</strong>の最初の子要素を取得する。</dd>
    <dt><code>XElement.Elements(XName)</code></dt>
    <dd>指定した名前の、当該要素<strong>直下</strong>の子要素の一覧を取得する。<br>引数を指定しなければ当該要素直下の全ての子要素の一覧を取得する。</dd>
    <dt><code>XElement.Descendants(XName)</code></dt>
    <dd>指定した名前の、当該要素の<strong>全ての子孫</strong>の子要素の一覧を取得する。<br>引数を指定しなければ当該要素の全ての子孫要素の一覧を取得する。</dd>
</dl>

通常は `Descendants`で事足りるが、構成が大事な場合は`Element`を使うことになる。

ここで、引数が`XName`型になっているが、**名前空間の無い** XML の場合は通常の`string`で問題無い。自動で型変換が入る。ここは C# でなかったら、いちいち`root.Element(new XName("Name"))`と書かねばならなかったり、`XElement.Element()`のメソッド定義を複数作らねばならず煩雑だっただろう。

### 値を取得する

XML 要素が持つ値は、単純に`Value`で取得可能だ。例えばこんな XML があったとする。

``` XML
<Pikotaro>
    <I>Pen</I>
    <I>Apple</I>
</Pikotaro>
```

ここから`I`が持つものを抽出する C# コードを書くと以下のようになるだろう。

```C#
// ここでpikotaroは上のXMLを何らかの方法で開いたものである
var iHave = pikotaro
    .Elements("I")
    .Select(i => i.Value)
    .ToList();
```

### 属性を取得する

XML 要素の属性はを取得するメソッドは以下の通り。

<dl>
    <dt><code>XElement.Attribute(XName)</code></dt>
    <dd>当該要素の指定した名前の属性を取得する。</dd>
    <dt><code>XElement.Attributes()</code></dt>
    <dd>当該要素の属性をすべて取得する。</dd>
    <dt><code>XElement.Attributes(XName)</code></dt>
    <dd>当該要素の指定した名前の属性を、コレクションとして取得する。</dd>
</dl>

`Attributes`はオーバーロードにより使い方が異なるため分割して表にした。`XElement.Attributes(XName)`はおそらくLINQを使ったメソッドチェイン処理をしたい場合や、(遅延実行のため) 即座に取得させたくない場合に使うものだと思われる。

属性取得に関しても XML の例を作ろう。

``` XML
<MobileSuit>
    <Name ModelNumber="MS-06">ザクⅡ</Name>
    <OperatedBy>ジオン公国軍</OperatedBy>
</MobileSuit>
```

ここから`ModelNumber`を抽出する C# コードは以下の通りだ。

```C#
// ここでmobileSuitは上のXMLを何らかの方法で開いたものである
var modelNumber = mobileSuit
    .Element("Name")
    .Attribute("ModelNumber")
    .Value;
```

## 名前空間のある XML は要注意！

ただ、注意すべきは **XML 自体に名前空間指定`xmlns`が適用されていると、`XElement.Element(XName)`の引数に`string`を使っても、指定した要素が選択されない**ことだ。つまりどういうことかというと、例えばExcelファイル内の`workbook.xml`の構成 (抜粋) はこんな感じである。

``` XML
<workbook xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
    <fileVersion appName="xl" lastEdited="7" lowestEdited="4" rupBuild="24326"/>
    <workbookPr defaultThemeVersion="166925"/>
</workbook>
```

先ほどのコードの要領で、このXMLからfileVersionを抜き出す際にどうしてもこうしたくなるが。

```C#
// ここでelementはworkbook.xmlを開いているものとする
var fileVersion = element.Element("fileVersion") // この点は出ねえよ！
```

残念ながら、これでは`fileVersion`要素が (いや、それどころか何も) 抽出できない。名前空間が加味されていないからである。ではどうするか、誘惑振り切ってこうだ。

```C#
// ここでelementはworkbook.xmlを開いているものとする
var fileVersion = element.Element(element.Name.Namespace + "fileVersion")
```

こうすることで、要素の名前空間 (`XNamespace`) と"fileVersion"が結合された`XName`が生成される。めでたしめでたし。

ここでさらに解説すれば、`XElement.Name`の型は`XName`であり、さらに`XName.Namespace`は`XNamespace`型である。`XNamespace`と`string`の間に演算子`+`が設定されており、その結果が`XName`型になるという寸法だ。ここら辺も C# の強力な演算子多重定義が光る場面である。Java だったら`element.name.namespace.getElementName("fileVersion")`のような設計になっていただろう。ここで、メソッド名は Java に合わせてキャメルケースにした。

### 名前空間付きの要素はさらに注意！

さらに面倒なのは名前空間に属する要素である。例えば Excel ファイルの図面の XML (ファイル名は例えば drawing1.xml となる。この数字は1以上の任意の整数となる) はこんな感じだ[^2]。

[^2]: これは本物の xlsx の図面ファイルからとったものである。実業務データだが、機微のある情報は含まれていないため、そのまま抜粋した。

``` XML
<xdr:wsDr xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"
    xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
    <xdr:twoCellAnchor editAs="oneCell">
        <xdr:from>
            <xdr:col>4</xdr:col>
            <xdr:colOff>104775</xdr:colOff>
            <xdr:row>8</xdr:row>
            <xdr:rowOff>0</xdr:rowOff>
        </xdr:from>
        <xdr:to>
            <xdr:col>4</xdr:col>
            <xdr:colOff>2628900</xdr:colOff>
            <xdr:row>8</xdr:row>
            <xdr:rowOff>1009650</xdr:rowOff>
        </xdr:to>
        <xdr:pic>
            <xdr:nvPicPr>
                <xdr:cNvPr id="3" name="図 1">
                    <a:extLst>
                        <a:ext uri="{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}">
                            <a16:creationId
                                xmlns:a16="http://schemas.microsoft.com/office/drawing/2014/main"
                                id="{A6C559F0-7AC2-6DEF-2740-190054F72023}" />
                        </a:ext>
                    </a:extLst>
                </xdr:cNvPr>
                <xdr:cNvPicPr>
                    <a:picLocks noChangeAspect="1" />
                </xdr:cNvPicPr>
            </xdr:nvPicPr>
            <xdr:blipFill>
                <a:blip
                    xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
                    r:embed="rId1" />
                <a:stretch>
                    <a:fillRect />
                </a:stretch>
            </xdr:blipFill>
            <xdr:spPr>
                <a:xfrm>
                    <a:off x="5648325" y="1847850" />
                    <a:ext cx="2524125" cy="1009650" />
                </a:xfrm>
                <a:prstGeom prst="rect">
                    <a:avLst />
                </a:prstGeom>
            </xdr:spPr>
        </xdr:pic>
        <xdr:clientData />
    </xdr:twoCellAnchor>
</xdr:wsDr>
```

ここで、`a:blip`要素の`r:embed`の値を抜き出したいとする。思わずこう書きたくなるだろう。

``` C#
// ここでelementはdrawing1.xmlのルート要素「xdr:wsDr」を開いているものとする
var rId = element.Descendants(element.Name.Namespace + "a:blip")[0]
    .Attribute("r:embed").Value;
```

だが、やはりこのコードは動かない。要素や属性の指定に ":" を含む文字列が使えないというエラーが出てコードが止まるからだ。ではこのコロンは何者か。

実は、これこそが**要素の名前空間を表す記号**なのである。つまり「`a:blip`要素」というのは正確ではない (が、簡単のために以後もこの記法を使う)。「`a`名前空間プレフィックスの `blip`要素」だったのだ。この`a`名前空間プレフィックスの定義は`xdr:wsDr`でなされている。`xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"`というのがそれだ。つまり、先のコードは名前空間指定の時点で間違っていたのである。

この名前空間を指定するために、.NET チームは`XElement.GetNamespaceOfPrefix(string)` というメソッドを用意した。ここで引数はプレフィックスの名前そのものである。つまり、先のコードの`Descendants`指定は次のように書く。

``` C#
var blips = element.Descendants(element.GetNamespaceOfPrefix("a") + "blip")
```

ここで、賢明な読者諸君は「なぜ`.Attribute`まで取得しないのだろう」という疑問が湧いたに違いない。~~勘のいいガキは嫌いだよ~~ここがミソで、`r`プレフィックスの定義は`xdr:wsDr`に無く、`a:blip` で定義されているのだ。つまり、こうしてもデータは取得できない。

``` C#
// ここでelementはdrawing1.xmlのルート要素「xdr:wsDr」を開いているものとする
var rId = element.Descendants(element.GetNamespaceOfPrefix("a") + "blip")[0]
    .Attribute(element.GetNamespaceOfPrefix("r") + "embed").Value;
```

というのも、 (くどい繰り返しになるが) `element`は`r`という名前空間プレフィックスを持っていないからである。これは地味に面倒だが、冷静になって考えれば、何らかの手段で`a:blip`要素を取得できればいい話だ。つまり、

``` C#
// ここでelementはdrawing1.xmlのルート要素「xdr:wsDr」を開いているものとする
var blip = element.Descendants(element.GetNamespaceOfPrefix("a") + "blip")[0];
var rId = blip.Attribute(blip.GetNamespaceOfPrefix("r") + "embed").Value;
```

とすれば、めでたく`r:embed`の値が取得できる。2行に分かれるのがお気に召さないなら、LINQ が使える。

``` C#
// ここでelementはdrawing1.xmlのルート要素「xdr:wsDr」を開いているものとする
var rId = element
    .SelectMany(e => e.Descendants(e.GetNamespaceOfPrefix("a") + "blip"))
    .Select(e => e.Attribute(e.GetNamespaceOfPrefix("r") + "embed").Value)
    .FirstOrDefault();
```

ここで、`Select`の戻り値は`IEnumerable<string>`なのでインデクサーが使えないため、`FirstOrDefault()`で代用した。

## まとめ

さて、読者の皆さんは[序盤の説明](#linq-to-xml-における-xml-の抽象)で「比較的」を鍵括弧でくくっていたことを覚えているだろうか？　これはもちろん意図的だが、その理由は「名前空間がある場合の取り扱いの難しさ」にある。

1. 名前空間が指定されていることを忘れて、要素名だけでアクセスしようとしがち。
1. デフォルト名前空間は`.`を2回使わないとアクセスできないので面倒。
1. 名前空間プレフィックス指定は要素を半ば再帰的に取り出さないといけないので、これも面倒。

という躓きポイントが多い仕様である。しかし、それさえ分かれば C# の XML 操作は強力無比であることも間違いないだろう。