CSVのヘッダーをキーにしてDictionaryへ読み込む(C#・VB.NET)
公開
この記事は2022年に書いたものです。ソフトウェアのバージョンや配布状況など、いまとは異なる場合があります。
CSVのヘッダーをキーに、2行目以降のデータを値として、Dictionaryに取り込みたいと思い、試行錯誤しました。備忘録として残しておきます。
やりたいこと
たとえば次のようなCSVをDictionary<string, List<string>>に読み込み、"No."というキーで検索すると、値として{1, 2, 3}のリストが得られるようにするのが目的です。
| No. | データ1 | データ2 | データ3 |
|---|---|---|---|
| 1 | 1431 | 1313 | すいか |
| 2 | 4131 | 31 | メロン |
| 3 | 413 | 3131 | バナナ |
コード
先に結果から示します。C#ではこうなります。
public Dictionary<string, List<string>> CsvToDictionary(string filePath)
{
var lines = File.ReadLines(filePath)
.Where(s => !string.IsNullOrEmpty(s))
.ToList();
var list = new List<List<string>>();
foreach (var line in lines)
{
list.Add(line.Split(',').ToList());
}
var transposed = Transpose(list);
var dict = new Dictionary<string, List<string>>();
foreach (var column in transposed)
{
dict[column.First()] = column.Skip(1).ToList();
}
return dict;
}
// 行と列を入れ替える
private IEnumerable<IEnumerable<T>> Transpose<T>(IEnumerable<IEnumerable<T>> values)
{
return Enumerable.Range(0, values.Max(c => c.Count()))
.Select(i => values.Select(c => i < c.Count() ? c.ElementAt(i) : default(T)));
}
VB.NETではこうなります。
Public Function CsvToDictionary(ByVal filePath As String) As Dictionary(Of String, List(Of String))
' ファイルの読み出し
Dim lines = File.ReadLines(filePath) _
.Where(Function(s) Not String.IsNullOrEmpty(s))
Dim list As New List(Of List(Of String))
For Each line In lines
list.Add(New List(Of String)(line.Split(","c)))
Next
Dim transposed = Transpose(list)
Dim dict As New Dictionary(Of String, List(Of String))
For Each column In transposed
dict.Add(column.First(), column.Skip(1).ToList())
Next
Return dict
End Function
' 行と列を入れ替える
Private Function Transpose(value As IEnumerable(Of IEnumerable(Of String))) As IEnumerable(Of IEnumerable(Of String))
Return Enumerable.Range(0, value.Max(Function(x) x.Count)) _
.Select(Function(i) value.Select(Function(x) If(i < x.Count(), x.ElementAt(i), Nothing)))
End Function
考え方
CSVを普通に読み込むと、上から1行ずつ処理することになります。すぐ思いつくのは頭から順に処理する方法ですが、その場合は「1行目をすべてキーにし、2行目以降の値を順にそれぞれのキーへ振り分ける」という手順が必要になります。コードが込み入りそうだったので、先にCSVの行と列を入れ替えることにしました。
入れ替える前はこうです。
| No. | データ1 | データ2 | データ3 |
|---|---|---|---|
| 1 | 1431 | 1313 | すいか |
| 2 | 4131 | 31 | メロン |
| 3 | 413 | 3131 | バナナ |
入れ替えるとこうなります。
| No. | 1 | 2 | 3 |
|---|---|---|---|
| データ1 | 1431 | 4131 | 413 |
| データ2 | 1313 | 31 | 3131 |
| データ3 | すいか | メロン | バナナ |
こうしておけば、1行ごとに「先頭をキー、残りを値」とする同じ処理を繰り返すだけで済みます。LINQで簡潔に書けるので読みやすく、バグも入りにくいコードになると思います。
ループの回数を考えると少し効率は落ちるかもしれません。効率が求められる場面が来たら、そのとき別の書き方を考えればいい、という立場です。そうした場面はめったにないと思います。
LINQを学ぶなら
LINQがよく分からないという方には、『実戦で役立つ C#プログラミングのイディオム/定石&パターン』(出井秀行 著、技術評論社)がおすすめです。LINQを使えるようになると、コードがぐっと短くなり、バグも入りにくくなります。読み終えると、過去のコードをすべてLINQで書き直したくなるはずです。私がそうでした。VB.NETにも応用できます。