pyparsing解析动作中的上下文(除了globals)

6 投票
4 回答
2158 浏览
提问于 2025-04-17 09:21

我想要能够解析两个(或者更多)表达式,每个表达式都有自己的一套变量定义或其他上下文。

似乎没有明显的方法可以将一个上下文与特定的pyparsing.ParseExpression.parseString()调用关联起来。最自然的方式似乎是使用某个类的实例方法作为解析动作。但这个方法的问题在于,每个解析上下文都必须重新定义语法(比如在类的__init__里),这看起来非常低效。

使用pyparsing.ParseExpression.copy()来复制规则也没有帮助;虽然单独的表达式可以被克隆,但它们所组成的子表达式并没有以任何明显的方式被更新,因此任何嵌套表达式的解析动作都不会被调用。

我能想到的唯一其他方法是定义一个不带上下文的抽象解析树的语法,然后在第二步进行处理。即使对于简单的语法,这种方法也显得很尴尬:如果一旦使用了未识别的名称就能抛出异常,那就太好了,而且这仍然无法解析像C语言这样的语言,因为C语言实际上需要知道之前的内容才能确定哪个规则匹配。

有没有其他方法可以将上下文(当然不使用全局变量)注入到pyparsing表达式的解析动作中呢?

4 个回答

4

虽然有点晚了,但搜索一下 pyparsing reentrancy 这个话题,发现了这个问题,所以我来分享一下我的解决方案。
我通过将上下文信息附加到正在解析的字符串上,解决了解析器实例重用和可重入性的问题。你可以创建一个新的字符串类,继承自 str,然后把上下文信息放在这个新类的一个属性里,再把这个类的实例传给 pyparsing,在执行某个动作时就能拿回上下文信息。

在 Python 2.7 中:

from pyparsing import LineStart, LineEnd, Word, alphas, Optional, Regex, Keyword, OneOrMore

# subclass str; note that unicode is not handled
class SpecStr(str):
    context = None  # will be set in spec_string() below
    # override as pyparsing calls str.expandtabs by default
    def expandtabs(self, tabs=8):
        ret = type(self)(super(SpecStr, self).expandtabs(tabs))
        ret.context = self.context
        return ret    

# set context here rather than in the constructor
# to avoid messing with str.__new__ and super()
def spec_string(s, context):
    ret = SpecStr(s)
    ret.context = context
    return ret    

class Actor(object):
    def __init__(self):
        self.namespace = {}

    def pair_parsed(self, instring, loc, tok):
        self.namespace[tok.key] = tok.value

    def include_parsed(self, instring, loc, tok):
        # doc = open(tok.filename.strip()).read()  # would use this line in real life
        doc = included_doc  # included_doc is defined below
        parse(doc, self)  # <<<<< recursion

def make_parser(actor_type):
    def make_action(fun):  # expects fun to be an unbound method of Actor
        def action(instring, loc, tok):
            if isinstance(instring, SpecStr):
                return fun(instring.context, instring, loc, tok)
            return None  # None as a result of parse actions means 
            # the tokens has not been changed

        return action

    # Sample grammar: a sequence of lines, 
    # each line is either 'key=value' pair or '#include filename'
    Ident = Word(alphas)
    RestOfLine = Regex('.*')
    Pair = (Ident('key') + '=' +
            RestOfLine('value')).setParseAction(make_action(actor_type.pair_parsed))
    Include = (Keyword('#include') +
               RestOfLine('filename')).setParseAction(make_action(actor_type.include_parsed))
    Line = (LineStart() + Optional(Pair | Include) + LineEnd())
    Document = OneOrMore(Line)
    return Document

Parser = make_parser(Actor)  

def parse(instring, actor=None):
    if actor is not None:
        instring = spec_string(instring, actor)
    return Parser.parseString(instring)


included_doc = 'parrot=dead'
main_doc = """\
#include included_doc
ham = None
spam = ham"""

