この記事は 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以外はスペースにする等の処理したりして回避したりしてます。(今後に期待?)
情報がない?
あまり情報がなさげです(ググる力がないともいう。。。)トライ&エラーが必要です
放流的な🐟