正则表达式(C#)对于RFC 4180的CSV

时间:2015-12-07 11:23:21

标签: c# regex csv

specification RFC 4180需要通用CSV解析器。 有csv文件,包含规范的所有问题:

Excel打开文件,因为它写在规范中:

任何人都可以使用正则表达式进行解析吗?

CSV文件

  

"
  b
  C"" X
  ÿ
  ž" 357
  试验;试验,XXX; XXX,152个
  " TEST2,TEST2"" XXX2,XXX2" 123
  " TEST3"" TEST3"" XXX3"" XXX3",987
  ,QWE,13
  ASD,123,
  ,,
  ,123,
  ,, 123个
  123 ,,
  123123个

预期结果

Table by EXCEL

2 个答案:

答案 0 :(得分:4)

注意:虽然以下解决方案可能适用于其他正则表达式引擎,但使用 as-is 将要求您的正则表达式引擎将multiple named capture groups using the same name视为一个单一的捕获组。 (.NET默认执行此操作)

关于模式

当CSV文件/流(匹配RFC standard 4180)的一行或多行/记录传递到下面的正则表达式时,它将返回每个非空行/记录的匹配项。每个匹配项都将包含一个名为Value的捕获组,其中包含该行/记录中捕获的值(如果在行尾有一个打开的引号,则可能包含OpenValue捕获组记录)

这是注释模式(测试它on Regexstorm.net):

(?<=\r|\n|^)(?!\r|\n|$)                       // Records start at the beginning of line (line must not be empty)
(?:                                           // Group for each value and a following comma or end of line (EOL) - required for quantifier (+?)
  (?:                                         // Group for matching one of the value formats before a comma or EOL
    "(?<Value>(?:[^"]|"")*)"|                 // Quoted value -or-
    (?<Value>(?!")[^,\r\n]+)|                 // Unquoted value -or-
    "(?<OpenValue>(?:[^"]|"")*)(?=\r|\n|$)|   // Open ended quoted value -or-
    (?<Value>)                                // Empty value before comma (before EOL is excluded by "+?" quantifier later)
  )
  (?:,|(?=\r|\n|$))                           // The value format matched must be followed by a comma or EOL
)+?                                           // Quantifier to match one or more values (non-greedy/as few as possible to prevent infinite empty values)
(?:(?<=,)(?<Value>))?                         // If the group of values above ended in a comma then add an empty value to the group of matched values
(?:\r\n|\r|\n|$)                              // Records end at EOL


这是没有所有注释或空格的原始模式。

(?<=\r|\n|^)(?!\r|\n|$)(?:(?:"(?<Value>(?:[^"]|"")*)"|(?<Value>(?!")[^,\r\n]+)|"(?<OpenValue>(?:[^"]|"")*)(?=\r|\n|$)|(?<Value>))(?:,|(?=\r|\n|$)))+?(?:(?<=,)(?<Value>))?(?:\r\n|\r|\n|$)


Here is a visualization from Debuggex.com(为清晰起见而命名的捕获组): Debuggex.com visualization

用法示例:

