読者です 読者をやめる 読者になる 読者になる

C# + CsvHelperで、複数フォーマットのCSVの読込処理を共通化する

C# CSV

CsvHelperを使う場合、CSVファイルの読込処理は

public static void Import(string path, ...)
{
    using (var sr = new System.IO.StreamReader(path))
    using (var csv = new CsvHelper.CsvReader(sr))
    {
        csv.Configuration.RegisterClassMap();
        var records = csv.GetRecords();

        foreach (var record in records)
        {
            // 読み込んだあとの処理
        }
    }
}

のようなイメージで、ある程度定型的なものになるかと思います。

そんな中、一つのプログラムで複数フォーマットのCSVファイルを連続して読み込む必要が出てきました。何も考えずにいると定型的な部分をコピペしそうな感じだったので、いろいろと試したメモを残します。

 

環境

 

準備

方針

今回は、

とします。

 

CSVファイル
date.csv
name,date
hoge,20160101
content.csv
name,content
hoge,Hello world

 

マッピング先のオブジェクト用クラス
public class DateCsv
{
    public string Name { get; set; }
    public DateTime Date{ get; set; }
}
public class ContentCsv
{
    public string Name { get; set; }
    public string Content { get; set; }
}

 

マッピングクラス
public class DateMapper : CsvHelper.Configuration.CsvClassMap<DateCsv>
{
    public DateMapper()
    {
        Map(m => m.Name).Index(0);
        Map(m => m.Date).Index(1).TypeConverterOption("yyyyMMdd");
    }
}
public class ContentMapper : CsvHelper.Configuration.CsvClassMap<ContentCsv>
{
    public ContentMapper()
    {
        Map(m => m.Name).Index(0);
        Map(m => m.Content).Index(1);
    }
}

 

RegisterClassMap()やGetRecords()にTypeを渡して共通化する

ジェネリックを使おうかと考えつつVisualStudioを使っていたところ、

f:id:thinkAmi:20160210051041p:plain

のようなインテリセンスが出ました。

RegisterClassMap()にてTypeを引数に取れるのが気になったので、CsvHelperのソースコードを読むと

などを見つけました。

2013年のプルリクエストで取り込まれたようです。
Added new method to CsvConfiguration, ClassMapping(Type classMapType) by vcaraulean · Pull Request #159 · JoshClose/CsvHelper

 
そのため、まずはRegisterClassMap()やGetRecords()にTypeを渡す共通化を試してみます。

Typeを渡すには

  • Type.GetType("対象の名前空間付クラスの文字列")を使う
  • typeof演算子を使う

などが考えられたため、それぞれ実装してみました。

 

Type.GetType()を使う
共通処理部分

Type.GetType()で使うため、共通処理のメソッド

public static void ImportUsingGetType(Action<object> action, string path, string csvClassName, string mapperClassName)

と、名前空間付のクラス名を引数で受け取るようにします。
文字列で指定したクラスのインスタンスを作成するには? - @IT

その引数を

csv.Configuration.RegisterClassMap(Type.GetType(mapperClassName));
var records = csv.GetRecords(Type.GetType(csvClassName));

と、RegisterClassMap()やGetRecords()で使います。

 

独自処理部分

上記のcsv.GetRecords(Type.GetType(csvClassName));の戻り値はIEnumerable<object>となります。

そのため、共通処理に渡すActionデリゲートでは

Action<object> dateActionByObject = obj =>
{
    var record = (DateCsv)obj;
    Console.WriteLine($"date.csvの中身:{record.Name} - {record.Date}");
};

と、object型から本来の型へと変換してから、実際の独自処理を記述します。

 

呼び出し元
ImportUsingGetType(dateActionByObject, datePath, "FunctionSharingSample.DateCsv", "FunctionSharingSample.DateMapper");

のようにして、共通処理を呼び出します。

ここまでが、Type.GetType()を使って共通化する方法です。

 

型引数(型パラメータ) + typeof演算子を使う

Type.GetType()を使う方法だと、クラス名を文字列で渡す必要があり、VisualStudioのインテリセンスに頼れません。

そのため、文字列を排除できるよう、Type.GetType()の代わりにtypeof演算子を使います。

 

共通処理部分

typeof演算子で使うため、共通処理のメソッドに型引数を追加します。今回は、

の2つを型引数として受け取ります。

public static void ImportUsingTypeof<TCsv, TMapper>(Action<TCsv> action, string path)

 
メソッド内では、typeof演算子と型引数を使い、

csv.Configuration.RegisterClassMap(typeof(TMapper));
var records = csv.GetRecords(typeof(TCsv));

という形でTypeを取得するように変更します。

 
また、foreachの変数も

foreach (TCsv record in records)
{
    action(record);
}

と、型引数のTCsvを使います。

 

独自処理部分

foreachで型引数を使ったことから、Actionデリゲートも型を指定できるようになったため、

Action<DateCsv> dateActionByGeneric = obj =>
{
    Console.WriteLine($"date.csvの中身:{obj.Name} - {obj.Date}");
};

という形へと変更します。

 

呼び出し元
ImportUsingTypeof<DateCsv, DateMapper>(dateActionByGeneric, datePath);

となります。

ここまでが、型引数(型パラメータ) + typeof演算子を使って共通化する方法です。

 

RegisterClassMap()やGetRecords()に型引数を渡して共通化する

CSVHelperのドキュメントを読むと、RegisterClassMap()・GetRecords()メソッドにも型引数を指定できます。

 
そのため、共通処理内を

csv.Configuration.RegisterClassMap<TMapper>();
var records = csv.GetRecords<TCsv>();

へと置き換えて、typeof演算子を排除できます。

ただ、単純に置き換えるだけだと、

エラー CS0314 型 'TMapper' はジェネリック型またはメソッド 'CsvConfiguration.RegisterClassMap()' 内で型パラメーター 'TMap' として使用できません。'TMapper' から 'CsvHelper.Configuration.CsvClassMap' へのボックス変換または型パラメーター変換がありません。

というコンパイルエラーが発生します。
コンパイラ エラー CS0314 - MSDN

そのため、whereコンテキストキーワードを使って、型引数TMapperに対する制限を追加します。

public static void ImportUsingTypeParameter<TCsv, TMapper>(Action<TCsv> action, string path)
    where TMapper : CsvHelper.Configuration.CsvClassMap

 
あとは、foreachのところの型名をvarにしておきます。

foreach (var record in records)
{
    action(record);
}

 
独自処理部分や呼び出し元の実装は、typeof演算子の時の実装と変わりませんので、省略します。

ここまでが、RegisterClassMapやGetRecordsに型引数を渡して共通化する方法です。

 

動作確認

上記の3パターンを含むコンソールアプリを実行すると、

f:id:thinkAmi:20160210051240p:plain

いずれのパターンでも複数フォーマットのCSVファイル読込処理ができているようでした。

 

ソースコード

GitHubに上げました。FunctionSharingSampleプロジェクトが今回のソースコードです。
thinkAmi-sandbox/CsvHelperSample

 

参考