除全局变量之外的pyparsing解析操作中的上下文

时间:2012-01-01 18:59:28

标签: python pyparsing

我希望能够解析两个(或任意数量)的表达式,每个表达式都有自己的变量定义或其他上下文。

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

在规则上使用pyparsing.ParseExpression.copy()无济于事;单个表达式克隆得很好,但它们组成的子表达式不会以任何明显的方式更新,因此任何嵌套表达式的解析操作都不会被调用。

我能想到的唯一另一种方法就是定义一个语法,该语法返回一个无上下文的抽象解析树,然后在第二步中处理它。即使对于简单的语法,这看起来也很尴尬:在使用无法识别的名称时提出异常会很好,并且它仍然不会解析像C这样的语言,这些语言实际上需要关于之前的内容的上下文来知道匹配的规则。 / p>

是否有另一种方法可以将上下文注入(当然不使用全局变量)到pyparsing表达式的解析操作中?

4 个答案:

答案 0 :(得分:3)

我不知道这是否必然会回答您的问题,但这是将解析器自定义为上下文的一种方法:

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)

答案 1 :(得分:3)

有点晚了,但谷歌搜索pyparsing reentrancy显示了这个主题,所以我的回答 通过将上下文附加到正在解析的字符串,我已经解决了解析器实例重用/重入的问题。 你继承了str,将你的上下文放在新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本身完全可重用且可重入。 pyparsing内部结构通常也是可重入的,只要您不触及ParserElement的静态字段即可。 唯一的缺点是pyparsing在每次调用parseString时重置其packrat缓存,但这可以通过 覆盖SpecStr.__hash__(使其像object一样,而不是str)和一些monkeypatching。在我的数据集中,这根本不是问题,因为性能损失可以忽略不计,这甚至有利于内存使用。

答案 2 :(得分:1)

如何让解析操作成为像你说的实例方法,但只是不重新实现类?相反,当您想要解析另一个翻译单元时,请重置同一个解析器对象中的上下文。

这样的事情:

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”是您的上下文。

如果你试图在不同的线程中进行并行解析,那么这会失败,但是我不知道如何通过共享解析操作以合理的方式工作。

答案 3 :(得分:1)

我遇到了这个确切的限制,并使用threading.local()将解析器上下文信息作为线程本地存储附加。在我的例子中,我保留了一堆被解析的术语,这些术语在解析动作函数中被推送和弹出,但显然你也可以用它来存储对类实例或类似物的引用。

看起来有点像这样:

import threading

__tls = threading.local()

def parse_term(t):
  __tls.stack.append(convert_term(t))

def parse_concatenation(t):
  rhs = __tls.stack.pop()
  lhs = __tls.stack.pop()

  __tls.stack.append(convert_concatenation(t, lhs, rhs)

# parse a string s using grammar EXPR, that has parse actions parse_term and
# parse_concatenation for the rules that parse expression terms and concatenations
def parse(s):
  __tls.stack = []

  parse_result = EXPR.parseString(s)

  return __tls.stack.pop()

在我的情况下,所有线程本地存储内容,设置堆栈,解析操作和语法本身都被推送到公共API之外,因此从外部没有人可以看到正在发生的事情或弄乱它。 API中的某个地方只有一个解析方法,它接受一个字符串并返回一个解析后的转换后的查询表示,这是线程安全的,不必为每个解析调用重新创建语法。