65.9K
CodeProject 正在变化。 阅读更多。
Home

FileUtilities - 将平面文件读入 POCO 的库

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (14投票s)

2015 年 3 月 26 日

CPOL

5分钟阅读

viewsIcon

29760

downloadIcon

948

一个帮助将平面文件(.csv、制表符分隔等)转换为具有属性验证的类型安全 CLR 对象的实用程序

引言

尽管许多现代应用程序以定义明确的标准方式(如 XML 或 JSON)交换数据,但现实情况是,仍有大量数据采用平面文件(我指的是逗号分隔值或固定宽度文本文件)进行打包。

该库旨在将这些文件转换为类型安全的对象的数组,并使用对象中的语义来验证相关文件。

背景

在 Entity Framework 和 System.Runtime.Serialization 中,使用属性标记类的属性是很常见的。该库非常遵循这种模式,但如果您不熟悉它,我建议您首先阅读这篇文章

Using the Code

作为实践示例,我们将从一个包含雅虎提供的 Microsoft 股票历史价格信息的平面文件开始。

我们希望将此文件转换为一个名为 StockPrice 的类的 IEnumerable,该类可以这样声明:

Public Class YahooStockPrice
    Public Property PriceDate As Date
    Public Property OpenPrice As Nullable(Of Decimal)
    Public Property HighPrice As Nullable(Of Decimal)
    Public Property LowPrice As Nullable(Of Decimal)
    Public Property ClosingPrice As Nullable(Of Decimal)
    Public Property Volume As Nullable(Of Decimal)
    Public Property AdjustedClose As Nullable(Of Decimal)
End Class

字段分隔符和字段定义

您使用 RecordFieldSeperator 属性在类级别指定字段分隔符。有四种可能的标准分隔符(逗号、制表符、管道符或分号),或者您可以指定自己的自定义分隔符作为字符数组。

然后,您要读取的每个字段都由 RecordColumn 属性指定。它接收一个列标题名称和字段的序数位置。您不必指定每个字段,但(显然)任何尝试将两个属性指定到同一列位置的尝试都会抛出错误。

Imports FileUtilities

<RecordFieldSeperator(FileUtilities.RecordFieldSeperatorAttribute.StandardSeparators.Comma)>
Public Class YahooStockPrice

  <RecordColumn("Date", 0)>
  Public Property PriceDate As Date

  <RecordColumn("Open", 1)>
  Public Property OpenPrice As Nullable(Of Decimal)

  <RecordColumn("High", 2)>
  Public Property HighPrice As Nullable(Of Decimal)

  <RecordColumn("Low", 3)>
  Public Property LowPrice As Nullable(Of Decimal)

  <RecordColumn("Closing", 4)>
  Public Property ClosingPrice As Nullable(Of Decimal)

  <RecordColumn("Volume", 5)>
  Public Property Volume As Nullable(Of Decimal)

  <RecordColumn("Adj Close", 6)>
  Public Property AdjustedClose As Nullable(Of Decimal)

End Class

固定宽度文件

文本文件的另一种可能性是它们没有记录分隔符,而是具有给定固定宽度的隐含字段。(这通常是来自大型机/COBOL 风格应用程序的记录的常见情况。)在这种情况下,您不需要指定记录分隔符,但每个字段都需要有关字段长度和字段起始位置的附加信息。

例如,如果您的账号字段定义为从第 6 个字符位置开始的 5 个字母,则其指定方式如下:

<RecordColumn("Account Number", 2, False, 5, 6)>

Excel 文件

在第二版中,有一个读取 Excel 2007 文件的阅读器。这依赖于 CodePlex 的ClosedXML 库(所以我单独包含了该版本 - 如果您不需要 Excel,请使用第一版)。

要指定应从文件中的特定工作表中读取记录,请使用 RecordExcelWorksheet 属性标记该类。

<RecordExcelSourceSheet("House Summary")>

如果未指定工作表,则代码将使用文件中的第一个工作表。您可以指定类的每个属性来自特定单元格或 Excel 工作表中的特定列。

<RecordsSpecificExcelCell("Business Date:", RecordColumnExcelCellAttribute.ExcelColumn.I, 4)>

语义含义

在最初读取时,所有字段都被视为 string。为了将其转换为正确的类型并应用任何数据格式规则,您需要添加适当的语义标记。这些是 RecordColumnDateRecordColumnNumber 等。

Imports FileUtilities

<RecordFieldSeperator(FileUtilities.RecordFieldSeperatorAttribute.StandardSeparators.Comma)>
Public Class YahooStockPrice

  <RecordColumn("Date", 0)>
  <RecordColumnDate("M/d/yyyy")>
  Public Property PriceDate As Date

  <RecordColumnNumber()>
  <RecordColumn("Open", 1)>
  Public Property OpenPrice As Nullable(Of Decimal)

  <RecordColumnNumber()>
  <RecordColumn("High", 2)>
  Public Property HighPrice As Nullable(Of Decimal)

  <RecordColumnNumber()>
  <RecordColumn("Low", 3)>
  Public Property LowPrice As Nullable(Of Decimal)

  <RecordColumnNumber()>
  <RecordColumn("Closing", 4)>
  Public Property ClosingPrice As Nullable(Of Decimal)

  <RecordColumnNumber()>
  <RecordColumn("Volume", 5)>
  Public Property Volume As Nullable(Of Decimal)

  <RecordColumnNumber()>
  <RecordColumn("Adj Close", 6)>
  Public Property AdjustedClose As Nullable(Of Decimal)

