【问题标题】:F# Read Fixed Width Text FileF# 读取固定宽度的文本文件
【发布时间】:2015-09-16 21:08:39
【问题描述】:

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

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

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

并逐行读取,按格式元组分配每个字段(其中第一项是开始字符,第二项是字符宽)。

任何指针将不胜感激。

【问题讨论】:

  • 解析复杂的格式可能需要一个特殊的工具,比如解析器组合器。看看fparsec;关于这个主题有很多问题。支持这种方法的最大优势是您可以单独定义(和调试)各个解析器,然后将它们链接起来以处理复杂的输入。

标签: f# fixed-width


【解决方案1】:

最困难的部分可能是根据列格式拆分单行。可以这样做:

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 从文件中获取惰性求值的字符串序列。

请注意,以上只是可能解决方案的概要。我省略了边界检查、错误处理等。上面的代码可能包含一个错误。

【讨论】:

    【解决方案2】:

    这是一个专注于每个字段的自定义验证和错误处理的解决方案。对于仅包含数字数据的数据文件,这可能是矫枉过正!

    首先,对于这类事情,我喜欢使用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 中,各种字段使用应用样式(&lt;!&gt;&lt;*&gt;)进行组合。 有关这方面的更多信息,请参阅http://fsharpforfunandprofit.com/posts/elevated-world-3/#validation

    最后,readRecords 将每个输入行映射到记录 Result 并仅选择成功的行。失败的将写入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 解析库可以在解析时进行验证),但它确实展示了如何完全控制验证和错误处理如果你需要的话。

    【讨论】:

      【解决方案3】:

      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";}
      

      【讨论】:

        猜你喜欢
        • 2013-01-01
        • 2016-01-18
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-05-19
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多