F# 读取定长文本文件

6

你好,我正在寻找使用F#读取固定宽度文本文件的最佳方法。该文件将是纯文本,长度从一到几千行不等,每行大约1000个字符宽。每行包含大约50个字段,每个字段长度不同。我的初步想法是采用以下方式:

type MyRecord = {
    Name : string
    Address : string
    Postcode : string
    Tel : string
}

let format = [
    (0,10)
    (10,50)
    (50,7)
    (57,20)
]

逐行阅读,按照格式元组分配每个字段(其中第一个项目是起始字符,第二个项目是字符宽度)。

如果有任何指针或建议,将不胜感激。


解析复杂格式可能需要特殊工具,例如解析器组合器。请查看 [tag:fparsec];这里有很多关于此主题的问题。这种方法最大的优点是您可以单独定义(和调试)各个解析器,然后将它们链接起来处理复杂输入。 - Be Brave Be Like Ukraine
3个回答

4

最困难的部分可能是根据列格式拆分单行内容。可以像这样完成:

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,函数返回一个 string list

您可以像这样映射一组行:

let result = lines |> List.map (splitLine format)

你也可以使用 Seq.mapArray.map, 取决于 lines 的定义方式。这样的 result 将是一个 string list list,你现在可以映射这样的列表以生成一个 MyRecord list
你可以使用File.ReadLines从文件中获取一个惰性评估的字符串序列。
请注意,上述只是可能解决方案的概述。 我省略了边界检查、错误处理等。 上面的代码可能包含一位偏移错误。

4

以下是一个专注于自定义验证和为每个字段处理错误的解决方案。对于仅包含数字数据的数据文件,这可能有些多余!

首先,对于这类问题,我喜欢使用Microsoft.VisualBasic.dll中的解析器,因为它已经可以在不使用NuGet的情况下使用。

对于每一行,我们可以返回字段数组和行号(用于错误报告)。

#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}

然后,您可以定义特定领域的解析代码。对于每个字段,我都创建了一个验证函数(validateIdvalidateName等)。 不需要验证的字段可以通过原始数据传递(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解析库可以在解析时进行验证),但它确实展示了如何完全控制验证和错误处理,如果需要的话。


1
一条包含50个字段的记录有点不方便,因此可能更喜欢允许动态生成数据结构的替代方法(例如System.Data.DataRow)。
如果必须是记录,您仍然可以使用Reflection来避免手动分配每个记录字段并填充它。这个技巧依赖于它们被定义的字段顺序。我假设每个固定宽度的列都代表一个记录字段,因此启动索引是暗示的。
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";}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接