読者です 読者をやめる 読者になる 読者になる

独自のXML名前空間を持つXMLを、LINQやXPathを使い、いろいろな方法でデータ取得してみた

C#

独自のXML名前空間を持つXMLC#で解析する機会があったため、いろいろな方法でのデータ取得を試してみました。

■環境

■対象のXML

XMLを自作するのは手間なので、このエントリでは Amazon Product Advertising API のレスポンスを例として使います。
参考: API Reference - Product Advertising API


なお、Amazon Product Advertising API へのリクエストには署名が必要ですが、Amazonが公式に公開しているサンプルを使います。
Product Advertising API Signed Requests Sample Code - C# REST/QUERY : Sample Code & Libraries : Amazon Web Services


ただ、サンプルにて使用しているAPIはバージョンが古くそのままでは動作しないため、以下のエントリを参考に修正したものを使います。
Amazon Product Advertising API はじめの一歩 | Express for Web


リクエストの主な内容は、以下の通りです。

項目
オペレーション ItemLookup
レスポンスグループ Offers, Small
取得する項目 Title, TotalNew, LowestNewPrice, TotalUsed, LowestUsedPrice, TotalOffers

■試してみる方法

以下の4つの方法を試してみました。なお、LINQはメソッド構文を使います*1

  1. LINQ to XML + XPathSelectElements
  2. LINQ to XML + Descendants
  3. XmlDocument + SelectNodes
  4. XPathNavigator + Evaluate


それぞれの方法で、レスポンスを受け取った後の流れを以下の通りに実装します。

  1. レスポンスのストリームをXML用の型にLoad
  2. 独自のXML名前空間の設定
  3. XMLよりデータを取得して、以下のようなクラスへと詰め込む
public class Result
{
    public string Title { get; set; }
    public int TotalNew { get; set; }
    public int LowestNewPrice { get; set; }
    public int TotalUsed { get; set; }
    public int LowestUsedPrice { get; set; }
    public int TotalOffers { get; set; }
}

■1. LINQ to XML + XPathSelectElements

取得したレスポンスのストリームは、XElement型にLoadします。

var root = XElement.Load(stream);

参考:c# - What's the difference between xelement.load and xdocument.load? - Stack Overflow


名前空間は XmlNamespaceManager オブジェクトに追加します。

string nameSpace = "http://webservices.amazon.com/AWSECommerceService/2011-08-01";
var nsmgr = new XmlNamespaceManager(new NameTable());
nsmgr.AddNamespace("ns", nameSpace);

参考:c# - How do I get an IXmlNamespaceResolver - Stack Overflow


あとはLINQとXPathSelectElementでXPathを使って、項目を取得します。Selectのところで詰め込み先の「Program.Result」クラスを指定しています。

var result = root.XPathSelectElements("//ns:Item", nsmgr)
                 .Select(item => new Program.Result
                 {
                     Title = item.XPathSelectElement("./ns:ItemAttributes/ns:Title", nsmgr).Value,
                     TotalNew = int.Parse(item.XPathSelectElement("./ns:OfferSummary/ns:TotalNew", nsmgr).Value),
                     LowestNewPrice = int.Parse(item.XPathSelectElement("./ns:OfferSummary/ns:LowestNewPrice/ns:Amount", nsmgr).Value),
                     TotalUsed = int.Parse(item.XPathSelectElement("./ns:OfferSummary/ns:TotalUsed", nsmgr).Value),
                     LowestUsedPrice = int.Parse(item.XPathSelectElement("./ns:OfferSummary/ns:LowestUsedPrice/ns:Amount", nsmgr).Value),
                     TotalOffers = int.Parse(item.XPathSelectElement("./ns:Offers/ns:TotalOffers", nsmgr).Value),
                 }).First();

参考:Extensions.XPathSelectElement メソッド (XNode, String, IXmlNamespaceResolver) (System.Xml.XPath)


なお、XPathSelectElementなどのSystem.Xml.XPath.Extensionsクラスにある拡張メソッドを使うと、パフォーマンスが若干低下するとのことです。(2013/12/11 追記)
参考:Extensions クラス (System.Xml.XPath)



■2. LINQ to XML + Descendants

ストリームの取得は 1.同様、XElement型にLoadします。
名前空間は、他と異なりXNameSpace型にセットします。

XNamespace nameSpace = "http://webservices.amazon.com/AWSECommerceService/2011-08-01";


あとはDescendantsで目的のノードへ移動しElement()で取得するため、XPathに詳しくなくても大丈夫そうです。なお、数値型へは直接パースできました。

