田舎人の書斎作ったり、試したり、書き留めたり。

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にも応用できます。