F# and CSVHelper でCSVファイルを読み込んでみた！

# Summary

ここ一年近くCSVファイルを読み込むのに苦労してましたが、

だいぶ形になってきたので書いてみたいと思います

F# and CSVHelper でCSVファイルを読み込んでみました。

## 動作概要

1.　CSVファイル（フルーツ.csv）を読み込む
2.　読込んだレコードを出力する

読込元CSV

```csvs
りんご,赤,300,2020/12/11
チェリー,黒,450,2020/12/12
```

```csvs
ドラゴンフルーツ,赤紫,"1,200",2020/12/24
キャベツ,緑,,2020/12/30
```

出力例

```fsharp
[|seq
    [{ ファイル作成日時 = 2021/01/27 11:33:32
       果物名 = "ドラゴンフルーツ"
       色 = "赤紫"
       価格 = "1,200"
       購入日 = 2020/12/24 0:00:00 }; { ファイル作成日時 = 2021/01/27 11:33:32
                                     果物名 = "キャベツ"
                                     色 = "緑"
                                     価格 = ""
                                     購入日 = 2020/12/30 0:00:00 };
     { ファイル作成日時 = 2021/01/27 11:33:32
       果物名 = "りんご"
       色 = "赤"
       価格 = "300"
       購入日 = 2020/12/11 0:00:00 }; { ファイル作成日時 = 2021/01/27 11:33:32
                                     果物名 = "チェリー"
                                     色 = "黒"
                                     価格 = "450"
                                     購入日 = 2020/12/12 0:00:00 }]|]

```

## ソースコードの構成

こんな感じ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/149269/db5fd771-ecea-31a0-b08c-80ed66291e48.png)

### csvRead_log.fs

エラーになった場合の受け皿を作ります

```fsharp
namespace CsvRead

module Log =

  type ErrorRecordLog = {
    処置済 : bool
    ファイル作成日時 : System.DateTime
    ファイル名 : string
    エラーレコード : string
  }

```


### csvRead_util.fs

csvを読み込む際のちょっとした自作便利関数をここに書きます

```fsharp
namespace CsvRead

module public CsvUtil =

  module public Encoding =

    open System.Text

    let public shiftJIS =
      Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)
      Encoding.GetEncoding 932

    let public utf8 =
      Encoding.GetEncoding 65001

  module public Files =

    open System.IO
    open System.Text.RegularExpressions

    let targetFiles1 srcDir fileExtension filePattern : option<string[]> =
      System.IO.Directory.EnumerateFiles(srcDir, "*" + fileExtension)
      |> Seq.filter(fun fp -> Regex.IsMatch(fp,filePattern) )
      |> Seq.sortByDescending(fun fp -> File.GetLastWriteTime(fp))
      |> Seq.map(fun fp -> Path.Combine(Directory.GetCurrentDirectory() , fp ) |> fun s -> Path.GetFullPath(s))
      |> fun x -> x |> Seq.toArray
      |> fun x -> if Array.isEmpty x then None else Some x
```

### csvRead_global.fs

CSVを読み込む際の一般的な情報をここに書きます（フォルダ場所とか）

```fsharp
namespace CsvRead

module public GlobalDataStore =
  open System.Collections.Concurrent
  // ★ multiple record type の場合は必要に応じて数量を増やす
  let num = 1
  let cqGoods = Array.init num (fun _ -> new ConcurrentQueue<obj>() )
  let cqBad  = new ConcurrentQueue<obj>()


/// ★読込対象のCSVファイルに関する一般設定
module public GlobalSetting =

  let csvSetting = {|
    csvFolder = "./../csv"
    extension = "*.csv"
    fileRegPattern = ".*フルーツ.*"
    fileEncoding = Encoding.utf8
  |}
```


### csvRead_csv.fs

読込対象CSVの構造をここに書きます

