首页 > 解决方案 > 来自非终结符的 CTF 语法的 Python 解析器

问题描述

在 Python 3 中,最常见的解析器生成器(如 ANTLR 或 Lark)通过从字符串的终结符派生非终结符来定义语法,并构造一个词法分析器和一个解析器来评估字符串。

取而代之的是,我正在寻找一个解析器生成器,它可以在仅由非终结符和终结符组成的“中间输入”上工作,这意味着我会事先进行词法分析和部分分析。

例如,如果输入文法是

S -> AB
A -> a | aA
B -> b | aB

其中大写字母是非终结符,小写字母是终结符,可能的输入可以是,然后可以从中构造aaaB具有根的解析树。S

输入实际上不能只是一串 ASCII 字符,例如aaaB,因为非终结符必须存储有关它们自己的子树的信息。所以至少那些必须是任意对象,输入更有可能是对象列表。

是否有提供此功能的库或包?

标签: pythonparsinggrammarcontext-free-grammar

解决方案


注意:这不是背书。可能还有许多其他 Python 解析包提供类似的功能。它只是作为实现(假定的)目标的可能机制而呈现的。

Ply 解析包确实包含词法分析器生成器,但您没有义务使用它;你可以自由使用任何你喜欢的函数来提供词法标记。标记只是对象类型,其中包含一个value属性。value然而,除了要求它存在之外,Ply 不会对对象做任何假设。

在此处此处引用手册:

当 lex 返回标记时,它们具有存储在value属性中的值。通常,该值是匹配的文本。但是,该值可以分配给任何 Python 对象。

如果你要创建一个手写的词法分析器并且你打算将它与 yacc.py 一起使用,它只需要符合以下要求:

  • 它必须提供一个token()返回下一个令牌或者None如果没有更多令牌可用的方法。
  • token()方法必须返回一个tok具有typevalue属性的对象。如果正在使用行号跟踪,则令牌还应定义一个lineno属性。

为了将非终端传递给解析器,您需要使它们看起来像终端。最简单的方法是为每个非终端制作一个伪终端,并将伪终端转换为单位生产。例如,假设伪终端的名称以一个结尾_(这将使它们很容易以编程方式生成,您可以修改规则

B : b | a B

进入

B : b
  | a B
  | B_

如果我们假设您将正确的 AST 值注入到B_终端返回的令牌对象中,您只需在语法中添加一个动作函数:

 def p_pseudo_terminals(p):
     '''A : A_
        B : B_
     '''
     p[0] = p[1]

这是一个完整的可运行示例,使用问题中的语法:

文件:inject.py

from ply import yacc
from collections import namedtuple
Token = namedtuple('Token', ['type', 'value'])
Node = namedtuple('Node', ['type', 'children'])

tokens = ['a', 'b', 'A_', 'B_']
def p_S(p):
    '''S : A B'''
    p[0] = Node('S', [ p[1], p[2] ])

def p_A(p):
    '''A : a'''
    p[0] = Node('A', [ p[1] ])

def p_Al(p):
    '''A : a A'''
    p[0] = Node('A', [ p[1], p[2] ])

def p_B(p):
    '''B : b'''
    p[0] = Node('B', [ p[1] ])

def p_Bl(p):
    '''B : a B'''
    p[0] = Node('B', [ p[1], p[2] ])

def p_pseudo(p):
    '''A : A_
       B : B_
    '''
    p[0] = p[1]

class Lexer(object):
    def __init__(self, iter):
        self.iter = iter.__iter__()

    def token(self):
        try:
            retval = next(self.iter)
            if type(retval) == Token:
                # Input is a token, just pass it through
                return retval
            else:
                # Input is an AST node; fabricate a pseudo-token
                return Token(retval.type + '_', retval)
        except StopIteration:
            return None

parser = yacc.yacc()

和一个示例运行:

$ python3
Python 3.6.9 (default, Nov  7 2019, 10:44:02) 
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from inject import *
>>> token_stream = [Token('a', 'a')] * 3 + [Node('B', ['a', Node('B', children=['a', Node('B', children=['b'])])])]
>>> lexer = Lexer(token_stream)
>>> print(parser.parse(None, lexer=lexer))
Node(type='S', children=[Node(type='A', children=['a', Node(type='A', children=['a', Node(type='A', children=['a'])])]), Node(type='B', children=['a', Node(type='B', children=['a', Node(type='B', children=['b'])])])])

在真正的语法中,键入所有这些伪标记名称和单位产生式可能很乏味。您可以利用这样一个事实,即您可以自己生成文档字符串,只要它在您通过调用构建解析器之前就位即可yacc.yacc。您还需要将伪标记名称添加到标记名称列表中,以便 Ply 知道这B_是一个标记。


推荐阅读