異なるスキーマのXMLを1つのCSVに統合する方法|C#での設計・実装とExcel活用

XMLファイルが33個あり、しかもファイルごとにタグ構造や項目名が違う――この状態で「一括でCSV化」はほぼ失敗します。鍵は、共通の列(共通フォーマット)を決め、各XMLをそこへマッピングして正規化すること。本記事では設計の考え方からC#実装例、欠損値・型変換の注意点、Excel/Power Queryでの代替案までまとめます。

目次

なぜ「XML→CSVの一括変換」がうまくいかないのか

XMLは「タグでデータを表現する」という点では共通ですが、実務で受け取るXMLはベンダーやシステムごとに設計が違います。とくに、次の差分が混ざると単純な変換ツールでは同じCSVヘッダーに揃えられません。

  • 同じ意味でも項目名が違う(例:UserID / RecordID / CustomerId)
  • 階層が違う(同じ値が別階層にある、親子関係が逆、属性で持つ/要素で持つ)
  • 繰り返し要素(明細行)がある/ない(1ファイル=1レコードとは限らない)
  • 名前空間(namespace)が付いていて、単純なElement検索で取れない
  • 日付・数値・通貨などの表現がバラバラ(YYYY/MM/DD、ISO 8601、カンマ小数など)

結論として、スキーマがバラバラなXMLを「一発で綺麗なCSVにする」ワンステップの解決策はほぼありません。代わりに、共通フォーマットを決めて正規化する設計にすると、多少の個別対応があっても、再現性のある統合処理にできます。

まず決めるべきはCSVの「共通フォーマット」:列設計のコツ

最初にやるべきは、CSVの列(ヘッダー)を決めることです。ポイントは「XMLに列を合わせる」のではなく、業務で使う意味(セマンティクス)に合わせて列を設計すること。全XMLに共通する項目だけに絞ると安定しますが、後工程で必要な項目が欠けることもあるため、次の3層で考えるのが現実的です。

層内容例メリット注意点
必須列必ず出したい・欠けると困るID、日付、取引種別分析・突合が安定欠損時の扱いを必ず決める
推奨列あれば便利、なくても処理は止めない氏名、部署、金額、通貨利用価値が高いスキーマ差が出やすい
拡張列ソース固有だが残したいVendorCode、CustomField*後から要求が来ても対応しやすい列が膨らみやすい

33個のXMLを「とにかく全部列にする(全項目ユニオン)」は、後で扱いにくいCSVになりがちです。まずは必須列・推奨列を固め、拡張列は「必要になったら足す」ほうが運用しやすいです。

列名の揺れを吸収する:項目名マッピングの作り方

スキーマ差でよくあるのが「同じ意味だが名前が違う」です。ここはif文を増やすより、意味→候補名の対応表(マッピング)として管理すると保守が楽になります。

CSVの共通列意味XML側で出てきがちな項目名(例)
IDレコードの識別子UserID / RecordID / CustomerId / Id
Name人名・顧客名などName / FullName / CustomerName / Person
Date発生日・登録日Date / RegistDate / CreatedAt / Timestamp
Amount金額Amount / Price / Total / Charge
Currency通貨Currency / Curr / CurrencyCode

注意点は、名前が似ていても意味が違う場合があることです。最初の数件は目視で突合し、「このスキーマではIDはここ」まで決めてから自動化すると事故が減ります。

欠損項目・複数値・不正値をどう扱うかを先に決める

統合CSVは「後工程が読みやすい」ことが大切です。そのため、実装に入る前に欠損や例外の扱いをルール化しておきます。おすすめは“止める/埋める/退避する”の3択で整理することです。

状況推奨対応CSVへの出力ログ・別出力
必須列が欠損処理は継続しつつ、後で調査できる形にする空欄または特定値(例:N/A)エラー一覧CSVに記録(SourceFileと行キー必須)
推奨列が欠損空欄でOK空欄必要に応じて警告
複数値(繰り返し要素)要件に合わせて「行を増やす」か「1行に畳む」明細は複数行、メモは結合など変換ルールをドキュメント化
型変換できない元文字列を残して、正規化列は空欄にする数値・日付列は空欄Raw列またはエラー一覧に元値を保存

現場では「C#で一旦統合して、最後はExcelで微修正」という運用も珍しくありません。ただし、その手作業の量は、ここでルールを固めておくほど減らせます。

