LoginSignup
3
1

More than 1 year has passed since last update.

CSVを読み込んでみた!(F# and Csvhelper)

Last updated at Posted at 2021-12-03

この記事は F# advent calendar 2021 の4日目です。

Intro

callmekoheiは事務員さんなので、CSVというファイルをよく扱います。昔はVBAというエクセルについてる言語で扱ってたのですが、データ件数が多くて処理できなくなったのでF#に切替えました。で、Csvhelperがとても便利なのでメモしたいと思います!

CSVとは?

りんご、ばなな、ちぇりー

みたいな感じです(RFC4180的な...)

Csvhelperとは?

F#でCSVをらくに読み書きできるライブラリです。(VBとかC#という言語でも使用できます)

使い方1

下記の様な感じで使用します

// class(CSVの構造)
type foo = {
  // ...
}

// class map (csvの構造に肉付け)
type fooMap {
  // ...
}

// read(実際に読み込む)
read<foo>()

使い方2

callmekoheiは下記の様なやり方でcsvを読み込んでます

1. class (csvの構造)

mutable を使用して、csvのデータ以外を書き込んだりします
e.g. ファイル名に書かれてる会社コードとか

[<CLIMutableAttribute>] // おまじない
type MyCsv =
  {
    mutable foo:string // 後で変更することができる
    bar:int
    baz:string
  }

2. class map (肉付け)

ここでデータチェックとかしたりします

type MyCsvMap () as this =
  inherit ClassMap<MyCsv>()
  let sb = System.Text.StringBuilder()
  let csv = Unchecked.defaultof<MyCsv>
  let dic = Microsoft.FSharp.Reflection.FSharpType.GetRecordFields(typeof<MyCsv>) |> Seq.mapi(fun idx x -> x.Name,idx) |> Map.ofSeq
  let fieldValue (rObj:CsvHelper.ConvertFromStringArgs) columnName = ( columnName |> dic.TryFind |> fun x -> x.Value) |> rObj.Row.GetField<'T>
  do

    this.Map(fun x -> x.商品名).Index(0) |> ignore
    this.Map(fun x -> x.価格).Index(1) |> ignore
    // validate all fields at last column
    this.Map(fun x -> x.)
    |> fun x -> x.Convert(this.RowFieldsValidate)
    |> fun x -> x.Index(2)
    |> ignore

  // validateする場合
  member this.RowFieldsValidate:CsvHelper.ConvertFromStringArgs -> string =
    fun rowObj ->
      sb.Clear() |> ignore

      (fieldValue rowObj (nameof(csv.商品名)))
      |> MyValidate.guardOverLen "商品名" sb 10
      |> ignore

      if sb.ToString() <> ""
      then raise <| MyCsvException (sb.ToString())
      (fieldValue rowObj (nameof(csv.)))

データチェックには下記を使用したりします(まぁお好みで...)

// validate用の便利関数
module MyValidate =
  open System.Text
  open FSharp.Core
  let either fOk fError = function
    | Ok x -> fOk x
    | Error x -> fError x
  let either2 f = function
    | Ok x -> f x
    | Error x -> f x
  let (|OverLen|_|) n = function
    | (s:string) when s.Length > n -> Some s
    | _ -> None
  // 任意の文字数より上の文字数のみ通さない guard
  let guardOverLen label (sb:StringBuilder) n = function
    | OverLen n s -> Error(sb.Append($"{label}:letters is over {n}.") |> ignore ; s)
    | s -> Ok s

3. reading by hand

大体は by hand で読み込んでます

  (*

    A. CSVレコード格納用変数(Array.Resize()とかの方が速度的によさげ?)

  *)

  let cqBad = new ConcurrentQueue<obj>()
  let cq = new ConcurrentQueue<MyCsv>()

  // 一時変数が必要な場合は下記を使用したりすると便利かも...
  // let tmp_hoge = ref Unchecked.defaultof<Csvhoge>


  (*

    B. CSV読込設定(お好みで...)

  *)

  // util func(ヘッダーをスキップする際に使用する)
  let skipRows (csv:CsvReader) i =
    for j in [1..i] do
      csv.Read() |> ignore

  let config = CsvConfiguration(CultureInfo.CurrentCulture)
  // config.NewLine <- "\n"(多分これは効かない...笑)
  config.Delimiter <- ","
  config.IgnoreBlankLines <- true
  config.HasHeaderRecord <- false
  config.TrimOptions <- TrimOptions.Trim
  config.ShouldSkipRecord <- (fun arg -> arg.Record.[0] = "EOF" )
  let csv = new CsvReader( streamReader , config)
  // Multiple Record type の場合はここをレコードタイプ毎に追加していく e.g. <csv1>, <csv2>
  csv.Context.RegisterClassMap<MyCsvMap>() |> ignore

  (*

    C. CSV読込(Reading by hand)

  *)

  // ファイル名から情報を取得する
  // let fileCreatedTime = finfo.Name.Replace(".zip","").Split('_') |> Array.item 4 |> fun dt -> System.DateTime.ParseExact(dt,"yyyyMMddHHmmss", DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None)

  let mutable flg = true
  while flg do
    try
      flg <- csv.Read()
      if flg
      then

        let tmp = csv.GetRecord<MyCsv>()
        tmp.ファイル作成日時 <- fileCreatedTime
        cq.Enqueue(tmp)

  (*

    D. エラー処理(お好みで...)

  *)

    with
      | :? CsvHelperException as e ->

        match e with

        // classmapエラーはここに来る
        | :? CsvHelper.TypeConversion.TypeConverterException as te ->
          let errMsg = ...
          let errLog:ErrorRecordLog = {...}
          cqBad.Enqueue(errLog)

        | _ ->

          match e.InnerException with
          // 各々にて作成したvalidationエラーはここに来る
          | :? MyCsvException as myerr ->
            let errMsg = myerr.Data0 // 自作のエラーメッセージ
            let errLog:ErrorRecordLog = {...}
            cqBad.Enqueue(errLog)

          | _ -> ...

      | otherErr -> ...

  (*

    E. 返り値

  *)

  (
      cqBad
    , cq
  )

データ補正

例えば下記みたいなデータを補正する必要があったりします

あああ,300,いいい
ううう,1,980,えええ

補正できる場合(readをtryではさむ)


try 
  read<foo>()
with e -> 
  ここで補正する

補正できない場合

1. CSV作成元に『頼むぽよー』とお願いする
2. 泣き寝入りして、こちらで手作業で修正する

上記1,2は関係性で決まります

その他

規模感

毎日、一ファイルで1000万レコードとかも普通にサクサク読込めるので、この辺りのボリュームだったらオケかと。(zipされてるcsvをよみこんでます)

改行コードの入り乱れ

config.NewLine <- "\n"(多分これは効かない...笑)

この設定が、効かない?感じなので、とりあえずバイナリで読み込んでLF以外はスペースにする等の処理したりして回避したりしてます。(今後に期待?)

情報がない?

あまり情報がなさげです(ググる力がないともいう。。。)トライ&エラーが必要です

放流的な🐟

現場からは以上です(。・ω・。)ノ Enjoy!

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1