注意:虽然以下解决方案可能适用于其他正则表达式引擎,但按原样使用它需要您的正则表达式引擎将 multiple named capture groups using the same name 视为一个单一的捕获组。 (.NET 默认会这样做)
###关于模式
当 CSV 文件/流的一个或多个行/记录(匹配 RFC standard 4180)传递给下面的正则表达式时,它将为每个非空行/记录返回一个匹配项。每个匹配项都将包含一个名为 Value 的捕获组,其中包含该行/记录中的捕获值(如果行/记录末尾有一个开放引号,则可能是一个 OpenValue 捕获组) em>。
这是注释模式(测试它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
这是没有所有 cmets 或空格的原始模式。
(?<=\r|\n|^)(?!\r|\n|$)(?:(?:"(?<Value>(?:[^"]|"")*)"|(?<Value>(?!")[^,\r\n]+)|"(?<OpenValue>(?:[^"]|"")*)(?=\r|\n|$)|(?<Value>))(?:,|(?=\r|\n|$)))+?(?:(?<=,)(?<Value>))?(?:\r\n|\r|\n|$)
[这是来自 Debuggex.com 的可视化][3](捕获组以清晰命名):
![Debuggex.com 可视化][4]
###用法示例:
一次读取整个 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 文件/流的更好示例(在 [在 C# Pad][6] 上测试它)。
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 = 记录 1 值 2
R1:V3 = val3
R1:V4 = 值 4
R1:V5 = 测试“嵌入式双引号”
R1:V6 = 测试引用的“,”字符
R1:V7 = 值 7
R1:V8 =
R1:V9 = 值 9
R1:V10 = 测试空的“”嵌入引号
R1:V11 = 测试引用值
包括 CR/LF 模式
(我们希望永远不会发生 - 但它确实发生了)
R1:V12 = CR/LF 之后
错误 - 开放式引用值: 测试开放式引用值
,值 2 ,值 3,
R3:V1 = 测试
(注意粗体“错误...”行表明开放式引用值 - testing an open ended quoted value - 已导致正则表达式匹配该值,以及所有后续值,直到正确引用 "test" 值,作为OpenValue 组中捕获的错误)
###Key 功能优于我在此之前发现的其他正则表达式解决方案:
-
支持带有嵌入/转义引号的引用值。
-
支持跨多行的引用值
value1,"value 2 line 1 value 2 line 2",value3
-
保留/捕获空值(除了在RFC standard 4180 中未明确涵盖的空行,并且此正则表达式假定为错误。这可以通过删除第二组模式来更改 - @ 987654342@ - 来自正则表达式)
-
行/记录可能以 CR+LF 或仅以 CR 或 LF 结尾
-
一次解析 CSV 的多行/记录,为记录中的值返回每个记录和组的匹配项(感谢 .NET 将多个值捕获到单个命名捕获组中的能力)。
-
将大部分解析逻辑保留在正则表达式本身中。您不需要将 CSV 传递给此正则表达式,然后检查代码中的条件 x、y 或 z 以获得实际值(以下限制中突出显示的例外情况)。
###Limitations (变通方法需要正则表达式外部的应用程序逻辑):
-
无法通过量化正则表达式中的值模式来可靠地限制记录匹配。也就是说,使用(<value pattern>){10}(\r\n|\r|\n|$) 之类的东西而不是(<value pattern>)+?(\r\n|\r|\n|$) 可能会将您的行/记录匹配限制为仅包含十个值的那些。但是,它也会强制模式尝试仅匹配十个值,即使这意味着将一个值拆分为两个值或在一个空值的空间中捕获九个空值。
-
转义/双引号字符不是“未转义/非双引号”。
-
仅出于调试目的支持带有开放式引用值(缺少结束引号)的记录/行。需要外部逻辑来确定如何通过对OpenValue 捕获组执行额外的解析来更好地处理这种情况。
由于 RFC 标准中没有定义如何处理这种情况的规则,因此无论如何都需要由应用程序定义这种行为。但是,我认为当这种情况发生时正则表达式模式的行为非常好(捕获打开引号和下一个有效记录之间的所有内容作为打开值的一部分)。
注意:可以将模式更改为提前失败(或根本不失败)并且不捕获后续值(例如,通过从正则表达式中删除 OpenValue 捕获)。但是,通常这会导致其他错误出现。
###为什么?:
在被问到之前,我想先解决一个常见问题 - “您为什么要努力创建这种复杂的正则表达式模式,而不是使用更快、更好或其他的解决方案 X?”
我意识到有数百个正则表达式答案,但我找不到一个能达到我的高期望的答案。大多数期望都包含在问题中引用的RFC standard 4180 中,但主要/另外是捕获跨越多行的引用值,以及在需要时使用正则表达式解析多行/记录(或整个 CSV 内容)的能力,而不是而不是一次将一行传递给正则表达式。
我也意识到大多数人都是abandoning the regex approach 为TextFieldParser 或其他库(如FileHelpers)来处理CSV 解析。而且,这很棒 - 很高兴它对你有用。我选择不使用这些是因为:
-
(主要原因)我认为在正则表达式中做这件事是一个挑战,我喜欢一个很好的挑战。
-
TextFieldParser 实际上不符合要求,因为它不处理文件中可能包含引号或不包含引号的字段。一些 CSV 文件仅在需要时引用值以节省空间。 (它可能在其他方面有所不足,但这让我什至无法尝试)
-
我不喜欢依赖third part libraries 有几个原因,但主要是因为我无法控制它们的兼容性(即它是否适用于 OS/framework X?)、安全漏洞或及时的错误修复和/或维护。