【C#】 CSVをDictionaryのListとして読み込む with 黒魔術
うぃろぅです。
前回の記事の関連です。
そもそもDataTableなんて使わずともDictionary型のListにしてしまえばいいのではないか、と考えたので書いておきます。
で、書いてたらなんか楽しくなってきたので黒魔術コードが生成されました。
せっかくなので残しておこうという趣旨。
何がしたいか
ヘッダーつきのCSVファイルを読み込んでListとして返却したい
いよいよCsvHelper
に頼ればいいって?私もそう思う。
車輪の再発明から得られる知見もあるはず。というかただコード書きたいだけ。
まずは素直に書いてみる
今回もTextFieldParser
を利用していく。
※CSVの先頭行にヘッダーがあるとする。
以下ソース。using System.ほげほげ
はよしなに書いてください…。
private static List<Dictionary<string, string>> CsvToList(string path) { // お約束 var parser = new TextFieldParser(path, Encoding.GetEncoding("Shift-JIS")); { TextFieldType = FieldType.Delimited, Delimiters = new string[] { "," } }; // 読み込み var rows = new List<string[]>(); while (!parser.EndOfData) { rows.Add(parser.ReadFields()); } // 列名設定 var header = new List<string>(); foreach(var head in rows.First()) { header.Add(head); } // 行追加 var dicList = new List<Dictionary<string, string>>(); foreach(var row in rows.Skip(1)) { var dic = new Dictionary<string, string>(); foreach(var i in Enumerable.Range(0, row.Length)) { dic.add(header[i], row[i]); } dicList.Add(dic); } return dicList; }
こんな感じ? 若干面倒に見えるのは
foreach(var i in Enumerable.Range(0, row.Length)) { dic.Add(header[i], row[i]); }
の箇所だろうか。rows
のループ回数は行数なので、行数分のDictionaryを生成しているのだが、例えば
name,age,address alice,17,jp bob,20,us charlie,19,uk
こんなCSVがあったとして、
ヘッダー(header) | 要素(row) | 添え字[i] |
---|---|---|
name | alice | 0 |
age | 17 | 1 |
address | jp | 2 |
ヘッダー(header) | 要素(row) | 添え字[i] |
---|---|---|
name | bob | 0 |
age | 20 | 1 |
address | us | 2 |
イメージ的にはこう順番に追加している。
2要素を同時に回す
dic
の名前がセンスない。できるだけ脳のワーキングメモリは減らしたい。
添え字を使って回さない方法を考えたい。
添え字使うのってなんかもっさりしてるイメージない??ないか。そうか。
Zip
メソッドを使えば対処できそう。なのでやってみる。
var dicList = new List<Dictionary<string, string>>(); foreach(var row in rows.Skip(1)) { dicList.Add(header.Zip(row, (head, data) => new KeyValuePair<string, string>(head, data)) .ToDictionary(keyVal => keyVal.Key, keyVal => keyVal.Value)); }
こんな感じ?
値がnullになる可能性がある場合はkeyVal.Value ?? ""
とかでいけるんじゃないかな。
(1つめの要素).Zip((一緒に回したい要素), (一緒に回した値に対して行いたい処理))
が書き方。
今回はDictionary
型に変換したかったためKeyValuePair
を使った。
もっといいやり方がありそうな気配しかしないので知っている方はコメントお願いします…。
もう少し短くしてみる
可読性的にはさっきのがギリギリな気がする。
でもdicList
って名前がイケてない。それっぽい名前をつければそれでいいと思います
return rows .Skip(1) .Aggregate(new List<Dictionary<string, string>>(), (list, row) => { list.Add(header.Zip(row, (head, item) => new KeyValuePair<string, string>(head, item)) .ToDictionary(kv => kv.Key, kv => kv.Value ?? "")); return list; });
これでいけるはず!!なんと1行。
- ヘッダーと要素を
KeyValuePair
として1要素に集約 KeyValuePair
をDictionary
に変換- 変換した
Dictionary
をList
に追加 - 行数分(ヘッダー除く)畳み込み演算
- 演算した結果を返却
の順で処理が走っている。
試しに改行しないで書いてみると
return rows.Skip(1).Aggregate(new List<Dictionary<string, string>>(), (list, row) =>{list.Add(header.Zip(row, (head, item) =>new KeyValuePair<string, string>(head, item)).ToDictionary(kv => kv.Key, kv => kv.Value ?? ""));return list;});
こうなる。空白入れて236文字。Cobol的にはエラー3回分くらい。へへ。
ここまで書いてAggregate
使う必要ないのではということに気がついた。
改良案。
return rows .Skip(1) .Select(row => new Dictionary<string, string>( header .Zip(row, (head, item) => new KeyValuePair<string, string>(head, item)) .ToDictionary(kv => kv.Key, kv => kv.Value ?? "")) .ToList();
これだ(?)。
return rows.Skip(1).Select(row => new Dictionary<string, string>(header.Zip(row, (head, item) => new KeyValuePair<string, string>(head, item)).ToDictionary(kv => kv.Key, kv => kv.Value ?? "")).ToList();
空白入れて202文字。圧倒的エコ。
ちなみにデバッガー動かして確認したらちゃんと同じ中身になっていた。逆になぜうまくいってしまったのか。
†黒魔術完成†
順番に読んでいけば難しい要素なんてほとんどないのに書いていくうちに膨れ上がる。
こうして黒魔術は世に解き放たれていくのであった。
まあこれくらいのコードならよくあることでしょう。別にレビューしてもらうわけでもなし、楽しんだもん勝ちということでひとつ。
ではまた。