1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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ではないので注意) を列挙する。

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

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

XML 要素を準備する

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

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 メソッドは以下のものがある。

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

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

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

値を取得する

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

<Pikotaro>
    <I>Pen</I>
    <I>Apple</I>
</Pikotaro>

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

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

属性を取得する

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

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

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

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

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

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

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

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

ただ、注意すべきは XML 自体に名前空間指定xmlnsが適用されていると、XElement.Element(XName)の引数にstringを使っても、指定した要素が選択されないことだ。つまりどういうことかというと、例えばExcelファイル内のworkbook.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を抜き出す際にどうしてもこうしたくなるが。

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

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

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

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

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

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

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

<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の値を抜き出したいとする。思わずこう書きたくなるだろう。

// ここで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指定は次のように書く。

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

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

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

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

// ここで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 が使える。

// ここで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()で代用した。

まとめ

さて、読者の皆さんは序盤の説明で「比較的」を鍵括弧でくくっていたことを覚えているだろうか? これはもちろん意図的だが、その理由は「名前空間がある場合の取り扱いの難しさ」にある。

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

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

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

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

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?