在 cl-ppcre 正则表达式中转义引号
Escaping quotes in cl-ppcre regex
背景
我需要解析 CSV 文件,cl-csv et. al. are too slow on large files, and have a dependency on cl-unicode, which my preferred lisp implementation does not support. So, I am improving cl-simple-table, one that Sabra-on-the-hill benchmarked as the fastest csv reader in a review。
目前,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 代码中。
有人知道如何转义这个例子中的引号吗?
在这个回答中,我重点关注您代码中的错误,并尝试解释您如何让它工作。正如@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 reader 将 \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
将每个字段放入列表或向量中,以便稍后处理。
背景
我需要解析 CSV 文件,cl-csv et. al. are too slow on large files, and have a dependency on cl-unicode, which my preferred lisp implementation does not support. So, I am improving cl-simple-table, one that Sabra-on-the-hill benchmarked as the fastest csv reader in a review。
目前,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 代码中。
有人知道如何转义这个例子中的引号吗?
在这个回答中,我重点关注您代码中的错误,并尝试解释您如何让它工作。正如@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 reader 将 \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
将每个字段放入列表或向量中,以便稍后处理。