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

快速 CSV 阅读器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (529投票s)

2005年1月9日

MIT

8分钟阅读

viewsIcon

9212706

downloadIcon

165182

一个提供快速、非缓存、仅向前访问 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.DataGridSystem.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 模式;
  • 修复了 SupportsMultilinefalse 时的 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 最终版;
  • 修复了在未读取任何数据时访问某些属性(特别是 FieldHeaders)的问题。

版本 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:首次发布。
© . All rights reserved.