在.NET中解析分隔符CSV

25

我有一个文本文件,格式是逗号分隔的,大多数字段使用"作为定界符。 我想将其转换为可以枚举的形式(例如泛型集合)。 我无法控制文件输出的方式或用于分隔符的字符。

在这种情况下,字段由逗号分隔,文本字段用"括起来。 我遇到的问题是有些字段中有引号(例如8" Tray),并且被错误地认为是下一个字段。 对于数字字段,它们没有引号,但以+或-符号开头(表示正/负数)。

我考虑使用正则表达式,但我的技能不太好,希望有人能提出一些我可以尝试的想法。 这个文件中有大约19,000条记录,所以我尽可能高效地处理它。以下是几个示例数据行:

"00","000000112260   ","Pie Pumpkin                             ","RET","6.99 ","     ","ea ",+0000000006.99000
"00","000000304078   ","Pie Apple caramel                       ","RET","9.99 ","     ","ea ",+0000000009.99000
"00","StringValue here","8" Tray of Food                             ","RET","6.99 ","     ","ea ",-00000000005.3200

虽然字段还有很多,但你可以了解清楚...

我正在使用VB.NET,并且已经设置了一个泛型列表来接受数据。我尝试使用CSVReader,它似乎工作得很好,直到遇到第三个记录(其中包含一个引用的文本字段)。如果我能以某种方式处理额外的引号,那么CSVReader选项将非常有效。

谢谢!


