CSV文件格式解析与自动检测分隔符

CSV文件是一种简单文本文件,其规则简单明了,使得它们在不同系统和程序间具有很高的互操作性。CSV文件的读写器相对容易实现,这可能是人们选择CSV格式存储数据的主要原因。尽管CSV文件的读写规则相对已知且广泛接受,但有一个规则是例外的——确定用作分隔符的字符。虽然CSV文件的名称暗示应该使用逗号[,]作为分隔符,但实际上许多CSV文件使用分号[;]或水平制表符[\t]作为分隔符。

为了构建一个通用的CSV读取器,能够读取任何分隔符的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文件夹中。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485