var result = root.Descendants(nameSpace + "Item")
                 .Select(item => new Program.Result
                 {
                     Title = item.Element(nameSpace + "ItemAttributes").Element(nameSpace + "Title").Value,
                     TotalNew = (int)item.Element(nameSpace + "OfferSummary").Element(nameSpace + "TotalNew"),
                     LowestNewPrice = (int)item.Element(nameSpace + "OfferSummary").Element(nameSpace + "LowestNewPrice").Element(nameSpace + "Amount"),
                     TotalUsed = (int)item.Element(nameSpace + "OfferSummary").Element(nameSpace + "TotalUsed"),
                     LowestUsedPrice = (int)item.Element(nameSpace + "OfferSummary").Element(nameSpace + "LowestUsedPrice").Element(nameSpace + "Amount"),
                     TotalOffers = (int)item.Element(nameSpace + "Offers").Element(nameSpace + "TotalOffers")
                 }).First();


なお、今回のケースでは省略しましたが、実際には null の入ってくる場合も考慮したほうが良いかもしれません。
参考:

■3. XmlDocument + SelectNodes

LINQを使わないため、ストリームはXmlDocument型にLoadします。

var root = new XmlDocument();
root.Load(stream);


名前空間は XmlNamespaceManager オブジェクトへの追加でした。


あとはSelectNodesでXPathを使って設定しますが、「Item(0)」や「InnerText」が出てきて、少し分かりづらい感じです。詰め込みはオブジェクト初期化子でやりました。

var result = new Program.Result
{
    Title = root.SelectNodes("//ns:Title", nsmgr).Item(0).InnerText,
    TotalNew = int.Parse(root.SelectNodes("//ns:TotalNew", nsmgr).Item(0).InnerText),
    LowestNewPrice = int.Parse(root.SelectNodes("//ns:LowestNewPrice/ns:Amount", nsmgr).Item(0).InnerText),
    TotalUsed = int.Parse(root.SelectNodes("//ns:TotalUsed", nsmgr).Item(0).InnerText),
    LowestUsedPrice = int.Parse(root.SelectNodes("//ns:LowestUsedPrice/ns:Amount", nsmgr).Item(0).InnerText),
    TotalOffers = int.Parse(root.SelectNodes("//ns:TotalOffers", nsmgr).Item(0).InnerText)
};

■4. XPathNavigator + Evaluate

ストリームはXPathNavigator型に入れます。

var root = new XPathDocument(stream).CreateNavigator();


名前空間は XmlNamespaceManager オブジェクトへの追加でした。


あとは結果を詰め込むだけですが、XmlNamespaceManager.Evaluate() の結果は foreachで回さないと取得できないため、強引なワンライナーになっています。キャストが必要だったり var を使えないなど、いろいろな制限もありました。

var result = new Program.Result();
foreach (XPathNavigator r in root.Evaluate("//ns:Title", nsmgr) as XPathNodeIterator) { result.Title = r.Value; }
foreach (XPathNavigator r in root.Evaluate("//ns:TotalNew", nsmgr) as XPathNodeIterator) { result.TotalNew = int.Parse(r.Value); }
foreach (XPathNavigator r in root.Evaluate("//ns:LowestNewPrice/ns:Amount", nsmgr) as XPathNodeIterator) { result.LowestNewPrice = int.Parse(r.Value); }
foreach (XPathNavigator r in root.Evaluate("//ns:TotalUsed", nsmgr) as XPathNodeIterator) { result.TotalUsed = int.Parse(r.Value); }
foreach (XPathNavigator r in root.Evaluate("//ns:LowestUsedPrice/ns:Amount", nsmgr) as XPathNodeIterator) { result.LowestUsedPrice = int.Parse(r.Value); }
foreach (XPathNavigator r in root.Evaluate("//ns:TotalOffers", nsmgr) as XPathNodeIterator) { result.TotalOffers = int.Parse(r.Value); }

参考:名前空間を指定してXPathを実行する。(XmlNamespaceManager, XmlReader, XPathDocument, XPathNavigator, Evaluate) - いろいろ備忘録日記


■結果

当然ながら、どれも同じ結果になります。

                                                    • -
LINQ to XML + Descendants ver
                                                    • -
Title: 青森県のりんご―市販の品種とりんごの話題 TotalNew: 1 LowestNewPrice: 2940 TotalUsed: 5 LowestUsedPrice: 5775 TotalOffers: 2
                                                    • -
                                                    • -
LINQ to XML + Descendants ver
                                                    • -
Title: 青森県のりんご―市販の品種とりんごの話題 TotalNew: 1 LowestNewPrice: 2940 TotalUsed: 5 LowestUsedPrice: 5775 TotalOffers: 2
                                                    • -
                                                    • -
XmlDocument + SelectNodes ver
                                                    • -
Title: 青森県のりんご―市販の品種とりんごの話題 TotalNew: 1 LowestNewPrice: 2940 TotalUsed: 5 LowestUsedPrice: 5775 TotalOffers: 2
                                                    • -
                                                    • -
XPathNavigator + Evaluate ver
                                                    • -
Title: 青森県のりんご―市販の品種とりんごの話題 TotalNew: 1 LowestNewPrice: 2940 TotalUsed: 5 LowestUsedPrice: 5775 TotalOffers: 2
                                                    • -
Enterキーで終了

■感想

LINQ to XMLはありがたい。



*1:Web上ではクエリ構文の方をよく見かけた気がします