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 }]|]
ソースコードの構成
こんな感じ
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
現場からは以上です