いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

FileHelpers入門記-01 (区切り文字があるファイルのマッピング)(Delimited, 区切り文字, デリミタ)


最近、FileHelpersというライブラリを知りまして、結構面白そうなので
使い方を勉強しながら、メモしていこうと思います。


FileHelpersライブラリは、以下のサイトで公開されています。


要約すると以下のような事をやってくれるライブラリです。

O/Rマッピングのように可変長及び固定長のファイルデータを特定のクラスにマッピングするライブラリ


電文データのような固定長とか、CSVファイルのような区切り文字があるデータを
自分で定義したクラスにマッピング出来るって感じですね。
こういうのは、仕事でもよく出てくる処理の一つだと思いますが、結構自前で同じようなマッピング処理を
行うライブラリ作成して、プログラムを作っている場合が多いと思います。


ライセンスは、LGPLなので案件で利用する場合も使いやすいです。


FileHelpersは以下のような機能も持っていますので、こちらを使う方が簡単かも知れません。
以下、機能をざっくり羅列していきます。(てか、本家のページをちょこっと訳しただけです。w)

  • 固定長データのマッピング
  • 区切り文字を持つデータのマッピング
  • 型の自動変換
    • 例えば、特定の列はマッピングした際に文字列ではなくDateTimeでマッピングする等
  • DataTableへのマッピング
  • ランタイム時のクラスマッピング機能(Version 1.6.0より)
    • これを利用すると予めマッピングクラスを定義していなくても実行時に動的にマッピングが行えます。
  • Master-Detailパターンのサポート
    • つまり、一つのファイル内に親子関係を持っているレコードが有る場合をサポートしているということのようです。
  • 複数レコードフォーマットのサポート
    • つまり、一つのファイル内に複数のフォーマットのレコードが有る場合をサポートしているということのようです。
  • イベントのサポート
  • Excelファイルのサポート(Excel 2000以降)
  • データリンクのサポート
    • つまり、異なるストレージ(データベースなど)との連係が行えるということのようです。
  • 非同期処理のサポート
  • レコードデータの比較処理のサポート
    • 差分のレコードのみを抽出したり出来るようです。
  • クォート文字のサポート
  • 特定のエンコーディングを指定できる。


ざっくり上記のような事ができるみたいです。
機能豊富ですね〜。後は、使い方を知るのみです。


で、今回は最もよく現れるパターンと思われる、区切り文字があるデータのパターンです。
やり方は以下の手順で行います。

まず、データファイルを用意。今回は以下のようなCSVファイルとします。
以下の内容を持つPersons.csvというファイルが実行時にカレントフォルダにあるとします。

100,gsf_zero1,30,090-0000-1111,1979/1/1
200,gsf_zero2,31,090-0000-1112,1979/1/2
300,gsf_zero3,,090-0000-1113,1979/1/3
400,gsf_zero4,33,090-0000-1114,1979/1/4
500,gsf_zero5,34,090-0000-1115,1979/1/5


次にマッピングクラスを定義します。

    [DelimitedRecord(",")]
    public class Person {
        public int    Id;
        
        public string Name;
        
        [FieldNullValue(-1)]
        public int    Age;
        
        public string Tel;

        [FieldConverter(ConverterKind.Date, "yyyy/MM/dd hh:mm:ss")]
        public DateTime Birthday;

        public override int GetHashCode() {
            return this.ToString().GetHashCode();
        }

        public override bool Equals(object obj) {
            if (base.Equals(obj)) {
                return true;
            }

            return (this.ToString() == ( (Person) obj).ToString());
        }
        public override string ToString() {
            return string.Format("Id={0},Name={1},Age={2},Tel={3},BirthDay={4}", Id, Name, Age, Tel, Birthday);
        }
    }

クラスの属性として以下の属性を利用しています。

DelimitedRecord(",")

見たまんまですが、区切り文字はカンマですと定義しています。
後は普通にフィールドを切るだけです。ちなみにフィールドの定義した順に
データがマッピングされます。なので、データファイルの列順とフィールドの定義順を
あわせておきます。

途中、Ageフィールドには以下の属性を付与しています。

FieldNullValue(-1)

これもみたまんまですが、値が空の場合に指定した値がマッピングされるようになります。
後、BirthDayフィールドはDateTime型として定義しているのですが、データファイルから
そのまま取得すると文字列となっているので、変換するように指定しています。

FieldConverter(ConverterKind.Date, "yyyy/MM/dd hh:mm:ss")


ここまできたら後は使うだけです。
以下サンプル全体のソースです。

using System;
using System.Linq;
using System.Text;

using FileHelpers;

namespace FileHelpersSamples {

    class Program {

        const string DATA_FILE  = @".\Persons.csv";
        const string DATA_FILE2 = @".\Persons2.csv";
        const string DATA_FILE3 = @".\Persons3.csv";

        public void Execute() {
            //
            // エンジンを構築.
            //
            FileHelperEngine engine = new FileHelperEngine(Encoding.GetEncoding("sjis"));

            //
            // データ読み取り.
            //
            Person persons = engine.ReadFile(DATA_FILE);
            persons.ToList().ForEach(Console.WriteLine);

            //
            // データ書き込み. (同じデータ)
            //
            engine.WriteFile(DATA_FILE2, persons);

            //
            // 同じデータが書き込めているか?
            //
            Console.WriteLine("同じデータ?⇒{0}", persons.SequenceEqual(engine.ReadFile(DATA_FILE2)));

            //
            // データを一部改変し、書き込み.
            //
            persons.Take(2).ToList().ForEach((p) => { p.Age += 100; });
            persons.ToList().ForEach(Console.WriteLine);

            engine.WriteFile(DATA_FILE3, persons);

            //
            // 違うデータとなっているか?
            //
            Console.WriteLine("同じデータ?⇒{0}", engine.ReadFile(DATA_FILE).SequenceEqual(engine.ReadFile(DATA_FILE3)));
        }

        static void Main(string args) {

            (new Program()).Execute();

            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }
    }

    [DelimitedRecord(",")]
    public class Person {
        public int    Id;
        
        public string Name;
        
        [FieldNullValue(-1)]
        public int    Age;
        
        public string Tel;

        [FieldConverter(ConverterKind.Date, "yyyy/MM/dd hh:mm:ss")]
        public DateTime Birthday;

        public override int GetHashCode() {
            return this.ToString().GetHashCode();
        }

        public override bool Equals(object obj) {
            if (base.Equals(obj)) {
                return true;
            }

            return (this.ToString() == ( (Person) obj).ToString());
        }
        public override string ToString() {
            return string.Format("Id={0},Name={1},Age={2},Tel={3},BirthDay={4}", Id, Name, Age, Tel, Birthday);
        }
    }
}


手順として、

  1. FileHelperEngineを構築
  2. ReadFileメソッドで読み取り
  3. WriteFileメソッドで書き込み

となります。
簡単ですね〜。確かに使いやすいです。

区切り文字がタブ区切りの場合は、|で区切っているデータの場合も同様です。
DelimitedRecord属性に指定する値を変更するだけです。


次は、固定長のファイルについて調べてみます。