python:用BNF或pyparsing替换正则表达式

1 投票
1 回答
3102 浏览
提问于 2025-04-16 03:53

我正在解析一段相对简单的文本,每一行描述一个游戏单位。我对解析技术了解不多,所以我用了一个临时的解决方案:

class Unit:
    # rules is an ordered dictionary of tagged regex that is intended to be applied in the given order
    # the group named V would correspond to the value (if any) for that particular tag
    rules = (
        ('Level', r'Lv. (?P<V>\d+)'),
        ('DPS', r'DPS: (?P<V>\d+)'),
        ('Type', r'(?P<V>Tank|Infantry|Artillery'),
        #the XXX will be expanded into a list of valid traits
        #note: (XXX| )* wouldn't work; it will match the first space it finds,
        #and stop at that if it's in front of something other than a trait
        ('Traits', r'(?P<V>(XXX)(XXX| )*)'),
        # flavor text, if any, ends with a dot
        ('FlavorText', r'(?P<V>.*\."?$)'),
        )
    rules = collections.OrderedDict(rules)
    traits = '|'.join('All-Terrain', 'Armored', 'Anti-Aircraft', 'Motorized')
    rules['Traits'] = re.sub('XXX', effects, rules['Traits'])

    for x in rules:
        rules[x] = re.sub('<V>', '<'+x+'>', rules[x])
        rules[x] = re.compile(rules[x])

    def __init__(self, data)
        # data looks like this:
        # Lv. 5 Tank DPS: 55 Motorized Armored
        for field, regex in Item.rules.items():
            data = regex.sub(self.parse, data, 1)
        if data:
            raise ParserError('Could not parse part of the input: ' + data)

    def parse(self, m):
        if len(m.groupdict()) != 1:
            Exception('Expected a single named group')
        field, value = m.groupdict().popitem()
        setattr(self, field, value)
        return ''

这个方法运行得还不错,但我感觉正则表达式的能力到了极限。特别是在处理特性(Traits)时,最后得到的值是一个字符串,我需要在后面把它拆分并转换成一个列表。例如,在这段代码中,obj.Traits 被设置为 'Motorized Armored',但在后面的函数中需要改成 ('Motorized', 'Armored')。

我在考虑把这段代码改成使用 EBNF 或者 pyparsing 语法,或者其他类似的东西。我的目标是:

  • 让这段代码更整洁,减少出错的可能性
  • 避免处理值列表时的麻烦(我需要先在正则表达式中做替换,然后再后处理结果,把字符串转换成列表)

你有什么建议可以用来改进代码吗?

附注:为了避免代码过于杂乱,我省略了一些部分;如果因此引入了错误,抱歉——原始代码是可以工作的 :)

1 个回答

4

我开始写一个关于pyparsing的指导手册,但看了你的规则后,发现它们可以很容易地转化为pyparsing的元素,而不需要处理EBNF,所以我快速做了一个示例:

from pyparsing import Word, nums, oneOf, Group, OneOrMore, Regex, Optional

integer = Word(nums)
level = "Lv." + integer("Level")
dps = "DPS:" + integer("DPS")
type_ = oneOf("Tank Infantry Artillery")("Type")
traits = Group(OneOrMore(oneOf("All-Terrain Armored Anti-Aircraft Motorized")))("Traits")
flavortext = Regex(r".*\.$")("FlavorText")

rule = (Optional(level) & Optional(dps) & Optional(type_) & 
        Optional(traits) & Optional(flavortext))

我加入了正则表达式的例子,这样你就可以看到如何把正则表达式放进现有的pyparsing语法中。使用'&'运算符组合的rule意味着这些单独的项目可以以任何顺序出现(所以语法会自动处理所有规则的迭代,而不是你在自己的代码中去做)。pyparsing利用运算符重载来从简单的解析器构建复杂的解析器:'+'表示顺序,'|'和'^'表示选择(优先匹配或最长匹配),等等。

这是解析结果的样子——注意我添加了结果名称,就像你在正则表达式中使用命名组一样:

data = "Lv. 5 Tank DPS: 55 Motorized Armored"

parsed_data = rule.parseString(data)
print parsed_data.dump()
print parsed_data.DPS
print parsed_data.Type
print ' '.join(parsed_data.Traits)

输出结果是:

['Lv.', '5', 'Tank', 'DPS:', '55', ['Motorized', 'Armored']]
- DPS: 55
- Level: 5
- Traits: ['Motorized', 'Armored']
- Type: Tank
55
Tank
Motorized Armored

请去维基看看其他例子。你可以通过easy_install来安装pyparsing,但如果你从SourceForge下载源代码分发版,会有很多额外的文档。

撰写回答