【问题标题】:Is csv format regular grammar or context-free grammar?csv格式是常规语法还是上下文无关语法?
【发布时间】:2014-07-04 05:10:40
【问题描述】:

我目前正在编写一个 csv 解析器。 csv 格式的定义由 ABNF 定义的RFC4180 给出。所以csv的定义绝对是contex-free的语法。但是,我想知道 csv 是否是常规语法?这样我就可以只用一个有限状态机来解析它。再者,如果是正则文法,能被有限状态机解析,是不是也能被正则表达式解析?

【问题讨论】:

  • 有点挑剔以澄清您的术语:“如果 csv 是常规语法”:csv 文件是一种语言的句子(字母表上所有可能字符串的子集)。您是否必须使用解析器或可以摆脱 FSA 取决于您为该语言编写的语法。如果您编写的语法是常规的,那么您对 ​​FSA 很满意(这是最快的方法),如果您的语法是上下文无关的,那么您需要一个解析器。顺便说一句,REs 只是编写正则语法的一种形式:您可以使用通用生产系统来描述 RGs,但是您必须验证正则性才能应用 FSM。
  • @user1666959 谢谢。现在我明白了“这取决于你写什么语法”。但是,我认为在这种情况下,我想知道的是“可以使用最低语法来解析 csv”。现在我认为这个“最低语法”是正则语法。

标签: regex parsing csv context-free-grammar


【解决方案1】:

所以基于理论的答案是否定的,CSV 文件格式不是常规语言(基于该 RFC)。

它不是的主要原因是基于规范中的这一行:

文件中的每一行都应包含相同数量的字段。

要正式证明文件格式不是常规语言,您可以使用pumping lemma for regular languages

