python - 来自非终结符的 CTF 语法的 Python 解析器
问题描述
在 Python 3 中,最常见的解析器生成器(如 ANTLR 或 Lark)通过从字符串的终结符派生非终结符来定义语法,并构造一个词法分析器和一个解析器来评估字符串。
取而代之的是,我正在寻找一个解析器生成器,它可以在仅由非终结符和终结符组成的“中间输入”上工作,这意味着我会事先进行词法分析和部分分析。
例如,如果输入文法是
S -> AB
A -> a | aA
B -> b | aB
其中大写字母是非终结符,小写字母是终结符,可能的输入可以是,然后可以从中构造aaaB
具有根的解析树。S
输入实际上不能只是一串 ASCII 字符,例如aaaB
,因为非终结符必须存储有关它们自己的子树的信息。所以至少那些必须是任意对象,输入更有可能是对象列表。
是否有提供此功能的库或包?
解决方案
注意:这不是背书。可能还有许多其他 Python 解析包提供类似的功能。它只是作为实现(假定的)目标的可能机制而呈现的。
Ply 解析包确实包含词法分析器生成器,但您没有义务使用它;你可以自由使用任何你喜欢的函数来提供词法标记。标记只是对象类型,其中包含一个value
属性。value
然而,除了要求它存在之外,Ply 不会对对象做任何假设。
当 lex 返回标记时,它们具有存储在
value
属性中的值。通常,该值是匹配的文本。但是,该值可以分配给任何 Python 对象。
…
如果你要创建一个手写的词法分析器并且你打算将它与 yacc.py 一起使用,它只需要符合以下要求:
- 它必须提供一个
token()
返回下一个令牌或者None
如果没有更多令牌可用的方法。 - 该
token()
方法必须返回一个tok
具有type
和value
属性的对象。如果正在使用行号跟踪,则令牌还应定义一个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_
是一个标记。
推荐阅读
- bash - 尝试大写第一个字母时 Bash 错误替换
- nginx - nginx 使用 IP 和本地名称将流量定向到非域名
- browser - 某些表示形式中的 Unicode 字符问题
- sql - 一张表中的两个唯一键 SQL
- r - 在两个数据集之间匹配列表中的值
- django - Django Orm,包括像 asp.net core 这样的功能
- regex - 如何在电子表格中替换 =textjoin 范围的值
- ios - 在每个单元格都包含表格视图的表格视图中选择新单元格时,无法取消选择先前选择的单元格
- selenium - XPath 中 contains 和 equals 的区别 - selenium
- linux - 为什么 GNU 排序会在这个特定文件上挂起 10 小时以上