こんにちは!トミセンです。こんにちは、トミセン(@tomisenblog)です。
C#でCSVの読み込みに便利なCsvHelperですが、
「マッピングしない読み込みってどうやるの?」
「マッピングしない読み込みって、マッピングするよりも難しいんだけど・・・」
やり方が分からないし、情報が少なくて探すだけで疲れてしまいがちです。
また、簡単に出来そうなサンプルを見つけて使ってみても、上手くいかなくて思ったより時間がかかってしまいますよね。
今回はそんなお悩みを解決します。ずばり!
「CsvHelper マッピングしない読み込みの方法」についてご紹介します。
それでは一緒に学んでいきましょう!
現場レベルのCsvHelperの実装サンプルを作りました。
CSVデータの取得から、入力チェック、モデル変換まで網羅しています。
5つのシステムで実証したコピペで使い回せる実装法
» 【CsvHelper マスター講座】を見る
CsvHelper マッピングしない読み込みの方法
ここからはCsvHelperでマッピングしない読み込みをするための具体的な方法について解説していきます。サンプルコードを使って解説していきますので、一緒に学習していきましょう。
方法は2つあります!
実は、CsvHelperでCsvHelperでマッピングしない読み込みをするには、2つの方法があります!
最初は、Dictionaryでデータを取得する方法について詳しく解説します。
念のため、Dictionaryについて簡単に説明します。
Dictionaryは、KeyとValueがセットになった配列で連想配列といいます。インデックス番号の代わりにKeyを指定することで、Valueを取得することができます。JavaでいうとHashMapに相当する機能です。
では、さっそく見ていきましょう。
方法①:Dictionaryで取得する方法
Dictionaryで取得してみよう!
検証したCsvHelperは、バージョン 15.0.0です。
テストデータ
テストデータはヘッダー付きのデータを用意しました。番号1のデータは日付列が空になっています。
番号,名前,日付,金額
1,鈴木,,555
2,佐藤,20201011,666
ソースコード
public class Test
{
public void Main()
{
using (var reader = new StreamReader(@"C:\csv\test.csv", Encoding.GetEncoding("SHIFT_JIS")))
using (var csv = new CsvHelper.CsvReader(reader, new CultureInfo("ja-JP", false)))
{
csv.Configuration.HasHeaderRecord = true; // Headerあり
int row = 0; // テスト用
while (csv.Read())
{
var columns = csv.GetRecord<dynamic>() as IDictionary<string, object>;
Console.WriteLine($"■ {++row}行目"); // テスト用
foreach (var column in columns)
{
Console.WriteLine($"Key:{column.Key} Value:{column.Value}");
}
}
}
}
}
// 【実行結果】
// ■ 1行目
// Key:番号 Value:1
// Key:名前 Value:鈴木
// Key:日付 Value:
// Key:金額 Value:555
// ■ 2行目
// Key:番号 Value:2
// Key:名前 Value:佐藤
// Key:日付 Value:20201011
// Key:金額 Value:666
ソースコードは「Mapping to a Dictionary|stackoverflow」を参考にしています。
ソースコードだけでは分かりにくい部分があるので、いくつかのポイントをコードを見ながら解説していきます!
CSVファイルは複数行あるので、CSVファイルを最初から最後まで読み込む必要があります。それを指示しているのが次の場所になります。
// データが読み込めている間は、繰り返し読み込む
while (csv.Read())
{
}
while (csv.Read())で、CSVファイルのデータが読み込めている間は、繰り返し読み込むという意味になります。これで最後まで読み込むことができます。
さきほどの処理は、「繰り返しの判断」をしているだけなので、実際に1行の文字列データを読み込む必要があります。CsvHelperに用意されているGetRecord()メソッドを使ってデータを取得します。
// 1行の文字列データを IDictionary<string, object>型として読み込む
var columns = csv.GetRecord<dynamic>() as IDictionary<string, object>;
csv.GetRecord<dynamic>() as IDictionary<string, object>;は決まりごとのようなものです。変更する必要はないので、そのまま使ってください。<dynamic>とdynamic型を渡しています。dynamic型は面白いのですが、長くなるので今は無視しておきます。書きたくなったら記事を書くので、その時はどうかお付き合いください。
あとは取得したデータを取り出して一通りの処理が完了ですね。実際にデータを取り出すところを見ていきましょう。次のソースを見てください。
// 取得データの取り出し
foreach (var column in columns)
{
Console.WriteLine($"Key:{column.Key} Value:{column.Value}");
}
Dictionaryはforeachでループ処理することができます。ループしながら、変数名.Key、変数名.Valueで、それぞれKeyとValueを取得することができます。
最後に実行結果はどうなっているでしょうか。実行結果を確認したいと思います。
// 【実行結果】
// ■ 1行目
// Key:番号 Value:1
// Key:名前 Value:鈴木
// Key:日付 Value:
// Key:金額 Value:555
// ■ 2行目
// Key:番号 Value:2
// Key:名前 Value:佐藤
// Key:日付 Value:20201011
// Key:金額 Value:666
csv.Configuration.HasHeaderRecord = true;を指定してあるので、ヘッダー行は読み飛ばされて取得されません。便利ですね。変数名.Keyはヘッダーの値、変数名.Valueはカラムの値がそれぞれセットされます。
ポイント
- ヘッダー行がない場合、KeyはField1・Field2・Field3・・・のように連番になります。
foreachで回して取得する方法だけでなく、Keyで指定して個別にデータを取り出す方法も確認しておきましょう。
int row = 0; // テスト用
while (csv.Read())
{
var columns = csv.GetRecord<dynamic>() as IDictionary<string, object>;
Console.WriteLine($"■ {++row}行目"); // テスト用
Console.WriteLine($"番号 Value:{columns["番号"]}");
Console.WriteLine($"名前 Value:{columns["名前"]}");
Console.WriteLine($"日付 Value:{columns["日付"]}");
Console.WriteLine($"金額 Value:{columns["金額"]}");
}
実行結果です。
// 【実行結果】
// ■ 1行目
// 番号 Value:1
// 名前 Value:鈴木
// 日付 Value:
// 金額 Value:555
// ■ 2行目
// 番号 Value:2
// 名前 Value:佐藤
// 日付 Value:20201011
// 金額 Value:666
問題ないですね、Keyに対するValueが正しく取得できています。
以上で「CsvHelper マッピングしない読み込み」ができました!!
方法②:String[]で取得する方法
String[]で取得してみよう!
もう一つの方法は、String[]でデータを取得する方法です。
Dictionaryで取得する方法でも問題ないと思いますが、foreachを使わずにKeyを指定して個別でデータを取り出す場合は、ヘッダーの変更による影響を受けないString[]の方が比較的簡単に取得できます。
こちらもコードの解説をしていきます。是非マスターしてみてくださいね!
テストデータ
先ほどのと同じデータです。
ソースコード
public class Test
{
public void Main()
{
using (var reader = new StreamReader(@"C:\csv\test.csv", Encoding.GetEncoding("SHIFT_JIS")))
using (var csv = new CsvHelper.CsvReader(reader, new CultureInfo("ja-JP", false)))
{
csv.Configuration.HasHeaderRecord = true; // Headerあり
int row = 0; // テスト用
string[] columns;
while ((columns = csv.Parser.Read()) != null)
{
Console.WriteLine($"■ {++row}行目"); // テスト用
foreach (var column in columns)
{
Console.WriteLine($"Value:{column}");
}
}
}
}
}
// 【実行結果】
// ■ 1行目
// Value:番号
// Value:名前
// Value:日付
// Value:金額
// ■ 2行目
// Value:1
// Value:鈴木
// Value:
// Value:555
// ■ 3行目
// Value:2
// Value:佐藤
// Value:20201011
// Value:666
こちらも、いくつかのポイントをコードを見ながら解説していきます!先ほどと同じところは説明を省きますね。
早速見てみましょう。最初から先ほどと少し違います。CSVファイルを最初から最後まで読み込む判定と1行の文字列データを読み込む処理が一緒になっています。次のソースを見てください。
// データが読み込めている間は、繰り返し読み込む
// 1行の文字列データを string[]型として読み込む
string[] columns;
while ((columns = csv.Parser.Read()) != null)
{
}
(columns = csv.Parser.Read())のcsv.Parser.Read()で1行の文字列データをstring[]型で取得してcolumnsにセットされます。セットされた結果がnullになるまで処理を繰り返すので、空行を読み込んでcolumnsがnullになって繰り返しが終了になります。
実際にデータを取り出すところを見ていきましょう。次のソースを見てください。
// 取得データの取り出し
foreach (var column in columns)
{
Console.WriteLine($"Value:{column}");
}
string[]なので、foreachでループ処理することができます。これは普段から使っていると思うので、問題ないと思います。
次は実行結果です。結果はどうなっているでしょうか。実はここにちょっと問題があります。
// 【実行結果】
// ■ 1行目
// Value:番号
// Value:名前
// Value:日付
// Value:金額
// ■ 2行目
// Value:1
// Value:鈴木
// Value:
// Value:555
// ■ 3行目
// Value:2
// Value:佐藤
// Value:20201011
// Value:666
結果が先ほどと少し違っていますよね?
1行目に明らかにヘッダー行の内容が取得されています。
csv.Configuration.HasHeaderRecord = true;を使って「ヘッダーあり」と指定しているのに、ヘッダー行も1行として読み込まれてしまいます。
公式の対処方法が見つかりませんでしたので、暫定的な対応方法を載せておきます。
次のソースをどうぞ。
csv.Configuration.HasHeaderRecord = true; // Headerあり
int row = 0; // テスト用
string[] columns;
while ((columns = csv.Parser.Read()) != null)
{
if (csv.Configuration.HasHeaderRecord && csv.Context.Row == 1)
{
continue;
}
Console.WriteLine($"■ {++row}行目"); // テスト用
foreach (var column in columns)
{
Console.WriteLine($"Value:{column}");
}
}
ヘッダー行のデータ取得をスキップさせる処理を追加しました。
少し詳しく説明していきます。
// ヘッダーあり かつ 1行目はスキップ
if (csv.Configuration.HasHeaderRecord && csv.Context.Row == 1)
{
continue;
}
追加したのはこの部分にです。csv.Context.Rowはデータの行番号を取得できるので、csv.Context.Row == 1で1行目の意味になります。csv.Configuration.HasHeaderRecordと合わせて「ヘッダーあり」かつ「1行目」の場合となり、データの取得処理をスキップさせることができます。
修正後のソースで、もう一度結果を確認します。
// 【実行結果】
// ■ 1行目
// Value:1
// Value:鈴木
// Value:
// Value:555
// ■ 2行目
// Value:2
// Value:佐藤
// Value:20201011
// Value:666
ヘッダー行のデータが取得されずに、期待していたとおりの結果になりました。
foreachで回して取得する方法だけでなく、インデックスを指定して個別にデータを取り出す方法も確認しておきましょう。
int row = 0; // テスト用
string[] columns;
while ((columns = csv.Parser.Read()) != null)
{
if (csv.Configuration.HasHeaderRecord && csv.Context.Row == 1)
{
continue;
}
Console.WriteLine($"■ {++row}行目"); // テスト用
Console.WriteLine($"columns[0] Value:{columns[0]}");
Console.WriteLine($"columns[1] Value:{columns[1]}");
Console.WriteLine($"columns[2] Value:{columns[2]}");
Console.WriteLine($"columns[3] Value:{columns[3]}");
}
実行結果です。
// 【実行結果】
// ■ 1行目
// columns[0] Value:1
// columns[1] Value:鈴木
// columns[2] Value:
// columns[3] Value:555
// ■ 2行目
// columns[0] Value:2
// columns[1] Value:佐藤
// columns[2] Value:20201011
// columns[3] Value:666
問題ないですね、インデックスに対するValueが正しく取得できています。
このやり方でも「CsvHelper マッピングしない読み込み」ができました!
まとめ
CsvHelper マッピングしない読み込みの方法について紹介しました。
マッピングや型変換はCsvHelperでけっこう分かりにくい部分なのかなって感じてます。
Configuration設定や使い方をマスターするとCSVの読み込みがもっと楽になりますよ。
興味のある方は「CsvHelper Configurationの設定」の記事をどうぞ。