最近LINQを勉強したので、忘れないうちに、LINQ to Objectsに続いてLINQ to XMLの備忘録を掲載します。LINQ to Objectsほど複数の記事にしない予定なのです。

確認環境

  • Windows Vista Enterprise
  • 開発環境: Visual Studio 2008 Professional
  • .NET 3.5

0. 使用するデータの定義

最初はXMLに対してクエリを行いたいので、ファイルから読み取るXMLの中身を定義します。本記事では以下のXMLファイルの内容にしたいしてLINQクエリを実行します。

ファイル名はproducts.xmlとします。

<?xml version="1.0" encoding="utf-8"?>
<products>
  <product id="product1" category="book">
	<name>Programing Something</name>
	<price>2000</price>
	<publishDate>2008/10/10</publishDate>
	<authors>
		<author>Tarou Book</author>
		<author>Hanako Book</author>
	</authors>
  </product>
  <product id="product2" category="book">
	<name>Administrating Something</name>
	<price>5000</price>
	<publishDate>2008/10/12</publishDate>
	<authors>
		<author>Kotarou Book1</author>
		<author>Kohanako Book2</author>
	</authors>
  </product>
  <product id="product3" category="novel">
	<name>Suspection novel</name>
	<price>500</price>
	<publishDate>2007/12/12</publishDate>
	<authors>
		<author>Jirou Tarou</author>
	</authors>
  </product>
  <product id="product4" category="novel">
	<name>Fantasy novel</name>
	<price>540</price>
	<publishDate>2008/9/14</publishDate>
	<authors>
		<author>Tarou Book</author>
	</authors>
  </product>
  <product id="product5" category="cram">
	<name>Study English</name>
	<price>2400</price>
	<publishDate>2008/9/14</publishDate>
	<authors>
		<author>English Tarou</author>
		<author>English Jirou</author>
	</authors>
  </product>
</products>

 

1. LINQクエリメモ

1.1 XMLファイルのロード

XElement,XDocumentのLoadメソッドを使用するとファイルやネットワークからXMLファイルをロードし、XElement,XDocumentを初期化することができます。

XElement elem = XElement.Load(@"D:\temp\products.xml");
Console.WriteLine(elem);

XDocument doc = XDocument.Load(@"D:\temp\products.xml");
Console.WriteLine(doc.ToString());

1.2 選択処理

LINQ to Objectsと同じように、XMLに対してクエリを発行します。以下サンプルプログラムです。

XElement products = XElement.Load(@"D:\temp\products.xml");

var query = from p in products.Elements("product")
            where (string)p.Attribute("category") == "book"
            select new
            {
                ID = (string)p.Attribute("id"),
                Name = (string)p.Element("name"),
                Price = (decimal)p.Element("price")
            };
foreach (var item in query)
{
    Console.WriteLine(item);
}

実行結果は次のようになります。

{ ID = product1, Name = Programing Something, Price = 2000 }
{ ID = product2, Name = Administrating Something, Price = 5000 }

LINQ to Objectと同時に使用することができます。以下サンプルプログラムです。

XElement products = XElement.Load(@"D:\temp\products.xml");
var categories = new string[] { "book", "novel" };
// on節でp.Element("category")とするとコンパイルエラーとなるので、
// (string) p.Element("category")と明示的にキャストする必要があります
var query = from p in products.Elements("product")
            let xCategory = (string) p.Attribute("category")
            join c in categories
              on xCategory equals c
           orderby xCategory
            select new
            {
                Name = (string)p.Element("name"),
                Price = (decimal)p.Element("price"),
                Category = xCategory
             };
foreach (var item in query)
{
     Console.WriteLine(item);
}

実行結果は次のようになります。

{ Name = Programing Something, Price = 2000, Category = book }
{ Name = Administrating Something, Price = 5000, Category = book }
{ Name = Suspection novel, Price = 500, Category = novel }
{ Name = Fantasy novel, Price = 540, Category = novel }

1.3 ドメインオブジェクトの作成

XMLファイルからLINQ to XML を使用してクラスのインスタンスを作成する方法を記述します。前提として読み込むファイルの内容は"使用するデータの定義"で掲載したデータとします。

XMLに対応するクラスは次のようになっているとします。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Linq2Xml
{
    public class Product
    {
        public string ID { get; set; }
        public decimal Price { get; set; }
        public DateTime PublishData { get; set; }
        public string Category { get; set; }
        public string Name { get; set; }
        public Author[] Authors { get; set; }
        public override string ToString()
        {
            StringBuilder builder = new StringBuilder();
            builder.Append(string.Format("ID:{0}, Name:{1}, Price:{2:c}", this.ID, this.Name, this.Price));
            if (Authors != null)
            {
                foreach (Author a in Authors)
                {
                    builder.Append("\n" + a.ToString());
                }
            }
            return builder.ToString();
        }
    }
    public class Author
    {
        public Author() { }
        public Author(string name) { Name = name; }
        public String Name { get; set; }
        public override string ToString()
        {
            return "Author:" + Name;
        }
    }

}

