Pyparsing中的关键字匹配:非贪婪的令牌啜食

时间:2009-12-15 05:05:23

标签: python parsing grammar pyparsing

Pythonistas:

假设您要使用Pyparsing解析以下字符串:

'ABC_123_SPEED_X 123'

ABC_123是标识符; SPEED_X是一个参数,123是一个值。我想到了使用Pyparsing的以下BNF:

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 个答案:

答案 0 :(得分:7)

我的回答是基于this one,因为你要做的就是得到一个非贪婪的比赛。似乎这很难在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'], {})

请注意,这是妥协;因为我使用SkipToIdentifier可能充满邪恶,恶心的角色,而不仅仅是美丽的alphanums偶尔有下划线。

编辑:感谢Paul McGuire,我们可以通过将Identifier设置为以下内容来制定一个真正优雅的解决方案:

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

让我们检查一下它是如何工作的。首先,忽略外部Combine;我们稍后会谈到这个。从Word(alphanums)开始,我们知道我们将获得引用字符串'ABC'的{​​{1}}部分。重要的是要注意,在这种情况下,我们不允许“单词”包含下划线。我们将它分别构建到逻辑中。

接下来,我们需要捕获'ABC_123_SPEED_X 123'部分,而不是吸入'_123'。我们此时也会跳过'_SPEED_X'并稍后返回。我们从下划线开始为ZeroOrMore,但我们可以使用Literal快捷方式,这将使我们成为领先的下划线,但不是全部'_'。从本质上讲,我们会放置另一个'_123'来捕获其余部分,但这正是通过消耗所有剩余的Word(alphanums)而使我们陷入困境的原因。相反,我们说,“只要下划线后面的内容 '_123_SPEED_X',就将其解析为我Parameter的一部分。我们将pyparsing中的术语称为{{ 1}}。由于我们假设我们可以有任意数量的下划线+ WordButNotParameter重复,我们将该表达式包装为Identifier构造。(如果你总是期望在初始化后至少有下划线+ WordButNotParameter,你可以使用{{ 1}}。)

最后,我们需要将初始Word和特殊下划线+ Word重复包装在一起,以便理解它们是连续的,不用空格分隔,因此我们将整个表达式包装在'_' + ~Parameter + Word(alphanums)构造中。这种方式ZeroOrMore会引发解析错误,但OneOrMore会正确解析。

另请注意,我必须将Combine更改为'ABC _123_SPEED_X',因为前者的方式过于微妙而且很快就会发怒。我不相信'ABC_123_SPEED_X',也不相信它们。

答案 1 :(得分: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)

答案 2 :(得分: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()方法。

答案 3 :(得分:-1)

这回答了一个你可能也问自己的问题:“reduce的真实应用是什么?”:

>>> keys = ['CAT', 'DOG', 'HORSE', 'DEER', 'RHINOCEROS']
>>> p = reduce(lambda x, y: x | y, [Keyword(x) for x in keys])
>>> p
{{{{"CAT" | "DOG"} | "HORSE"} | "DEER"} | "RHINOCEROS"}

修改

这是对原始问题的一个很好的答案。我将不得不处理新的问题。

进一步修改:

我很确定你不能做你想做的事。 pyparsing创建的解析器不会做前瞻。因此,如果你告诉它匹配Word(alphanums + '_'),它将保持匹配的字符,直到它找到一个不是字母,数字或下划线的字符。