スキーマが違うなら、変換ロジックも分ける:設計の現実解

「全部同じコードで読む」発想を捨て、スキーマ単位で読み取り・正規化を用意します。おすすめの手順は次の通りです。

  1. 33個のXMLを眺め、ルート要素・名前空間・主要タグでグルーピングする
  2. グループ(=スキーマ)ごとに「取りたい意味(ID/日付/金額…)」がどこにあるか決める
  3. スキーマごとにコンバーター(変換クラス)を作り、共通モデルへ変換する
  4. 共通モデルの一覧をCSVへ書き出す

33個すべてが完全に別物とは限らず、実際には「似たXMLが数種類ある」ケースが多いです。最初のグルーピングを丁寧にやるほど、以降の実装が速くなります。

C#で実装する場合の定番パターン

ここでは「スキーマごとに抽出 → 共通モデルへ正規化 → CSVへ出力」という流れを、保守しやすい形で組み立てます。サンプルは概念を示すための骨格なので、実際のタグ名や階層に合わせて調整してください。

共通モデル(CSVの1行)をクラスで表す

後で原因調査できるように、SourceFile(元ファイル名)やSchema(スキーマ種別)を持たせるのがポイントです。さらに、ソース固有の値を残したい場合は拡張領域(Extra)を用意します。

public sealed class CommonRecord
{
    public string Id { get; set; } = "";
    public string Name { get; set; } = "";
    public DateTime? Date { get; set; }
    public decimal? Amount { get; set; }
    public string Currency { get; set; } = "";

    // 追跡用(後で原因調査しやすい)
    public string SourceFile { get; set; } = "";
    public string Schema { get; set; } = "";

    // ソース固有の値を残したい場合(拡張列)
    public Dictionary<string, string> Extra { get; } = new();
}

スキーマごとの変換を分離する(Strategyパターン)

スキーマ差をif文で増やし続けると破綻しやすいので、スキーマごとに変換クラスを分けます。判定(CanHandle)と変換(Convert)をセットにすると見通しが良くなります。

using System.Xml.Linq;

public interface IXmlConverter
{
    string SchemaName { get; }
    bool CanHandle(XElement root);
    IEnumerable<CommonRecord> Convert(XDocument doc, string sourceFile);
}

コンバーターの選択とマージは次のようなイメージです。

using System.Globalization;
using System.Text;
using System.Xml.Linq;

var converters = new IXmlConverter[]
{
    new SchemaAConverter(),
    new SchemaBConverter(),
    // ...必要なだけ追加
};

var records = new List<CommonRecord>();

foreach (var file in Directory.EnumerateFiles(inputDir, "*.xml"))
{
    XDocument doc;
    try
    {
        doc = XDocument.Load(file);
    }
    catch (Exception ex)
    {
        Log($"Parse failed: {Path.GetFileName(file)} - {ex.Message}");
        continue;
    }

    var root = doc.Root;
    if (root is null)
    {
        Log($"No root: {Path.GetFileName(file)}");
        continue;
    }

    var conv = converters.FirstOrDefault(c => c.CanHandle(root));
    if (conv is null)
    {
        Log($"Unknown schema: {Path.GetFileName(file)} / root={root.Name}");
        continue;
    }

    records.AddRange(conv.Convert(doc, Path.GetFileName(file)));
}

スキーマAの例:要素名が素直なケース

要素の階層がシンプルな場合はLINQ to XML(XDocument)で十分です。

using System.Globalization;
using System.Xml.Linq;

public sealed class SchemaAConverter : IXmlConverter
{
    public string SchemaName => "SchemaA";

    public bool CanHandle(XElement root)
        => root.Name.LocalName == "Users";

    public IEnumerable<CommonRecord> Convert(XDocument doc, string sourceFile)
    {
        var root = doc.Root!;

        foreach (var user in root.Elements().Where(e => e.Name.LocalName == "User"))
        {
            yield return new CommonRecord
            {
                Id = (string?)user.Element("UserID") ?? "",
                Name = (string?)user.Element("Name") ?? "",
                Date = TryParseDate((string?)user.Element("CreatedAt")),
                Amount = TryParseDecimal((string?)user.Element("Amount")),
                Currency = (string?)user.Element("Currency") ?? "",
                SourceFile = sourceFile,
                Schema = SchemaName
            };
        }
    }

