2種類のフォーマットを含むCSVファイルの読み込み方

（※2020/5/07時点の CSVHelper 14.x では動作していましたが、2021年度2月時点の 23.x では仕様が変わり手直しが必要です）

# はじめに

　1つの CSV ファイルには、1種類のフォーマットが一般的だと思います。データの途中でデータのフォーマットが変わってしまうと、読み込み処理も書き込み処理も煩雑になるためです。しかし、たまに複数のフォーマットを含む CSV ファイルがあり、どうしても読み込んで処理しなければならない場合があります。（汎用機からのデータで多いような気がします）

　CsvHelper は、1つのCSVファイルに2つ以上のレコードフォーマットが定義されている場合にも対応しています。読み込み時の csv.Configuration.RegisterClassMap は、複数定義することができ、読み込んだヘッダの項目名を判別させることで、異なるフォーマットの読み込みを実現させます。

1種類のフォーマットの場合の CSV ファイルの読み込み方と、2種類のフォーマットが含まれる CSV ファイルの読み込み方を、サンプルで見比べてみます。

# 1種類のフォーマットの場合

　下のサンプルでは、test.csv ファイルに入っているデータを、TestDataMap が指定する順番に従って TestData クラスにマッピングし、records に登録していきます。

```c#
using System;
using System.Globalization;
using System.IO;
using System.Text;
using CsvHelper;
using CsvHelper.Configuration;

namespace MyProject
{
  class Program
  {
    static void Main(string[] args)
    {
      using (var sr = new StreamReader("test.csv", Encoding.GetEncoding("SHIFT_JIS")))
      {
        using (var csv = new CsvReader(sr, CultureInfo.InvariantCulture))
        {
          csv.Configuration.RegisterClassMap<TestDataMap>();
          var records = csv.GetRecords<TestData>();

          foreach(var l in records)
          {
            Console.WriteLine(l.Name, l.Param1);
          }
        }
      }
    }
  }
  //データクラス
  public class TestData
  {
    public string Name { get; set; }
    public string Param1 { get; set; }
  }
  //マップクラス 
  class TestDataMap : ClassMap<TestData>
  { 
    public TestDataMap()
    {
      Map(x => x.Name).Name("Name");
      Map(x => x.Param1);
    }
  }
}
```

見慣れてしまうと、難しいところは特にありません。

# 2種類のフォーマットが混在する場合

　2種類のフォーマットの混在の仕方に特に共通にルールがあるわけではないので、CsvHelper を使ったとしても、仕様に従ってある程度の実装が必要です。

とりあえずの例として、次のようなフォーマットだったとします。

```plaintext
Id,Name
1,A社
2,B社

Name,Param1,Param2
A社,2019,20
A社,2020,19
B社,2019,10
```

1つめのフォーマットは、Id、Name の2つの項目です。2つめのフォーマットは、Name、Param1、Param2 の3つの項目によるフォーマットで、1つ目の Name 項目に関するパラメータを扱っています。
なお、フォーマット間は空行で区分けされているとします。ただし、空行は1行のみとします。

```c#
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using CsvHelper;
using CsvHelper.Configuration;

namespace MyProject
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var reader = new StreamReader("test2.csv", Encoding.GetEncoding("SHIFT_JIS")))
            {
                using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
                {
                    csv.Configuration.IgnoreBlankLines = false;
                    csv.Configuration.RegisterClassMap<TestData1Map>();
                    csv.Configuration.RegisterClassMap<TestData2Map>();
                    var TestData1Records = new List<TestData1>();
                    var TestData2Records = new List<TestData2>();
                    var isHeader = true;
                    while (csv.Read())
                    {
                        if (isHeader)
                        {
                            csv.ReadHeader();
                            isHeader = false;
                            continue;
                        }

                        if (string.IsNullOrEmpty(csv.GetField(0)))
                        {
                            isHeader = true;
                            continue;
                        }

                        switch (csv.Context.HeaderRecord[0])
                        {
                            case "Id":
                               TestData1Records.Add(csv.GetRecord<TestData1>());
                               break;
                            case "Name":
                               TestData2Records.Add(csv.GetRecord<TestData2>());
                               break;
                            default:
                               throw new InvalidOperationException("Unknown record type.");
                        }
                    }
                    foreach (var l in TestData1Records)
                    {
                        Console.WriteLine($"Id = {l.Id}, {l.Name}");
                        foreach (var m in TestData2Records)
                        {
                            if (l.Name == m.Name)
                            {
                                Console.WriteLine($"Param1 = {m.Param1}, Param2 = {m.Param2}");
                            }
                        }
                    }
                }
            }
        }
        public class TestData1
        {
            public int Id { get; set; }
            public string Name { get; set; }
        }

        public class TestData2
        {
            public string Name { get; set; }
            public string Param1 { get; set; }
            public string Param2 { get; set; }
        }

        public sealed class TestData1Map : ClassMap<TestData1>
        {
            public TestData1Map()
            {
                Map(m => m.Id).Name("Id");
                Map(m => m.Name);
            }
        }

        public sealed class TestData2Map : ClassMap<TestData2>
        {
            public TestData2Map()
            {
                Map(m => m.Name).Name("Name");
                Map(m => m.Param1);
                Map(m => m.Param2);
            }
        }
    }
}
```

簡単に解説します。

IgnoreBlankLines = false とすることで、空行の読み飛ばしを無効にします。これでフォーマットの仕切りを認識できるようになります。

```c#
csv.Configuration.IgnoreBlankLines = false;
```

RegisterClassMap に2種類のマッピングクラスを登録します。

```c#
csv.Configuration.RegisterClassMap<TestData1Map>();
csv.Configuration.RegisterClassMap<TestData2Map>();
```

それぞれのフォーマットの保存先を用意します。

```c#
var TestData1Records = new List<TestData1>();
var TestData2Records = new List<TestData2>();
```

ヘッダ行かどうかを判断するフラグを定義します。1行目はヘッダでしょうから、初期値は true です。

```c#
var isHeader = true;
```

空行を判断した場合の処理です。空行の次はヘッダですので、isHeader を true にして、次のループに移ります。

```c#
if (string.IsNullOrEmpty(csv.GetField(0)))
{
  isHeader = true;
  continue;
}
```

現在のヘッダ情報から、フォーマットを判別して、それぞれの保存先に格納します。想定していないヘッダだと、throw で例外が発生します。

```c#
switch (csv.Context.HeaderRecord[0])
{
  case "Id":
    TestData1Records.Add(csv.GetRecord<TestData1>());
    break;
  case "Name":
    TestData2Records.Add(csv.GetRecord<TestData2>());
    break;
  default:
    throw new InvalidOperationException("Unknown record type.");
}
```

読み込んだデータを出力します。1つめのフォーマットで定義された Name を使って、2つ目のフォーマットで定義される Param1 と Param2 を表示しています。

```c#
foreach (var l in TestData1Records)
{
  Console.WriteLine($"Id = {l.Id}, {l.Name}");
  foreach (var m in TestData2Records)
  {
    if (l.Name == m.Name)
    {
      Console.WriteLine($"Param1 = {m.Param1}, Param2 = {m.Param2}");
    }
  }
}
```

出力結果は次のようになります。

```Plantext:出力結果
Id = 1, A社
Param1 = 2019, Param2 = 20
Param1 = 2020, Param2 = 19
Id = 2, B社
Param1 = 2019, Param2 = 10
```