2
最好有格式正确的CSV文件作为起点。 - FlySwat
3
在引号内的双引号应该加倍转义。因此,“8” Tray of Food”在格式中是不被允许的。这样,“hi”,“there”这样的字符串才能存在。当它被转义并加上引号后,变成了“hi”,“”,“there”。如果没有加倍转义,它就变成了“hi”,“there”,看起来像两个字符串。 - user66363
我同意上面两个评论,但不幸的是,我无法控制文件的导出方式。这就是软件导出文件的方式。 - hacker
2
请不要自己编写CSV解析器,也不要使用正则表达式。请使用免费、开源、经过实战检验的FileHelpers库。http://www.filehelpers.com - Judah Gabriel Himango
FileHelpers源代码已不再可用 :( - ajeh
11个回答

82

我建议查看.Net中的TextFieldParserClass。你需要包含它。

Imports Microsoft.VisualBasic.FileIO.TextFieldParser

以下是一个快速示例:

        Dim afile As FileIO.TextFieldParser = New FileIO.TextFieldParser(FileName)
        Dim CurrentRecord As String() ' this array will hold each line of data
        afile.TextFieldType = FileIO.FieldType.Delimited
        afile.Delimiters = New String() {","}
        afile.HasFieldsEnclosedInQuotes = True

        ' parse the actual file
        Do While Not afile.EndOfData
            Try
                CurrentRecord = afile.ReadFields
            Catch ex As FileIO.MalformedLineException
                Stop
            End Try
        Loop

5
FYI:TextFieldParser 实现了 IDisposable 接口,应该用 'using' 语句包装或显式地进行处理。 - chilltemp
1
如果在引号字段内部存在换行符,则似乎无法正常工作。遗憾。 - Carter Medlin
2
有人知道为什么这样一个通用的类会存在于VisualBasic命名空间中吗? - pseudocoder
1
2021年报道--我发现Avi的答案仍然是我在使用Framework 4.6解析CSV文件中遇到某些字段包含嵌入逗号时找到的最佳解决方案。我来这里查找是因为其他方法不够易用。 - MarkF

7

以下内容翻译自这里

Encoding fileEncoding = GetFileEncoding(csvFile);
// get rid of all doublequotes except those used as field delimiters
string fileContents = File.ReadAllText(csvFile, fileEncoding);
string fixedContents = Regex.Replace(fileContents, @"([^\^,\r\n])""([^$,\r\n])", @"$1$2");
using (CsvReader csv =
       new CsvReader(new StringReader(fixedContents), true))
{
       // ... parse the CSV

这个工作得还不错,但是对于像 Product "A" Name 这样的名称却出了问题。我相信这与正则表达式有关,但我似乎无法弄对它。 - hacker
查看我的答案,了解我如何实现这个。 - hacker
这是我使用过的一个好解决方案,但 GetFileEncoding 函数并没有提供。如果有人需要,我稍后会发布它。 - Daver

7

正如这个链接所说... 不要自己编写CSV解析器!

像Avi建议的那样使用TextFieldParser。微软已经为您完成了这项工作。如果您最终编写了一个解析器,并且发现其中存在错误,请考虑替换它而不是修复该错误。我最近就是这样做的,这节省了我很多时间。


5
您可以尝试使用我维护的库CsvHelper,它可通过NuGet获得。它遵循CSV的RFC 4180标准。它能够处理字段中包含逗号、引号和换行符等任何内容。
CsvHelper易于使用,但也可以轻松配置以处理许多不同类型的分隔文件。
CsvReader csv = new CsvReader( streamToFile );
IEnumerable<MyObject> myObjects = csv.GetRecords<MyObject>();

如果你想在较低级别上读取CSV文件,可以直接使用解析器,并将每行作为字符串数组返回。

var parser = new CsvParser( myTextReader );
while( true )
{
    string[] line = parser.ReadLine();
    if( line == null )
    {
        break;
    }
}

此外,它是我目前见过的最快速的通用CSV解析器。 - Kemal Erdogan

5

看起来不错,但我发现使用起来非常令人沮丧。不支持自动属性而是私有字段的缺乏支持非常笨拙。 - Alex
这并不是原问题的一个因素,但该页面指出FileHelpers使用动态代码生成。这意味着它在某些受限环境(对我来说是MonoTouch)中无法使用。 - James Moore

1

我发布这篇回答是为了解释我是如何做到的以及原因... Mitch Wheat的答案给了我关于这种情况最佳的解决方案,因为这个数据的格式稍有不同,我只需要稍作修改即可。

以下是VB代码:

Dim fixedContents As String = Regex.Replace(
                            File.ReadAllText(csvFile, fileEncoding),
                            "(?<!,)("")(?!,)", 
                            AddressOf ReplaceQuotes)

使用的正则表达式是我需要更改的,因为某些字段中有非转义引号,而提供的正则表达式似乎不能在所有示例上工作。这个正则表达式使用“向前查看”和“向后查看”来查看引号是否紧跟在逗号后面或者紧靠在逗号前面。在这种情况下,它们都是负数(意味着告诉我双引号不在逗号之前或之后)。这应该意味着引号在字符串的中间。
在这种情况下,我没有直接进行替换,而是使用ReplaceQuotes函数来处理。我使用这个函数的原因是因为我需要一些额外的逻辑来检测它是否在行的开头。如果我花更多的时间去调整正则表达式以考虑行的开头(使用MultiLine等),我相信我可以做到,但当我快速尝试时,它似乎根本不起作用。
有了这个设置,在一个32MB的CSV文件(大约19000行)上使用CSV读取器,它只需要大约2秒钟就可以读取文件,执行正则表达式,将其加载到CSV读取器中,将所有数据添加到我的通用类中并完成。真的很快!

1

排除第一个和最后一个引号的正则表达式应该是 (?<!^)(?<,)"("")(?!,)(?!$)。当然,你需要使用RegexOptions.Multiline。

这样就不需要评估函数了。我的代码用单引号替换不想要的双引号。

完整的 C# 代码如下。

string fixedCSV = Regex.Replace(
            File.ReadAllText(fileName),
            @"(?<!^)(?<!;)("")(?!;)(?!$)", "'", RegexOptions.Multiline);

0
        public static Encoding GetFileEncoding(String fileName)
    {
        Encoding Result = null;
        FileInfo FI = new FileInfo(fileName);
        FileStream FS = null;

        try
        {
            FS = FI.OpenRead();
            Encoding[] UnicodeEncodings = { Encoding.BigEndianUnicode, Encoding.Unicode, Encoding.UTF8 };
            for (int i = 0; Result == null && i < UnicodeEncodings.Length; i++)
            {
                FS.Position = 0;
                byte[] Preamble = UnicodeEncodings[i].GetPreamble();
                bool PreamblesAreEqual = true;
                for (int j = 0; PreamblesAreEqual && j < Preamble.Length; j++)
                {
                    PreamblesAreEqual = Preamble[j] == FS.ReadByte();
                }
                if (PreamblesAreEqual)
                {
                    Result = UnicodeEncodings[i];
                }
            }
        }
        catch (System.IO.IOException)
        {
        }
        finally
        {
            if (FS != null)
            {
                FS.Close();
            }
        }

        if (Result == null)
        {
            Result = Encoding.Default;
        }

        return Result;
    }

0

至少有ODBC驱动程序可用于CSV文件。但是,CSV有不同的变体。

这些文件是由什么产生的?很可能有一个匹配的驱动程序,基于源应用程序的要求。


这是一个名为Business Vision Delta的旧DOS会计软件包。不幸的是,该公司已被新供应商收购,他们不再支持旧的DOS软件。这是我提取数据并集成到新软件中的唯一方法。 - hacker
你能告诉我它使用了什么类型的数据表吗?也许是dbfs?另外,尝试使用Excel、Access或其他可以导入CSV文件的应用程序打开CSV文件。尽量避免将编写软件作为首选项。 - dkretz

0
你使用CSVReader的问题在于第三条记录中的引号没有用另一个引号进行转义(也就是双引号)。如果你不进行转义,那么你如何处理文本字段中间的“,”呢?

http://en.wikipedia.org/wiki/Comma-separated_values

我最终不得不处理带有不同分隔符的文件,但是文本值内的引号字符没有被转义,因此我最终编写了自己的定制解析器。我不知道这是否绝对必要。


那是我的问题...我无法避免它们。我无法控制文件的导出方式。我试图避免编写逐个字符检查引号后是否有逗号等解析器,但最终可能不得不这样做。 - hacker
好的,如果您选择自己编写(我仍然相信有解决方案可以处理这种情况),请尽可能验证字段计数和数据。(我也想分享我的,但是我是在工作中完成的。) - llamaoo7

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接