【问题标题】:Escaping quotes in cl-ppcre regex在 cl-ppcre 正则表达式中转义引号
【发布时间】:2021-09-10 06:37:36
【问题描述】:

背景

我需要解析 CSV 文件和cl-csv 等。人。在大文件上速度太慢,并且依赖于cl-unicode,我首选的 lisp 实现不支持。所以,我正在改进 cl-simple-table,Sabra-on-the-hill 将其作为 review 中最快的 csv 阅读器进行基准测试。

目前,simple-table 的行解析器相当脆弱,如果分隔符出现在带引号的字符串中,它就会中断。我正在尝试用 cl-ppcre 替换行解析器。

尝试

使用 Regex Coach,我发现了一个几乎适用于所有情况的正则表达式:

("[^"]+"|[^,]+)(?:,\s*)?

挑战在于将这个 Perl 正则表达式字符串转换成我可以在 cl-ppcre 中使用到 split 行的东西。我试过传递正则表达式字符串," 有各种转义:

(defparameter bads "\"AER\",\"BenderlyZwick\",\"Benderly and Zwick Data: Inflation, Growth and Stock returns\",31,5,0,0,0,0,5,\"https://vincentarelbundock.github.io/Rdatasets/csv/AER/BenderlyZwick.csv\",\"https://vincentarelbundock.github.io/Rdatasets/doc/AER/BenderlyZwick.html\"
"Bad string, note a separator character in the quoted field, near Inflation")

(ppcre:split "(\"[^\"]+\"|[^,]+)(?:,\s*)?" bads)
NIL

单人、双人、三人或四人\ 都不起作用。

我已经解析了字符串以查看解析树的样子:

(ppcre:parse-string "(\"[^\"]+\"|[^,]+)(?:,s*)?")
(:SEQUENCE (:REGISTER (:ALTERNATION (:SEQUENCE #\" (:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #\")) #\") (:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #\,)))) (:GREEDY-REPETITION 0 1 (:GROUP (:SEQUENCE #\, (:GREEDY-REPETITION 0 NIL #\s)))))

并将生成的树传递给split

(ppcre:split '(:SEQUENCE (:REGISTER (:ALTERNATION (:SEQUENCE #\" (:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #\")) #\") (:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #\,)))) (:GREEDY-REPETITION 0 1 (:GROUP (:SEQUENCE #\, (:GREEDY-REPETITION 0 NIL #\s))))) bads)
NIL

我也试过各种形式的*allow-quoting*

 (let ((ppcre:*allow-quoting* t))
  (ppcre:split "(\\Q\"\\E[^\\Q\"\\E]+\\Q\"\\E|[^,]+)(?:,\s*)?" bads))

我已经阅读了cl-ppcre docs,但是使用解析树的例子很少,也没有转义引号的例子。

似乎没有任何效果。

我希望 Regex Coach 能够提供一种查看 Perl 语法字符串的 S 表达式解析树形式的方法。这将是一个非常有用的功能,允许您试验正则表达式字符串,然后将解析树复制并粘贴到 Lisp 代码中。

有人知道如何在这个例子中转义引号吗?

【问题讨论】:

  • 我认为您无法使用正则表达式击败特定解析器的速度。
  • 查看 cl-simple-table,他们做的第一件事就是拆分行。这是错误的,尝试使用正则表达式拆分(而不是在同一个地方)要么也是错误的,要么会以不可接受的方式影响性能。我有理由相信这不是你前进的方向。
  • 你使用的是什么实现方式?
  • @Svante,你可能是对的,我也有同样的想法。为一般情况编写 CSV 解析器是一项相当大的任务。不幸的是,对于常见的 lisp 来说似乎没有任何既健壮又性能良好的产品。我不必使用正则表达式,我愿意接受替代解决方案。
  • 我一直对 fare-csv 很满意,但我不知道它在性能方面的比较。

标签: common-lisp cl-ppcre


【解决方案1】:

在此答案中,我将重点关注您代码中的错误,并尝试解释如何使其工作。正如@Svante 所解释的,这可能不是您的用例的最佳做法。特别是,您的正则表达式可能过于针对您已知的测试输入量身定制,并且可能会错过以后可能出现的案例。

例如,您的正则表达式将字段视为由没有内部双引号(甚至转义)的双引号分隔的字符串,或与逗号不同的字符序列。但是,如果您的字段以普通字母开头,然后包含双引号,它将成为字段名称的一部分。

修复测试字符串

可能在格式化您的问题时出现问题,但介绍bads 的表格格式错误。 这是*bads* 的固定定义(注意特殊变量周围的星号,这是一个有用的约定,有助于将它们与词法变量区分开(名称周围的星号也称为“耳罩”)):

(defparameter *bads*
  "\"AER\",\"BenderlyZwick\",\"Benderly and Zwick Data: Inflation, Growth and Stock returns\",31,5,0,0,0,0,5,\"https://vincentarelbundock.github.io/Rdatasets/csv/AER/BenderlyZwick.csv\",\"https://vincentarelbundock.github.io/Rdatasets/doc/AER/BenderlyZwick.html\"")

正则表达式中的转义字符

您获得的解析树包含以下内容:

(... (:GREEDY-REPETITION 0 NIL #\s) ...)

您的解析树中有一个文字字符#\s。为了理解原因,让我们定义两个辅助函数:

(defun chars (string)
  "Convert a string to a list of char names"
  (map 'list #'char-name string))

(defun test (s)
  (list :parse (chars s)
        :as (ppcre:parse-string s)))

例如,下面是解析不同字符串的方式:

(test "s")
=> (:PARSE ("LATIN_SMALL_LETTER_S") :AS #\s)

(test "\s")
=> (:PARSE ("LATIN_SMALL_LETTER_S") :AS #\s)

(test "\\s")
=> (:PARSE ("REVERSE_SOLIDUS" "LATIN_SMALL_LETTER_S")
    :AS :WHITESPACE-CHAR-CLASS)

仅在反斜杠(反斜杠)被转义的最后一种情况下,PPCRE 解析器会同时看到这个反斜杠和下一个字符#\s,并将这个序列解释为:WHITESPACE-CHAR-CLASS。 Lisp 阅读器将\s 解释为s,因为它不是可以在Lisp 中转义的字符的一部分。

我倾向于直接使用解析树,因为很多麻烦 w.r.t.转义消失了(在我看来,\Q 和 \E 会加剧这种情况)。一个固定的解析树例如下面这个,我用期望的关键字替换了#\s,并删除了无用的:register节点:

 (:sequence
   (:alternation
    (:sequence #\"
     (:greedy-repetition 1 nil
      (:inverted-char-class #\"))
     #\")
    (:greedy-repetition 1 nil (:inverted-char-class #\,)))
   (:greedy-repetition 0 1
    (:group
     (:sequence #\,
      (:greedy-repetition 0 nil :whitespace-char-class)))))

为什么结果是 NIL

请记住,您正在尝试使用此正则表达式split 字符串,但正则表达式实际上描述了一个字段和以下逗号。你有一个 NIL 结果的原因是你的字符串只是一个分隔符序列,就像这个例子:

(split #\, ",,,,,,")
NIL

通过一个更简单的示例,您可以看到将单词拆分为分隔符:

(split "[a-z]+" "abc0def1z3")
=> ("" "0" "1" "3")

但如果分隔符还包含数字,则结果为 NIL:

(split "[a-z0-9]+" "abc0def1z3")
=> NIL

循环字段

使用您定义的正则表达式,使用do-register-groups 会更容易。它是一个循环结构,通过尝试在字符串上连续匹配正则表达式来迭代字符串,将正则表达式中的每个(:register ...) 绑定到一个变量。

如果将(:register ...) 放在第一个(:alternation ...) 周围,有时会捕获双引号(交替的第一个分支):

(do-register-groups (field)
    ('(:SEQUENCE
       (:register
        (:ALTERNATION
         (:SEQUENCE #\"
          (:GREEDY-REPETITION 1 NIL
           (:INVERTED-CHAR-CLASS #\"))
          #\")
         (:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #\,))))
       (:GREEDY-REPETITION 0 1
        (:GROUP
         (:SEQUENCE #\,
          (:GREEDY-REPETITION 0 NIL :whitespace-char-class)))))
     *bads*)
  (print field))

"\"AER\"" 
"\"BenderlyZwick\"" 
"\"Benderly and Zwick Data: Inflation, Growth and Stock returns\"" 
"31" 
"5" 
"0" 
"0" 
"0" 
"0" 
"5" 
"\"https://vincentarelbundock.github.io/Rdatasets/csv/AER/BenderlyZwick.csv\"" 
"\"https://vincentarelbundock.github.io/Rdatasets/doc/AER/BenderlyZwick.html\"" 

另一种选择是添加两个:register 节点,一个用于交替的每个分支;这意味着绑定两个变量,其中一个对于每个成功的匹配都是 NIL:

(do-register-groups (quoted simple)
    ('(:SEQUENCE
       (:ALTERNATION
        (:SEQUENCE #\"
         (:register ;; <- quoted (first register)
          (:GREEDY-REPETITION 1 NIL
           (:INVERTED-CHAR-CLASS #\")))
         #\")
        (:register ;; <- simple (second register)
         (:GREEDY-REPETITION 1 NIL (:INVERTED-CHAR-CLASS #\,))))
       (:GREEDY-REPETITION 0 1
        (:GROUP
         (:SEQUENCE #\,
          (:GREEDY-REPETITION 0 NIL :whitespace-char-class)))))
     *bads*)
  (print (or quoted simple)))

"AER" 
"BenderlyZwick" 
"Benderly and Zwick Data: Inflation, Growth and Stock returns" 
"31" 
"5" 
"0" 
"0" 
"0" 
"0" 
"5" 
"https://vincentarelbundock.github.io/Rdatasets/csv/AER/BenderlyZwick.csv" 
"https://vincentarelbundock.github.io/Rdatasets/doc/AER/BenderlyZwick.html" 

在循环中,您可以将每个字段push 放入一个列表或向量中以供稍后处理。

【讨论】:

  • 这是一个很好的回应。您是否使用引用来解开解析树版本?在我的搜索中我没有看到太多。也许 CL 食谱的第 2 版会有更多内容,但现在这似乎是一种魔法。
  • 除了edicl.github.io/cl-ppcre/#create-scanner2,不多。我使用的大多数正则表达式都不是很复杂(主要是序列、交替、重复和字符类)
猜你喜欢
  • 1970-01-01
  • 2021-06-04
  • 1970-01-01
  • 2018-10-20
  • 2011-05-01
  • 2013-05-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多