一种便携高效的通用平面文件解析器






4.93/5 (104投票s)
GenericParser 是一个用 C# 实现的、用于解析分隔符格式和固定宽度格式文件的解析器。
引言
作为开发者,我们经常面临将数据从一种格式转换为另一种格式的任务。为了工作中的一个项目,我需要一个可移植的解决方案,它要高效、外部依赖项最少,并且能解析分隔符和固定宽度数据。如下所示,`GenericParser` 是任何 Microsoft 提供的解决方案的一个很好的替代品,并提供了一些独特的功能。代码组织良好,易于理解,方便按需修改。
注意:该项目使用 Visual Studio 2010 构建,但代码是为 .NET 2.0 设计的。
定义
- 分隔符数据 - 其列由特定字符分隔的数据(例如,CSV - 逗号分隔值)。
- 固定宽度数据 - 其列具有固定字符宽度的数据。
特点
`GenericParser`(及其派生的 `GenericParserAdapter`)包含以下特性:
- 高效 - 更多详情请参阅下方的基准测试。
- 时间:比任何 Microsoft 提供的解决方案快约 3 到 10 倍
- 内存:与任何 Microsoft 提供的解决方案的内存使用量大致相等或更少
- 支持分隔符格式和固定宽度格式
- 支持自定义分隔符字符(仅限单个字符)
- 支持注释行(单个字符标记)
- 支持转义字符(仅限单个字符)
- 支持自定义文本限定符,允许忽略列/行分隔符(例如,多行数据)
- 通过将文本限定符加倍来支持转义文本限定符
- 支持忽略/包含不含任何字符的行
- 支持标题行
- 支持动态添加更多列以匹配数据的能力
- 支持将列数强制为特定数字的能力
- 支持根据第一行强制列数的能力
- 支持修剪列中的字符串
- 支持去除控制字符
- 支持重用解析器的同一个实例处理不同的数据源
- 支持 `TextReader` 和 `String`(文件位置)作为数据源
- 支持限制读取的最大行数
- 支持自定义内部缓冲区的大小
- 支持在标题行之后的起始数据行中跳过行
- 支持 XML 配置,可以以多种格式加载/保存
- 支持通过列名访问数据(当提供了标题行时)
- 支持 Unicode 编码
- `GenericParserAdapter` 支持跳过数据末尾的行
- `GenericParserAdapter` 支持向每行输出添加行号
- `GenericParserAdapter` 支持以下输出 - XML、`DataTable` 和 `DataSet`
- 彻底的单元测试 - 91.94% 代码覆盖率(测试在源代码下载中提供)
- 代码中有完整的 XML 文档(包括二进制/源代码下载中的一个 .chm 帮助文件)
基准测试
为了对 `GenericParser` 进行基准测试,我选择将其与以下内容进行比较:
- Microsoft 的 Text Driver
- Microsoft 的 Text Field Parser
- Sebastien Lorion 的 `CsvReader` 3.7(仅限 CSV - 代码可在此处找到 [^])
GenericParser
1.0
为了获得现实的数据源进行基准测试,我从 Northwind 数据库中选取了 10 行数据,并将其复制并扩大为越来越大的 CSV 和 `FixedWidth` 数据集。使用 `System.Diagnostics.Stopwatch` 来测量 CPU 使用情况,我执行了每个基准测试 10 次,并对结果取平均值,以尽量减少检测的误差。对于内存使用情况,我使用了 Visual Studio 2010 的内存分析功能,并且每个基准测试只执行一次。
我试图生成能够让每个解决方案平等地运行代码的测试。需要注意的是,这些测试并未涵盖所有可能的场景 - 您的结果可能会有所不同。请随时使用我的代码作为基础(或创建您自己的测试)来比较代码,然后再得出任何结论。
例如,下面的测试没有考虑转义字符。在 `GenericParser` 1.0 中,它为转义字符分配了一个额外的缓冲区,这基本上使内存需求翻倍。在 `GenericParser` 1.1 中,它重用了现有缓冲区来反转义数据列。除非您专门设计测试来考虑这一点,否则您不会看到这种好处。
我预料到会有人评论这一点,我知道有 FileHelpers [^],但我认为它们属于不同的类别,不容易与上述解决方案进行直接比较。FileHelpers 依赖于通过具体类上的属性来声明性地定义文件模式。我的解决方案则依赖于通过属性或 XML 来定义模式。如果您认为它们适合您的问题领域,可以随意进行比较。
CPU 使用率
内存使用
注意:由于内存分析会生成高达 2GB 的 `.vsp` 文件,并且内存使用量看起来相当稳定,所以我只对 10 到 10,000 行数据进行了内存分析。
结论
如图所示,`GenericParser` 在所有方面都能满足或超越 Microsoft 提供的任何解决方案。此外,版本 1.1 在性能上比版本 1.0 有显著提升,尤其考虑到错误修复和新增功能。
如图所示,Sebastien Lorion 的 `CsvReader` 绝对是解析分隔符文件的首选。因此,如果您只打算解析分隔符文件,我强烈推荐您查看他的库。否则,我认为我的库是一个能够解析两种格式的有效实现。
在源代码下载中,您可以找到我所有的性能测试和结果,包括一个 Excel 2010 工作簿,其中包含所有收集到的原始数据,用于绘图。
Using the Code
代码本身模仿了 .NET Framework 中的大多数读取器,但使用遵循四个基本步骤:
- 通过构造函数或 `SetDataSource()` 方法设置数据源。
- 通过属性或加载 XML CONFIG 文件(通过 `Load()` 方法)来配置解析器以适应数据源的格式。
- 调用 `Read()` 方法并访问下方的数据列,或者对于 `GenericParserAdapter`,调用 `GetXml()`、`GetDataTable()`、`GetDataSet()` 来提取数据。
- 调用 `Close()` 或 `Dispose()`。
DataSet dsResult;
// Using an XML Config file.
using (GenericParserAdapter parser = new GenericParserAdapter("MyData.txt"))
{
parser.Load("MyData.xml");
dsResult = parser.GetDataSet();
}
// Or... programmatically setting up the parser for TSV.
string strID, strName, strStatus;
using (GenericParser parser = new GenericParser())
{
parser.SetDataSource("MyData.txt");
parser.ColumnDelimiter = "\t".ToCharArray();
parser.FirstRowHasHeader = true;
parser.SkipStartingDataRows = 10;
parser.MaxBufferSize = 4096;
parser.MaxRows = 500;
parser.TextQualifier = '\"';
while (parser.Read())
{
strID = parser["ID"];
strName = parser["Name"];
strStatus = parser["Status"];
// Your code here ...
}
}
// Or... programmatically setting up the parser for Fixed-width.
using (GenericParser parser = new GenericParser())
{
parser.SetDataSource("MyData.txt");
parser.ColumnWidths = new int[4] {10, 10, 10, 10};
parser.SkipStartingDataRows = 10;
parser.MaxRows = 500;
while (parser.Read())
{
strID = parser["ID"];
strName = parser["Name"];
strStatus = parser["Status"];
// Your code here ...
}
}
致谢
虽然我没有创建 Sebastien Lorion 的 `CsvReader` 的派生类,但在 `GenericParser` 中,我确实借鉴了他 `CsvReader` 中提供的一些功能概念。
使用的工具
- Visual Studio 2010(包括单元测试/分析)
- .NET Framework 2.0
- HTML Help Workshop(文档) [^]
- SandCastle(文档) [^]
- Sandcastle Help File Builder(文档) [^]
- Microsoft Excel 2010
历史
- 2005 年 9 月 17 日 - 1.0 - 首次发布
- 2010 年 6 月 20 日 - 1.1
- 新功能
- 支持忽略/包含空白数据行(行中未找到任何字符)
- 支持根据第一行强制列数的能力
- 支持去除控制字符
- `GenericParserAdapter` 支持跳过数据末尾的行
- 使用转义字符时减少内存开销
- 支持指定数据编码
- Bug 修复
- 修复了处理具有标题但没有数据的解析错误
- 修复了文本限定符/转义/注释字符处理不一致的错误
- 修复了在高延迟网络上读取文件时的错误
- 修复了文本限定符在列中间被解释的错误(仅在列的开始和结束处有效)
- 修复了在缓冲区末尾跳过行结束符的错误
- 破坏性更改
- 固定宽度解析将不再考虑文本限定符或转义字符
- 以下属性已转换为 `char` 类型?
ColumnDelimiter
CommentCharacter
EscapeCharacter
TextQualifier
- `RowDelimiter` 已被移除,代码会自动查找 '\n' 或 '\r' 来指示新行(假设 '\r' 不是列分隔符)。如果找到其中一个字符,它将跳过配对的 '\n' 或 '\r'(假设 '\r' 不是列分隔符)。
- `SkipDataRows` 已重命名为 `SkipStartingDataRows`
- `FixedWidth` 属性已被一个名为 `TextFieldType` 的属性替换,该属性是 `FieldType` 枚举类型
- 由于上述属性的变化,版本 1.0 生成的 XML 与版本 1.1 不完全兼容
- `Read()` 如果解析了标题行但没有数据,将返回 `true`
- `ParserSetupException` 已被 `InvalidOperationException` 替换
- 重新编写了异常中的消息,使其更具描述性。
- 新功能
- 2010 年 6 月 26 日 - 1.1.1
- 新功能
- 重新编写了基准测试,使其更能代表真实世界的数据,并将测试切换为不使用 `DataSet`
- 加载配置文件略微提高了效率
- 新功能
- 2012 年 2 月 5 日 - 1.1.2
- Bug 修复
- 修复了 `MaxBufferSize` 过小时抛出异常的问题,尽管它实际上足够大(由 uberblue 报告)。
- Bug 修复
- 2012 年 3 月 16 日 - 1.1.3
- Bug 修复
- 修复了意外移除控制字符的问题(由 John Voelk 报告)。
- 修复了未正确提取流末尾数据的问题(在版本 1.1.2 中引入)。
- Bug 修复
- 2017 年 10 月 14 日 - 1.1.4
- Bug 修复
- 修复了在抛出解析异常时 `FileRowNumber` 的一个小问题(由 webdevx 报告)。
- Bug 修复
- 2017 年 10 月 14 日 - 1.1.5
- Bug 修复
- 小更新,以解决 GenericParserAdapter 在未找到数据行时阻止添加列的问题。(由 tschlarm 报告)。
- Bug 修复