CSV文件是一种简单文本文件,其规则简单明了,使得它们在不同系统和程序间具有很高的互操作性。CSV文件的读写器相对容易实现,这可能是人们选择CSV格式存储数据的主要原因。尽管CSV文件的读写规则相对已知且广泛接受,但有一个规则是例外的——确定用作分隔符的字符。虽然CSV文件的名称暗示应该使用逗号[,]作为分隔符,但实际上许多CSV文件使用分号[;]或水平制表符[\t]作为分隔符。
为了构建一个通用的CSV读取器,能够读取任何分隔符的CSV文件,读取器必须首先确定使用的是哪个字符作为分隔符。本文提供了解决这个问题的一种可能方案。
编写CSV文件的规则相当简单:
这两个简单的规则使得编写CSV写入器变得非常容易,只需几分钟。实现CSV读取器则更为复杂,因为必须逐个字符地解析CSV流,并提供额外的状态存储——这实际上使CSV读取器成为一个状态机。市面上有很多CSV读取器的实现是错误的,因为它们没有遵循上述规则。
既然已经定义了CSV文件的规则,可以实施一个能够自动检测CSV流中分隔符的CSV读取器。以下是C#源代码,用于检测CSV流中的分隔符:
public static char Detect(TextReader reader, int rowCount, IList<char> separators)
{
IList<int> separatorsCount = new int[separators.Count];
int character;
int row = 0;
bool quoted = false;
bool firstChar = true;
while (row < rowCount)
{
character = reader.Read();
switch (character)
{
case '\"':
if (quoted)
{
if (reader.Peek() != '\"')
{
quoted = false;
}
else
{
reader.Read();
}
}
else
{
quoted = true;
}
break;
case '\n':
if (!quoted)
{
++row;
firstChar = true;
continue;
}
break;
case -1:
row = rowCount;
break;
default:
if (!quoted)
{
int index = separators.IndexOf((char)character);
if (index != -1)
{
++separatorsCount[index];
firstChar = true;
continue;
}
}
break;
}
if (firstChar)
{
firstChar = false;
}
}
int maxCount = separatorsCount.Max();
return maxCount == 0 ? '\0' : separators[separatorsCount.IndexOf(maxCount)];
}
CSV流由reader
参数表示,该参数用于从CSV流中读取字符,rowCount
参数告诉方法在确定分隔符之前应该读取多少行,separators
参数是字符列表,告诉方法哪些字符可能是分隔符。
方法使用以下参数维护内部状态:
separatorsCount
- 用于计算CSV流中可能分隔符作为分隔符出现的次数character
- 从CSV流中读取的最后一个字符row
- 当前处理行在CSV流中的索引quoted
- 如果接下来读取的字符被引号包围,则为true
,否则为false
firstChar
- 如果接下来要读取的字符是CSV条目的第一个字符,则为true
。这个参数是必要的,因为只有在打开引号是CSV条目的第一个字符时才认为值被引号包围。当读取了rowCount
行或读取到CSV流的末尾时,方法返回CSV流中作为分隔符出现次数最多的可能分隔符。如果任何可能的分隔符在CSV流中从未作为分隔符出现,则返回‘\0’。
方法在读取引号、分隔符和作为引用值一部分的新行字符时会小心处理。在这种情况下,如果读取到引号,方法将查看CSV流中的下一个字符是否也是引号,否则将考虑这个引号为关闭引号。如果引号值中包含新行和分隔符字符,则忽略它们。
例如,在以下Employees.csv
文件中:
Name,Surname,Salary
John,Doe,"$2,130"
Fred;Nurk;"$1,500"
Hans;Meier;"$1,650"
Ivan;Horvat;"$3,200"
方法检测到CSV分隔符是[;],尽管[;]的总出现次数是6,[,]的总出现次数是8。这是因为最后4次[,]的出现被引号包围,所以它们不被视为可能的分隔符。因此,[,]作为分隔符的总出现次数是4,[;]作为分隔符的总出现次数是6,这使得[;]成为最可能的CSV分隔符。
本文附带了一个WPF解决方案,演示了CSV分隔符的自动检测。解决方案可以在这里下载。应用程序位于bin/Release文件夹中。