65.9K
CodeProject 正在变化。 阅读更多。
Home

自动检测 CSV 分隔符

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (111投票s)

2011 年 7 月 26 日

CPOL

4分钟阅读

viewsIcon

100321

downloadIcon

2372

解释如何检测 CSV 文件中用作分隔符的字符

auto-detect-csv-separator/AutoDetectCsvSeparator.png

引言

CSV 文件因其简单的文本格式和极少的规则而非常适合存储表格数据。这使得它们具有很高的互操作性,因为 CSV 读取器和写入器的实现相对容易。互操作性可能是人们选择以 CSV 格式保存数据的首要原因。

尽管 CSV 文件写入和读取的规则(将在下一章解释)相对已知且被广泛接受,但有一条规则是例外——确定用作分隔符的字符。顾名思义,CSV 文件(Comma Separated Values,逗号分隔值)应该使用逗号 [,] 作为分隔符,但许多 CSV 文件使用分号 [;] 或水平制表符 [\t] 作为分隔符。

因此,为了构建一个通用的 CSV 读取器,它可以读取任何 CSV 文件而不管分隔符是什么,读取器必须首先找出使用了哪个字符作为分隔符。本文提供了一种解决此问题的方法。

CSV 格式

写入 CSV 文件的规则非常简单

  • 如果值包含分隔符字符、换行符或以引号开头,则用引号将该值括起来。
  • 如果值被引号括起来,则值中包含的任何引号字符都应后跟另一个引号字符。

这两个简单的规则使我们能够轻松地编写 CSV 写入器,只需几分钟即可完成。实现 CSV 读取器则要复杂得多,因为 CSV 流必须逐个字符地顺序解析,并且需要提供额外的状态存储——这实际上使 CSV 读取器成为一个状态机。市面上有很多 CSV 读取器实现不正确,因为它们没有遵循上述规则。

实现

现在我们已经定义了 CSV 文件的规则,就可以实现一个能够找出分隔符字符的 CSV 读取器了。 

以下是检测 CSV 流中分隔符的方法的完整 C# 源代码: 

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() != '"') // Value is quoted and 
			// current character is " and next character is not ".
                        quoted = false;
                    else
                        reader.Read(); // Value is quoted and current and 
				// next characters are "" - read (skip) peeked qoute.
                }
                else
                {
                    if (firstChar) 	// Set value as quoted only if this quote is the 
				// first char in the value.
                        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 文件夹中。

替代方案

从本文提供的代码推导出整个 CSV 读取器应该不难,但是表格数据可能以多种不同的格式出现,为每种格式实现读取器和写入器可能并不容易,并且可能会严重影响您的工作效率。

因此,您可以使用支持各种文件格式的第三方组件。这可能需要一些费用,但像 XLS、XLSX、CSV、ODS、HTML 等格式很可能在同一个 API 中得到支持,这样您的应用程序就可以使用相同的代码来处理不同的文件格式。

© . All rights reserved.