    private static DateTime? TryParseDate(string? s)
    {
        if (string.IsNullOrWhiteSpace(s)) return null;
        return DateTime.TryParse(s, out var dt) ? dt : null;
    }

    private static decimal? TryParseDecimal(string? s)
    {
        if (string.IsNullOrWhiteSpace(s)) return null;

        // まずはInvariantCultureで試す(小数点が.のケースに強い)
        if (decimal.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var d))
            return d;

        // 次にCurrentCulture(カンマ小数など)を試す
        if (decimal.TryParse(s, NumberStyles.Any, CultureInfo.CurrentCulture, out d))
            return d;

        return null;
    }
}

名前空間(namespace)があるXMLの読み方

ハマりやすいのが名前空間付きXMLです。要素名が同じでも、名前空間が違うとElement("UserID")では取れません。基本は、XMLのルートから名前空間を取得してXNamespaceで結合します。

var root = doc.Root!;
XNamespace ns = root.Name.Namespace;

var id = (string?)root.Element(ns + "UserID");

「同じスキーマでも名前空間が揺れる」ような場合は、性能と安全性のバランスを見て、LocalNameで探索する方法もあります(ただし大きいXMLでは遅くなりやすい)。

var id = doc.Descendants()
            .FirstOrDefault(e => e.Name.LocalName == "UserID")
            ?.Value;

明細(繰り返し要素)をCSVに落とす設計:1行を何にするか

XMLには「ヘッダー情報+明細の繰り返し」という構造が多くあります。この場合、CSVの1行を「明細1行」にすると扱いやすいことが多いです。ヘッダー項目は明細行に繰り返して持たせ、明細番号(LineNo)を追加します。

設計CSVの1行向いている用途注意点
明細行ベース注文(ヘッダー)+商品(明細)集計、BI、突合ヘッダーが繰り返しで冗長になる
ヘッダーベース注文1件=1行(明細は結合)一覧表、簡易閲覧明細が長いと列/セルが壊れやすい

「何を1行にするか」が決まると、コンバーターの実装も迷いにくくなります。要件が曖昧なときは、まず明細行ベースで作り、必要ならヘッダーベースを別CSVとして追加するのが安全です。

CSV出力でハマりやすいポイント

  • 文字コード:Excelで開く前提ならUTF-8(BOMあり)を検討。文字化け対策になります。
  • 改行・カンマ・ダブルクォート:フィールドに改行やカンマが入るとCSVが崩れます。手書き実装よりライブラリ利用が安全です。
  • 日付・数値のフォーマット:CSVは文字列なので、後工程で安定する表記に統一します(例:日付はyyyy-MM-dd、日時はISO 8601)。
  • 列順:列を動的に増やす場合でも、列順を固定すると差分比較や再処理が楽になります。

C#なら、CSV周りはCsvHelperなどの実績あるライブラリを使うと安心です。固定列だけ出すならWriteRecordsで簡単ですが、拡張列(Extra)も出したい場合はヘッダーを自前で組むのが分かりやすいです。

using CsvHelper;
using System.Globalization;
using System.Text;

var baseHeader = new[] { "ID", "Name", "Date", "Amount", "Currency", "SourceFile", "Schema" };
var extraKeys = records.SelectMany(r => r.Extra.Keys)
                       .Distinct()
                       .OrderBy(k => k)
                       .ToList();

