C# XmlReaderの使い方完全ガイド|XML読み込み・要素取得・大容量ファイル対応をサンプルコードで解説
はじめに
C#でXMLファイルを読み込む方法はいくつかありますが、大容量XMLを効率よく処理したい場合に特に有力なのがXmlReaderです。
XmlReaderは、XMLを先頭から順番に読み進めるストリーミング型のAPIです。XmlDocumentやXDocumentのようにXML全体をメモリ上に展開しないため、ファイルサイズが大きい場合でも低メモリで処理できます。
この記事では、C#のXmlReaderの基本的な使い方から、要素・テキスト・属性の取得方法、大容量XMLファイルの読み込み、非同期処理、よくあるエラー対策まで、サンプルコード付きで解説します。
1. C#のXmlReaderとは?XMLを高速・低メモリで読み込むための基本
XmlReaderは、C#でXMLを読み込むためのクラスです。
XMLファイルやXML文字列を1ノードずつ順番に読み進めながら、要素名、テキスト、属性、階層の深さなどを取得できます。
C#using System.Xml;
using XmlReader reader = XmlReader.Create("sample.xml");
while (reader.Read())
{
Console.WriteLine($"{reader.NodeType}: {reader.Name} = {reader.Value}");
}
このように、Read()メソッドでXMLを先頭から末尾まで読み進めていくのが基本的な使い方です。
1-1. XmlReaderの特徴:前方専用・読み取り専用・ストリーミング処理
XmlReaderには、次のような特徴があります。
XmlReaderは前方専用です。一度読み進めたノードに戻ることはできません。そのため、XMLを上から順番に処理する用途に向いています。
また、読み取り専用です。XMLを読み込むことはできますが、XMLの内容を書き換えることはできません。XMLを書き込みたい場合はXmlWriterを使用します。
さらに、ストリーミング処理に対応しています。XML全体をメモリに読み込まず、必要な部分だけを順番に処理できるため、大容量XMLファイルでも効率よく扱えます。
1-2. XmlReaderが向いているケース:大容量XML・逐次処理・一部要素の抽出
XmlReaderが特に向いているのは、次のようなケースです。
大容量のXMLファイルを読み込む場合、XmlReaderは非常に有効です。ファイル全体をオブジェクトとして保持しないため、数百MB以上のXMLでも比較的少ないメモリで処理できます。
また、XMLを先頭から順番に読みながら、1件ずつデータベースに登録したり、必要な要素だけを抽出したりする処理にも向いています。
たとえば、商品一覧XMLからproduct要素だけを読み取り、id、name、priceだけを取得するような処理では、XmlReaderを使うと無駄の少ない実装ができます。
1-3. XmlDocument・XDocument・XmlReaderの違いと使い分け
C#でXMLを扱う代表的な方法には、XmlDocument、XDocument、XmlReaderがあります。
XmlDocumentはDOM形式でXML全体をメモリに読み込み、ノードを自由に移動しながら操作できます。昔からあるAPIで、既存システムでもよく使われています。
XDocumentはLINQ to XMLで使われるAPIです。LINQを使って要素を検索したり、条件に一致するデータを取得したりしやすく、コードも比較的読みやすくなります。
一方、XmlReaderはXMLを先頭から順番に読み込むAPIです。全体をメモリに保持しないため、大容量XMLや逐次処理に向いています。
使い分けの目安は次のとおりです。
小さなXMLを簡単に検索・加工したい → XDocument
既存コードでDOM操作が必要 → XmlDocument
大容量XMLを低メモリで順番に処理したい → XmlReader
1-4. XmlReaderでできること・できないこと
XmlReaderでできることは、XMLファイルの読み込み、要素名の取得、テキストの取得、属性の取得、ノード種別の判定、階層の深さの確認などです。
一方で、XMLを直接編集することはできません。また、前方専用のため、読み終わったノードに戻ることもできません。XML全体を自由に検索したい場合は、XDocumentやXmlDocumentの方が適しています。
つまり、XmlReaderは「XMLを上から順番に読みながら必要な情報を取り出す」ためのAPIです。
2. C#でXmlReaderを使う準備
ここからは、C#でXmlReaderを使うための基本的な準備を見ていきます。
2-1. System.Xml名前空間の追加
XmlReaderを使うには、System.Xml名前空間を追加します。
C#using System.Xml;
.NETのプロジェクトであれば、通常は追加のNuGetパッケージなしで利用できます。
2-2. XmlReader.CreateでXMLファイルを開く基本構文
XMLファイルを読み込むには、XmlReader.Create()を使います。
C#using System.Xml;
using XmlReader reader = XmlReader.Create("sample.xml");
while (reader.Read())
{
// XMLを1ノードずつ処理する
}
XmlReader.Create()には、ファイルパス、Stream、TextReaderなどを指定できます。
ファイルから読み込む場合は、次のように書くのが基本です。
C#using XmlReader reader = XmlReader.Create("data.xml");
文字列として持っているXMLを読み込む場合は、StringReaderを使います。
C#using System.IO;
using System.Xml;
string xml = "<root><name>Apple</name></root>";
using StringReader stringReader = new StringReader(xml);
using XmlReader reader = XmlReader.Create(stringReader);
while (reader.Read())
{
Console.WriteLine($"{reader.NodeType}: {reader.Name} {reader.Value}");
}
2-3. using文でリソースを安全に解放する書き方
XmlReaderはファイルやストリームを扱うため、使い終わったらリソースを解放する必要があります。
C#ではusing文を使うことで、処理が終わったときに自動的にDispose()が呼ばれます。
C#using XmlReader reader = XmlReader.Create("sample.xml");
while (reader.Read())
{
// 読み込み処理
}
古い書き方では、次のようにブロック形式のusingも使えます。
C#using (XmlReader reader = XmlReader.Create("sample.xml"))
{
while (reader.Read())
{
// 読み込み処理
}
}
どちらの書き方でも問題ありません。現在のC#では、前者のusing宣言を使うとコードをすっきり書けます。
2-4. サンプルで使用するXMLファイルの構造
この記事では、主に次のような商品一覧XMLをサンプルとして使用します。
XML<?xml version="1.0" encoding="utf-8"?>
<products>
<product id="1" category="fruit">
<name>Apple</name>
<price>120</price>
<stock>30</stock>
</product>
<product id="2" category="fruit">
<name>Banana</name>
<price>80</price>
<stock>50</stock>
</product>
<product id="3" category="drink">
<name>Coffee</name>
<price>300</price>
<stock>15</stock>
</product>
</products>
このXMLには、ルート要素productsの中に複数のproduct要素があります。
各product要素には、属性としてidとcategoryがあり、子要素としてname、price、stockがあります。
3. XmlReaderでXMLファイルを読み込む基本サンプル
XmlReaderの基本は、Read()メソッドでXMLを1ノードずつ読み進めることです。
3-1. ReadメソッドでXMLを1ノードずつ読み進める
Read()メソッドは、次のノードへ移動します。
C#while (reader.Read())
{
// 現在のノードを処理する
}
戻り値はboolです。次のノードが存在する場合はtrue、XMLの末尾に到達した場合はfalseを返します。
つまり、XML全体を読み込む基本形は次のようになります。
C#using System.Xml;
using XmlReader reader = XmlReader.Create("products.xml");
while (reader.Read())
{
Console.WriteLine(reader.NodeType);
}
3-2. NodeTypeでElement・Text・EndElementを判定する
reader.NodeTypeを使うと、現在のノードの種類を判定できます。
代表的なNodeTypeは次のとおりです。
XmlNodeType.Element 開始タグ
XmlNodeType.Text テキスト
XmlNodeType.EndElement 終了タグ
XmlNodeType.Comment コメント
XmlNodeType.CDATA CDATA
XmlNodeType.Whitespace 空白
たとえば、開始タグだけを処理したい場合は次のように書きます。
C#if (reader.NodeType == XmlNodeType.Element)
{
Console.WriteLine($"要素: {reader.Name}");
}
テキストノードだけを処理したい場合は、次のようにします。
C#if (reader.NodeType == XmlNodeType.Text)
{
Console.WriteLine($"テキスト: {reader.Value}");
}
3-3. Name・Value・Depthプロパティで現在のノード情報を取得する
XmlReaderでは、現在のノードに関する情報をプロパティから取得できます。
Nameは現在のノード名を表します。要素ノードの場合はタグ名、属性ノードの場合は属性名が入ります。
Valueは現在のノードの値を表します。テキストノードや属性ノードでは値が入りますが、要素ノードでは通常空文字になります。
DepthはXML内での階層の深さを表します。ルート要素は0、その子要素は1、さらにその子要素は2になります。
C#Console.WriteLine($"NodeType: {reader.NodeType}");
Console.WriteLine($"Name : {reader.Name}");
Console.WriteLine($"Value : {reader.Value}");
Console.WriteLine($"Depth : {reader.Depth}");
Valueが空になる原因としてよくあるのが、要素ノードでValueを取得しようとしているケースです。
たとえば、次のXMLがあるとします。
XML<name>Apple</name>
<name>の開始タグにいるとき、reader.Nameはnameですが、reader.Valueは空です。
Appleを取得したい場合は、次のテキストノードに進むか、ReadElementContentAsString()を使います。
3-4. XML全体を順番に読み込むサンプルコード
次のコードは、XML全体を1ノードずつ読み込み、ノード種別、名前、値、深さを出力するサンプルです。
C#using System;
using System.Xml;
class Program
{
static void Main()
{
using XmlReader reader = XmlReader.Create("products.xml");
while (reader.Read())
{
Console.WriteLine(
$"Type={reader.NodeType}, Name={reader.Name}, Value={reader.Value}, Depth={reader.Depth}"
);
}
}
}
実行すると、開始要素、テキスト、終了要素などが順番に出力されます。
空白や改行もノードとして扱われる場合があるため、実際の出力にはWhitespaceが含まれることがあります。空白ノードを無視したい場合は、後述するXmlReaderSettings.IgnoreWhitespaceを使用します。
4. XmlReaderで要素・テキスト・属性を取得する方法
ここでは、XmlReaderでよく使う要素名、テキスト、属性の取得方法を解説します。
4-1. 指定した要素名だけを取得する方法
特定の要素だけを取得したい場合は、NodeTypeとNameを組み合わせて判定します。
C#using System;
using System.Xml;
class Program
{
static void Main()
{
using XmlReader reader = XmlReader.Create("products.xml");
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "name")
{
Console.WriteLine("name要素を見つけました");
}
}
}
}
reader.Name == "name"だけで判定すると、要素以外のノードで意図しない動作になる可能性があります。
そのため、基本的には次のようにNodeTypeも一緒に確認するのがおすすめです。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "name")
{
// name要素の処理
}
4-2. ReadElementContentAsStringで要素内テキストを取得する
要素内のテキストを簡単に取得したい場合は、ReadElementContentAsString()を使います。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "name")
{
string name = reader.ReadElementContentAsString();
Console.WriteLine(name);
}
たとえば、次のXMLがある場合、
XML<name>Apple</name>
ReadElementContentAsString()を使うと、Appleを取得できます。
価格のように数値として扱いたい場合は、文字列として取得してから変換できます。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "price")
{
string priceText = reader.ReadElementContentAsString();
int price = int.Parse(priceText);
Console.WriteLine(price);
}
ただし、XMLの値が必ず数値とは限らない場合は、int.TryParse()を使うと安全です。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "price")
{
string priceText = reader.ReadElementContentAsString();
if (int.TryParse(priceText, out int price))
{
Console.WriteLine($"価格: {price}");
}
}
4-3. GetAttributeで属性値を取得する
XMLの属性値を取得するには、GetAttribute()を使います。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "product")
{
string? id = reader.GetAttribute("id");
string? category = reader.GetAttribute("category");
Console.WriteLine($"id={id}, category={category}");
}
対象の属性が存在しない場合、GetAttribute()はnullを返します。
そのため、属性が必須でないXMLを扱う場合は、nullチェックを入れると安全です。
C#string? id = reader.GetAttribute("id");
if (id != null)
{
Console.WriteLine($"id={id}");
}
else
{
Console.WriteLine("id属性がありません");
}
4-4. MoveToNextAttributeで複数属性を順番に取得する
要素に付いている属性をすべて順番に取得したい場合は、MoveToNextAttribute()を使います。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "product")
{
while (reader.MoveToNextAttribute())
{
Console.WriteLine($"属性名: {reader.Name}, 値: {reader.Value}");
}
reader.MoveToElement();
}
MoveToNextAttribute()を呼ぶと、現在位置が属性ノードに移動します。
属性を読み終わった後に要素ノードへ戻りたい場合は、MoveToElement()を呼びます。
C#reader.MoveToElement();
これを忘れると、その後の処理で現在位置を誤解しやすくなるため注意しましょう。
4-5. 空要素・ネストした要素を扱うときの注意点
空要素とは、次のような要素です。
XML<product id="4" />
または、次のような開始タグと終了タグの間に値がない要素です。
XML<name></name>
空要素かどうかは、IsEmptyElementで判定できます。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "product")
{
if (reader.IsEmptyElement)
{
Console.WriteLine("空のproduct要素です");
}
}
ネストした要素を扱う場合は、現在の階層を意識することが重要です。
たとえば、次のようなXMLでは、name要素が複数の意味で登場します。
XML<company>
<name>Sample Company</name>
<products>
<product>
<name>Apple</name>
</product>
</products>
</company>
単純にreader.Name == "name"だけで取得すると、会社名と商品名の両方を取得してしまいます。
このような場合は、Depthや現在処理中の親要素を使って、どの階層のnameなのかを判定します。
C#if (reader.NodeType == XmlNodeType.Element &&
reader.Name == "name" &&
reader.Depth == 3)
{
string productName = reader.ReadElementContentAsString();
Console.WriteLine(productName);
}
ただし、XML構造が変わる可能性がある場合は、Depthだけに依存しすぎず、親要素の状態を変数で管理する方法も有効です。
5. 実践例:XmlReaderで必要なデータだけを抽出する
ここからは、実際の業務でもよくある「XMLから必要なデータだけを取り出す」サンプルを紹介します。
5-1. 商品一覧XMLからid・name・priceを取得するサンプル
次のサンプルでは、商品一覧XMLからid、name、priceを取得します。
C#using System;
using System.Xml;
class Program
{
static void Main()
{
using XmlReader reader = XmlReader.Create("products.xml");
string? id = null;
string? name = null;
int price = 0;
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "product")
{
id = reader.GetAttribute("id");
name = null;
price = 0;
}
if (reader.NodeType == XmlNodeType.Element && reader.Name == "name")
{
name = reader.ReadElementContentAsString();
}
if (reader.NodeType == XmlNodeType.Element && reader.Name == "price")
{
string priceText = reader.ReadElementContentAsString();
int.TryParse(priceText, out price);
}
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "product")
{
Console.WriteLine($"id={id}, name={name}, price={price}");
}
}
}
}
このコードでは、product開始時に変数を初期化し、nameとpriceを読み取り、product終了時に1件分のデータとして出力しています。
XmlReaderではXMLを順番に読むため、「開始要素で準備」「子要素で値を取得」「終了要素で確定」という流れを意識すると実装しやすくなります。
5-2. 条件に一致する要素だけを読み込むサンプル
次の例では、category属性がfruitの商品だけを処理します。
C#using System;
using System.Xml;
class Program
{
static void Main()
{
using XmlReader reader = XmlReader.Create("products.xml");
bool isTargetProduct = false;
string? id = null;
string? name = null;
int price = 0;
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "product")
{
string? category = reader.GetAttribute("category");
isTargetProduct = category == "fruit";
id = reader.GetAttribute("id");
name = null;
price = 0;
}
if (!isTargetProduct)
{
continue;
}
if (reader.NodeType == XmlNodeType.Element && reader.Name == "name")
{
name = reader.ReadElementContentAsString();
}
if (reader.NodeType == XmlNodeType.Element && reader.Name == "price")
{
string priceText = reader.ReadElementContentAsString();
int.TryParse(priceText, out price);
}
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "product")
{
Console.WriteLine($"id={id}, name={name}, price={price}");
isTargetProduct = false;
}
}
}
}
条件に一致しない商品は処理をスキップすることで、必要なデータだけを効率よく抽出できます。
5-3. 階層構造のあるXMLから子要素を取得するサンプル
次のような階層構造のXMLを考えます。
XML<?xml version="1.0" encoding="utf-8"?>
<orders>
<order id="1001">
<customer>
<name>Yamada</name>
</customer>
<items>
<item>
<name>Apple</name>
<quantity>3</quantity>
</item>
<item>
<name>Coffee</name>
<quantity>1</quantity>
</item>
</items>
</order>
</orders>
このXMLでは、customerのnameとitemのnameが両方存在します。
商品名だけを取得したい場合は、現在item要素の中にいるかどうかをフラグで管理します。
C#using System;
using System.Xml;
class Program
{
static void Main()
{
using XmlReader reader = XmlReader.Create("orders.xml");
bool inItem = false;
string? itemName = null;
int quantity = 0;
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "item")
{
inItem = true;
itemName = null;
quantity = 0;
}
if (inItem &&
reader.NodeType == XmlNodeType.Element &&
reader.Name == "name")
{
itemName = reader.ReadElementContentAsString();
}
if (inItem &&
reader.NodeType == XmlNodeType.Element &&
reader.Name == "quantity")
{
string quantityText = reader.ReadElementContentAsString();
int.TryParse(quantityText, out quantity);
}
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "item")
{
Console.WriteLine($"itemName={itemName}, quantity={quantity}");
inItem = false;
}
}
}
}
このように、階層構造のあるXMLでは、現在どの要素の中を処理しているかを状態として持つと安全に読み込めます。
5-4. 取得したXMLデータをListやクラスに格納する方法
実際のアプリケーションでは、XMLから取得したデータをクラスに格納することが多いです。
まず、商品を表すクラスを作成します。
C#public class Product
{
public string? Id { get; set; }
public string? Category { get; set; }
public string? Name { get; set; }
public int Price { get; set; }
public int Stock { get; set; }
}
次に、XmlReaderで読み込んだデータをList<Product>に追加します。
C#using System;
using System.Collections.Generic;
using System.Xml;
class Program
{
static void Main()
{
List<Product> products = new List<Product>();
using XmlReader reader = XmlReader.Create("products.xml");
Product? currentProduct = null;
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "product")
{
currentProduct = new Product
{
Id = reader.GetAttribute("id"),
Category = reader.GetAttribute("category")
};
}
if (currentProduct != null &&
reader.NodeType == XmlNodeType.Element &&
reader.Name == "name")
{
currentProduct.Name = reader.ReadElementContentAsString();
}
if (currentProduct != null &&
reader.NodeType == XmlNodeType.Element &&
reader.Name == "price")
{
string text = reader.ReadElementContentAsString();
if (int.TryParse(text, out int price))
{
currentProduct.Price = price;
}
}
if (currentProduct != null &&
reader.NodeType == XmlNodeType.Element &&
reader.Name == "stock")
{
string text = reader.ReadElementContentAsString();
if (int.TryParse(text, out int stock))
{
currentProduct.Stock = stock;
}
}
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "product")
{
if (currentProduct != null)
{
products.Add(currentProduct);
currentProduct = null;
}
}
}
foreach (Product product in products)
{
Console.WriteLine($"{product.Id}: {product.Name} - {product.Price}円");
}
}
}
この方法を使うと、XMLを読み込みながらアプリケーションで扱いやすいオブジェクトに変換できます。
6. 大容量XMLファイルをXmlReaderで効率よく読み込む方法
XmlReaderの大きなメリットは、大容量XMLファイルを効率よく読み込めることです。
6-1. XmlReaderが大容量ファイルに強い理由
XmlReaderは、XML全体をメモリに展開しません。
Read()で現在のノードを読み込み、処理が終わったら次のノードへ進みます。そのため、メモリ使用量を抑えながらXMLを処理できます。
たとえば、100万件の商品データが入ったXMLを処理する場合でも、1件ずつ読み込んで処理すれば、全件をメモリに保持する必要はありません。
C#while (reader.Read())
{
// 1件分を読み込んで処理する
// 処理が終わったら次のデータへ進む
}
この仕組みにより、XmlReaderは大容量ファイルの読み込みに向いています。
6-2. XDocumentやXmlDocumentでメモリ不足になるケース
XDocumentやXmlDocumentは、XML全体をメモリ上に読み込んでツリー構造として保持します。
小さなXMLであれば扱いやすいですが、ファイルサイズが大きくなると、読み込み時に多くのメモリを消費します。
C#// XML全体をメモリに読み込む
// 大容量XMLではメモリ使用量が大きくなりやすい
var doc = System.Xml.Linq.XDocument.Load("large.xml");
XMLファイルのサイズが数百MB、数GBになるようなケースでは、XDocumentやXmlDocumentで全体を読み込むとメモリ不足になる可能性があります。
このような場合は、XmlReaderで必要な部分だけを順番に処理する設計にするのが基本です。
6-3. 1件ずつ読み込んで処理するストリーミング実装
大容量XMLでは、読み込んだデータをすべてListに保持するのではなく、1件ずつ処理する方法が有効です。
次のサンプルでは、商品を1件読み込むたびに処理しています。
C#using System;
using System.Xml;
class Program
{
static void Main()
{
using XmlReader reader = XmlReader.Create("large-products.xml");
string? id = null;
string? name = null;
int price = 0;
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "product")
{
id = reader.GetAttribute("id");
name = null;
price = 0;
}
if (reader.NodeType == XmlNodeType.Element && reader.Name == "name")
{
name = reader.ReadElementContentAsString();
}
if (reader.NodeType == XmlNodeType.Element && reader.Name == "price")
{
string text = reader.ReadElementContentAsString();
int.TryParse(text, out price);
}
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "product")
{
ProcessProduct(id, name, price);
}
}
}
static void ProcessProduct(string? id, string? name, int price)
{
// ここでDB登録、CSV出力、集計などを行う
Console.WriteLine($"{id}, {name}, {price}");
}
}
このようにすれば、メモリに保持するのは現在処理中の1件分だけで済みます。
6-4. 大容量XML読み込み時のパフォーマンス改善ポイント
大容量XMLをXmlReaderで処理するときは、次の点を意識するとパフォーマンスを改善しやすくなります。
まず、必要な要素だけを処理しましょう。すべてのノードに対して重い処理を行うと、ストリーミング処理のメリットが薄れます。
C#if (reader.NodeType != XmlNodeType.Element)
{
continue;
}
このように、不要なノードを早めにスキップすると処理が読みやすくなります。
次に、文字列比較を大量に行う場合は、判定条件をシンプルに保ちます。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "product")
{
// product要素のみ処理
}
また、ログ出力を大量に行うと処理速度が低下することがあります。大容量データでは、1件ごとにConsole.WriteLine()するのではなく、必要に応じて件数単位でログを出すとよいでしょう。
さらに、読み取ったデータをすべてListに追加すると、結局メモリを大量に使います。大容量XMLでは、読み込んだデータを逐次処理し、不要になったら保持しない設計が重要です。
6-5. StreamReaderとXmlReaderを組み合わせる方法
エンコーディングを明示したい場合や、ストリームを細かく制御したい場合は、StreamReaderとXmlReaderを組み合わせることもできます。
C#using System.IO;
using System.Text;
using System.Xml;
using FileStream fileStream = File.OpenRead("products.xml");
using StreamReader streamReader = new StreamReader(fileStream, Encoding.UTF8);
using XmlReader reader = XmlReader.Create(streamReader);
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
Console.WriteLine(reader.Name);
}
}
通常、XML宣言にエンコーディングが正しく書かれている場合は、XmlReader.Create("file.xml")だけで十分です。
ただし、ファイルの文字コードが特殊な場合や、外部から受け取ったXMLのエンコーディングを明示的に扱いたい場合は、StreamReaderを使う方法も選択肢になります。
7. XmlReaderSettingsで読み込み設定をカスタマイズする
XmlReaderSettingsを使うと、XmlReaderの読み込み動作をカスタマイズできます。
空白ノードの無視、コメントの無視、DTDの扱い、非同期読み込みの有効化などを設定できます。
7-1. IgnoreWhitespaceで空白ノードを無視する
XMLには、見やすくするための改行やインデントが含まれることがあります。
XML<products>
<product>
<name>Apple</name>
</product>
</products>
このような改行やインデントは、Whitespaceノードとして読み込まれる場合があります。
空白ノードを無視したい場合は、IgnoreWhitespaceをtrueにします。
C#XmlReaderSettings settings = new XmlReaderSettings
{
IgnoreWhitespace = true
};
using XmlReader reader = XmlReader.Create("products.xml", settings);
これにより、不要な空白ノードを処理対象から外せます。
7-2. IgnoreCommentsでコメントを無視する
XML内のコメントを無視したい場合は、IgnoreCommentsをtrueにします。
C#XmlReaderSettings settings = new XmlReaderSettings
{
IgnoreComments = true
};
using XmlReader reader = XmlReader.Create("products.xml", settings);
次のようなコメントノードが読み込み対象から除外されます。
XML<!-- 商品一覧 -->
コメントを処理する必要がない場合は、IgnoreCommentsを有効にしておくと判定処理がシンプルになります。
7-3. DtdProcessingでDTD処理を制御する
XMLにDTDが含まれる場合、DtdProcessingで処理方法を指定できます。
C#XmlReaderSettings settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit
};
代表的な設定値は次のとおりです。
DtdProcessing.Prohibit DTDを禁止する
DtdProcessing.Ignore DTDを無視する
DtdProcessing.Parse DTDを解析する
外部から受け取ったXMLを処理する場合は、セキュリティ面を考慮してDTDを禁止または無視する設計がよく使われます。
C#XmlReaderSettings settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit
};
特に、信頼できないXMLを読み込む場合は、不要なDTD処理を許可しないようにしましょう。
7-4. 非同期読み込みを有効にするAsync設定
XmlReaderで非同期読み込みを行う場合は、XmlReaderSettings.Asyncをtrueにします。
C#XmlReaderSettings settings = new XmlReaderSettings
{
Async = true
};
using XmlReader reader = XmlReader.Create("products.xml", settings);
Async = trueにしたうえで、ReadAsync()やReadElementContentAsStringAsync()を使います。
C#while (await reader.ReadAsync())
{
// 非同期で読み込み
}
非同期処理は、ファイルI/Oやネットワーク越しのXML読み込みなど、待ち時間が発生しやすい処理で効果を発揮します。
7-5. XmlReaderSettingsを使った実装サンプル
次のサンプルでは、空白とコメントを無視し、DTDを禁止した設定でXMLを読み込みます。
C#using System;
using System.Xml;
class Program
{
static void Main()
{
XmlReaderSettings settings = new XmlReaderSettings
{
IgnoreWhitespace = true,
IgnoreComments = true,
DtdProcessing = DtdProcessing.Prohibit
};
using XmlReader reader = XmlReader.Create("products.xml", settings);
while (reader.Read())
{
Console.WriteLine($"{reader.NodeType}: {reader.Name} = {reader.Value}");
}
}
}
実務では、このようにXmlReaderSettingsを使って読み込み条件を明示しておくと、予期しないノードや危険なXML構造への対策がしやすくなります。
8. XmlReaderの非同期読み込みサンプル
XmlReaderは同期読み込みだけでなく、非同期読み込みにも対応しています。
8-1. ReadAsyncを使う基本構文
非同期でXMLを読み込むには、XmlReaderSettings.Asyncをtrueにして、ReadAsync()を使用します。
C#using System;
using System.Threading.Tasks;
using System.Xml;
class Program
{
static async Task Main()
{
XmlReaderSettings settings = new XmlReaderSettings
{
Async = true,
IgnoreWhitespace = true
};
using XmlReader reader = XmlReader.Create("products.xml", settings);
while (await reader.ReadAsync())
{
Console.WriteLine($"{reader.NodeType}: {reader.Name}");
}
}
}
ReadAsync()はTask<bool>を返します。次のノードがあればtrue、末尾に到達するとfalseになります。
8-2. 大容量XMLを非同期で処理するメリット
非同期読み込みのメリットは、読み込み待ちの間にスレッドを占有しにくくなることです。
特に、Webアプリケーションやサーバーアプリケーションでは、ファイルI/Oの待ち時間中にスレッドを解放できるため、全体のスケーラビリティ向上につながります。
ただし、非同期にしただけでCPU処理が速くなるわけではありません。
XMLの解析そのものがCPU負荷の中心である場合は、非同期化よりも、不要なノードを処理しないことや、逐次処理にすることの方が重要です。
8-3. async/awaitでXML要素を取得するサンプル
次のサンプルでは、async/awaitを使って商品名と価格を非同期で取得します。
C#using System;
using System.Threading.Tasks;
using System.Xml;
class Program
{
static async Task Main()
{
XmlReaderSettings settings = new XmlReaderSettings
{
Async = true,
IgnoreWhitespace = true
};
using XmlReader reader = XmlReader.Create("products.xml", settings);
string? name = null;
int price = 0;
while (await reader.ReadAsync())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "name")
{
name = await reader.ReadElementContentAsStringAsync();
}
if (reader.NodeType == XmlNodeType.Element && reader.Name == "price")
{
string priceText = await reader.ReadElementContentAsStringAsync();
if (int.TryParse(priceText, out int parsedPrice))
{
price = parsedPrice;
}
}
if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "product")
{
Console.WriteLine($"{name}: {price}円");
name = null;
price = 0;
}
}
}
}
非同期メソッドを使う場合は、対応するメソッドを一貫してawaitすることが重要です。
8-4. 同期処理と非同期処理の使い分け
コンソールアプリやバッチ処理で単純にローカルファイルを読むだけであれば、同期処理でも十分なケースが多いです。
一方、WebアプリケーションでアップロードされたXMLを処理する場合や、ネットワークストリームからXMLを読み込む場合は、非同期処理が有効です。
目安としては、次のように使い分けるとよいでしょう。
単純なローカルファイル読み込み → 同期処理
バッチ処理で順番に処理するだけ → 同期処理
WebアプリでXMLを受け取って処理 → 非同期処理
ネットワーク越しにXMLを読み込む → 非同期処理
大量リクエストを処理するサーバー側 → 非同期処理
重要なのは、非同期処理は「待ち時間に強い」仕組みであり、XML解析そのものを魔法のように高速化するものではないという点です。
9. XmlReaderでよくあるエラーと対処法
XmlReaderを使っていると、XML形式の不正、ファイルパスの誤り、属性の取得失敗、文字コードの問題などが発生することがあります。
ここでは、よくあるエラーと対処法を解説します。
9-1. XMLの形式が不正な場合のXmlException
XMLの形式が不正な場合、XmlExceptionが発生します。
たとえば、終了タグが不足しているXMLは不正です。
XML<products>
<product>
<name>Apple</name>
</product>
このようなXMLを読み込むと、解析時に例外が発生します。
C#using System;
using System.Xml;
try
{
using XmlReader reader = XmlReader.Create("broken.xml");
while (reader.Read())
{
// 読み込み処理
}
}
catch (XmlException ex)
{
Console.WriteLine("XMLの形式が不正です。");
Console.WriteLine(ex.Message);
}
外部から受け取るXMLを処理する場合は、XmlExceptionを捕捉してログ出力やエラー応答を行うようにしましょう。
9-2. ファイルが見つからない場合の例外処理
指定したXMLファイルが存在しない場合は、ファイル関連の例外が発生します。
C#using System;
using System.IO;
using System.Xml;
try
{
using XmlReader reader = XmlReader.Create("notfound.xml");
while (reader.Read())
{
// 読み込み処理
}
}
catch (FileNotFoundException ex)
{
Console.WriteLine("XMLファイルが見つかりません。");
Console.WriteLine(ex.Message);
}
catch (DirectoryNotFoundException ex)
{
Console.WriteLine("指定されたディレクトリが見つかりません。");
Console.WriteLine(ex.Message);
}
catch (XmlException ex)
{
Console.WriteLine("XMLの解析に失敗しました。");
Console.WriteLine(ex.Message);
}
ファイルパスを指定する場合は、相対パスと実行ディレクトリの関係にも注意が必要です。
開発中は、次のように絶対パスを出力して確認すると原因を見つけやすくなります。
C#string path = "products.xml";
Console.WriteLine(Path.GetFullPath(path));
9-3. 要素名が取得できない・Valueが空になる原因
reader.Valueが空になる原因として多いのは、現在位置が要素ノードであることです。
たとえば、次のXMLがあるとします。
XML<name>Apple</name>
<name>の開始要素にいるとき、reader.Nameはnameですが、reader.Valueは空です。
テキストAppleを取得したい場合は、次のようにReadElementContentAsString()を使います。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "name")
{
string name = reader.ReadElementContentAsString();
Console.WriteLine(name);
}
または、テキストノードに移動したときにValueを取得します。
C#if (reader.NodeType == XmlNodeType.Text)
{
Console.WriteLine(reader.Value);
}
XmlReaderでは、開始要素、テキスト、終了要素が別々のノードとして扱われることを理解しておくことが大切です。
9-4. 属性値がnullになる原因と確認ポイント
GetAttribute()がnullを返す場合、主な原因は次のいずれかです。
属性名が間違っている場合、属性は取得できません。XMLでは大文字と小文字が区別されるため、idとIDは別の属性として扱われます。
C#string? id = reader.GetAttribute("id");
また、現在位置が対象の要素ではない場合も属性を取得できません。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "product")
{
string? id = reader.GetAttribute("id");
}
属性を取得する前に、必ず現在のノードが目的の要素であることを確認しましょう。
名前空間付き属性の場合は、ローカル名や名前空間URIを考慮する必要があります。
C#string? value = reader.GetAttribute("code", "http://example.com/ns");
9-5. 文字コード・エンコーディングの問題への対応
XMLの文字化けや読み込みエラーは、文字コードが原因で起こることがあります。
XML宣言にエンコーディングが書かれている場合は、基本的にその指定が使われます。
XML<?xml version="1.0" encoding="utf-8"?>
ファイルの実際の文字コードとXML宣言の内容が一致していないと、文字化けや解析エラーの原因になります。
外部システムから受け取るXMLでは、実際のファイルエンコーディングを確認し、必要に応じてStreamReaderで明示します。
C#using System.IO;
using System.Text;
using System.Xml;
using StreamReader streamReader = new StreamReader("products.xml", Encoding.UTF8);
using XmlReader reader = XmlReader.Create(streamReader);
while (reader.Read())
{
// 読み込み処理
}
ただし、XML宣言とStreamReaderのエンコーディング指定が矛盾しないように注意が必要です。
10. XmlReaderを使うときのベストプラクティス
XmlReaderは高性能ですが、前方専用で状態を意識する必要があります。安全で読みやすいコードにするためのポイントを押さえておきましょう。
10-1. 必要なノードだけを処理して無駄な読み込みを避ける
XMLには、要素、テキスト、終了要素、空白、コメントなど、さまざまなノードが含まれます。
すべてのノードを処理しようとすると、コードが複雑になります。
必要なノードだけを処理するようにしましょう。
C#while (reader.Read())
{
if (reader.NodeType != XmlNodeType.Element)
{
continue;
}
if (reader.Name == "product")
{
// product要素の処理
}
}
このように早めに不要なノードを除外すると、処理の見通しが良くなります。
10-2. NodeTypeとNameを組み合わせて安全に判定する
XmlReaderでは、NameだけでなくNodeTypeも一緒に確認するのが基本です。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "name")
{
string name = reader.ReadElementContentAsString();
}
Nameだけで判定すると、終了要素や属性ノードなど、意図しないノードを処理してしまう可能性があります。
特に、同じ名前の開始タグと終了タグを区別したい場合は、NodeTypeの確認が重要です。
10-3. 例外処理を入れてXML読み込み失敗に備える
XML読み込みでは、ファイルが存在しない、XML形式が壊れている、アクセス権限がない、文字コードが不正など、さまざまなエラーが起こります。
業務アプリケーションでは、例外処理を入れておくことが重要です。
C#using System;
using System.IO;
using System.Xml;
try
{
using XmlReader reader = XmlReader.Create("products.xml");
while (reader.Read())
{
// XML読み込み処理
}
}
catch (FileNotFoundException ex)
{
Console.WriteLine("ファイルが見つかりません。");
Console.WriteLine(ex.Message);
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("ファイルへのアクセス権限がありません。");
Console.WriteLine(ex.Message);
}
catch (XmlException ex)
{
Console.WriteLine("XMLの形式が不正です。");
Console.WriteLine(ex.Message);
}
catch (Exception ex)
{
Console.WriteLine("予期しないエラーが発生しました。");
Console.WriteLine(ex.Message);
}
ただし、Exceptionだけを広く捕捉して原因を握りつぶすのは避けましょう。ログには詳細な例外情報を残すことが大切です。
10-4. 大容量ファイルでは全件保持せず逐次処理する
大容量XMLでXmlReaderを使う最大の理由は、メモリ使用量を抑えることです。
それにもかかわらず、読み取ったデータをすべてListに格納してしまうと、結局メモリを大量に使います。
C#// 大容量XMLでは注意
List<Product> products = new List<Product>();
大容量ファイルでは、1件読み込んだらすぐに処理する設計が有効です。
C#static void ProcessProduct(Product product)
{
// DB登録、ファイル出力、集計など
}
処理済みのデータを保持しないことで、ファイルサイズが大きくなっても安定して動作しやすくなります。
10-5. 読みやすいコードにするためのメソッド分割
XmlReaderの処理は、すべてをMainメソッドに書くと複雑になりがちです。
読み込み処理、要素ごとの処理、変換処理をメソッドに分けると、保守しやすくなります。
C#static Product ReadProduct(XmlReader reader)
{
Product product = new Product
{
Id = reader.GetAttribute("id"),
Category = reader.GetAttribute("category")
};
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "name")
{
product.Name = reader.ReadElementContentAsString();
}
else if (reader.NodeType == XmlNodeType.Element && reader.Name == "price")
{
string text = reader.ReadElementContentAsString();
if (int.TryParse(text, out int price))
{
product.Price = price;
}
}
else if (reader.NodeType == XmlNodeType.EndElement && reader.Name == "product")
{
break;
}
}
return product;
}
このように、1つのメソッドが1つの役割を持つようにすると、コードの見通しがよくなります。
11. XmlReaderと他のXML読み込み方法の比較
C#でXMLを読み込む方法はXmlReaderだけではありません。目的に応じて、XmlDocumentやXDocumentと使い分けることが重要です。
11-1. XmlReaderとXmlDocumentの違い
XmlDocumentは、XML全体をDOMツリーとしてメモリに読み込みます。
読み込んだ後は、親ノード、子ノード、兄弟ノードなどを自由に移動しながら操作できます。
C#XmlDocument doc = new XmlDocument();
doc.Load("products.xml");
XmlNodeList nodes = doc.GetElementsByTagName("product");
一方、XmlReaderは前方専用です。自由に戻ったり、任意の場所へジャンプしたりすることはできません。
ただし、XML全体をメモリに保持しないため、大容量XMLではXmlReaderの方が有利です。
XmlDocument → XML全体を読み込み、自由に操作しやすい
XmlReader → XMLを順番に読み込み、低メモリで処理しやすい
11-2. XmlReaderとXDocumentの違い
XDocumentはLINQ to XMLで使われるクラスです。
LINQを使って要素を検索できるため、コードを直感的に書けます。
C#using System.Linq;
using System.Xml.Linq;
XDocument doc = XDocument.Load("products.xml");
var names = doc.Descendants("product")
.Select(x => x.Element("name")?.Value);
foreach (var name in names)
{
Console.WriteLine(name);
}
XDocumentは読みやすいコードを書きやすい一方、XML全体をメモリに読み込みます。
小〜中規模のXMLを扱うならXDocumentは便利です。大容量XMLを逐次処理するならXmlReaderが適しています。
11-3. LINQ to XMLを使うべきケース
LINQ to XMLは、XMLの検索、抽出、変換を簡潔に書きたい場合に向いています。
たとえば、商品一覧から価格が100円以上の商品だけを取得する場合、XDocumentなら次のように書けます。
C#using System;
using System.Linq;
using System.Xml.Linq;
XDocument doc = XDocument.Load("products.xml");
var products = doc.Descendants("product")
.Where(p => (int?)p.Element("price") >= 100)
.Select(p => new
{
Id = (string?)p.Attribute("id"),
Name = (string?)p.Element("name"),
Price = (int?)p.Element("price")
});
foreach (var product in products)
{
Console.WriteLine($"{product.Id}: {product.Name} - {product.Price}");
}
このように、XML全体を読み込んでも問題ないサイズであれば、LINQ to XMLは非常に便利です。
11-4. 大容量XMLではどれを選ぶべきか
大容量XMLでは、基本的にXmlReaderを選ぶのがおすすめです。
理由は、XML全体をメモリに保持しないためです。
XDocumentやXmlDocumentは便利ですが、大きなXMLを読み込むとメモリ使用量が増えやすくなります。
一方、XmlReaderは処理中のノードを順番に読み進めるため、1件ずつ処理する設計と相性が良いです。
大容量XMLを1件ずつ処理する → XmlReader
小さなXMLを簡単に検索する → XDocument
DOM操作が必要な既存処理 → XmlDocument
11-5. 目的別のおすすめXML読み込み方法
目的別に整理すると、次のようになります。
XMLを高速・低メモリで読みたい
→ XmlReader
XMLをLINQで簡潔に検索したい
→ XDocument
XMLのノードを自由に追加・削除・編集したい
→ XDocument または XmlDocument
既存のDOMベース処理と合わせたい
→ XmlDocument
巨大なXMLをDB登録やCSV変換に使いたい
→ XmlReader
設定ファイルのような小さなXMLを読むだけ
→ XDocument または XmlReader
迷った場合は、XMLのサイズと処理内容で判断するとよいでしょう。
小さくて検索中心ならXDocument、大きくて逐次処理中心ならXmlReaderです。
12. C# XmlReaderに関するよくある質問
最後に、C#のXmlReaderに関するよくある質問をまとめます。
12-1. XmlReaderでXMLを書き込みできますか?
XmlReaderではXMLを書き込みできません。
XmlReaderは読み取り専用のAPIです。XMLを書き込みたい場合は、XmlWriterを使います。
C#using System.Xml;
using XmlWriter writer = XmlWriter.Create("output.xml");
writer.WriteStartDocument();
writer.WriteStartElement("products");
writer.WriteStartElement("product");
writer.WriteAttributeString("id", "1");
writer.WriteElementString("name", "Apple");
writer.WriteElementString("price", "120");
writer.WriteEndElement();
writer.WriteEndElement();
writer.WriteEndDocument();
XMLを読み込むならXmlReader、XMLを書き込むならXmlWriterと覚えておくとよいでしょう。
12-2. XmlReaderで特定のタグだけ読み飛ばせますか?
はい、Skip()を使うと現在のノードとその子孫ノードを読み飛ばせます。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "details")
{
reader.Skip();
}
たとえば、大きなdetails要素が不要な場合、Skip()でその中身をまとめてスキップできます。
C#while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element && reader.Name == "details")
{
reader.Skip();
continue;
}
// 必要な要素だけ処理する
}
大容量XMLで不要な大きな要素を読み飛ばす場合に便利です。
12-3. XmlReaderで名前空間付きXMLを読み込むには?
名前空間付きXMLでは、NameではなくLocalNameやNamespaceURIを使うと扱いやすくなります。
たとえば、次のようなXMLを考えます。
XML<products xmlns="http://example.com/products">
<product id="1">
<name>Apple</name>
</product>
</products>
名前空間を考慮して要素を判定する場合は、次のようにします。
C#if (reader.NodeType == XmlNodeType.Element &&
reader.LocalName == "product" &&
reader.NamespaceURI == "http://example.com/products")
{
string? id = reader.GetAttribute("id");
Console.WriteLine(id);
}
プレフィックス付きXMLでも、LocalNameを使えばプレフィックスを除いた要素名を取得できます。
XML<p:product xmlns:p="http://example.com/products" id="1" />
この場合、reader.Nameはp:product、reader.LocalNameはproductになります。
名前空間があるXMLでは、Nameだけで判定するより、LocalNameとNamespaceURIを組み合わせる方が安全です。
12-4. XmlReaderでCDATAを取得するには?
CDATAは、XmlNodeType.CDATAとして読み取れます。
次のようなXMLを考えます。
XML<description><![CDATA[<b>Apple</b> is fruit.]]></description>
CDATAを取得するには、NodeTypeを確認します。
C#using XmlReader reader = XmlReader.Create("sample.xml");
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.CDATA)
{
Console.WriteLine(reader.Value);
}
}
ReadElementContentAsString()を使って、要素内のテキストとして取得できる場合もあります。
C#if (reader.NodeType == XmlNodeType.Element && reader.Name == "description")
{
string description = reader.ReadElementContentAsString();
Console.WriteLine(description);
}
CDATAを特別に区別したい場合はXmlNodeType.CDATAを使い、通常のテキストとして扱えればよい場合はReadElementContentAsString()を使うとよいでしょう。
12-5. XmlReaderは.NET Coreや.NET 6以降でも使えますか?
はい、XmlReaderは.NET Frameworkだけでなく、.NET Coreや.NET 6以降でも使用できます。
基本的な使い方は大きく変わりません。
C#using System.Xml;
using XmlReader reader = XmlReader.Create("products.xml");
while (reader.Read())
{
if (reader.NodeType == XmlNodeType.Element)
{
Console.WriteLine(reader.Name);
}
}
コンソールアプリ、Webアプリ、バッチ処理など、さまざまな種類のC#アプリケーションで利用できます。
.NET 6以降のプロジェクトでも、System.Xml名前空間を使えばXmlReaderを利用できます。
まとめ
C#のXmlReaderは、XMLを高速かつ低メモリで読み込むための便利なAPIです。
XmlReaderは前方専用・読み取り専用のストリーミング型リーダーであり、XMLを1ノードずつ順番に処理します。XML全体をメモリに展開しないため、大容量XMLファイルの読み込みに特に向いています。
基本的な使い方は、XmlReader.Create()でXMLを開き、Read()メソッドでノードを読み進める形です。現在のノード種別はNodeType、要素名はName、値はValue、階層の深さはDepthで取得できます。
要素内のテキストを取得する場合は、ReadElementContentAsString()が便利です。属性値を取得する場合は、GetAttribute()を使います。複数の属性を順番に処理したい場合は、MoveToNextAttribute()を使用します。
大容量XMLを扱う場合は、読み込んだデータをすべてListに保持するのではなく、1件ずつ処理する設計が重要です。これにより、メモリ使用量を抑えながら安定した処理を実現できます。
また、XmlReaderSettingsを使えば、空白ノードやコメントの無視、DTD処理の制御、非同期読み込みの有効化などを設定できます。外部から受け取るXMLを扱う場合は、例外処理やDTD設定も含めて安全に実装しましょう。
小さなXMLを簡単に検索したい場合はXDocument、DOM操作が必要な場合はXmlDocument、大容量XMLを効率よく読み込みたい場合はXmlReaderを選ぶのがおすすめです。
C#でXML読み込みを実装する際は、XMLのサイズ、処理内容、必要な操作に応じて適切な方法を選びましょう。

