LoginSignup
1
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-01-27

Summary

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

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

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

動作概要

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

読込元CSV

りんご,,300,2020/12/11
チェリー,,450,2020/12/12
ドラゴンフルーツ,赤紫,"1,200",2020/12/24
キャベツ,,,2020/12/30

出力例

[|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

csvRead_log.fs

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

namespace CsvRead

module Log =

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

csvRead_util.fs

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

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を読み込む際の一般的な情報をここに書きます(フォルダ場所とか)

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の構造をここに書きます

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のラッパーをここに書きます

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

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

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

open CsvRead
open GlobalDataStore

[<EntryPoint>]
let main argv =

  Main.CsvRead1 ()

  cqGoods
  |> printfn "%A"

  0

現場からは以上です

1
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
1
1