使用pyparsing解析以反斜杠结尾的引号字符串的方法是什么
我正在尝试使用 pyparsing 来解析带引号的字符串,具体条件如下:
- 这个带引号的字符串可能会包含内部的引号。
- 我想用反斜杠来转义内部的引号。
- 这个带引号的字符串可能以反斜杠结尾。
我在定义一个成功的解析器时遇到了困难。同时,我开始怀疑 pyparsing 用于这种类型的带引号字符串的正则表达式是否正确(请看我下面的替代正则表达式)。
我是在错误地使用 pyparsing(很可能是这样)还是 pyparsing 本身有bug?
这里有一个脚本展示了这个问题(注意:忽略这个脚本;请关注下面的更新。):
import pyparsing as pp
import re
# A single-quoted string having:
# - Internal escaped quote.
# - A backslash as the last character before the final quote.
txt = r"'ab\'cd\'"
# Parse with pyparsing.
# Does not work as expected: grabs only first 3 characters.
parser = pp.QuotedString(quoteChar = "'", escChar = '\\', escQuote = '\\')
toks = parser.parseString(txt)
print
print 'txt: ', txt
print 'pattern:', parser.pattern
print 'toks: ', toks
# Parse with a regex just like the pyparsing pattern, but with
# the last two groups flipped -- which seems more correct to me.
# This works.
rgx = re.compile(r"\'(?:[^'\n\r\\]|(?:\\.)|(?:\\))*\'")
print
print rgx.search(txt).group(0)
输出:
txt: 'ab\'cd\'
pattern: \'(?:[^'\n\r\\]|(?:\\)|(?:\\.))*\'
toks: ["ab'"]
'ab\'cd\'
更新
感谢大家的回复。我怀疑我因为提问方式不当而让事情变得复杂,所以让我再试一次。
假设我们正在尝试解析一种语言,这种语言的引号规则大致像 Python 的。我们希望用户能够定义可以包含内部引号(用反斜杠保护)的字符串,并且这些字符串可以以反斜杠结尾。这里有一个我们语言的示例文件。注意,这个文件在 Python 语法中也是有效的,如果我们在 Python 中打印 foo
,输出将是字面值:ab'cd\
# demo.txt
foo = 'ab\'cd\\'
我的目标是使用 pyparsing 来解析这样的语言。有没有办法做到这一点?上面的问题基本上是我在几次失败尝试后得出的结论。下面是我最初的尝试。它失败了,因为结尾有两个反斜杠,而不是一个。
with open('demo.txt') as fh:
txt = fh.read().split()[-1].strip()
parser = pp.QuotedString(quoteChar = "'", escChar = '\\')
toks = parser.parseString(txt)
print
print 'txt: ', txt
print 'pattern:', parser.pattern
print 'toks: ', toks # ["ab'cd\\\\"]
我想问题在于 QuotedString
只把反斜杠当作引号转义,而 Python 把反斜杠当作更通用的转义符。
有没有我忽略的简单方法?我想到的一个解决方法是使用 .setParseAction(...)
来处理多余的反斜杠,也许像这样,这似乎有效:
qHandler = lambda s,l,t: [ t[0].replace('\\\\', '\\') ]
parser = pp.QuotedString(quoteChar = "'", escChar = '\\').setParseAction(qHandler)
3 个回答
PyParsing的QuotedString
解析器不能处理以反斜杠结尾的带引号字符串。这是一个基本的限制,目前没有什么简单的方法可以解决这个问题。如果你想支持这种字符串,你需要使用其他的方式,而不是QuotedString
。
这种限制其实并不罕见。Python本身也不允许在“原始”字符串字面量的末尾有奇数个反斜杠。你可以试试:r"foo\"
会引发一个异常,而r"bar\\"
会把两个反斜杠都包含在输出中。
你现在的代码输出被截断(而不是引发异常)的原因是因为你把反斜杠作为escQuote
参数传入了。我认为这个参数是用来替代指定转义字符的,而不是作为补充。发生的情况是,第一个反斜杠被解析器当作内部引号(它会取消转义),而且后面跟着一个实际的引号字符,所以解析器认为已经到达了带引号字符串的末尾。因此,你得到的结果是ab'
。
我觉得你对 escQuote
的用法有些误解。根据文档的说明:
escQuote - 特殊的引号序列,用于转义嵌入的引号字符串(比如 SQL 中的 "" 用于转义嵌入的 ")(默认值为 None)
所以 escQuote
是用来指定一个完整的序列,这个序列会被解析为字面上的引号。在文档中的例子里,你可以指定 escQuote='""'
,这样它就会被解析为 "
。如果你把反斜杠指定为 escQuote
,那么一个单独的反斜杠就会被当作引号来解释。在你的例子中看不到这一点,因为你只转义了引号而没有转义其他东西。不过,如果你尝试转义其他字符,你会发现这样做是无效的:
>>> txt = r"'a\Bc'"
>>> parser = pyp.QuotedString(quoteChar = "'", escChar = '\\', escQuote = "\\")
>>> parser.parseString(txt)
(["a'Bc"], {})
注意到反斜杠被替换成了 '
。
至于你的替代方案,我认为 pyparsing(还有很多其他解析器)不这样做的原因是,它涉及到在字符串中的一个特定位置进行特殊处理。在你的正则表达式中,单个反斜杠是一个转义字符,除了在字符串的最后一个字符位置外,其他地方都是如此,在这个位置它会被当作字面值处理。这意味着你无法“局部”判断一个给定的引号是否真的代表字符串的结束——即使它有反斜杠,如果后面还有一个没有反斜杠的引号,它可能并不是结束。这可能导致解析的模糊性和意外的解析行为。例如,考虑这些例子:
>>> txt = r"'ab\'xxxxxxx"
>>> print rgx.search(txt).group(0)
'ab\'
>>> txt = r"'ab\'xxxxxxx'"
>>> print rgx.search(txt).group(0)
'ab\'xxxxxxx'
通过在字符串末尾添加一个撇号,我突然让之前的撇号不再是结束,并一次性把所有的 x 加到了字符串里。在实际使用中,这可能导致一些混淆的情况,比如不匹配的引号会悄悄地导致字符串重新解析,而不是出现解析错误。
虽然我现在想不出具体的例子,但我也怀疑这可能会导致“灾难性的回溯”,如果你真的尝试解析一个包含多个这种类型字符串的大文件。(这就是我提到的“100MB 的其他文本”的意思。)因为解析器无法知道一个给定的 \'
是否是字符串的结束,除非继续解析下去,它可能需要一直解析到文件的末尾,以确保没有更多的引号。如果文件的剩余部分包含了更多这种类型的字符串,可能会变得复杂,难以判断哪些引号是用来分隔哪些字符串的。例如,如果输入包含类似这样的内容:
'one string \' 'or two'
我们无法判断这是否是两个有效的字符串(one string \
和 or two
)还是一个后面有无效内容的字符串(one string \'
和后面的非字符串标记 or two
,后面跟着一个不匹配的引号)。这种情况在许多解析场景中是不可取的;你希望字符串的开始和结束的判断是可以局部确定的,而不依赖于文档中很久之后出现的其他标记。
这段代码有什么地方对你不起作用呢?
from pyparsing import *
s = r"foo = 'ab\'cd\\'" # <--- IMPORTANT - use a raw string literal here
ident = Word(alphas)
strValue = QuotedString("'", escChar='\\')
strAssign = ident + '=' + strValue
results = strAssign.parseString(s)
print results.asList() # displays repr form of each element
for r in results:
print r # displays str form of each element
# count the backslashes
backslash = '\\'
print results[-1].count(backslash)
输出结果是:
['foo', '=', "ab'cd\\\\"]
foo
=
ab'cd\\
2
编辑:
所以 "\'" 变成了单引号 "'",但是 "\" 被解析后仍然是 "\",而不是变成转义后的 "\\"。看起来这是 QuotedString 的一个bug。目前你可以使用这个临时解决办法:
import re
strValue.setParseAction(lambda t: re.sub(r'\\(.)', r'\g<1>', t[0]))
这个方法会把每个转义字符序列处理成只有转义后的字符,而不会保留前面的 '\'。
我会在下一个 pyparsing 的补丁版本中加入这个修复。