项目

CsvHelper 入门指南(C# 读写 CSV 文件库)

安装

包管理器控制台

Install-Package CsvHelper

.NET CLI 控制台

dotnet add package CsvHelper

先决条件

使用此文档时,默认您具备一些基础的 .NET 知识。请检查先决条件,确保您已理解它们。先决条件

文化信息(CultureInfo)

CsvHelper 要求您指定想要使用的 CultureInfo。文化信息用于确定默认分隔符、默认行尾字符以及类型转换时的格式化。如果您愿意,也可以更改这些配置中的任何一个。请为您的数据选择合适的文化。InvariantCulture 对于编写文件并在之后再次读取是最便携的,因此大多数示例中都会使用它。

新行

默认情况下,CsvHelper 会遵循 RFC 4180,无论运行在哪个操作系统上,都使用 \r\n 来写入新行。CsvHelper 无需任何配置更改就能读取 \r\n\r\n 。如果您想以非标准格式读取或写入,可以更改 NewLine 的配置。

var config = new CsvConfiguration(CultureInfo.InvariantCulture) { NewLine = Environment.NewLine, };

读取 CSV 文件

假设我们有一个如下所示的 CSV 文件。

Id,Name
1,one
2,two

以及一个这样的类定义。

public class Foo
{
    public int Id { get; set; }
    public string Name { get; set; }
}

如果我们的类属性名称与 CSV 文件的标题名称匹配,我们可以无需任何配置就可读取文件。

using (var reader = new StreamReader("path\\to\\file.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
    var records = csv.GetRecords<Foo>();
}

GetRecords 方法将返回一个 IEnumerable,它会按需 yield 记录。这意味着当你遍历记录时,每次只返回一条记录。这也意味着只有文件的一小部分被读入内存。但请注意,如果您执行任何 LINQ 投影操作,如调用 .ToList(),则整个文件将被读入内存。CsvReader 是单向的,所以如果你想对数据运行任何 LINQ 查询,你需要将整个文件加载到内存中。只需知道你在做的是什么即可。

假设我们的 CSV 文件名与类属性略有不同,且我们不想让属性匹配。

id,name
1,one
2,two

在这种情况下,名称都是小写的。我们希望属性名称为帕斯卡命名法(Pascal Case),所以我们只需改变属性名与标题名的匹配方式。

var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
    PrepareHeaderForMatch = args => args.Header.ToLower(),
};

using (var reader = new StreamReader("path\\to\\file.csv"))
using (var csv = new CsvReader(reader, config))
{
    var records = csv.GetRecords<Foo>();
}

使用配置 PrepareHeaderForMatch,我们能够改变头匹配属性名称的方式。标题和属性名称都会通过 PrepareHeaderForMatch 函数处理。当读取器需要找到与标题对应的属性时,它们现在就能匹配了。你可以使用这个函数来做其他事情,比如移除空白字符或其他字符。

假设我们的 CSV 文件根本没有标题。

1,one
2,two

首先,我们需要通过配置告诉读取器没有标题记录。

var config = new CsvConfiguration(CultureInfo.InvariantCulture)
{
    HasHeaderRecord = false,
};

using (var reader = new StreamReader("path\\to\\file.csv"))
using (var csv = new CsvReader(reader, config))
{
    var records = csv.GetRecords<Foo>();
}

CsvReader将使用类中属性的位置作为索引位置。但这存在一个问题:不能依赖于.NET 中的成员排序。我们可以通过映射属性到 CSV 文件中的位置来解决这个问题。

一种方法是使用属性映射。

public class Foo
{
    [Index(0)]
    public int Id { get; set; }

    [Index(1)]
    public string Name { get; set; }
}

IndexAttribute允许您指定 CSV 字段的位置,该位置用于属性。

你也可以按名称映射。让我们使用之前的全小写标题示例,看看如何使用属性而非改变标题匹配方式。

public class Foo
{
    [Name("id")]
    public int Id { get; set; }

    [Name("name")]
    public string Name { get; set; }
}

还有许多其他可用的属性。

如果我们无法控制要映射的类,从而无法向其添加属性该怎么办?在这种情况下,我们可以使用流畅的 ClassMap 来进行映射。

public sealed class FooMap : ClassMap<Foo>
{
    public FooMap()
    {
        Map(m => m.Id).Name("id");
        Map(m => m.Name).Name("name");
    }
}

为了使用映射,我们需要在上下文中注册它。

using (var reader = new StreamReader("path\\to\\file.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
    csv.Configuration.RegisterClassMap<FooMap>();
    var records = csv.GetRecords<Foo>();
}

创建类映射是 CsvHelper 推荐的文件映射方式,因为它功能更强大。

你也可以手动读取行。

using (var reader = new StreamReader("path\\to\\file.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
    csv.Read();
    csv.ReadHeader();
    while (csv.Read())
    {
        var record = csv.GetRecord<Foo>();
        // 对记录进行处理
    }
}

Read 会前进到下一行。ReadHeader 会读取行作为标题值。将 ReadReadHeader 分开,允许在移动到下一行之前对标题行进行其他操作。GetRecord 也不会前进读取器,允许你对可能需要做的行进行其他操作。你可能需要 GetField 获取单个字段,或者多次调用 GetRecord 填充多个对象。

写入 CSV 文件

现在我们来看看如何写入 CSV 文件。基本上是相同的过程,只是顺序相反。

我们仍然使用之前的类定义。

public class Foo
{
    public int Id { get; set; }
    public string Name { get; set; }
}

我们有一组这样的记录。

var records = new List<Foo>
{
    new Foo { Id = 1, Name = "one" },
    new Foo { Id = 2, Name = "two" },
};

我们可以无需任何配置就将记录写入文件。

using (var writer = new StreamWriter("path\\to\\file.csv"))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
    csv.WriteRecords(records);
}

WriteRecords 方法会将所有记录写入文件。写入完成后,建议调用 writer.Flush() 以确保写入器内部缓冲区的所有数据都已刷新到文件中。一旦 using 块退出,写入器会自动刷新,所以在这种情况下我们不需要显式去做。推荐总是使用 using 块包裹任何 IDisposable 对象。对象会在 using 块退出后尽快自行释放(并在此例中也会刷新)。

还记得我们不能依赖.NET 中的属性顺序吗?如果我们正在写入一个带有标题的类,没关系,只要我们稍后使用标题进行读取。如果我们想在 CSV 文件中定位标题的顺序,我们需要指定索引来保证其顺序。写入时推荐始终设置索引。

public sealed class FooMap : ClassMap<Foo>
{
    public FooMap()
    {
        Map(m => m.Id).Index(0).Name("id");
        Map(m => m.Name).Index(1).Name("name");
    }
}

你也可以手动写入行。

using (var writer = new StreamWriter("path\\to\\file.csv"))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
    csv.WriteHeader<Foo>();
    csv.NextRecord();
    foreach (var record in records)
    {
        csv.WriteRecord(record);
        csv.NextRecord();
    }
}

WriteHeader 不会使你前进到下一行。将 NextRecordWriteHeader 分开,允许在需要时在标题行中写入更多信息。WriteRecord 也不会使你前进到下一行,以便你有能力写入多个对象或使用 WriteField 写入单个字段。

在本文档中