C# - 轻量级且快速的 CSV 解析器






4.83/5 (14投票s)
轻量级但功能完善的 CSV 解析器,支持自定义分隔符和限定符,并使用 yield 返回记录。
引言
解析 CSV 文件听起来可能是一项简单的任务,但实际上并非如此。以下是一个 CsvParser
类实现,我将其用于我自己的项目中。它支持我认为至关重要的以下功能:
- 自定义分隔符和限定符字符
- 支持引用符号(允许分隔符字符成为值的一部分)
- 支持引用转义(允许引用字符成为值的一部分)
- 支持 '
\n
' 和 '\r\n
' 两种换行符 - 设计为通过
yield return
返回IEnumerable
(无内存缓冲区) - 设计为分别返回 Header 和其余行(使用
Tuple
)
源代码
public static class CsvParser
{
private static Tuple<T, IEnumerable<T>> HeadAndTail<T>(this IEnumerable<T> source)
{
if (source == null)
throw new ArgumentNullException("source");
var en = source.GetEnumerator();
en.MoveNext();
return Tuple.Create(en.Current, EnumerateTail(en));
}
private static IEnumerable<T> EnumerateTail<T>(IEnumerator<T> en)
{
while (en.MoveNext()) yield return en.Current;
}
public static IEnumerable<IList<string>>
Parse(string content, char delimiter, char qualifier)
{
using (var reader = new StringReader(content))
return Parse(reader, delimiter, qualifier);
}
public static Tuple<IList<string>, IEnumerable<IList<string>>>
ParseHeadAndTail(TextReader reader, char delimiter, char qualifier)
{
return HeadAndTail(Parse(reader, delimiter, qualifier));
}
public static IEnumerable<IList<string>>
Parse(TextReader reader, char delimiter, char qualifier)
{
var inQuote = false;
var record = new List<string>();
var sb = new StringBuilder();
while (reader.Peek() != -1)
{
var readChar = (char) reader.Read();
if (readChar == '\n' || (readChar == '\r' && (char) reader.Peek() == '\n'))
{
// If it's a \r\n combo consume the \n part and throw it away.
if (readChar == '\r')
reader.Read();
if (inQuote)
{
if (readChar == '\r')
sb.Append('\r');
sb.Append('\n');
}
else
{
if (record.Count > 0 || sb.Length > 0)
{
record.Add(sb.ToString());
sb.Clear();
}
if (record.Count > 0)
yield return record;
record = new List<string>(record.Count);
}
}
else if (sb.Length == 0 && !inQuote)
{
if (readChar == qualifier)
inQuote = true;
else if (readChar == delimiter)
{
record.Add(sb.ToString());
sb.Clear();
}
else if (char.IsWhiteSpace(readChar))
{
// Ignore leading whitespace
}
else
sb.Append(readChar);
}
else if (readChar == delimiter)
{
if (inQuote)
sb.Append(delimiter);
else
{
record.Add(sb.ToString());
sb.Clear();
}
}
else if (readChar == qualifier)
{
if (inQuote)
{
if ((char) reader.Peek() == qualifier)
{
reader.Read();
sb.Append(qualifier);
}
else
inQuote = false;
}
else
sb.Append(readChar);
}
else
sb.Append(readChar);
}
if (record.Count > 0 || sb.Length > 0)
record.Add(sb.ToString());
if (record.Count > 0)
yield return record;
}
}
Using the Code
这是一个读取 CSV 文件的示例。以下代码片段解析前 5 条记录,并将它们以键/值对的形式打印到 Console
中:
const string fileName = @"C:\Temp\file.csv";
using (var stream = File.OpenRead(fileName))
using (var reader = new StreamReader(stream))
{
var data = CsvParser.ParseHeadAndTail(reader, ',', '"');
var header = data.Item1;
var lines = data.Item2;
foreach (var line in lines.Take(5))
{
for (var i = 0; i < header.Count; i++)
if (!string.IsNullOrEmpty(line[i]))
Console.WriteLine("{0}={1}", header[i], line[i]);
Console.WriteLine();
}
}
Console.ReadLine();
历史
- 2014 年 9 月 27 日:初始版本