Summary
CsvHelperのサンプルをF#で書いてみたというはなし
サンプル
C#という言語でサンプルコードが書かれてます
-- CsvHelper Sample
https://joshclose.github.io/CsvHelper/examples/
Thanks
いろいろと教えていただきました!(感謝!)
みどりさん(twitter id is @_midoliy_)
はぇ~さん(twitter id is @Haxe)
Read
sample1
Get Class Record
(*
Reading
--------------------------
01. Get Class Records
Convert CSV rows into class objects.
csv is
Id,Name
1,one
result is
seq [{ Id = 1 ; Name = "one" }]
*)
open System.IO
open System.Globalization
open CsvHelper
[<CLIMutable>]
type Foo = {
Id:int
Name:string
}
[<EntryPoint>]
let main argv =
use reader = new StreamReader("./foo.csv")
use csv = new CsvReader(reader,CultureInfo.CurrentCulture)
csv.GetRecords<Foo>()
|> printfn "%A"
0
sample2
Get Dynamic Records
(*
Reading
--------------------------
02. Get Dynamic Records
Convert CSV rows into dynamic objects.
Since there is no way to tell what type the properties should be,
all the properties on the dynamic object are strings.
csv is
Id,Name
1,one
result is
seq [Id, Name]
seq [seq [1, one]]
*)
open System.IO
open System.Globalization
open CsvHelper
open CsvHelper.Configuration
[<EntryPoint>]
let main argv =
use reader = new StreamReader("./foo.csv")
use csv = new CsvReader(reader,CultureInfo.CurrentCulture)
let arr =
// CSVを読込む
csv.GetRecords<obj>()
// obj を dict に型付けする
|> Seq.map( fun x -> x :?> ExpandoObject :> IDictionary<string,obj>)
// memory に乗せる(実際に計算する)
|> Seq.toArray
let keys = arr |> Seq.head |> Seq.map(fun (KeyValue(k,_)) -> k)
let vals = arr |> Seq.map(fun x -> x |> Seq.map(fun (KeyValue(_,v)) -> v))
printfn "%A" keys
printfn "%A" vals
0
sample3
Get Anonyumouse Type Records
下記のコードは実現不可(いまのところ)
(*
Reading
--------------------------
03. Get Anonyumouse Type Records
Convert CSV rows into anonymous type objects.
You just need to supply the anonymous type definition.
-> unEnable in F# way
fsharp / fslang-suggestions
Mutable contents in Anonymous Records #732
https://github.com/fsharp/fslang-suggestions/issues/732
csv is
Id,Name
1,one
*)
open System.IO
open System.Globalization
open CsvHelper
[<EntryPoint>]
let main argv =
use reader = new StreamReader("./foo.csv")
use csv = new CsvReader(reader,CultureInfo.CurrentCulture)
csv.GetRecords(
// unEnable in F# way, just now.
{| mutable Id = 9999; mutable Name = "" |}
)
|> printfn "%A"
0
sample4
Enumerate Class Records
(*
Reading
--------------------------
04. Enumerate Class Records
Convert CSV rows into a class object that is re-used on every iteration of the enumerable.
Each enumeration will hydrate the given record, but only the mapped members.
If you supplied a map and didn't map one of the members, that member will not get hydrated with the current row's data.
Be careful.
Any methods that you call on the projection that force the evaluation of the IEnumerable, such as ToList(),
you will get a list where all the records are the same instance you provided that is hydrated with the last record in the CSV file.
csv is
Id,Name
1,one
result is
{ Id = 1; Name = "one" }
*)
open System.IO
open System.Globalization
open CsvHelper
[<CLIMutable>]
type Foo = {
Id:int
Name:string
}
[<EntryPoint>]
let main argv =
use reader = new StreamReader("./foo.csv")
use csv = new CsvReader(reader,CultureInfo.CurrentCulture)
let records = csv.EnumerateRecords({ Id = 0 ; Name = "" })
for r in records do
printfn "%A" r
0
sample5
Reading by Hand
(*
Reading
--------------------------
05. Reading by Hand
Sometimes it's easier to not try and configure a mapping to match your class definition for various reasons.
It's usually only a few more lines of code to just read the rows by hand instead.
csv is
Id,Name
1,one
result is
seq [{ Id = 1 ; Name = "one" }]
*)
open System.IO
open System.Globalization
open CsvHelper
[<CLIMutable>]
type Foo = {
Id:int
Name:string
}
[<EntryPoint>]
let main argv =
use reader = new StreamReader("./foo.csv")
use csv = new CsvReader(reader,CultureInfo.CurrentCulture)
let records = new ResizeArray<Foo>()
csv.Read() |> ignore
// You must call ReadHeader() before any fields can be retrieved by name
csv.ReadHeader() |> ignore
while (csv.Read()) do
records.Add(
{
Id = csv.GetField<int>("Id")
Name = csv.GetField<string>("Name")
}
)
records
|> printfn "%A"
0
sample6
Reading Multiple Data Sets
下記のコードだとエラーになる(追って調査)
(*
Reading
--------------------------
06. Reading Multiple Data Sets
For some reason there are CSV files out there that contain multiple sets of CSV data in them.
You should be able to read files like this without issue.
You will need to detect when to change class types you are retreiving.
csv is
FooId,Name
1,foo
BarId,Name
07a0fca2-1b1c-4e44-b1be-c2b05da5afc7,bar
result is
seq [{ Id = "FooId"; Name = "Name" }; { Id = "1"; Name = "foo" }]
seq [{ Id = "BarId"; Name = "Name" }; { Id = "07a0fca2-1b1c-4e44-b1be-c2b05da5afc7"; Name = "bar" }]
*)
open System
open System.IO
open System.Globalization
open CsvHelper
open CsvHelper.Configuration
[<CLIMutable>]
type Foo = {
(*
Id:int にするとエラーになる
Unhandled exception. CsvHelper.TypeConversion.TypeConverterException: The conversion cannot be performed.
Text: 'FooId'
MemberType: System.Int32
TypeConverter: 'CsvHelper.TypeConversion.Int32Converter'
*)
// Id:int
Id:string
Name:string
}
[<Sealed>]
type FooMap () as this =
inherit ClassMap<Foo>()
do
this.Map(fun x -> x.Id).Name([|"FooId"|]) |> ignore
this.Map(fun x -> x.Name) |> ignore
[<CLIMutable>]
type Bar = {
(*
Id: System.Guidにするとエラーになる
Unhandled exception. CsvHelper.ReaderException: An unexpected error occurred.
---> System.FormatException: Guid should contain 32 digits with 4 dashes (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).
*)
// Id: System.Guid
Id: string
Name:string
}
[<Sealed>]
type BarMap () as this =
inherit ClassMap<Bar>()
do
this.Map(fun x -> x.Id).Name([|"BarId"|]) |> ignore
this.Map(fun x -> x.Name) |> ignore
[<EntryPoint>]
let main argv =
let csvConfig : CsvConfiguration =
CsvConfiguration(CultureInfo.CurrentCulture)
|> fun x ->
x.IgnoreBlankLines <- false
x.RegisterClassMap<FooMap>() |> ignore
x.RegisterClassMap<BarMap>() |> ignore
x
use reader = new StreamReader("./multipleDataSets.csv")
use csv = new CsvReader(reader,csvConfig)
let fooRecords = new ResizeArray<Foo>()
let barRecords = new ResizeArray<Bar>()
let mutable isHeader = true
while (csv.Read()) do
if isHeader then
csv.ReadHeader() |> ignore
isHeader <- false
if String.IsNullOrEmpty(csv.GetField(0)) then
isHeader <- true
else
match ( csv.Context.HeaderRecord.[0] )with
| "FooId" -> fooRecords.Add(csv.GetRecord<Foo>())
| "BarId" -> barRecords.Add(csv.GetRecord<Bar>())
| _ -> failwithf "error"
fooRecords |> printfn "%A"
barRecords |> printfn "%A"
0
sample7
Reading Multiple Record Types
(*
Reading
--------------------------
07. Reading Multiple Record Types
If you have CSV data where each row may be a different record type,
you should be able to read based on a row type or something similar.
csv is
A,1,foo
B,"あいうえお",bar
result is
seq [{ Id = 1 ; Name = "foo" }]
seq [{ Id = "あいうえお" ; Name = "bar" }]
*)
open System.IO
open System.Globalization
open CsvHelper
open CsvHelper.Configuration
[<CLIMutable>]
type Foo = {
Id:int
Name:string
}
[<Sealed>]
type FooMap () as this =
inherit ClassMap<Foo>()
do
this.Map(fun x -> x.Id).Index(1) |> ignore
this.Map(fun x -> x.Name).Index(2) |> ignore
[<CLIMutable>]
type Bar = {
// Id: System.Guid
Id : string
Name:string
}
[<Sealed>]
type BarMap () as this =
inherit ClassMap<Bar>()
do
this.Map(fun x -> x.Id).Index(1) |> ignore
this.Map(fun x -> x.Name).Index(2) |> ignore
[<EntryPoint>]
let main argv =
let csvConfig : CsvConfiguration =
CsvConfiguration(CultureInfo.CurrentCulture)
|> fun x ->
x.HasHeaderRecord <- false
x.RegisterClassMap<FooMap>() |> ignore
x.RegisterClassMap<BarMap>() |> ignore
x
use reader = new StreamReader("./multipleRecordTypes.csv")
use csv = new CsvReader(reader,csvConfig)
let fooRecords = new ResizeArray<Foo>()
let barRecords = new ResizeArray<Bar>()
while (csv.Read()) do
match (csv.GetField(0)) with
| "A" -> fooRecords.Add(csv.GetRecord<Foo>())
| "B" -> barRecords.Add(csv.GetRecord<Bar>())
| _ -> failwithf "error"
fooRecords |> printfn "%A"
barRecords |> printfn "%A"
0
Configuration ClassMap
ConstantValue
(*
Configuration
--------------------------
ConstantValue
You can set a constant value to a property
instead of mapping it to a field.
=> headerless csv only( header row needs to skip 1 Rows )
=> The constant statement must be listed below.
csv is
Id,Name
1,one
2,two
result is
{ Id = 1
Name = "one"
IsDirty = true }
{ Id = 2
Name = "two"
IsDirty = true }
*)
open System.IO
open System.Globalization
open CsvHelper
open CsvHelper.Configuration
[<CLIMutable>]
type Foo = {
Id:int
Name:string
IsDirty:bool
}
[<Sealed>]
type FooMap () as this =
inherit ClassMap<Foo>()
do
this.Map(fun m -> m.Id) |> ignore
this.Map(fun m -> m.Name) |> ignore
// The constant statement must be listed below.
this.Map(fun m -> m.IsDirty).Constant(true) |> ignore
// util func
let skipRows (csv:CsvReader) i =
for j in [1..i] do
csv.Read() |> ignore
[<EntryPoint>]
let main argv =
let csvConfig : CsvConfiguration =
CsvConfiguration(CultureInfo.CurrentCulture)
|> fun x ->
// headerless csv only
x.HasHeaderRecord <- false
x.RegisterClassMap<FooMap>() |> ignore
x
use reader = new StreamReader("./CsvHelperSample/csv/foo.csv")
use csv = new CsvReader(reader,csvConfig)
skipRows csv 1
while (csv.Read()) do
csv.GetRecord<Foo>()
|> printfn "%A"
0
Validation
(*
Configuration
--------------------------
Validation
If you want to ensure your data conforms to some sort of standard,
you can validate it.
csv is
Id,Name
1,on-e
2,two
result is
データ形式がおかしいです
該当行番号:2
該当データ:1,on-e
データOK: { Id = 2 ; Name = "two" }
*)
open System.IO
open System.Globalization
open CsvHelper
open CsvHelper.Configuration
[<CLIMutable>]
type Foo = {
Id:int
Name:string
}
[<Sealed>]
type FooMap () as this =
inherit ClassMap<Foo>()
do
this.Map(fun x -> x.Id) |> ignore
this.Map(fun x -> x.Name).Validate(fun field -> field.Contains("-") |> not ) |> ignore
[<EntryPoint>]
let main argv =
let csvConfig : CsvConfiguration =
CsvConfiguration(CultureInfo.CurrentCulture)
|> fun x ->
x.RegisterClassMap<FooMap>() |> ignore
x
use reader = new StreamReader("./CsvHelperSample/csv/bar.csv")
use csv = new CsvReader(reader,csvConfig)
while (csv.Read()) do
try
csv.GetRecord<Foo>()
|> printfn "データOK: %A"
with
| :? CsvHelper.FieldValidationException ->
printfn "データ形式がおかしいです"
printfn $"該当行番号:{csv.Context.RawRow}"
printfn $"該当データ:{csv.Context.RawRecord.TrimEnd('\n')}"
| _ -> printfn "その他エラー"
0
現場からは以上です