TERRYのブログ

最近は競技プログラミングにお熱。

Sponsored Link

【C#】CsvHelperでMappingクラスを使わずにCSVファイルを読み込む

CSVをいじりたい

C#からCSVをいじりたいとき、ありますよね。
Webの世界ではJSONあたりが一般的ですが、社内システムなんかではまだまだ現役だったりします(ですよね?)。
C#でCSVを読み込むとなると、以下の3つの方法が考えられるかと思います。

1. string.Split(',')でがんばる

CSVは単にカンマで区切られただけのファイルなので、場合によってはこれが一番簡単です。
ただし、各要素の中にカンマが入っていたり改行が入っていたりすると途端にめんどくさくなります。というか無理なのでは。

2. TextFieldParserを使う

こいつは結構優秀で、カンマが紛れ込んでいようが改行が突っ込まれてようがパースしてくれます。
ただし各要素を string[]で返してくるので、クラスに詰め込み直すのがちょっと面倒です。1行読み込んだら1インスタンスで返して欲しくないですか?(わがまま)

3. CsvHelperを使う

本命です。CSVを読み込んでIEnumerable<T>で返してくれるのでコードがスッキリします。イケてます。
公式サイトはこちら。
joshclose.github.io
というわけで今回はこいつの使い方について。

CSVファイルの読み込み

CsvHelperにはマッピング機能があり、CSVのフィールドをクラスに詰め込む際にかなり柔軟な指定をすることができます。ただし、別途マッピングクラスを作成する必要があるためちょっと面倒です。
今回は属性(Attribute)を使ってマッピングクラスを使わずに読み込んでみましょう。こんな感じのCSVを用意しました。

商品,価格,消費期限,備考
おにぎり,"100",2019/12/22 21:00,""
パン,"120",2019/12/25 0:00,""
うどん,"400",2019/12/24 0:00,"あたため1分,お箸を付ける"
からあげ,"150",2019/12/22 18:00,"なぜか改行を
含む備考"
クリスマスケーキ,"3,000",2019/12/26 0:00,"要予約"

価格が3桁区切りだったり備考欄にカンマや改行が入っていたりでなかなかやんちゃなCSVですね。

インデックスで列を指定して読み込み

データクラス

各プロパティにIndexAttributeを付けて、CSVファイルの何列目に各プロパティが入っているか指定してあげればOKです。
今回はついでにNumberStylesAttributeにNumberStyles.AllowThousandsを指定して3桁カンマ区切りの数値も読み込めるようにしてあげましょう。なお不要なら削除しても大丈夫です。
余談ですが、普通のint.ParseなんかでもNumberStyles列挙体を用いてどんな書式を許容するか指定できます。必要に応じて使ってあげるとカンマの置換だとかの前処理が不要になって便利です。

using System;
using CsvHelper.Configuration.Attributes;

namespace ConsoleApp1
{
    class Item
    {
        [Index(0)]
        public string Name { get; set; }
        [Index(1)]
        [NumberStyles(System.Globalization.NumberStyles.AllowThousands)] // 3桁カンマ区切りを許容(不要なら削除してOK)
        public int Price { get; set; }
        [Index(2)]
        public DateTime ExpirationDate { get; set; }
        [Index(3)]
        public string Remarks { get; set; }

        public override string ToString() => $"{Name} {Price}円(消費期限 {ExpirationDate}): {Remarks}";
    }
}
読み込み側

あとはこんな感じで読み込んであげます。CsvReader.GetRecords<T>はインスタンス化とデータのパースを内部でよろしくやってくれるので楽ちんです。

using System;
using System.IO;
using System.Text;
using CsvHelper;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            // .Net CoreでShift_JISを使用する場合はNugetから
            // System.Text.Encoding.CodePagesを落としてこの一行を追加。
            Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

            using (var reader = new StreamReader("hoge.csv", Encoding.GetEncoding("Shift_JIS")))
            using (var csv = new CsvReader(reader))
            {
                csv.Configuration.HasHeaderRecord = true; // ヘッダーの有無
                var items = csv.GetRecords<Item>(); // データ読み出し(IEnumerable<Item>として受け取る)

                foreach (var item in items)
                {
                    Console.WriteLine(item);
                }
            }
        }
    }
}
実行結果
おにぎり 100円(消費期限 2019/12/22 21:00:00):
パン 120円(消費期限 2019/12/25 0:00:00):
うどん 400円(消費期限 2019/12/24 0:00:00): あたため1分,お箸を付ける
からあげ 150円(消費期限 2019/12/22 18:00:00): なぜか改行を
含む備考
クリスマスケーキ 3000円(消費期限 2019/12/26 0:00:00): 要予約

できました!簡単ですね。

ヘッダの名称で列を指定して読み込み

データクラス

インデックスではなく、CSVのヘッダの名称で列を指定する方法もあります。NameAttributeを使いましょう。これで弊社システムのように勝手にCSVのフォーマットが変わっても安心ですね!

using System;
using CsvHelper.Configuration.Attributes;

namespace ConsoleApp1
{
    class Item
    {
        [Name("商品")]
        public string Name { get; set; }
        [Name("価格")]
        [NumberStyles(System.Globalization.NumberStyles.AllowThousands)] // 3桁カンマ区切りを許容(不要なら削除してOK)
        public int Price { get; set; }
        [Name("消費期限")]
        public DateTime ExpirationDate { get; set; }
        [Name("備考")]
        public string Remarks { get; set; }

        public override string ToString() => $"{Name} {Price}円(消費期限 {ExpirationDate}): {Remarks}";
    }
}

まとめ

CsvHelperについて紹介するページはマッピングを使った方法について紹介するページが多いですが、Attributeを使うと別途マッピング用クラスを作らずとも簡単にCSVを読み込むことができます。ご参考になれば幸いです。