Pyparsing中的关键字匹配:非贪婪地吸取令牌

5 投票
4 回答
3687 浏览
提问于 2025-04-15 17:01

Python爱好者们:

假设你想用Pyparsing来解析下面这个字符串:

'ABC_123_SPEED_X 123'

其中ABC_123是一个标识符;SPEED_X是一个参数,而123是一个值。我想到了以下的BNF(巴科斯-诺尔范式)来使用Pyparsing:

Identifier = Word( alphanums + '_' )
Parameter = Keyword('SPEED_X') or Keyword('SPEED_Y') or Keyword('SPEED_Z')
Value = # assume I already have an expression valid for any value
Entry = Identifier + Literal('_') + Parameter + Value
tokens = Entry.parseString('ABC_123_SPEED_X 123')
#Error: pyparsing.ParseException: Expected "_" (at char 16), (line:1, col:17)

如果我把中间的下划线去掉(并相应地调整Entry的定义),它就能正确解析。

我该如何让这个解析器变得更“懒”,也就是说等到它匹配到关键字时再进行处理,而不是一开始就把整个字符串当作标识符来处理,并等待那个并不存在的_呢?

谢谢。

[注意:这是我问题的完全重写;我之前没有意识到真正的问题是什么]

4 个回答

1

你也可以把标识符和参数当作一个整体来解析,然后在解析的过程中再把它们分开:

from pyparsing import *
import re

def split_ident_and_param(tokens):
    mo = re.match(r"^(.*?_.*?)_(.*?_.*?)$", tokens[0])
    return [mo.group(1), mo.group(2)]

ident_and_param = Word(alphanums + "_").setParseAction(split_ident_and_param)
value = Word(nums)
entry = ident_and_param + value

print entry.parseString("APC_123_SPEED_X 123")

上面的例子假设标识符和参数的格式总是像这样:XXX_YYY(中间有一个下划线)。

如果情况不是这样,你就需要调整一下 split_ident_and_param() 这个方法。

1

如果你确定这个标识符(就是用来命名的东西)永远不会以下划线结尾,你可以在定义的时候强制这个规则:

from pyparsing import *

my_string = 'ABC_123_SPEED_X 123'

Identifier = Combine(Word(alphanums) + Literal('_') + Word(alphanums))
Parameter = Literal('SPEED_X') | Literal('SPEED_Y') | Literal('SPEED_Z')
Value = Word(nums)
Entry = Identifier + Literal('_').suppress() + Parameter  + Value
tokens = Entry.parseString(my_string)

print tokens # prints: ['ABC_123', 'SPEED_X', '123']

如果不是这样,但标识符的长度是固定的,你可以像这样定义标识符:

Identifier = Word( alphanums + '_' , exact=7)
7

我参考了这个链接来回答你的问题,因为你想要的是一种非贪婪的匹配方式。在pyparsing中实现这一点似乎有点难,但只要聪明点和妥协一下,还是可以做到的。下面的代码看起来可以工作:

from pyparsing import *
Parameter = Literal('SPEED_X') | Literal('SPEED_Y') | Literal('SPEED_Z')
UndParam = Suppress('_') + Parameter
Identifier = SkipTo(UndParam)
Value = Word(nums)
Entry = Identifier + UndParam + Value

当我们在交互式解释器中运行这个时,可以看到以下结果:

>>> Entry.parseString('ABC_123_SPEED_X 123')
(['ABC_123', 'SPEED_X', '123'], {})

需要注意的是,这是一种妥协;因为我使用了SkipTo,所以Identifier中可能会包含一些奇怪的、让人恶心的字符,而不仅仅是偶尔带下划线的漂亮字母数字。

编辑:多亏了Paul McGuire,我们可以通过将Identifier设置为以下内容,来构建一个真正优雅的解决方案:

Identifier = Combine(Word(alphanums) +
        ZeroOrMore('_' + ~Parameter + Word(alphanums)))

让我们来看看这个是怎么工作的。首先,忽略外层的Combine,我们稍后再讨论。我们从Word(alphanums)开始,我们知道可以得到引用字符串中的'ABC'部分,'ABC_123_SPEED_X 123'。需要注意的是,在这个情况下,我们不允许“单词”中包含下划线。我们会在逻辑中单独处理这个。

接下来,我们需要捕获'_123'部分,但又不想把'_SPEED_X'也包括进去。此时我们可以跳过ZeroOrMore,稍后再回到这个。我们以下划线作为Literal开始,但我们可以直接用'_'来简化,这样可以得到前面的下划线,但不会获取整个'_123'。直觉上,我们可能会放另一个Word(alphanums)来捕获剩下的部分,但这正是会让我们陷入麻烦的地方,因为它会消耗掉剩下的'_123_SPEED_X'。相反,我们说:“只要下划线后面跟的不是Parameter,就把它作为我的Identifier的一部分来解析。”在pyparsing中,我们可以这样表示:'_' + ~Parameter + Word(alphanums)。因为我们假设可以有任意数量的下划线加上不是Parameter的单词重复出现,所以我们把这个表达式放在ZeroOrMore构造中。(如果你总是期待在最初的部分后面至少有下划线加上不是Parameter的单词,可以使用OneOrMore。)

最后,我们需要把最初的单词和特殊的下划线加单词重复部分一起包裹起来,以便让它们被理解为是连续的,而不是被空格分开的,所以我们把整个表达式放在Combine构造中。这样'ABC _123_SPEED_X'会引发解析错误,但'ABC_123_SPEED_X'会正确解析。

还要注意,我不得不把Keyword改成Literal,因为前者的用法太微妙,容易出错。我不信任Keyword,而且我也无法与它们匹配。

撰写回答