快速 CSV 阅读器






4.93/5 (529投票s)
一个提供快速、非缓存、仅向前访问 CSV 数据的读取器。
重要更新 (2016-01-13)
首先,我感谢所有在此文章讨论区贡献的各位:令我惊叹的是,本库的用户们竟然以这种方式互相帮助。
所以……我从未预料到这个项目会如此受欢迎。它最初的目的是单一的:追求性能,而这显然影响了它的设计,事后看来也许过头了。自上次更新已过去五年,许多人询问在哪里可以贡献代码或提交问题。我已不再维护这个库,但 Paul Hatcher 已建立了一个 GitHub 仓库 和一个 NuGet 包。如果您需要维护一个使用此库的现有项目,我建议您前往那里。
如果您要开始一个新项目,或者愿意进行一些轻量级的重构,我也编写了一个新的库,它包含一个 CSV 和固定宽度读取器/写入器,它们的速度与此库一样快,但更灵活,并且可以处理更多用例。您可以在 GitHub 仓库 中找到 CSV 读取器源代码,并下载 NuGet 包。我将完全维护这个新库,并且它已经在许多生产项目中投入使用。
引言
人们可能会认为解析 CSV 文件是一项直接且枯燥的任务。我当时也这么想,直到我不得不解析几个 GB 大小的 CSV 文件。在尝试使用 OLEDB JET 驱动程序和各种正则表达式后,我仍然遇到了严重的性能问题。这时,我决定尝试自定义类选项。我搜索了网络上现有的代码,但找到一个正确、快速且高效的 CSV 解析器和读取器并不容易,无论您钟爱哪个平台/语言。
我说“正确”是指许多实现仅仅使用一些拆分方法,如 String.Split()
。这显然无法处理包含逗号的字段值。更好的实现可能会处理转义引号、修剪字段前后空格等,但我发现没有一个实现能做到所有这些,更重要的是,以快速且高效的方式。
于是,就诞生了我在这篇文章中介绍的 CSV 读取器类。它的设计基于 System.IO.StreamReader
类,因此它是一个非缓存、仅向前读取的读取器(类似于有时被称为的“水龙头式光标”)。
与 OLEDB 和正则表达式方法进行基准测试,它的性能大约快 15 倍,同时内存使用量非常低。
为了给出更切实的数字,对于一个包含 145 个字段和 50,000 条记录的 45 MB CSV 文件,读取器每秒处理约 30 MB 数据。所以总共只用了 1.5 秒!机器配置是 P4 3.0 GHz,1024 MB。
支持的功能
此读取器支持跨越多行的字段。唯一的限制是它们必须被引用,否则将无法区分格式错误的数据和多行值。
通过读取器实现的 System.Data.IDataReader
接口,可以实现基本的数据绑定。
您可以为以下参数指定自定义值
- 默认缺失字段操作;
- 默认格式错误的 CSV 操作;
- 缓冲区大小;
- 字段标题选项;
- 修剪空格选项;
- 字段分隔符字符;
- 引号字符;
- 转义字符(可以与引号字符相同);
- 注释行字符。
如果 CSV 包含字段标题,则可以使用它们来访问特定字段。
当 CSV 数据出现格式错误时,读取器将快速失败并抛出一个有意义的异常,说明错误发生的位置并提供缓冲区的当前内容。
仅为当前记录保留字段值的缓存,但如果您需要动态访问,我还包含了一个缓存版本的读取器,名为 CachedCsvReader
,它在读取流时在内部存储记录。当然,使用这样的缓存会大大增加内存需求,因为所有数据都保存在内存中。
最新更新 (3.8.1 版本)
- 修复了缺失字段处理的 bug;
- 将解决方案转换为 VS 2010(仍以 .NET 2.0 为目标)
基准测试和性能分析
您可以在演示项目中找到这些基准测试的代码。我试图公平地对待每种解析方法,并遵循相同的模式。使用的正则表达式来自 Jeffrey Friedl 的书籍,可在第 271 页找到。它不处理修剪和多行字段。
测试文件包含 145 个字段,大小约为 45 MB(包含在演示项目中的 RAR 压缩包里)。
我还包含了来自基准测试程序和 CLR Profiler for .NET 2.0 的原始数据。
Using the Code
类设计尽可能遵循 System.IO.StreamReader
。版本 2.0 中引入的解析机制有点棘手,因为我们自己处理缓冲和换行解析。尽管如此,由于任务逻辑被清晰地封装起来,流程更容易理解。所有代码都经过良好文档记录和结构化,但如果您有任何疑问,只需发表评论。
基本用法场景
using System.IO;
using LumenWorks.Framework.IO.Csv;
void ReadCsv()
{
// open the file "data.csv" which is a CSV file with headers
using (CsvReader csv =
new CsvReader(new StreamReader("data.csv"), true))
{
int fieldCount = csv.FieldCount;
string[] headers = csv.GetFieldHeaders();
while (csv.ReadNextRecord())
{
for (int i = 0; i < fieldCount; i++)
Console.Write(string.Format("{0} = {1};",
headers[i], csv[i]));
Console.WriteLine();
}
}
}
简单数据绑定场景 (ASP.NET)
using System.IO;
using LumenWorks.Framework.IO.Csv;
void ReadCsv()
{
// open the file "data.csv" which is a CSV file with headers
using (CsvReader csv = new CsvReader(
new StreamReader("data.csv"), true))
{
myDataRepeater.DataSource = csv;
myDataRepeater.DataBind();
}
}
复杂数据绑定场景 (ASP.NET)
由于 System.Web.UI.WebControls.DataGrid
和 System.Web.UI.WebControls.GridView
处理 System.ComponentModel.ITypedList
的方式,无法进行复杂的 ASP.NET 数据绑定。解决此限制的唯一方法是为每个字段包装一个实现 System.ComponentModel.ICustomTypeDescriptor
的容器。
无论如何,即使可能,使用简单的数据绑定方法也更有效。
对于你们中的好奇者来说,这个 bug 源于这两个网格控件完全忽略了 System.ComponentModel.ITypedList
返回的属性描述符,而是依赖 System.ComponentModel.TypeDescriptor.GetProperties(...)
,后者显然返回了字符串数组的属性而不是我们的自定义属性。请在反汇编器中查看 System.Web.UI.WebControls.BoundColumn.OnDataBindColumn(...)
。
复杂数据绑定场景 (Windows Forms)
using System.IO;
using LumenWorks.Framework.IO.Csv;
void ReadCsv()
{
// open the file "data.csv" which is a CSV file with headers
using (CachedCsvReader csv = new
CachedCsvReader(new StreamReader("data.csv"), true))
{
// Field headers will automatically be used as column names
myDataGrid.DataSource = csv;
}
}
自定义错误处理场景
using System.IO;
using LumenWorks.Framework.IO.Csv;
void ReadCsv()
{
// open the file "data.csv" which is a CSV file with headers
using (CsvReader csv = new CsvReader(
new StreamReader("data.csv"), true))
{
// missing fields will not throw an exception,
// but will instead be treated as if there was a null value
csv.MissingFieldAction = MissingFieldAction.ReplaceByNull;
// to replace by "" instead, then use the following action:
//csv.MissingFieldAction = MissingFieldAction.ReplaceByEmpty;
int fieldCount = csv.FieldCount;
string[] headers = csv.GetFieldHeaders();
while (csv.ReadNextRecord())
{
for (int i = 0; i < fieldCount; i++)
Console.Write(string.Format("{0} = {1};",
headers[i],
csv[i] == null ? "MISSING" : csv[i]));
Console.WriteLine();
}
}
}
使用事件的自定义错误处理场景
using System.IO;
using LumenWorks.Framework.IO.Csv;
void ReadCsv()
{
// open the file "data.csv" which is a CSV file with headers
using (CsvReader csv = new CsvReader(
new StreamReader("data.csv"), true))
{
// missing fields will not throw an exception,
// but will instead be treated as if there was a null value
csv.DefaultParseErrorAction = ParseErrorAction.RaiseEvent;
csv.ParseError += new ParseErrorEventHandler(csv_ParseError);
int fieldCount = csv.FieldCount;
string[] headers = csv.GetFieldHeaders();
while (csv.ReadNextRecord())
{
for (int i = 0; i < fieldCount; i++)
Console.Write(string.Format("{0} = {1};",
headers[i], csv[i]));
Console.WriteLine();
}
}
}
void csv_ParseError(object sender, ParseErrorEventArgs e)
{
// if the error is that a field is missing, then skip to next line
if (e.Error is MissingFieldCsvException)
{
Console.Write("--MISSING FIELD ERROR OCCURRED");
e.Action = ParseErrorAction.AdvanceToNextLine;
}
}
历史
版本 3.8.1 (2011-11-10)
- 修复了缺失字段处理的 bug。
- 将解决方案转换为 VS 2010(仍以 .NET 2.0 为目标)。
版本 3.8 (2011-07-05)
- CSV 文件中的空标题名称现在被替换为一个默认名称,该名称可以通过新的
DefaultHeaderName
属性进行自定义(默认是“Column”+列索引)。
版本 3.7.2 (2011-05-17)
- 修复了处理缺失字段时的 bug。
- 对主程序集进行了强命名。
版本 3.7.1 (2010-11-03)
- 修复了处理文件末尾的空白字符时的 bug。
版本 3.7 (2010-03-30)
- 破坏性更改:为字段值修剪添加了更多选项。
版本 3.6.2 (2008-10-09)
- 修复了在特定操作序列中调用
MoveTo
时的 bug; - 修复了多行记录中存在额外字段时的 bug;
- 修复了初始化时发生解析错误时的 bug。
版本 3.6.1 (2008-07-16)
- 修复了由每次迭代重用同一数组引起的
RecordEnumerator
的 bug。
版本 3.6 (2008-07-09)
- 添加了一个 Web 演示项目;
- 修复了将
CachedCsvReader
加载到DataTable
时,CSV 没有标题的 bug。
版本 3.5 (2007-11-28)
- 修复了在未读取记录前初始化
CachedCsvReader
的 bug。
版本 3.4 (2007-10-23)
- 修复了
IDataRecord
实现中GetValue
/GetValues
应在字段值为空或null
时返回DBNull.Value
的 bug; - 修复了在非最终引用字段后未引发分隔符不存在的异常的 bug;
- 修复了修剪未引用字段时,空白字符跨越两个缓冲区的 bug。
版本 3.3 (2007-01-14)
- 添加了通过
SkipEmptyLines
属性关闭跳过空行的选项(默认开启); - 修复了处理由引用字段之前的记录末尾的分隔符的 bug。
版本 3.2 (2006-12-11)
- 稍微修改了缺失字段的处理方式;
- 修复了包含单行但以换行符结尾且无标题的 CSV 文件中,调用
CsvReader.ReadNextRecord()
返回false
的 bug。
版本 3.1.2 (2006-08-06)
- 更新了 Dispose 模式;
- 修复了
SupportsMultiline
为false
时的 bug; - 修复了
IDataReader
schema 列“DataType”返回DbType.String
而不是typeof(string)
的 bug。
版本 3.1.1 (2006-07-25)
- 添加了一个
SupportsMultiline
属性,当不需要多行支持时有助于提高性能; - 添加了两个新构造函数以支持常见场景;
- 添加了对基础流返回长度为 0 的支持;
- 修复了在读取任何记录之前访问
FieldCount
属性时的 bug; - 修复了分隔符是空格时的 bug;
- 修复了在初始化标题时
ReadNextRecord(...)
的递归行为中的 bug; - 修复了读取第一条记录时到达 EOF 的 bug;
- 修复了当读取器已到达 EOF 且字段缺失时未抛出异常的 bug。
版本 3.0 (2006-05-15)
- 引入了对 .NET 1.1 和 .NET 2.0 的同等支持;
- 增加了对格式错误的 CSV 文件的广泛支持;
- 增加了对数据绑定的完整支持;
- 提供了当前原始数据;
- 字段标题现在通过数组访问(存在破坏性更改);
- 字段标题不区分大小写(感谢 Marco Dissel 的建议);
- 放宽了读取器已处置时的限制;
CsvReader
支持 2^63 条记录;- 增加了更多的测试覆盖;
- 升级到 .NET 2.0 最终版;
- 修复了在未读取任何数据时访问某些属性(特别是
FieldHeader
s)的问题。
版本 2.0 (2005-08-10)
- 将代码移植到 .NET 2.0(2005 年 7 月 CTP);
- 通过广泛的单元测试进行了彻底调试(特别感谢 shriop);
- 提高了速度(现在比 OLEDB 快 15 倍);
- 内存消耗是 1.0 版本的一半;
- 可以指定自定义缓冲区大小;
- 完整支持 Unicode;
- 自动检测行尾,无论是\r、\n还是\r\n;
- 更好的异常处理;
- 支持“field1\rfield2\rfield3\n”模式(Unix 使用);
- 解析代码完全重构,代码更清晰。
版本 1.1 (2005-01-15)
- 1.1:添加了对多行字段的支持。
版本 1.0 (2005-01-09)
- 1.0:首次发布。