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
会读取行作为标题值。将 Read
和 ReadHeader
分开,允许在移动到下一行之前对标题行进行其他操作。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
不会使你前进到下一行。将 NextRecord
与 WriteHeader
分开,允许在需要时在标题行中写入更多信息。WriteRecord
也不会使你前进到下一行,以便你有能力写入多个对象或使用 WriteField
写入单个字段。