考虑由 2 行和 p 列组成的字符串(其中 p 是来自抽水引理的抽水长度),其中每个单元格都是空的(因此如果 p = 3,它将是 ",,\n,,\n" . 为了满足 |xy| 1 的条件,那么 "y" 必须是文件第一行中的 1 个或多个逗号。如果你然后 "pump" y,那么你第一行的单元格将比第二行多。因此,它不是常规语言。

但是,通常情况下,理论上的答案可能不是您真正需要的。一方面,许多编程语言中的许多正则表达式语法(和有限状态机语法)实际上支持的不仅仅是真正的正则语言。

此外,仅仅因为您无法使用真正的正则表达式验证字符串是否真正符合 CSV 规范并不意味着您仍然无法使用正则表达式对其进行解析。您可能只接受格式稍有错误的 CSV 文件(例如行长不均匀的文件)。

【讨论】:

  • 忽略每行中相同数量字段的要求(“应该”与“必须”不同),CSV 文件中的一行是常规语法还是上下文无关RFC 4180 中指定的语法?关于使用两个双引号来转义引号等的规则可以编码在正则表达式中吗?
  • 基于对 RFC 的快速浏览(自从我回答这个问题以来已经快 2 年了)我相信如果您不要求每一行都具有相同的语法,它实际上是一种常规语法字段数。强制字段必须用双引号并且必须转义任何其他双引号应该没有任何问题。正则表达式会变得有点难看,我不打算在这里努力解决它,但我很确定它是可能的。
【解决方案2】:

我没有任何正式的理论可以验证这一点,但我很确定 CSV 文件可以用正则表达式可靠地解析。不过,最好使用两个正则表达式:

  • 一个正则表达式匹配整个 CSV 行(包括引用字段中的换行符)
  • 另一个正则表达式(用于第一个的匹配结果)来匹配单个字段

(除非您使用 .NET 正则表达式引擎,该引擎提供对重复捕获组的单个捕获的访问,或者除非您事先知道 CSV 文件中的列数并将其硬编码到您的正则表达式中)。

匹配整个 CSV 行的 PCRE 正则表达式可以是:

/^(?:(?:[^",\r\n]*|"(?:""|[^"]*)*+")(?:,|$))*+(?=$)/m

您需要在此处使用/m 修饰符以允许^$ 匹配换行符。如果您正在逐行处理文件,那么正则表达式将在不是完整 CSV 行的行上失败(即引用的字段尚未关闭),因此您需要阅读下一行,添加将其添加到您的测试字符串并重新应用正则表达式(在这种情况下,您可以删除 /m 修饰符)。重复直到匹配为止。

一旦你有了那一行,你就可以使用这个正则表达式来匹配每个连续的字段:

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

这里的匹配结果还包含分隔符(, 或换行符),因此必须从第 1 组中提取实际字段的内容。您还需要在匹配后处理周围和嵌入的引号。

说明:

^             # Start of line (/m modifier!)
(?:           # Start of non-capturing group (to contain the entire line):
 (?:          # Start of non-capturing group (to contain a single field):
  [^",\r\n]*  # Either match a run of character except quotes, commas or newlines
 |            # or
  "           # Match a quoted field, starting with a quote, followed by
  (?:         # either...
   ""         # an escaped quote
  |           # or
   [^"]*      # anything that's not a quote
  )*+         # repeated as often as possible, no backtracking allowed
  "           # Then match a closing quote
 )            # End of group (=field)
 (?:,|$)      # Match a delimiter or the end of the line
)*+           # repeated as often as possible, no backtracking allowed
(?=$)         # Assert that we're now at the end of a line

【讨论】:

  • 语言确实有规律。但我真的不建议用正则表达式解析它。一个简单的解析器通常更快更容易调试。
  • 关于(?=$)$ 已经是一个零宽度断言,因此您不需要通过将其粘贴在前瞻中来“断言”它。
【解决方案3】:

这个问题没有明确的答案,因为 CSV 是一种非常松散的格式。在我观察到的 CSV 阅读器中,上下文无关语法和常规语法都得到了维护。例如,如果在封闭值的末尾后跟逗号以外的任何内容,一些阅读器会抛出异常。

【讨论】:

  • 嗯,CSV 有各种松散的变体,但有一个真正的 RFC:tools.ietf.org/html/rfc4180
  • @Bart - RFC 是一种记录格式的微妙尝试,其中大部分是基于假设。
  • 是的,我现在才注意到文档中的“它没有指定任何类型的 Internet 标准”
  • 这仍然是最严肃的实现所坚持的。
【解决方案4】:

您应该能够使用简单的有限状态机解析 CSV 文件。或者,更准确地说,使用大量简单 FSM 中的一种,具体取决于精确的 CSV 格式。 (这并不意味着这是一个好主意。有 CSV 解析库可以更好地处理您可能在野外发现的 CSV 文件的所有奇怪变体和不成文规则。)

以下是一些(未经测试的)flex 规则,对于最简单的 CSV 变体没有良好的错误处理:

  • 字段用,

  • 分隔
  • 空格没有任何特殊之处,除了用于分隔记录的不带引号的换行符

  • 必须引用包含 " 或换行符的字段;可以引用任何字段。

  • 引用字段中的 " 表示为两个 " 字符。


%%
int record = 1;
int field = 1;

[^",\n]*/[^"]   { printf("Record %d Field %d: |%s|\n", record, field, yytext); }
[,]             { ++field; }
[\n]            { ++line; field = 1; }
["]([^"]|["]["]*)["]/[,\n] {
                  printf("Record %d Field %d: |%s|\n", record, field, yytext); }
.               { printf("Something bad happened in record %d field %d\n",
                          record, field); }

这不能正确处理带引号的字符串(即,它不会去掉引号或非双引号)。

处理引用字段的最简单方法是使用开始条件(仍然作为 FSM 的一部分实现):

%x QUOTED

%%
int record = 1;
int field = 1;

[^",\n]*/[^"]     { printf("Record %d Field %d: |%s|\n", record, field, yytext); }
[,]               { ++field; }
[\n]              { ++line; field = 1; }

["]               { printf("Record %d Field %d: |", record, field); BEGIN(QUOTED); }
<QUOTED>[^"]*     { printf("%s", yytext); }
<QUOTED>["]["]    { putchar('"'); }
<QUOTED>["]/[,\n] { putchar('|'); putchar('\n'); BEGIN(INITIAL); }

<*>.              { printf("Something bad happened in record %d field %d\n",
                           record, field); }

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-04-12
    • 2012-03-27
    • 2014-11-07
    • 2012-12-18
    • 1970-01-01
    • 2012-01-04
    • 1970-01-01
    相关资源
    最近更新 更多