HOWTO · Csharp
C# 读取 CSV 文件并将其值存储到数组中
本文介绍如何读取 CSV 文件并将其值存储在 C# 中的数组中的不同方法。它介绍了 OpenRead(),ReadLine()和 TextFieldParser 之类的方法
本页内容
CSV 文件是一种以逗号分隔的文件,用于以有组织的方式存储数据。它通常以表格形式存储数据。大多数企业组织都将数据存储在 CSV 文件中。
在 C# 中,我们可以对 CSV 文件执行各种操作。我们可以读取、写入和修改 CSV 文件。本文重点介绍读取 CSV 文件并将其值存储到数组中的方法:使用 StreamReader 逐行读取、使用 File.ReadAllLines() 一次加载全部行,以及使用 TextFieldParser 解析字段。
前三个示例使用同一个示例 Data.csv 文件:
Name,Age
Alice,30
Bob,25
使用 StreamReader 读取 CSV 文件
StreamReader 类将文件作为文本流读取。File.OpenRead() 打开文件,ReadLine() 一次返回一行,直到到达 EndOfStream。将每一行按逗号拆分即可得到各个值,示例将这些值按列存储到一个列表中。
使用 OpenRead() 和 ReadLine() 方法的正确语法如下:
// OpenRead() Syntax
File.OpenRead(@"FilePath");
// ReadLine() Syntax
StreamReaderObject.ReadLine();
示例代码:
using System.IO;
using System.Collections.Generic;
using System;
class ReadingCSV {
static void Main(string[] args) {
var reader = new StreamReader(File.OpenRead(@"D:\New folder\Data.csv"));
List<string> listA = new List<string>();
List<string> listB = new List<string>();
while (!reader.EndOfStream) {
var line = reader.ReadLine();
var values = line.Split(',');
listA.Add(values[0]);
listB.Add(values[1]);
}
foreach (var column1 in listA) {
Console.WriteLine(column1);
}
foreach (var column2 in listB) {
Console.WriteLine(column2);
}
}
}
输出:
Name
Alice
Bob
Age
30
25
按逗号拆分适用于字段本身不包含逗号的情况。像 "Smith, John" 这样带引号的字段会在错误的位置被拆开;对于这类文件,请使用 TextFieldParser。
使用 File.ReadAllLines 一次读取全部行
当文件小到可以装入内存时,File.ReadAllLines() 是得到数组的最短路径:它读取整个文件,并将每一行作为 string[] 的一个元素返回。下面的示例拆分每一行,将每个数据行存储为自己的 string[],并用 ToArray() 将行列表转换为交错数组,因此 data[i] 就是完整的第 i 行。循环从索引 1 开始以跳过标题行,跳过空白行,并在索引前检查字段数量,因此过短或为空的行不会让它崩溃。
示例代码:
using System;
using System.Collections.Generic;
using System.IO;
class ReadingAllLines {
static void Main() {
string[] lines = File.ReadAllLines("Data.csv");
List<string[]> rows = new List<string[]>();
// Start at index 1 to skip the header row.
for (int i = 1; i < lines.Length; i++) {
if (string.IsNullOrWhiteSpace(lines[i])) {
continue;
}
string[] values = lines[i].Split(',');
if (values.Length < 2) {
continue;
}
rows.Add(values);
}
string[][] data = rows.ToArray();
foreach (string[] row in data) {
Console.WriteLine(row[0] + ": " + row[1]);
}
}
}
输出:
Alice: 30
Bob: 25
取舍在于内存与简单性。ReadAllLines() 在处理第一行之前就加载整个文件,并返回一个可以立即索引、计数和重读的数组。StreamReader 循环一次只保留一行,因此对于只需处理一次就丢弃的超大文件,它仍然是更安全的选择。对于会多次查看的小文件,一次全部加载更简单,而且足够快。
使用 TextFieldParser 解析 CSV 字段
TextFieldParser 类定义在 Microsoft.VisualBasic.FileIO 命名空间中。它逐个字段地解析带分隔符的文件,并能理解带引号的字段。使用 SetDelimiters() 设置分隔符,然后调用 ReadFields() 将当前行的字段作为字符串数组获取;解析器在每次调用时移动到下一行。在 .NET Framework 项目中,请在运行下面的程序之前添加对 Microsoft.VisualBasic 的引用;在现代 .NET 中,该命名空间是共享框架的一部分。
使用此解析器的正确语法如下:
TextFieldParser ParserName = new TextFieldParser(PathString);
示例代码:
using System;
using System.Collections.Generic;
using Microsoft.VisualBasic.FileIO;
class ReadingCSV {
public static void Main() {
var path = @"D:\New folder\Data.csv";
List<string> listA = new List<string>();
List<string> listB = new List<string>();
using (TextFieldParser csvReader = new TextFieldParser(path)) {
csvReader.CommentTokens = new string[] { "#" };
csvReader.SetDelimiters(new string[] { "," });
csvReader.HasFieldsEnclosedInQuotes = true;
// Skip the row with the column names
csvReader.ReadLine();
while (!csvReader.EndOfData) {
// Read current line fields, pointer moves to the next line.
string[] fields = csvReader.ReadFields();
listA.Add(fields[0]);
listB.Add(fields[1]);
}
}
foreach (var column1 in listA) {
Console.WriteLine(column1);
}
foreach (var column2 in listB) {
Console.WriteLine(column2);
}
}
}
输出:
Alice
Bob
30
25
这一次,标题行通过最初的 ReadLine() 被跳过,并且由解析器而不是手动拆分来决定每个字段在哪里结束,因此字段内部带引号的逗号会保持完整。
处理带引号的字段和转义引号
真实的导出文件会给字段加引号。包含分隔符的字段会被双引号包围,而带引号字段内部的双引号会被写成两个。下面的示例 People.csv 文件同时使用了这两条规则:
Name,Note
"Smith, John","Likes ""quoted"" text"
Jones,Plain note
Split(',') 对这些规则一无所知:它会在名字内部的逗号处,把第一个数据行切成 3 段。TextFieldParser 则会应用这些规则:它去掉包围字段的引号,并把每一对重复引号合并成一个,因此字段会原封不动地按预期返回。这个程序先输出 Split(',') 的段数,然后输出解析后的字段。
示例代码:
using System;
using Microsoft.VisualBasic.FileIO;
class ReadingQuotedCsv {
public static void Main() {
string rawLine = "\"Smith, John\",\"Likes \"\"quoted\"\" text\"";
Console.WriteLine("Split(',') pieces: " + rawLine.Split(',').Length);
using (TextFieldParser parser = new TextFieldParser("People.csv")) {
parser.SetDelimiters(new string[] { "," });
parser.HasFieldsEnclosedInQuotes = true;
// Skip the row with the column names
parser.ReadLine();
while (!parser.EndOfData) {
string[] fields = parser.ReadFields();
Console.WriteLine(fields[0] + " | " + fields[1]);
}
}
}
}
输出:
Split(',') pieces: 3
Smith, John | Likes "quoted" text
Jones | Plain note
如果手动拆分得到的字段数比标题行的列数还多,原因通常就是带引号的字段。对这类文件应改用 TextFieldParser,而不是事后试图拼凑这些片段。
读取 CSV 文件时的常见错误
-
values[1]处的IndexOutOfRangeException:末尾的空白行或缺少某列的行会被拆成比代码预期更少的字段。应跳过纯空白行,并在索引前检查字段数量,就像ReadAllLines()示例所做的那样。 -
FileNotFoundException或DirectoryNotFoundException:运行程序的机器上不存在该路径。像D:\New folder\Data.csv这样写死的绝对路径只在作者的电脑上有效;应优先使用相对路径,或用Path.Combine()构建路径,并在调试时用File.Exists()检查。 -
标题行被当成数据出现:第一个
StreamReader示例把Name和Age与真实数据行一起存储。当文件带有标题时,应像其他示例那样,在循环之前跳过第一行。 -
文件一直被锁定:从未关闭的
StreamReader会一直持有其句柄,直到垃圾回收器运行。应将读取器声明在using块中,这样即使异常中断了循环,文件也会在循环结束时立即关闭。