```fsharp
namespace CsvRead

  open System
  open CsvHelper.Configuration

  [<CLIMutable>]
  type public Csv = {

    mutable ファイル作成日時 : DateTime
    果物名 : string
    色 : string
    価格 : string
    購入日 : Nullable<DateTime>

  }

  [<Sealed>]
  type public CsvMap () as this =
    inherit ClassMap<Csv>()
    do
      this.Map(fun x -> x.ファイル作成日時).Constant( System.DateTime(9999,12,31,12,59,59) ) |> ignore
      this.Map(fun x -> x.果物名).Index(0) |> ignore
      this.Map(fun x -> x.色).Index(1) |> ignore
      this.Map(fun x -> x.価格).Index(2) |> ignore
      this.Map(fun x -> x.購入日).Index(3) |> ignore
```


### csvRead_csvhelperWrap.fs

CSVHelperのラッパーをここに書きます

```fsharp
namespace CsvRead

module public CsvHelperWrap =

  open System.Globalization
  open System.IO
  open CsvHelper
  open CsvHelper.Configuration
  open GlobalDataStore
  open CsvRead.Log


  // ★Csv読込設定
  let private csvConfig (fp:string)  : CsvConfiguration  =

    CsvConfiguration(CultureInfo.CurrentCulture)
    |> fun x ->

      x.Delimiter <- ","
      x.HasHeaderRecord <- false
      x.TrimOptions <- TrimOptions.Trim
      x.IgnoreBlankLines <- true
      x.ShouldSkipRecord <- (fun arr -> arr.[0] = "EOF" )

      // ★Multiple Record type の場合はここをレコードタイプ毎に追加していく
      x.RegisterClassMap<CsvMap>() |> ignore

      // カラムが不足していてエラーを出したくない場合は null を設定する
      x.MissingFieldFound <- null

      x.BadDataFound <- fun ctx ->
        [|
          $"データ番号:{ctx.RawRow} : データ形式がおかしいです BadData"
          ctx.RawRecord.Trim()
        |]
        |> fun errRcd ->
          cqBad.Enqueue(
              {
                処置済 = false
                ファイル作成日時 = File.GetCreationTime(fp)
                ファイル名 = fp |> System.IO.Path.GetFileNameWithoutExtension
                エラーレコード = errRcd |> String.concat ","
              }
        )

      x

  let public csvRead (streamReader:StreamReader) (fp:string) =

    use csv = new CsvReader(streamReader , csvConfig fp)

    // ★ヘッダーがある場合
    // let skipRows (csv:CsvReader) i =
    //   for j in [1..i] do
    //     csv.Read() |> ignore
    // skipRows csv 1

    while (csv.Read()) do

      try
        cqGoods
        |> Array.item 0
        |> fun x ->
          let tmp = csv.GetRecord<Csv>()
          tmp.ファイル作成日時 <- File.GetCreationTime(fp)
          x.Enqueue( tmp )
      with _ ->

        [|
          $"データ番号:{csv.Context.RawRow} : 何かしらのエラーです"
          csv.Context.RawRecord.Trim()
        |]
        |> fun errRcd ->
          cqBad.Enqueue(
            {
              処置済 = false
              ファイル作成日時 = File.GetCreationTime(fp)
              ファイル名 = fp |> System.IO.Path.GetFileNameWithoutExtension
              エラーレコード = errRcd |> String.concat ","
            }
        )
```

### csvRead_main.fs

ファイル処理をここに書きます

```fsharp
namespace CsvRead

open GlobalSetting

module Main =

  let public CsvRead1 () =
    CsvUtil.Files.targetFiles1 csvSetting.csvFolder csvSetting.extension csvSetting.fileRegPattern
    |> fun x ->
      match Option.isNone x with
      | true -> ()
      | false ->
        Option.get x
        |> Array.iter(fun fp ->
            new System.IO.StreamReader( fp , csvSetting.fileEncoding)
            |> fun sr -> CsvHelperWrap.csvRead sr fp
        )
```

### main.fs

```fsharp
open CsvRead
open GlobalDataStore

[<EntryPoint>]
let main argv =

  Main.CsvRead1 ()

  cqGoods
  |> printfn "%A"

  0
```

現場からは以上です