本文のコードは CsvHelper 15.0.5 に基づいています
はじめに
CsvHelper は CSV ファイルを読み書きするための .NET ライブラリです。非常に高速で柔軟性が高く、使いやすいです。
CsvHelper は .NET Standard 2.0 上に構築されており、ほぼどこでも実行できます。
GitHub アドレス: https://github.com/joshclose/csvhelper
モジュール
| モジュール | 機能 |
|---|---|
| CsvHelper | CSV データを読み書きするコアクラス。 |
| CsvHelper.Configuration | CsvHelper の読み書き動作を設定するクラス。 |
| CsvHelper.Configuration.Attributes | CsvHelper の属性を設定するクラス。 |
| CsvHelper.Expressions | LINQ 式を生成するクラス。 |
| CsvHelper.TypeConversion | CSV フィールドと .NET 型を相互変換するクラス。 |
読み取り
テストクラス
public class Foo
{
public int ID { get; set; }
public string Name { get; set; }
}
csv ファイルのデータ
ID,Name
1,Tom
2,Jerry
すべてのレコードを読み取る
using (var reader = new StreamReader("foo.csv"))
{
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
var records = csv.GetRecords<Foo>();
}
}
csv ファイルの読み取り時、空行は無視されますが、空行にスペースが含まれているとエラーになります。 Excel で編集された CSV ファイルの場合、空行は区切り文字
,のみの行になり、同様にエラーになります。
1行ずつ読み取る
using (var reader = new StreamReader("foo.csv"))
{
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
while (csv.Read())
{
var record = csv.GetRecord<Foo>();
}
}
}
GetRecords<T>メソッドはyieldを使用してIEnumerable<T>を返し、ToListまたはToArrayを呼び出さない限りすべての内容を一度にメモリに読み込むことはありません。そのため、このような1行ずつ読み取る書き方はあまり必要ありません。
単一フィールドの読み取り
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
csv.Read();
csv.ReadHeader();
while (csv.Read())
{
var id = csv.GetField<int>(0);
var name = csv.GetField<string>("Name");
}
}
行ごとに読み取る場合、ヘッダー行を無視することもできますが、ここではできません。
csv.Read(); はヘッダーを読み取るためのものです。これがないと、while ループの最初でヘッダーを取得してしまい、エラーになります。
csv.ReadHeader(); はヘッダーを設定するためのものです。これがないと、csv.GetField<string>("Name") でヘッダーが見つからずエラーになります。
TryGetField を使用すると、予期しないエラーを防げます。
csv.TryGetField(0, out int id);
書き込み
すべてのレコードを書き込む
var records = new List<Foo>
{
new Foo { ID = 1, Name = "Tom" },
new Foo { ID = 2, Name = "Jerry" },
};
using (var writer = new StreamWriter("foo.csv"))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(records);
}
}
1行ずつ書き込む
using (var writer = new StreamWriter("foo.csv"))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
foreach (var record in records)
{
csv.WriteRecord(record);
}
}
}
フィールド単位で書き込む
using (var writer = new StreamWriter("foo.csv"))
{
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteHeader<Foo>();
csv.NextRecord();
foreach (var record in records)
{
csv.WriteField(record.ID);
csv.WriteField(record.Name);
csv.NextRecord();
}
}
}
属性
Index
Index 属性はフィールドの順序を指定します。
ファイルを読み取る際、ヘッダーがない場合は順序でしかフィールドを特定できません。
public class Foo
{
[Index(0)]
public int ID { get; set; }
[Index(1)]
public string Name { get; set; }
}
using (var reader = new StreamReader("foo.csv"))
{
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
csv.Configuration.HasHeaderRecord = false;
var records = csv.GetRecords<Foo>().ToList();
}
}
csv.Configuration.HasHeaderRecord = falseはCsvReaderにヘッダーがないことを伝えます。この行は必須で、追加しないとデフォルトで1行目をヘッダーとしてスキップし、結果が1行少なくなります。データ量が多いとこのバグを見つけるのが難しくなります。
ファイルに書き込む際も Index の順序で書き込まれます。ヘッダーを書き込みたくない場合も csv.Configuration.HasHeaderRecord = false; を追加する必要があります。
Name
フィールド名と列名が異なる場合、Name 属性を使用できます。
public class Foo
{
[Name("id")]
public int ID { get; set; }
[Name("name")]
public string Name { get; set; }
}
NameIndex
NameIndex は CSV ファイル内の同名列を処理するために使用します。
public class Foo
{
...
[Name("Name")]
[NameIndex(0)]
public string FirstName { get; set; }
[Name("Name")]
[NameIndex(1)]
public string LastName { get; set; }
}
Ignore
フィールドを無視します。
Optional
読み取り時に一致するフィールドが見つからない場合、無視します。
public class Foo
{
...
[Optional]
public string Remarks { get; set; }
}
Default
読み取るフィールドが空の場合、Default 属性でデフォルト値を指定できます。
Default 属性は読み取り時のみ有効で、書き込み時に空の値をデフォルト値に置き換えて書き込むことはありません。
NullValues
public class Foo
{
...
[NullValues("None", "none", "Null", "null")]
public string None { get; set; }
}
ファイルを読み取る際、CSV ファイル内の特定のフィールドの値が空の場合、読み取り後の値は "" であり null ではありません。NullValues 属性を指定すると、CSV ファイル内の特定のフィールドの値が NullValues で指定された値の場合、読み取り後に null になります。
Default 属性も同時に指定されている場合、この属性は無効になります。
厄介なのは、ファイル書き込み時にはこの属性が機能しないことです。そのため、読み取りと書き込みの一貫性が失われます。
Constant
Constant 属性はフィールドに定数を指定します。読み取り時も書き込み時もこの値が使用され、他のマッピングや設定は無視されます。
Format
Format は型変換時に使用する文字列形式を指定します。
たとえば、数値や日付型の場合に形式を指定することがよくあります。
public class Foo
{
...
[Format("0.00")]
public decimal Amount { get; set; }
[Format("yyyy-MM-dd HH:mm:ss")]
public DateTime JoinTime { get; set; }
}
BooleanTrueValues と BooleanFalseValues
これらの2つの属性は、bool 型を指定した形式に変換して表示するために使用します。
public class Foo
{
...
[BooleanTrueValues("yes")]
[BooleanFalseValues("no")]
public bool Vip { get; set; }
}
NumberStyles
public class Foo
{
...
[Format("X2")]
[NumberStyles(NumberStyles.HexNumber)]
public int Data { get; set; }
}
特に便利なのは NumberStyles.HexNumber と NumberStyles.AllowHexSpecifier です。これら2つの列挙体の動作はほぼ同じです。この属性は読み取り時のみ有効で、書き込み時に16進数に変換して書き込むことはありません。そのため読み取りと書き込みの一貫性が失われるため、Format 属性で書き込み形式を指定してください。
マッピング
マッピング対象のクラスに属性を付与できない場合、ClassMap を使用したマッピングが可能です。
マッピングを使用しても属性を使用しても効果は同じで、困った点も同じです。以下の例では、プロパティを使用して上記の属性の機能を実現しています。
public class Foo2
{
public int ID { get; set; }
public string Name { get; set; }
public decimal Amount { get; set; }
public DateTime JoinTime { get; set; }
public string Msg { get; set; }
public string Msg2 { get; set; }
public bool Vip { get; set; }
public string Remarks { get; set; }
public string None { get; set; }
public int Data { get; set; }
}
public class Foo2Map : ClassMap<Foo2>
{
public Foo2Map()
{
Map(m => m.ID).Index(0).Name("id");
Map(m => m.Name).Index(1).Name("name");
Map(m => m.Amount).TypeConverterOption.Format("0.00");
Map(m => m.JoinTime).TypeConverterOption.Format("yyyy-MM-dd HH:mm:ss");
Map(m => m.Msg).Default("Hello");
Map(m => m.Msg2).Ignore();
Map(m => m.Vip)
.TypeConverterOption.BooleanValues(true, true, new string[] { "yes" })
.TypeConverterOption.BooleanValues(false, true, new string[] { "no" });
Map(m => m.Remarks).Optional();
Map(m => m.None).TypeConverterOption.NullValues("None", "none", "Null", "null");
Map(m => m.Data)
.TypeConverterOption.NumberStyles(NumberStyles.HexNumber)
.TypeConverterOption.Format("X2");
}
}
マッピングを使用する前に、登録が必要です。
csv.Configuration.RegisterClassMap<Foo2Map>();
ConvertUsing
ConvertUsing を使用すると、デリゲートメソッドで型変換を実装できます。
// 定数
Map(m => m.Constant).ConvertUsing(row => 3);
// 2つの列を結合する
Map(m => m.Name).ConvertUsing(row => $"{row.GetField<string>("FirstName")} {row.GetField<string>("LastName")}");
Map(m => m.Names).ConvertUsing(row => new List<string> { row.GetField<string>("Name") } );
設定
Delimiter
区切り文字
csv.Configuration.Delimiter = ",";
HasHeaderRecord
この設定は前述しました。最初の行をヘッダーとするかどうかです。
csv.Configuration.HasHeaderRecord = false;
IgnoreBlankLines
空行を無視するかどうか。デフォルトは true です。
csv.Configuration.IgnoreBlankLines = false;
スペースのみまたは , のみの行は無視できません。
AllowComments
コメントを許可するかどうか。コメントは # で始まります。
csv.Configuration.AllowComments = true;
Comment
コメントアウトされた行を示す文字を取得または設定します。デフォルトは # です。
csv.Configuration.Comment = '/';
BadDataFound
不正なデータが見つかったときにトリガーされる関数を設定します。ログ記録などに使用できます。
IgnoreQuotes
解析時に引用符を無視し、他の文字と同様に扱うかどうかを取得または設定します。
デフォルトは false です。文字列内に引用符がある場合、3つの " が連続している場合のみ、読み取った文字列に1つの " が含まれます。1つの場合は無視され、2つの場合はエラーになります。
true の場合、" はそのまま文字列として返されます。
csv.Configuration.IgnoreQuotes = true;
CsvWriter にはこのプロパティはありません。文字列に " が含まれていると、3つの " が連続して出力されます。
TrimOptions
フィールドの前後の空白を削除します。
csv.Configuration.TrimOptions = TrimOptions.Trim;
PrepareHeaderForMatch
PrepareHeaderForMatch は、プロパティ名とヘッダーを照合するための関数を定義します。ヘッダーとプロパティ名の両方がこの関数を通過します。この機能は、ヘッダーからスペースを削除したり、ヘッダーとプロパティ名の大文字小文字が一致しない場合に統一してから比較するために使用できます。
csv.Configuration.PrepareHeaderForMatch = (string header, int index) => header.ToLower();