5

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

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

使用pyparsing.ParseExpression.copy()规则没有帮助;单个表达式可以被克隆,但是组成它们的子表达式不会以任何明显的方式更新,因此不会调用任何嵌套表达式的解析操作。

我能想到的获得这种效果的唯一其他方法是定义一个返回无上下文抽象解析树的语法,然后在第二步中对其进行处理。即使对于简单的语法,这似乎也很尴尬:最好在使用无法识别的名称时引发异常,并且它仍然不会解析像 C 这样的语言,这些语言实际上需要关于之前发生的内容的上下文才能知道哪个规则匹配。

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

4

4 回答 4

4

有点晚了,但谷歌搜索pyparsing reentrancy显示了这个主题,所以我的回答。
通过将上下文附加到正在解析的字符串,我已经解决了解析器实例重用/重入的问题。您子类str化,将您的上下文放入新 str 类的属性中,将其实例传递给pyparsing并在操作中获取上下文。

蟒蛇 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在每次调用 时都会重置其 Packrat 缓存parseString,但这可以通过覆盖SpecStr.__hash__(使其像object, not一样可散列str)和一些猴子补丁来解决。在我的数据集上,这根本不是问题,因为性能损失可以忽略不计,这甚至有利于内存使用。

于 2016-04-08T13:59:39.677 回答
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)
于 2012-01-02T06:10:55.050 回答
2

让解析动作像你说的那样成为实例方法,而不是重新实例化类怎么样?相反,当您要解析另一个翻译单元时,请重置同一解析器对象中的上下文。

像这样的东西:

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

如果您尝试在不同的线程中进行并行解析,这当然会失败,但我看不出这如何通过共享解析操作以理智的方式工作。

于 2012-01-03T22:12:59.603 回答
2

我遇到了这个确切的限制,并使用 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 中有一个简单的 parse 方法,它接受一个字符串并返回一个已解析、转换后的查询表示,这是线程安全的,并且不必为每个解析调用重新创建语法。

于 2013-05-10T18:23:18.343 回答