CSV格式可以由正则表达式定义吗?


19

我和一位同事最近就纯正则表达式是否能够完全封装csv格式进行了争论,以便它能够使用任何给定的转义字符,引号字符和分隔符char解析所有文件。

正则表达式不必在创建后就可以更改这些字符,但在任何其他情况下都不能失败。

我认为,这对于令牌生成器来说是不可能的。唯一能够做到这一点的正则表达式是一种非常复杂的PCRE样式,它不仅限于标记化。

我正在寻找以下方面的东西:

... csv格式是上下文无关的语法,因此,不可能仅使用正则表达式进行解析...

还是我错了?是否可以仅使用POSIX正则表达式来解析csv?

例如,如果转义字符和引号字符均为",则这两行都是有效的csv:

"""this is a test.""",""
"and he said,""What will be, will be."", to which I replied, ""Surely not!""","moving on to the next field here..."

它不是CSV,因为任何地方都没有嵌套(IIRC)
棘轮怪胎

1
但是边缘情况是什么?也许CSV比我想象的要多得多?
c69 2012年

1
@ c69转义和报价char都如何"。那么以下内容有效:"""this is a test.""",""
Spencer Rathbun 2012年

您从这里尝试过regexp 吗?
dasblinkenlight 2012年

1
您确实需要提防边缘情况,但是正则表达式应该能够像您描述的那样标记化csv。正则表达式不需要计算任意数量的引号-它只需要计算3个正则表达式即可。正如其他人提到的那样,您应该尝试写下一个清晰的表示形式,表示您期望的csv令牌...
comestorm 1999年

Answers:


20

理论上不错,实践上很糟糕

通过CSV,我将假设您的意思是RFC 4180中描述的约定。

匹配基本CSV数据很简单:

"data", "more data"

注意:顺便说一句,对于非常简单且结构良好的数据,使用.split('/ n')。split('“')函数效率要高得多。正则表达式可作为NDFSM(不确定性有限)状态机),一旦您开始添加转义字符(如转义字符)等极端情况,就会浪费大量时间进行回溯。

例如,这是我找到的最全面的正则表达式匹配字符串:

re_valid = r"""
# Validate a CSV string having single, double or un-quoted values.
^                                   # Anchor to start of string.
\s*                                 # Allow whitespace before value.
(?:                                 # Group for value alternatives.
  '[^'\\]*(?:\\[\S\s][^'\\]*)*'     # Either Single quoted string,
| "[^"\\]*(?:\\[\S\s][^"\\]*)*"     # or Double quoted string,
| [^,'"\s\\]*(?:\s+[^,'"\s\\]+)*    # or Non-comma, non-quote stuff.
)                                   # End group of value alternatives.
\s*                                 # Allow whitespace after value.
(?:                                 # Zero or more additional values
  ,                                 # Values separated by a comma.
  \s*                               # Allow whitespace before value.
  (?:                               # Group for value alternatives.
    '[^'\\]*(?:\\[\S\s][^'\\]*)*'   # Either Single quoted string,
  | "[^"\\]*(?:\\[\S\s][^"\\]*)*"   # or Double quoted string,
  | [^,'"\s\\]*(?:\s+[^,'"\s\\]+)*  # or Non-comma, non-quote stuff.
  )                                 # End group of value alternatives.
  \s*                               # Allow whitespace after value.
)*                                  # Zero or more additional values
$                                   # Anchor to end of string.
"""

它合理地处理单引号和双引号值,但不处理值的换行符,转义引号等。

资料来源:堆栈溢出-如何使用JavaScript解析字符串

一旦引入常见的边缘情况,这将成为一场噩梦。

"such as ""escaped""","data"
"values that contain /n newline chars",""
"escaped, commas, like",",these"
"un-delimited data like", this
"","empty values"
"empty trailing values",        // <- this is completely valid
                                // <- trailing newline, may or may not be included

仅换行符作为值的边缘情况就足以破坏在野生环境中发现的基于RegEx的解析器的99.9999%。唯一“合理”的选择是对基本控制/非控制字符(即终端与非终端)标记化使用RegEx匹配,并与用于更高级别分析的状态机配对。

资料来源:经验,即广泛的痛苦和折磨。

我是jquery-CSV的作者,jquery-CSV是世界上唯一基于JavaScript的,完全符合RFC的CSV解析器。我花了几个月的时间解决这个问题,与许多有才华的人交谈,并且尝试了很多不同的实现,包括对核心解析器引擎的3次完全重写。

tl; dr-故事的寓意,除最简单和严格的常规(即III类)语法外,PCRE本身就很烂。尽管如此,它对于标记终端字符串和非终端字符串很有用。


1
是的,这也是我的经验。试图完全封装比一个非常简单的CSV模式更多的东西都会碰到这些事情,然后您会遇到大规模正则表达式的效率问题和复杂性问题。您是否看过node-csv库?似乎也证实了这一理论。每个非平凡的实现都在内部使用解析器。
Spencer Rathbun

@SpencerRathbun是的。我确定我之前已经看过node-csv源代码。似乎使用典型的字符标记化状态机进行处理。jquery-csv解析器在相同的基本概念上工作,除了我使用正则表达式进行终端/非终端令牌化。regex能够一次匹配多个非终结符,并将它们作为一个组(即字符串)返回,而不是逐个字符地进行评估和连接。这样可以最大程度地减少不必要的串联,并“应该”提高效率。
Evan Plaice

20

正则表达式可以解析任何常规语言,并且不能解析诸如递归语法之类的奇特事物。但是CSV似乎很常规,因此可以用正则表达式解析。

让我们从定义开始工作:允许的是序列,选择形式的选择(|)和重复(Kleene star,the *)。

  • 无引号的值是常规值:[^,]*#除逗号以外的任何字符
  • 带引号的值是常规值:"([^\"]|\\\\|\\")*"#除引号"或转义引号\"或转义转义外的任何序列\\
    • 某些形式可能包括带引号的转义引号,这("")*"为上面的表达式添加了一个变体。
  • 允许的值是常规值:<unquoted-value> |<quoted-value>
  • 一条CSV行是常规的:<value> (,<value>)*
  • 一系列由分隔的线\n显然也是规则的。

我没有仔细测试每个表达式,也没有定义捕获组。我也掩盖了一些技术性问题,比如它可以用来代替字符的变种,"或者行分隔符:这些不破的规律,你只是得到一些稍微不同的语言。

如果您可以在此证明中发现问题,请发表评论!:)

但是,尽管如此,通过纯正则表达式实际解析CSV文件还是有问题的。您需要知道将哪个变体输入解析器,并且没有标准。您可以针对每一行尝试多个解析器,直到成功为止,或者以某种方式区分格式表单注释。但是,这可能需要除正则表达式以外的方法才能有效地执行或根本不执行。


4
实际点绝对是+1。我可以肯定的是,某个深处的例子是一个(人为)值的示例,它将破坏引用值的版本,我只是不知道它是什么。具有多个解析器的“乐趣”将是“这两个工作,但给出不同的答案”

1
显然,反斜杠转义引号和双引号转义引号需要不同的正则表达式。前一种类型的csv字段的正则表达式应类似[^,"]*|"(\\(\\|")|[^\\"])*",而后者应类似[^,"]*|"(""|[^"])*"。(请注意,因为我还没有测试任何一个!)
即将

寻找某种可能是标准的东西,会漏掉一种情况-一个带有记录定界符的值。当存在多种不同的处理方式时,这也使实用的解析变得更加有趣

好的答案,但是如果我运行perl -pi -e 's/"([^\"]|\\\\|\\")*"/yay/'并通过管道输入,"I have here an item,\" that is a test\""则结果是“是的,这是一个测试”。我认为您的正则表达式有缺陷。
Spencer Rathbun 2012年

@SpencerRathbun:当我有更多时间时,我将实际测试正则表达式,甚至可能粘贴一些通过测试的概念验证代码。抱歉,工作日正在进行。
9000

5

简单的答案-可能不是。

第一个问题是缺乏标准。虽然可以用严格定义的方式描述其csv,但不能指望获得严格定义的csv文件。“对自己的工作要保守,对别人接受的东西要开放”-乔恩·普林斯

假设确实有一种可以接受的标准样式,则存在转义字符以及是否需要平衡这些字符的问题。

许多csv格式的字符串定义为string value 1,string value 2。但是,如果该字符串包含逗号,则为now "string, value 1",string value 2。如果包含引号,则变为"string, ""value 1""",string value 2

在这一点上,我认为这是不可能的。问题是您需要确定已阅读了多少个引号,以及逗号是否在值的双引号模式之内或之外。括号之间是不可能的正则表达式问题。一些扩展的正则表达式引擎(PCRE)可以处理它,但是那时它不是正则表达式。

您可能会发现/programming/8629763/csv-parsing-with-a-context-free-grammar很有用。


修改:

我一直在寻找转义字符的格式,但没有找到任何需要任意计数的格式-因此这可能不是问题。

但是,存在什么是转义符和记录定界符(首先)的问题。 http://www.csvreader.com/csv_format.php可以很好地阅读各种格式的内容。

  • 带引号的字符串(如果是单引号字符串或双引号字符串)的规则不同。
    • 'This, is a value'"This, is a value"
  • 转义字符规则
    • "This ""is a value""""This \"is a value\""
  • 嵌入式记录定界符({rd})的处理
    • (原始嵌入)"This {rd}is a value"vs(转义)"This \{rd}is a value"vs(翻译)"This {0x1C}is a value"

这里的关键是可以有一个始终具有多个有效解释的字符串。

相关问题(对于极端情况)“是否有可能接受了无效的字符串?”

我仍然强烈怀疑是否存在一个可以匹配某个应用程序创建的每个有效CSV的正则表达式,并拒绝每个无法解析的csv。


1
引号内的引号不需要平衡。相反,嵌入引号之前必须有偶数个引号,这显然是常规的:("")*"。如果值的报价不平衡,则已经不是我们的事了。
9000

这是我的立场,过去曾遇到过这些可怕的借口来进行“数据传输”。唯一能正确处理它们的是解析器,纯正则表达式每隔几周就破裂一次。
Spencer Rathbun 2012年

2

首先为您的CSV定义语法(如果字段分隔符出现在文本中,是否会对其进行转义或编码?),然后可以确定它是否可以用正则表达式解析。首先是语法:语法是第二:http : //www.boyet.com/articles/csvparser.html应该注意的是,该方法使用了分词器-但我无法构造可匹配所有边缘情况的POSIX正则表达式。如果您对CSV格式的使用是非常规且不受上下文限制的,那么您的答案就在您的问题中。此处提供良好的概述:http : //nikic.github.com/2012/06/15/The-true-power-of-regular-expressions.html


2

如RFC中所述,此正则表达式可以标记普通CSV:

/("(?:[^"]|"")*"|[^,"\n\r]*)(,|\r?\n|\r)/

说明:

  • ("(?:[^"]|"")*"|[^,"\n\r]*) -CSV字段,带引号或不带引号
    • "(?:[^"]|"")*" -带引号的字段;
      • [^"]|""-每个字符不是""转义为""
    • [^,"\n\r]* -无引号的字段,其中可能不包含 , " \n \r
  • (,|\r?\n|\r)-以下分隔符(,换行符)
    • \r?\n|\r -换行符,其中之一 \r\n \n \r

通过重复使用此正则表达式,可以匹配并验证整个CSV文件。然后有必要修复引用的字段,并根据分隔符将其拆分为行。

这是基于regexp的Javascript CSV解析器的代码:

var csv_tokens_rx = /("(?:[^"]|"")*"|[^,"\n\r]*)(,|\r?\n|\r)/y;
var csv_unescape_quote_rx = /""/g;
function csv_parse(s) {
    if (s && s.slice(-1) != '\n')
        s += '\n';
    var ok;
    var rows = [];
    var row = [];
    csv_tokens_rx.lastIndex = 0;
    while (true) {
        ok = csv_tokens_rx.lastIndex == s.length;
        var m = s.match(csv_tokens_rx);
        if (!m)
            break;
        var v = m[1], d = m[2];
        if (v[0] == '"') {
            v = v.slice(1, -1);
            v = v.replace(csv_unescape_quote_rx, '"');
        }
        if (d == ',' || v)
            row.push(v);
        if (d != ',') {
            rows.push(row)
            row = [];
        }
    }
    return ok ? rows : null;
}

这个答案是否有助于解决您的争论,由您决定;我很高兴拥有一个小型,简单且正确的CSV解析器。

在我看来,lex程序或多或少是一个大的正则表达式,并且它们可以标记更复杂的格式,例如C编程语言。

参考RFC 4180定义:

  1. 换行符(CRLF)-正则表达式更灵活,允许CRLF,LF或CR。
  2. 文件中的最后一条记录可能有也可能没有换行符-正则表达式需要最后一个换行符,但解析器会对此进行调整。
  3. 可能有一个可选的标题行-这不会影响解析器。
  4. 每行在整个文件中应包含相同数量的字段-不强制使用
    空格被视为字段的一部分,不应忽略-好的
    记录中的最后一个字段不得后跟逗号-不强制使用
  5. 每个字段可能会或可能不会用双引号引起来...-好的
  6. 包含换行符(CRLF),双引号和逗号的字段应用双引号引起来-好的
  7. 出现在字段中的双引号必须通过在其前面加上另一个双引号来转义-好的

regexp本身可以满足大多数RFC 4180要求。我不同意其他观点,但是很容易调整解析器以实现它们。


1
这看起来更像是自我宣传不是解决问题的要求,请参阅如何回答
蚊蚋

1
@gnat,我编辑了答案以提供更多说明,对照RFC 4180检查regexp,并减少其自我推广。我相信这个答案是有价值的,因为它包含一个经过测试的正则表达式,可以对Excel和其他电子表格使用的最常见CSV形式进行标记。我认为这解决了问题。小型CSV解析器演示了使用此正则表达式很容易解析CSV。
山姆·沃特金斯

不希望过度提升自己,这是我完整的小型csv和tsv库,它们正在用作小型电子表格应用程序的一部分(Google表格对我来说太重了)。这是开源/公共领域/ CC0代码,就像我发布的所有内容一样。我希望这对其他人有用。sam.aiki.info/code/js
Sam Watkins
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.