# parsing without context is ok
print 'parsed data:', parse(main_doc)

actor = Actor()
parse(main_doc, actor)
print 'resulting namespace:', actor.namespace

这样做会得到

['#include', 'included_doc', '\n', 'ham', '=', 'None', '\n', 'spam', '=', 'ham']
{'ham': 'None', 'parrot': 'dead', 'spam': 'ham'}

这种方法让 Parser 本身变得非常可重用和可重入。只要不去碰 ParserElement 的静态字段,pyparsing 的内部结构通常也是可重入的。唯一的缺点是,pyparsing 在每次调用 parseString 时会重置它的缓存,但这个问题可以通过重写 SpecStr.__hash__ 来解决(让它像 object 一样可哈希,而不是像 str),再加上一些小修改。对我使用的数据集来说,这根本不是问题,因为性能影响微乎其微,反而有利于内存使用。

4

你可以考虑让解析的操作成为实例方法,就像你说的那样,但不需要重新创建类。相反,当你想解析另一个翻译单元时,只需在同一个解析器对象中重置上下文。

可以这样做:

from pyparsing import Keyword, Word, OneOrMore, alphas, nums

class Parser:
    def __init__(self):
        ident = Word(alphas)
        identval = Word(alphas).setParseAction(self.identval_act)
        numlit = Word(nums).setParseAction(self.numlit_act)
        expr = identval | numlit
        letstmt = (Keyword("let") + ident + expr).setParseAction(self.letstmt_act)
        printstmt = (Keyword("print") + expr).setParseAction(self.printstmt_act)
        program = OneOrMore(letstmt | printstmt)

        self.symtab = {}
        self.grammar = program

    def identval_act(self, (ident,)):
        return self.symtab[ident]
    def numlit_act(self, (numlit,)):
        return int(numlit)
    def letstmt_act(self, (_, ident, val)):
        self.symtab[ident] = val
    def printstmt_act(self, (_, expr)):
        print expr

    def reset(self):
        self.symtab = {}

    def parse(self, s):
        self.grammar.parseString(s)

P = Parser()
P.parse("""let foo 10
print foo
let bar foo
print bar
""")

print P.symtab
P.parse("print foo") # context is kept.

P.reset()
P.parse("print foo") # but here it is reset and this fails

在这个例子中,“symtab”就是你的上下文。

当然,如果你尝试在不同的线程中进行并行解析,这样做会出现很大问题,但我看不出在共享解析操作的情况下,这种方式能以合理的方式工作。

4

我不知道这是否能直接回答你的问题,但这是一个根据特定情况来定制解析器的方法:

from pyparsing import Word, alphas, alphanums, nums, oneOf, ParseFatalException

var = Word(alphas+'_', alphanums+'_').setName("identifier")
integer = Word(nums).setName("integer").setParseAction(lambda t:int(t[0]))
operand = integer | var

operator = oneOf("+ - * /")
ops = {'+' : lambda a,b:a+b,
       '-' : lambda a,b:a-b,
       '*' : lambda a,b:a*b,
       '/' : lambda a,b:a/b if b else "inf",
        }

binop = operand + operator + operand

# add parse action that evaluates the binary operator by passing 
# the two operands to the appropriate binary function defined in ops
binop.setParseAction(lambda t: ops[t[1]](t[0],t[2]))

# closure to return a context-specific parse action
def make_var_parseAction(context):
    def pa(s,l,t):
        varname = t[0]
        try:
            return context[varname]
        except KeyError:
            raise ParseFatalException("invalid variable '%s'" % varname)
    return pa

def eval_binop(e, **kwargs):
    var.setParseAction(make_var_parseAction(kwargs))
    try:
        print binop.parseString(e)[0]
    except Exception as pe:
        print pe

eval_binop("m*x", m=100, x=12, b=5)
eval_binop("z*x", m=100, x=12, b=5)

输出结果

1200
invalid variable 'z' (at char 0), (line:1, col:1)

撰写回答