読み込むファイルが小さなファイルの場合は、XElement.Loadメソッドを使用します。

public static void CreateObjectFromXml()
{
    XElement products = XElement.Load(@"D:\temp\products.xml");

    var query = from p in products.Elements("product")
                select new Product()
                {
                    ID = (string)p.Attribute("id"),
                    Category = (string)p.Attribute("category"),
                    Name = (string)p.Element("name"),
                    Price = (decimal)p.Element("price"),
                    Authors = ( from a in p.Descendants("author")
                                select new Author(a.Value)).ToArray()
                };
    foreach (var item in query)
    {
        Console.WriteLine(item);
    }
}

実行結果は次のようになります。

ID:product1, Name:Programing Something, Price:\2,000
Author:Tarou Book
Author:Hanako Book
ID:product2, Name:Administrating Something, Price:\5,000
Author:Kotarou Book1
Author:Kohanako Book2
ID:product3, Name:Suspection novel, Price:\500
Author:Jirou Tarou
ID:product4, Name:Fantasy novel, Price:\540
Author:Tarou Book
ID:product5, Name:Study English, Price:\2,400
Author:English Tarou
Author:English Jirou

上記の場合、メモリ上にXMLファイルの内容が読み込まれてからクエリの評価が行われますが、XMLファイルのサイズが大きかったり、リモートサイトにある場合は、メモリ上にすべて読み込んでからクエリの評価を行うとパフォーマンスが落ちる可能性があります。そこで、LINQの遅延クエリ評価を利用して、次のようにXmlReaderを使用して、ブロックごとに読み込むようにします。

public static void CreateObjectFromLargeXml()
{
    var products = BlockXmlReader(@"D:\temp\products.xml", "product");
    var query = from p in products
                select new Product()
                {
                    ID = (string)p.Attribute("id"),
                    Category = (string)p.Attribute("category"),
                    Name = (string)p.Element("name"),
                    Price = (decimal)p.Element("price"),
                    Authors = (from a in p.Descendants("author")
                               select new Author(a.Value)).ToArray()
                };
    foreach (var item in query)
    {
        Console.WriteLine(item);
    }
}
/// <summary>
/// 指定されたURIからelemNameを持つ要素のリストを取得する
/// </summary>
public static IEnumerable<XElement> BlockXmlReader(string uri, string elemName)
{
    XmlReaderSettings settings = new XmlReaderSettings();
    XmlReader reader = XmlReader.Create(uri, settings);

    while (reader.Read())
    {
        if (reader.NodeType == XmlNodeType.Element && reader.Name == elemName)
        {
            yield return XElement.ReadFrom(reader) as XElement;
        }
    }
}

実行結果はメモリにXMLファイルをすべて読み込む場合の実行結果例と同じです。

1.4 Xmlファイルの変換

Xmlファイルを別スキーマのXmlファイルに変換します。もちろんXSLTを使用することもできますが、LINQ to XMLを使ったサンプルを掲載します。XNamespaceも使ってみます。

public static void TransformingXml()
{
    XElement products = XElement.Load(@"D:\temp\products.xml");
    XNamespace ns = "http://www.pine4.net/Products";
    XElement transformedProducts =
        new XElement(ns + "products",
            new XAttribute(XNamespace.Xmlns + "p", ns),
            from p in products.Elements("product")
            select new XElement(ns + "product",
                new XAttribute("id", (string)p.Attribute("id")),
                new XElement(ns + "name", (string)p.Element("name")),
                new XElement(ns + "price", (decimal)p.Element("price")),
                from a in p.Descendants("author")
                select new XElement("author", a.Value))
                );
    transformedProducts.Save(@"D:\temp\transform.xml");
}

実行結果のファイルは次のようになります。

<?xml version="1.0" encoding="utf-8"?>
<p:products xmlns:p="http://www.pine4.net/Products">
  <p:product id="product1">
    <p:name>Programing Something</p:name>
    <p:price>2000</p:price>
    <authro>Tarou Book</authro>
    <authro>Hanako Book</authro>
  </p:product>
  <p:product id="product2">
    <p:name>Administrating Something</p:name>
    <p:price>5000</p:price>
    <authro>Kotarou Book1</authro>
    <authro>Kohanako Book2</authro>
  </p:product>
  <p:product id="product3">
    <p:name>Suspection novel</p:name>
    <p:price>500</p:price>
    <authro>Jirou Tarou</authro>
  </p:product>
  <p:product id="product4">
    <p:name>Fantasy novel</p:name>
    <p:price>540</p:price>
    <authro>Tarou Book</authro>
  </p:product>
  <p:product id="product5">
    <p:name>Study English</p:name>
    <p:price>2400</p:price>
    <authro>English Tarou</authro>
    <authro>English Jirou</authro>
  </p:product>
</p:products>

説明は以上です。誤り、指摘等がありましたら、ご連絡ください。