(※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 に登録していきます。
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 を使ったとしても、仕様に従ってある程度の実装が必要です。
とりあえずの例として、次のようなフォーマットだったとします。
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行のみとします。
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 とすることで、空行の読み飛ばしを無効にします。これでフォーマットの仕切りを認識できるようになります。
csv.Configuration.IgnoreBlankLines = false;
RegisterClassMap に2種類のマッピングクラスを登録します。
csv.Configuration.RegisterClassMap<TestData1Map>();
csv.Configuration.RegisterClassMap<TestData2Map>();
それぞれのフォーマットの保存先を用意します。
var TestData1Records = new List<TestData1>();
var TestData2Records = new List<TestData2>();
ヘッダ行かどうかを判断するフラグを定義します。1行目はヘッダでしょうから、初期値は true です。
var isHeader = true;
空行を判断した場合の処理です。空行の次はヘッダですので、isHeader を true にして、次のループに移ります。
if (string.IsNullOrEmpty(csv.GetField(0)))
{
isHeader = true;
continue;
}
現在のヘッダ情報から、フォーマットを判別して、それぞれの保存先に格納します。想定していないヘッダだと、throw で例外が発生します。
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 を表示しています。
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}");
}
}
}
出力結果は次のようになります。
Id = 1, A社
Param1 = 2019, Param2 = 20
Param1 = 2020, Param2 = 19
Id = 2, B社
Param1 = 2019, Param2 = 10