経緯
先日、CSVをC#プログラムから出力する必要が出てきまして、さて書くかあ……と思ったのですが世の中にはCSVを扱うよいライブラリが沢山あるはず! それを利用しない手はないですよね! ということで利用することにしましたCSVHelper。
本家
https://joshclose.github.io/CsvHelper/
概要
細かいコードは最後のコードサンプルをご参照。
ライブラリの概要
CSVHelperはクラス(or 構造体)とCSVの入出力をマッピングすることができます。
これにより列番号の制御やデータとファイルの変換ロジックの構造を考えることなく、楽々と実装ができます。
簡単な実装ではデータを格納するクラスのプロパティに属性を付与し、それをCSVHelperに喰わせるだけで動きます。
サンプルコード
クラス
using CsvHelper.Configuration.Attributes;
public class ClassData
{
[Index(0), Name("名前")]
public string Name { get; set; } = String.Empty;
[Index(1), Name("年齢")]
public int Age { get; set; } = 0;
}
出力部分
using CsvHelper.Configuration;
using System.Globalization;
using System.Text;
//リスト作成
List<ClassData> list = new();
list.Add(new ClassData() { Name = "User3-1", Age = 10});
list.Add(new ClassData() { Name = "User3-2", Age = 20});
//出力
string fileName = $"_{DateTime.Now.ToString("yyyyMMddHHmmss")}.csv";
using var sw = new StreamWriter(fileName, true, Encoding.UTF8);
var ci = new CultureInfo("ja-JP", false);
using var csv = new CsvHelper.CsvWriter(sw, ci);
try
{
csv.WriteRecords(list);
}
catch (Exception)
{
throw;
}
実際の使用感や簡単な解説
入出力がCSVしかないプログラムや、ごく小規模な構造でしたら上記のサンプルコードのようにクラスに直接属性をつけるのが手っ取り早いです。
ただしクラスに属性をつけて実装してしまうと、いわゆるエンティティクラスが完全にCSVの構造と1対1関係になり、柔軟性が低下します。
※CSVファイルの構造が変わると読めなくなる
この問題に対応するには、マップを作るのがよいと思います。
下記は年齢を空白にして出力する場合のマップクラスです。
internal class WithoutAgeDataClassMap : ClassMap<ClassData>
{
internal WithoutAgeDataClassMap()
{
Map(m => m.Name).Index(0).Name("名前");//そのまま出力
Map(m => m.Age).Convert(a => string.Empty).Index(1).Name("年齢");//空文字に変換して出力
}
}
※Ageはintですが、出力は文字列のためstring.Emptyで吐いています。
作ったマップをRegisterClassMap で登録するだけでマッピングは完了です。
csv.Context.RegisterClassMap<WithoutAgeDataClassMap>();//マップ設定
マップを作ってしまえば変換ロジックをインジェクションできるので、バージョンによってCSVの構造を変えたり、入力と出力でマップを切り替えて制御することも可能となります。
またクラスとファイルアクセスの構造が完全に一体化しているのは設計の観点からも不便なことが多いと考えられますので、基本的にはマップを作る方がよいのではないかな、と思います。
練習コードサンプル
https://github.com/okayamadaiti/CSVHelperSample
こちらには主に下記を格納しています。
- クラスに属性を付与するケースでの入出力
- クラスの属性を利用せず、マップを作成して入出力
- Enum型をマップする方法