using var writer = new StreamWriter(outputPath, false, new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
using var csv = new CsvWriter(writer, CultureInfo.InvariantCulture);

// ヘッダー
foreach (var h in baseHeader) csv.WriteField(h);
foreach (var k in extraKeys) csv.WriteField(k);
csv.NextRecord();

// 本文
foreach (var r in records)
{
    csv.WriteField(r.Id);
    csv.WriteField(r.Name);
    csv.WriteField(r.Date?.ToString("yyyy-MM-dd") ?? "");
    csv.WriteField(r.Amount?.ToString(CultureInfo.InvariantCulture) ?? "");
    csv.WriteField(r.Currency);
    csv.WriteField(r.SourceFile);
    csv.WriteField(r.Schema);

    foreach (var k in extraKeys)
        csv.WriteField(r.Extra.TryGetValue(k, out var v) ? v : "");

    csv.NextRecord();
}

大きなXMLでも落ちないようにする:XmlReaderでストリーミング

33ファイルでも、1ファイルが大きいとXDocument.Loadでメモリが厳しくなることがあります。その場合はXmlReaderでストリーミング処理にすると、読みながらCSVへ書けます(「全レコードをListに溜めない」構成にできるのが強みです)。

using System.Xml;

using var reader = XmlReader.Create(file);

while (reader.Read())
{
    // 例:<Item> 開始を見つけたら、その要素だけ読み込む
    if (reader.NodeType == XmlNodeType.Element && reader.Name == "Item")
    {
        var item = XElement.ReadFrom(reader) as XElement;
        if (item is null) continue;

        var record = new CommonRecord
        {
            Id = (string?)item.Element("RecordID") ?? "",
            // ...必要な項目を抽出
        };

        // ここで即CSVに書き出す(recordsに溜めない)
    }
}

ストリーミングは実装難度が少し上がりますが、「落ちない」「速い」「途中失敗でも再開しやすい」メリットがあります。運用で何度も回す処理なら、早めに検討する価値があります。

Excel / Power Queryでやる場合の現実的な落としどころ

プログラムを書かずに済ませたい場合、ExcelのPower Query(取得と変換)でXMLを読み込んで整形し、クエリを結合(Append)して1つのテーブルにする方法もあります。ただし、スキーマ差が大きいと結局は「スキーマごとにクエリを作る」ことになり、運用負荷が増えます。

やり方向いているケース弱いケースポイント
Excel / Power Queryスキーマ差が小さい、ファイル数が少ない、まず試したい階層が深い、明細が複雑、名前空間が混在、繰り返し運用最終的に「共通列だけ」に揃えてから結合すると安定
C#(専用変換)スキーマ差が大きい、定期的に回す、ログや例外処理が必要初期実装の時間が取れないコンバーター分離+共通モデルで保守しやすくする

時間がない場合は「Power Queryで叩き台を作り、足りない部分はC#で埋める」「C#で統合し、最後の微修正だけExcel」など、併用が最も現実的です。

運用を楽にする:マッピングを設定ファイル化する

スキーマが増えたり、項目名が変わったりする環境では、マッピングをコードに埋め込むと改修が大変です。そこで、マッピングをJSONなどの設定に寄せると変更に強くなります。階層が深い場合はXPathで指定すると管理しやすいです。

{
  "SchemaA": {
    "root": "Users",
    "recordPath": "/Users/User",
    "fields": {
      "ID": "UserID",
      "Name": "Name",
      "Date": "CreatedAt",
      "Amount": "Amount",
      "Currency": "Currency"
    }
  },
  "SchemaB": {
    "root": "Records",
    "recordPath": "/Records/Record",
    "fields": {
      "ID": "RecordID",
      "Name": "CustomerName",
      "Date": "Timestamp",
      "Amount": "Total"
    }
  }
}

もちろん、すべてを設定化できるわけではありません(複雑な条件分岐、明細展開、値の計算などはコードが必要です)。それでも、「項目の場所」だけでも設定に逃がすと、保守コストが大きく下がります。

品質を担保するためのチェックリスト

  • スキーマ判定が誤判定しないか(root名だけでなくnamespaceや必須要素も見る)
  • 必須列の欠損がどれくらい発生しているか(変換後に集計して把握)
  • 日付・数値の正規化ルールが決まっているか(出力フォーマットを固定)
  • 明細の扱い(行を増やす/畳む)が要件に合っているか
  • 例外時のログ(SourceFile、スキーマ名、キー、エラー内容)が残るか
  • 再実行のしやすさ(途中失敗時にどこまで出たか分かる、追記か作り直しかを決める)

まとめ:ワンステップ解はないが、設計すれば再現性は作れる

異なるスキーマのXMLを1つのCSVに統合する本質は、「共通フォーマットを定義し、各XMLをそこへマッピングして正規化する」ことです。スキーマがバラバラな以上、多少の個別対応は避けられません。しかし、スキーマ判定と変換ロジックを分離し、欠損や型変換のルールを先に決めておけば、手作業を最小化しながら安定運用できます。最初は小さく作り、必要な列とスキーマを順に増やしていくのが最短ルートです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次