F# 读取固定宽度的文本文件
F# Read Fixed Width Text File
您好,我正在寻找使用 F# 读取固定宽度文本文件的最佳方法。该文件将是纯文本,长一到几千行,宽约 1000 个字符。每行包含大约 50 个字段,每个字段的长度各不相同。我最初的想法是像下面这样
type MyRecord = {
Name : string
Address : string
Postcode : string
Tel : string
}
let format = [
(0,10)
(10,50)
(50,7)
(57,20)
]
并逐行读取,通过格式元组分配每个字段(其中第一项是起始字符,第二项是字符宽度)。
任何指点将不胜感激。
最难的部分可能是根据列格式拆分一行。可以这样做:
let splitLine format (line : string) =
format |> List.map (fun (index, length) -> line.Substring(index, length))
此函数的类型为 (int * int) list -> string -> string list
。换句话说,format
是一个 (int * int) list
。这与您的 format
列表完全一致。 line
参数是一个 string
,函数 returns 是一个 string list
.
您可以像这样映射行列表:
let result = lines |> List.map (splitLine format)
您也可以使用 Seq.map
或 Array.map
,具体取决于 lines
的定义方式。这样的 result
将是 string list list
,您现在可以映射这样的列表以生成 MyRecord list
.
您可以使用 File.ReadLines
从文件中获取延迟计算的字符串序列。
请注意,以上只是可能解决方案的概要。我省略了边界检查、错误处理等。以上代码可能存在差一错误。
50 个字段的记录有点笨拙,因此允许动态生成数据结构的替代方法可能更可取(例如 System.Data.DataRow)。
如果它无论如何都必须是一条记录,您至少可以省去对每个记录字段的手动分配,并在反射的帮助下填充它。这个技巧依赖于定义的字段顺序。我假设固定宽度的每一列都代表一个记录字段,因此隐含了起始索引。
open Microsoft.FSharp.Reflection
type MyRecord = {
Name : string
Address : string
City : string
Postcode : string
Tel : string } with
static member CreateFromFixedWidth format (line : string) =
let fields =
format
|> List.fold (fun (index, acc) length ->
let str = line.[index .. index + length - 1].Trim()
index + length, box str :: acc )
(0, [])
|> snd
|> List.rev
|> List.toArray
FSharpValue.MakeRecord(
typeof<MyRecord>,
fields ) :?> MyRecord
示例数据:
"Postman Pat " +
"Farringdon Road " +
"London " +
"EC1A 1BB" +
"+44 20 7946 0813"
|> MyRecord.CreateFromFixedWidth [16; 16; 16; 8; 16]
// val it : MyRecord = {Name = "Postman Pat";
// Address = "Farringdon Road";
// City = "London";
// Postcode = "EC1A 1BB";
// Tel = "+44 20 7946 0813";}
这是一个专注于每个字段的自定义验证和错误处理的解决方案。对于仅包含数字数据的数据文件,这可能有点矫枉过正!
首先,对于这类事情,我喜欢使用 Microsoft.VisualBasic.dll
中的解析器,因为它无需使用 NuGet 即可使用。
对于每一行,我们可以return字段数组和行号(用于错误报告)
#r "Microsoft.VisualBasic.dll"
// for each row, return the line number and the fields
let parserReadAllFields fieldWidths textReader =
let parser = new Microsoft.VisualBasic.FileIO.TextFieldParser(reader=textReader)
parser.SetFieldWidths fieldWidths
parser.TextFieldType <- Microsoft.VisualBasic.FileIO.FieldType.FixedWidth
seq {while not parser.EndOfData do
yield parser.LineNumber,parser.ReadFields() }
接下来,我们需要一个小的错误处理库(更多信息参见http://fsharpforfunandprofit.com/rop/)
type Result<'a> =
| Success of 'a
| Failure of string list
module Result =
let succeedR x =
Success x
let failR err =
Failure [err]
let mapR f xR =
match xR with
| Success a -> Success (f a)
| Failure errs -> Failure errs
let applyR fR xR =
match fR,xR with
| Success f,Success x -> Success (f x)
| Failure errs,Success _ -> Failure errs
| Success _,Failure errs -> Failure errs
| Failure errs1, Failure errs2 -> Failure (errs1 @ errs2)
然后定义你的领域模型。在这种情况下,它是记录类型,文件中的每个字段都有一个字段。
type MyRecord =
{id:int; name:string; description:string}
然后您可以定义特定于域的解析代码。我为每个字段创建了一个验证函数(validateId
、validateName
等)。
不需要验证的字段可以通过原始数据(validateDescription
)。
在 fieldsToRecord
中,各个字段使用应用样式(<!>
和 <*>
)组合。
有关更多信息,请参阅 http://fsharpforfunandprofit.com/posts/elevated-world-3/#validation.
最后,readRecords
将每个输入行映射到一个记录结果,并只选择成功的。失败的将写入 handleResult
.
中的日志
module MyFileParser =
open Result
let createRecord id name description =
{id=id; name=name; description=description}
let validateId (lineNo:int64) (fields:string[]) =
let rawId = fields.[0]
match System.Int32.TryParse(rawId) with
| true, id -> succeedR id
| false, _ -> failR (sprintf "[%i] Can't parse id '%s'" lineNo rawId)
let validateName (lineNo:int64) (fields:string[]) =
let rawName = fields.[1]
if System.String.IsNullOrWhiteSpace rawName then
failR (sprintf "[%i] Name cannot be blank" lineNo )
else
succeedR rawName
let validateDescription (lineNo:int64) (fields:string[]) =
let rawDescription = fields.[2]
succeedR rawDescription // no validation
let fieldsToRecord (lineNo,fields) =
let (<!>) = mapR
let (<*>) = applyR
let validatedId = validateId lineNo fields
let validatedName = validateName lineNo fields
let validatedDescription = validateDescription lineNo fields
createRecord <!> validatedId <*> validatedName <*> validatedDescription
/// print any errors and only return good results
let handleResult result =
match result with
| Success record -> Some record
| Failure errs -> printfn "ERRORS %A" errs; None
/// return a sequence of records
let readRecords parserOutput =
parserOutput
|> Seq.map fieldsToRecord
|> Seq.choose handleResult
下面是一个实际解析的例子:
// Set up some sample text
let text = """01name1description1
02name2description2
xxname3badid-------
yy badidandname
"""
// create a low-level parser
let textReader = new System.IO.StringReader(text)
let fieldWidths = [| 2; 5; 11 |]
let parserOutput = parserReadAllFields fieldWidths textReader
// convert to records in my domain
let records =
parserOutput
|> MyFileParser.readRecords
|> Seq.iter (printfn "RECORD %A") // print each record
输出将如下所示:
RECORD {id = 1;
name = "name1";
description = "description";}
RECORD {id = 2;
name = "name2";
description = "description";}
ERRORS ["[3] Can't parse id 'xx'"]
ERRORS ["[4] Can't parse id 'yy'"; "[4] Name cannot be blank"]
这绝不是解析文件的最有效方法(我认为 NuGet 上有一些 CSV 解析库可以在解析时进行验证)但它确实展示了如何完全控制验证和如果需要,可以进行错误处理。
您好,我正在寻找使用 F# 读取固定宽度文本文件的最佳方法。该文件将是纯文本,长一到几千行,宽约 1000 个字符。每行包含大约 50 个字段,每个字段的长度各不相同。我最初的想法是像下面这样
type MyRecord = {
Name : string
Address : string
Postcode : string
Tel : string
}
let format = [
(0,10)
(10,50)
(50,7)
(57,20)
]
并逐行读取,通过格式元组分配每个字段(其中第一项是起始字符,第二项是字符宽度)。
任何指点将不胜感激。
最难的部分可能是根据列格式拆分一行。可以这样做:
let splitLine format (line : string) =
format |> List.map (fun (index, length) -> line.Substring(index, length))
此函数的类型为 (int * int) list -> string -> string list
。换句话说,format
是一个 (int * int) list
。这与您的 format
列表完全一致。 line
参数是一个 string
,函数 returns 是一个 string list
.
您可以像这样映射行列表:
let result = lines |> List.map (splitLine format)
您也可以使用 Seq.map
或 Array.map
,具体取决于 lines
的定义方式。这样的 result
将是 string list list
,您现在可以映射这样的列表以生成 MyRecord list
.
您可以使用 File.ReadLines
从文件中获取延迟计算的字符串序列。
请注意,以上只是可能解决方案的概要。我省略了边界检查、错误处理等。以上代码可能存在差一错误。
50 个字段的记录有点笨拙,因此允许动态生成数据结构的替代方法可能更可取(例如 System.Data.DataRow)。
如果它无论如何都必须是一条记录,您至少可以省去对每个记录字段的手动分配,并在反射的帮助下填充它。这个技巧依赖于定义的字段顺序。我假设固定宽度的每一列都代表一个记录字段,因此隐含了起始索引。
open Microsoft.FSharp.Reflection
type MyRecord = {
Name : string
Address : string
City : string
Postcode : string
Tel : string } with
static member CreateFromFixedWidth format (line : string) =
let fields =
format
|> List.fold (fun (index, acc) length ->
let str = line.[index .. index + length - 1].Trim()
index + length, box str :: acc )
(0, [])
|> snd
|> List.rev
|> List.toArray
FSharpValue.MakeRecord(
typeof<MyRecord>,
fields ) :?> MyRecord
示例数据:
"Postman Pat " +
"Farringdon Road " +
"London " +
"EC1A 1BB" +
"+44 20 7946 0813"
|> MyRecord.CreateFromFixedWidth [16; 16; 16; 8; 16]
// val it : MyRecord = {Name = "Postman Pat";
// Address = "Farringdon Road";
// City = "London";
// Postcode = "EC1A 1BB";
// Tel = "+44 20 7946 0813";}
这是一个专注于每个字段的自定义验证和错误处理的解决方案。对于仅包含数字数据的数据文件,这可能有点矫枉过正!
首先,对于这类事情,我喜欢使用 Microsoft.VisualBasic.dll
中的解析器,因为它无需使用 NuGet 即可使用。
对于每一行,我们可以return字段数组和行号(用于错误报告)
#r "Microsoft.VisualBasic.dll"
// for each row, return the line number and the fields
let parserReadAllFields fieldWidths textReader =
let parser = new Microsoft.VisualBasic.FileIO.TextFieldParser(reader=textReader)
parser.SetFieldWidths fieldWidths
parser.TextFieldType <- Microsoft.VisualBasic.FileIO.FieldType.FixedWidth
seq {while not parser.EndOfData do
yield parser.LineNumber,parser.ReadFields() }
接下来,我们需要一个小的错误处理库(更多信息参见http://fsharpforfunandprofit.com/rop/)
type Result<'a> =
| Success of 'a
| Failure of string list
module Result =
let succeedR x =
Success x
let failR err =
Failure [err]
let mapR f xR =
match xR with
| Success a -> Success (f a)
| Failure errs -> Failure errs
let applyR fR xR =
match fR,xR with
| Success f,Success x -> Success (f x)
| Failure errs,Success _ -> Failure errs
| Success _,Failure errs -> Failure errs
| Failure errs1, Failure errs2 -> Failure (errs1 @ errs2)
然后定义你的领域模型。在这种情况下,它是记录类型,文件中的每个字段都有一个字段。
type MyRecord =
{id:int; name:string; description:string}
然后您可以定义特定于域的解析代码。我为每个字段创建了一个验证函数(validateId
、validateName
等)。
不需要验证的字段可以通过原始数据(validateDescription
)。
在 fieldsToRecord
中,各个字段使用应用样式(<!>
和 <*>
)组合。
有关更多信息,请参阅 http://fsharpforfunandprofit.com/posts/elevated-world-3/#validation.
最后,readRecords
将每个输入行映射到一个记录结果,并只选择成功的。失败的将写入 handleResult
.
module MyFileParser =
open Result
let createRecord id name description =
{id=id; name=name; description=description}
let validateId (lineNo:int64) (fields:string[]) =
let rawId = fields.[0]
match System.Int32.TryParse(rawId) with
| true, id -> succeedR id
| false, _ -> failR (sprintf "[%i] Can't parse id '%s'" lineNo rawId)
let validateName (lineNo:int64) (fields:string[]) =
let rawName = fields.[1]
if System.String.IsNullOrWhiteSpace rawName then
failR (sprintf "[%i] Name cannot be blank" lineNo )
else
succeedR rawName
let validateDescription (lineNo:int64) (fields:string[]) =
let rawDescription = fields.[2]
succeedR rawDescription // no validation
let fieldsToRecord (lineNo,fields) =
let (<!>) = mapR
let (<*>) = applyR
let validatedId = validateId lineNo fields
let validatedName = validateName lineNo fields
let validatedDescription = validateDescription lineNo fields
createRecord <!> validatedId <*> validatedName <*> validatedDescription
/// print any errors and only return good results
let handleResult result =
match result with
| Success record -> Some record
| Failure errs -> printfn "ERRORS %A" errs; None
/// return a sequence of records
let readRecords parserOutput =
parserOutput
|> Seq.map fieldsToRecord
|> Seq.choose handleResult
下面是一个实际解析的例子:
// Set up some sample text
let text = """01name1description1
02name2description2
xxname3badid-------
yy badidandname
"""
// create a low-level parser
let textReader = new System.IO.StringReader(text)
let fieldWidths = [| 2; 5; 11 |]
let parserOutput = parserReadAllFields fieldWidths textReader
// convert to records in my domain
let records =
parserOutput
|> MyFileParser.readRecords
|> Seq.iter (printfn "RECORD %A") // print each record
输出将如下所示:
RECORD {id = 1;
name = "name1";
description = "description";}
RECORD {id = 2;
name = "name2";
description = "description";}
ERRORS ["[3] Can't parse id 'xx'"]
ERRORS ["[4] Can't parse id 'yy'"; "[4] Name cannot be blank"]
这绝不是解析文件的最有效方法(我认为 NuGet 上有一些 CSV 解析库可以在解析时进行验证)但它确实展示了如何完全控制验证和如果需要,可以进行错误处理。