一次读取整个CSV文件/流的简单示例(测试on C# Pad):
(为了获得更好的性能并减少对系统资源的影响,您应该使用第二个示例)

using System.Text.RegularExpressions;

Regex CSVParser = new Regex(
    @"(?<=\r|\n|^)(?!\r|\n|$)" +
    @"(?:" +
        @"(?:" +
            @"""(?<Value>(?:[^""]|"""")*)""|" +
            @"(?<Value>(?!"")[^,\r\n]+)|" +
            @"""(?<OpenValue>(?:[^""]|"""")*)(?=\r|\n|$)|" +
            @"(?<Value>)" +
        @")" +
        @"(?:,|(?=\r|\n|$))" +
    @")+?" +
    @"(?:(?<=,)(?<Value>))?" +
    @"(?:\r\n|\r|\n|$)",
    RegexOptions.Compiled);

String CSVSample =
    ",record1 value2,val3,\"value 4\",\"testing \"\"embedded double quotes\"\"\"," +
    "\"testing quoted \"\",\"\" character\", value 7,,value 9," +
    "\"testing empty \"\"\"\" embedded quotes\"," +
    "\"testing a quoted value" + Environment.NewLine +
    Environment.NewLine +
    "that includes CR/LF patterns" + Environment.NewLine +
    Environment.NewLine +
    "(which we wish would never happen - but it does)\", after CR/LF" + Environment.NewLine +
    Environment.NewLine +
    "\"testing an open ended quoted value" + Environment.NewLine +
    Environment.NewLine +
    ",value 2 ,value 3," + Environment.NewLine +
    "\"test\"";

MatchCollection CSVRecords = CSVParser.Matches(CSVSample);

for (Int32 recordIndex = 0; recordIndex < CSVRecords.Count; recordIndex++)
{
    Match Record = CSVRecords[recordIndex];

    for (Int32 valueIndex = 0; valueIndex < Record.Groups["Value"].Captures.Count; valueIndex++)
    {
        Capture c = Record.Groups["Value"].Captures[valueIndex];
        Console.Write("R" + (recordIndex + 1) + ":V" + (valueIndex + 1) + " = ");

        if (c.Length == 0 || c.Index == Record.Index || Record.Value[c.Index - Record.Index - 1] != '\"')
        {
            // No need to unescape/undouble quotes if the value is empty, the value starts
            // at the beginning of the record, or the character before the value is not a
            // quote (not a quoted value)
            Console.WriteLine(c.Value);
        }
        else
        {
            // The character preceding this value is a quote
            // so we need to unescape/undouble any embedded quotes
            Console.WriteLine(c.Value.Replace("\"\"", "\""));
        }
    }

    foreach (Capture OpenValue in Record.Groups["OpenValue"].Captures)
        Console.WriteLine("ERROR - Open ended quoted value: " + OpenValue.Value);
}


读取大型CSV文件/流而不将整个文件/流读入字符串的更好示例(测试它on C# Pad)。

using System.IO;
using System.Text.RegularExpressions;

// Same regex from before shortened to one line for brevity
Regex CSVParser = new Regex(
    @"(?<=\r|\n|^)(?!\r|\n|$)(?:(?:""(?<Value>(?:[^""]|"""")*)""|(?<Value>(?!"")[^,\r\n]+)|""(?<OpenValue>(?:[^""]|"""")*)(?=\r|\n|$)|(?<Value>))(?:,|(?=\r|\n|$)))+?(?:(?<=,)(?<Value>))?(?:\r\n|\r|\n|$)",
    RegexOptions.Compiled);

String CSVSample = ",record1 value2,val3,\"value 4\",\"testing \"\"embedded double quotes\"\"\",\"testing quoted \"\",\"\" character\", value 7,,value 9,\"testing empty \"\"\"\" embedded quotes\",\"testing a quoted value," + 
    Environment.NewLine + Environment.NewLine + "that includes CR/LF patterns" + Environment.NewLine + Environment.NewLine + "(which we wish would never happen - but it does)\", after CR/LF," + Environment.NewLine + Environment
    .NewLine + "\"testing an open ended quoted value" + Environment.NewLine + Environment.NewLine + ",value 2 ,value 3," + Environment.NewLine + "\"test\"";

using (StringReader CSVReader = new StringReader(CSVSample))
{
    String CSVLine = CSVReader.ReadLine();
    StringBuilder RecordText = new StringBuilder();
    Int32 RecordNum = 0;

    while (CSVLine != null)
    {
        RecordText.AppendLine(CSVLine);
        MatchCollection RecordsRead = CSVParser.Matches(RecordText.ToString());
        Match Record = null;

        for (Int32 recordIndex = 0; recordIndex < RecordsRead.Count; recordIndex++)
        {
            Record = RecordsRead[recordIndex];

            if (Record.Groups["OpenValue"].Success && recordIndex == RecordsRead.Count - 1)
            {
                // We're still trying to find the end of a muti-line value in this record
                // and it's the last of the records from this segment of the CSV.
                // If we're not still working with the initial record we started with then
                // prep the record text for the next read and break out to the read loop.
                if (recordIndex != 0)
                    RecordText.AppendLine(Record.Value);

                break;
            }

            // Valid record found or new record started before the end could be found
            RecordText.Clear();            
            RecordNum++;

            for (Int32 valueIndex = 0; valueIndex < Record.Groups["Value"].Captures.Count; valueIndex++)
            {
                Capture c = Record.Groups["Value"].Captures[valueIndex];
                Console.Write("R" + RecordNum + ":V" + (valueIndex + 1) + " = ");
                if (c.Length == 0 || c.Index == Record.Index || Record.Value[c.Index - Record.Index - 1] != '\"')
                    Console.WriteLine(c.Value);
                else
                    Console.WriteLine(c.Value.Replace("\"\"", "\""));
            }

            foreach (Capture OpenValue in Record.Groups["OpenValue"].Captures)
                Console.WriteLine("R" + RecordNum + ":ERROR - Open ended quoted value: " + OpenValue.Value);
        }

        CSVLine = CSVReader.ReadLine();

        if (CSVLine == null && Record != null)
        {
            RecordNum++;

            //End of file - still working on an open value?
            foreach (Capture OpenValue in Record.Groups["OpenValue"].Captures)
                Console.WriteLine("R" + RecordNum + ":ERROR - Open ended quoted value: " + OpenValue.Value);
        }
    }
}


两个示例都返回相同的结果:

  

R1:V1 =
  R1:V2 = record1 value2
  R1:V3 = val3
  R1:V4 =值4
  R1:V5 =测试&#34;嵌入双引号&#34;
  R1:V6 =测试引用&#34;,&#34;性格
  R1:V7 =值7
  R1:V8 =
  R1:V9 =值9
  R1:V10 =测试空&#34;&#34;嵌入式报价
  R1:V11 =测试报价值
  
  包括CR / LF模式
  
  (我们希望永远不会发生 - 但确实如此)
  R1:V12 = CR / LF后   错误 - 开放式报价值: 测试开放式报价值
  
  ,值2,值3,

  R3:V1 =测试

(注意粗体&#34; ERROR ...&#34;行显示开放式引用值 - testing an open ended quoted value - 导致正则表达式匹配该值,以及所有后续值直到正确引用的"test"值,作为OpenValue组中捕获的错误)

在此之前我发现的其他正则表达式解决方案的主要特征:

  • 支持带嵌入/转义引号的引用值。

  • 支持跨越多行的引用值
    value1,"value 2 line 1 value 2 line 2",value3

  • 保留/捕获空值(除RFC standard 4180中未明确涵盖的空行以外,并且假定此正则表达式出错。这可以通过以下方式更改:从正则表达式删除第二组模式 - (?!\r|\n|$) -

  • 行/记录可能以CR + LF或CR或LF

  • 结束
  • 一次解析CSV的多行/记录,为记录中的值返回每个记录和组的匹配项(这要归功于.NET能够将多个值捕获到一个记录中命名捕获组)。

  • 将大部分解析逻辑保留在正则表达式中。您不应该将CSV传递给此正则表达式,然后检查代码中的条件x,y或z以获取实际值(以下限制中突出显示的例外情况)。

限制(解决方法需要正则表达式外部的应用程序逻辑)

  • 通过量化正则表达式中的值模式,无法可靠地限制记录匹配。也就是说,使用(<value pattern>){10}(\r\n|\r|\n|$)而不是(<value pattern>)+?(\r\n|\r|\n|$)之类的内容可能会将您的行/记录匹配仅限于包含十个值的行匹配。但是,它也会强制模式尝试仅匹配十个值,即使这意味着将一个值拆分为两个值,或者在一个空值的空间中捕获九个空值,这样做。

  • 转义/加倍引号字符不是&#34;未转义/取消加倍&#34;。

  • 仅支持调试目的支持具有开放式引用值(缺少结束引用)的记录​​/行。需要外部逻辑来确定如何通过在OpenValue捕获组上执行额外的解析来更好地处理这种情况。

    由于RFC标准中没有定义如何处理这种情况的规则,因此无论如何都需要由应用程序定义此行为。但是,我认为当发生这种情况时正则表达式模式的行为非常好(捕获开放引号和下一个有效记录之间的所有内容作为开放值的一部分)。

    注意:模式可以更改为先前失败(或根本不失败),也不会捕获后续值(例如,从正则表达式中删除OpenValue捕获)。但是,通常这会导致其他错误出现。

为什么:<?/ H3>

我想在被问到之前解决一个常见的问题 - &#34;为什么你要努力创建这个复杂的正则表达式模式而不是使用更快,更好或者更好的解决方案X? #34;

我意识到这里有数百个正则表达式的答案,但我找不到符合我期望的那个。这些期望中的大多数都在问题中引用的RFC standard 4180中涵盖,但主要/另外是捕获跨越多行的引用值以及使用正则表达式解析多行/记录(或整个CSV内容)的能力需要而不是一次一行地传递给正则表达式。

我也意识到abandoning the regex approach或其他库(例如TextFieldParser)的大多数人都FileHelpers来处理CSV解析。而且,这很棒 - 很高兴它对你有用。我选择不使用它们,因为:

  • (主要原因)我认为在正则表达式中进行此操作是一项挑战,我喜欢挑战。

  • TextFieldParser实际上达不到要求,因为它不会处理文件中可能有或没有引号的字段。某些CSV文件仅在需要时引用值以节省空间。 (在其他方面可能会达不到,但是那个让我无法尝试)

  • 我不喜欢依赖于third part libraries有几个原因,但主要是因为我无法控制它们的兼容性(即它是否适用于OS /框架X?),安全漏洞,或及时的错误修正和/或维护。

答案 1 :(得分:2)

我会说,忘掉正则表达式。 CSV可以通过TextFieldParser类轻松解析。要做到这一点,你需要

using Microsoft.VisualBasic.FileIO;

然后你可以使用它:

  using (TextFieldParser parser = new TextFieldParser(Stream))
  {
    parser.TextFieldType = FieldType.Delimited;
    parser.SetDelimiters(",");

    while (!parser.EndOfData)
    {
      string[] fields = parser.ReadFields();
      foreach (string field in fields)
      {
         // Do your stuff here ...
      }
    }
  }
相关问题