End Class

验证

现在我们已经将一些语义属性附加到我们的类上,我们还可以指定验证规则来跳过任何无效的行。这些是使用 RecordColumnValidation 属性指定的,并且内置了许多验证。

Public Enum ValidationRule
 ''' <summary>
 ''' No validation is applied to this column
 ''' </summary>
 NoValidation = 0
 ''' <summary>
 ''' Record field must not be empty
 ''' </summary>
 NotBlank = 1
 ''' <summary>
 ''' Column data does not contain the column name
 ''' </summary>
 NotColumnName = 2
 ''' <summary>
 ''' Column must contain a number that maps to its RecordColumnNumberAttribute settings
 ''' </summary>
 NumberValidation = 3
 ''' <summary>
 ''' Column must contain a date that maps to its RecordColumnDateAttribute settings
 ''' </summary>
 DateValidation = 4
 ''' <summary>
 ''' Record must equal the CompareTo property
 ''' </summary>
 MustEqual = 5
 ''' <summary>
 ''' Record must not equal the CompareTo property
 ''' </summary>
 MustNotEqual = 6
 ''' <summary>
 ''' The value must match the regular expression setting in the CompareTo property
 ''' </summary>
 RegularEpressionMatch = 7
 ''' <summary>
 ''' Test the value against the assigned instrument identifier attributes
 ''' </summary>
 InstrumentIdentifier = 8
End Enum

对于雅虎文件,我们需要验证字段是否与列名匹配,以及它们是否是有效的数字或日期数据。

在第二版中,还有一个将验证标记为“致命”的概念,方法是设置验证属性构造函数中的可选参数。例如,如果某个字段必须仅设置为大写字母 M,您可以使用以下验证来强制执行:

<RecordColumnValidation(RecordColumnValidationAttribute.ValidationRule.MustEqual, "M", True)>

不寻常的格式

除了 .NET 框架中提供的标准数字和日期格式外,还有一些不寻常的情况(通常也是来自大型机系统的输出)。例如,数字可能带有尾随符号,并且可能带有隐含的小数位数。对于这些情况,可以使用 RecordColumnCustomNumber 属性在转换数字之前有效地预处理该数字。

这些特殊格式表示为基于 Flags 的枚举类型,因为它们有时可以组合在一个字段中。

    <Flags()>
    Public Enum CustomNumberFormatFlags
        ''' <summary>
        ''' There are no custom transforms for this number
        ''' </summary>
        StandardNumber = &H0
        ''' <summary>
        ''' There is an implied decimal precision that is fixed for this field
        ''' </summary>
        FixedDecimalPoint = &H1
        ''' <summary>
        ''' The sign comes at the end of the number (e.g. 12345- means -12345)
        ''' </summary>
        ''' <remarks>
        ''' If no - then assume number is positive
        ''' </remarks>
        TrailingSign = &H2
        ''' <summary>
        ''' Negative numbers are in braces
        ''' </summary>
        ''' <remarks>
        ''' e.g. (123.45) means -123.45
        ''' </remarks>
        NegativeBracketed = &H4
        ''' <summary>
        ''' Number can be followed by a fraction - e.g. "37 1/2" or even "1.85 3/8"
        ''' </summary>
        FractionalSuffix = &H8
        'additional madness can be added here if files contain it
    End Enum

修改值

有时,来自文件的传入数据需要修改才能正常工作。例如,某些文件可能在字段中包含“NULL”或“n/a”,而该字段应指定为空,或者字段在读取时需要修剪。这通过 ValueSubstitutionValueTrim 等属性完成。

文化

如果您正在接收来自不同文化的文件(例如,如果它是由法国的一台机器生成的,并且正在被美国的机器导入),您可能需要明确指定文件的来源文化,以便转换数字和日期值。这是通过 ValidCulture 属性完成的。

读取数据

应用所有这些属性并生成结果的繁重工作由 ClassStreamReader 类完成,这是一个泛型类,它将带有属性的 POCO 类型作为输入。它可以通过 ReadNext 方法一次读取一个记录,或者通过 ReadToEnd 方法返回完整的记录集。

例如,要迭代指定的雅虎文件,您可以这样做:

Dim reader As New ClassStreamReader(GetType(YahooStockPrice), _
             New System.IO.FileInfo("table.csv"))

For Each rowdata As YahooStockPrice In reader.ReadToEnd()
   Debug.WriteLine(rowdata.PriceDate.ToShortDateString() & _
       " closed at " & rowdata.AdjustedClose.ToString())
Next

如果您的数据来自 Excel 文件(根据代码的第二版),请使用 ClassExcelReader 代替 ClassStreamReader

利用语义含义

定义记录中字段的语义含义所带来的强大功能之一是,您可以针对该记录验证给定的行,甚至在某些特殊情况下可以修复“有缺陷”的输入文件。

验证由 ClassColumnMapping 类的 GetValidationWarnings 方法执行。此方法将传入的 string 与装饰的 .NET 类进行匹配,并返回有关数据的零个或多个警告列表。

历史

  • 2015-03-26:初始版本
  • 2015-03-30:增加了关于如何处理固定宽度字段和更不寻常数据格式的详细信息
  • 2015-11-26:添加了 Excel 阅读器以及致命/非致命验证的概念
© . All rights reserved.