PLY 是 lex 和 yacc 这两种流行的编译器构造工具的纯 Python 实现。
Section 191.1: 开始使用 PLY
要在您的机器上为 Python 2/3 安装 PLY,请按照以下步骤操作:
- 从这里下载源代码。
- 解压下载的 zip 文件。
- 进入解压后的 ply-3.10 文件夹。
- 在终端中运行以下命令:python setup.py install。
如果您完成了上述所有步骤,您现在应该能够使用 PLY 模块了。您可以通过在 Python 解释器中输入 import ply.lex 来测试它。
注意:不要使用 pip 安装 PLY,它会在您的机器上安装一个损坏的版本。
Section 191.2: PLY 的“Hello, World!”示例 —— 一个简单的计算器
让我们通过一个简单的示例来展示 PLY 的强大功能:这个程序将接受一个算术表达式作为字符串输入,并尝试求解它。
打开您喜欢的编辑器并复制以下代码:
from ply import lex
import ply.yacc as yacc
tokens = (
'PLUS',
'MINUS',
'TIMES',
'DIV',
'LPAREN',
'RPAREN',
'NUMBER',
)
t_ignore = ' \t'
t_PLUS = r'\+'
t_MINUS = r'-'
t_TIMES = r'\*'
t_DIV = r'/'
t_LPAREN = r'\('
t_RPAREN = r'\)'
def t_NUMBER( t ) :
r'[0-9]+'
t.value = int( t.value )
return t
def t_newline( t ):
r'\n+'
t.lexer.lineno += len( t.value )
def t_error( t ):
print("Invalid Token:",t.value[0])
t.lexer.skip( 1 )
lexer = lex.lex()
precedence = (
( 'left', 'PLUS', 'MINUS' ),
( 'left', 'TIMES', 'DIV' ),
( 'nonassoc', 'UMINUS' )
)
def p_add( p ) :
'expr : expr PLUS expr'
p[0] = p[1] + p[3]
def p_sub( p ) :
'expr : expr MINUS expr'
p[0] = p[1] - p[3]
def p_expr2uminus( p ) :
'expr : MINUS expr %prec UMINUS'
p[0] = - p[2]
def p_mult_div( p ) :
'''expr : expr TIMES expr | expr DIV expr'''
if p[2] == '*' :
p[0] = p[1] * p[3]
else :
if p[3] == 0 :
print("Can't divide by 0")
raise ZeroDivisionError('integer division by 0')
p[0] = p[1] / p[3]
def p_expr2NUM( p ) :
'expr : NUMBER'
p[0] = p[1]
def p_parens( p ) :
'expr : LPAREN expr RPAREN'
p[0] = p[2]
def p_error( p ):
print("Syntax error in input!")
parser = yacc.yacc()
res = parser.parse("-4*-(3-5)") # 输入
print(res)
将此文件保存为 calc.py 并运行它。
输出:
-8
这是 -4 * - (3 - 5) 的正确答案。
Section 191.3: 第 1 部分:使用 Lex 对输入进行分词
第 1 部分的代码执行了两个步骤:第一步是对输入进行分词,即查找构成算术表达式的符号;第二步是解析,涉及分析提取的分词并计算结果。
本节提供了一个简单的示例,展示如何对用户输入进行分词,然后逐行进行解释。
import ply.lex as lex
# 分词名称列表。这是必需的
tokens = [
'NUMBER',
'PLUS',
'MINUS',
'TIMES',
'DIVIDE',
'LPAREN',
'RPAREN',
]
# 简单分词的正则表达式规则
t_PLUS = r'\+'
t_MINUS = r'-'
t_TIMES = r'\*'
t_DIVIDE = r'/'
t_LPAREN = r'\('
t_RPAREN = r'\)'
# 带有操作代码的正则表达式规则
def t_NUMBER(t):
r'\d+'
t.value = int(t.value)
return t
# 定义规则以便跟踪行号
def t_newline(t):
r'\n+'
t.lexer.lineno += len(t.value)
# 包含被忽略字符(空格和制表符)的字符串
t_ignore = ' \t'
# 错误处理规则
def t_error(t):
print("Illegal character '%s'" % t.value[0])
t.lexer.skip(1)
# 构建词法分析器
lexer = lex.lex()
# 给词法分析器一些输入
lexer.input(data)
# 分词
while True:
tok = lexer.token()
if not tok:
break # 没有更多输入
print(tok)
将此文件保存为 calclex.py。我们将在构建 Yacc 解析器时使用它。
逐行解释
- 使用 import ply.lex 导入模块。
- 所有词法分析器都必须提供一个名为 tokens 的列表,该列表定义了词法分析器可以产生的所有分词名称。此列表始终是必需的。
tokens = [
'NUMBER',
'PLUS',
'MINUS',
'TIMES',
'DIVIDE',
'LPAREN',
'RPAREN',
]
tokens 也可以是一个字符串元组(而不是字符串),其中每个字符串表示一个分词,如前所述。
- 每个字符串的正则表达式规则可以定义为字符串或函数。在任何情况下,变量名称都应以 t_ 为前缀,以表示它是匹配分词的规则。
- 对于简单分词,正则表达式可以指定为字符串:t_PLUS = r'\+'
- 如果需要执行某种操作,则可以将分词规则指定为函数。
def t_NUMBER(t):
r'\d+'
t.value = int(t.value)
return t
注意,规则是函数内的文档字符串。该函数接受一个参数,该参数是 LexToken 的一个实例,执行某些操作,然后返回该参数。
如果您想使用外部字符串作为函数的正则表达式规则,而不是指定文档字符串,请考虑以下示例:
@TOKEN(identifier) # identifier 是一个包含正则表达式的字符串
def t_ID(t):
... # 操作
LexToken 对象(我们称这个对象为 t)具有以下属性:
- t.type,即分词类型(作为字符串)(例如:'NUMBER'、'PLUS' 等)。默认情况下,t.type 被设置为 t_ 前缀后面的名称。
- t.value,即词素(实际匹配的文本)。
- t.lineno,即当前行号(词法分析器不知道行号,因此需要手动更新)。使用名为 t_newline 的函数更新 lineno。
def t_newline(t):
r'\n+'
t.lexer.lineno += len(t.value)
- t.lexpos,即分词相对于输入文本开头的位置。
- 如果从正则表达式规则函数中没有返回任何内容,则丢弃该分词。如果您想丢弃一个分词,您也可以在正则表达式规则变量中添加 t_ignore_ 前缀,而不是为相同规则定义一个函数。
def t_COMMENT(t):
r'\#.*'
pass
# 没有返回值。分词被丢弃
... 这与以下内容相同:
t_ignore_COMMENT = r'\#.*'
当然,如果在看到注释时执行某些操作,则使用函数来定义正则表达式规则。
如果您尚未为某些字符定义分词,但仍然希望忽略它们,请使用 t_ignore = "<要忽略的字符>"(这些前缀是必需的):
t_ignore_COMMENT = r'\#.*'
t_ignore = ' \t' # 忽略空格和制表符
- 在构建主正则表达式时,lex 会按照以下顺序添加文件中指定的正则表达式:
- 由函数定义的分词按照它们在文件中出现的顺序添加。
- 由字符串定义的分词按照定义该分词的正则表达式字符串长度的降序添加。
如果您在同一个文件中匹配 == 和 =,请利用这些规则。
- 字面量是直接返回的分词。t.type 和 t.value 都将被设置为该字符本身。定义一个字面量列表如下:
literals = [ '+', '-', '*', '/' ]
或者,
literals = "+-*/"
可以编写执行额外操作的分词函数,当匹配到字面量时。
然而,您需要正确设置分词类型。例如:
literals = [ '{', '}' ]
def t_lbrace(t):
r'\{'
t.type = '{' # 将分词类型设置为预期的字面量(如果是字面量,这是绝对必须的)
return t
- 使用 t_error 函数处理错误。
# Error handling rule
def t_error(t):
print("Illegal character '%s'" % t.value[0])
t.lexer.skip(1) # skip the illegal token (don't process it)
通常,t.lexer.skip(n) 会跳过输入字符串中的 n 个字符。
- 最后的准备:
使用 lexer = lex.lex() 构建词法分析器。
您也可以将所有内容放在一个类中,并使用该类的实例来定义词法分析器。例如:
import ply.lex as lex
class MyLexer(object):
... # 所有关于分词规则和错误处理的内容都像往常一样放在这里
# 构建词法分析器
def build(self, **kwargs):
self.lexer = lex.lex(module=self, **kwargs)
def test(self, data):
self.lexer.input(data)
for token in self.lexer.token():
print(token)
# 构建词法分析器并测试它
m = MyLexer()
m.build() # 构建词法分析器
m.test("3 + 4") #
使用 lexer.input(data) 提供输入,其中 data 是一个字符串。
要获取分词,使用 lexer.token(),它返回匹配的分词。您可以像以下代码那样在循环中迭代 lexer:
for i in lexer:
print(i)
Section 191.4: 第 2 部分:使用 Yacc 解析分词输入
本节解释了如何处理第 1 部分中的分词输入 —— 使用上下文无关文法(CFG)。必须指定文法,并根据文法处理分词。在底层,解析器使用 LALR 解析器。
# Yacc 示例
import ply.yacc as yacc
# 从词法分析器中获取分词映射。这是必需的。
from calclex import tokens
def p_expression_plus(p):
'expression : expression PLUS term'
p[0] = p[1] + p[3]
def p_expression_minus(p):
'expression : expression MINUS term'
p[0] = p[1] - p[3]
def p_expression_term(p):
'expression : term'
p[0] = p[1]
def p_term_times(p):
'term : term TIMES factor'
p[0] = p[1] * p[3]
def p_term_div(p):
'term : term DIVIDE factor'
p[0] = p[1] / p[3]
def p_term_factor(p):
'term : factor'
p[0] = p[1]
def p_factor_num(p):
'factor : NUMBER'
p[0] = p[1]
def p_factor_expr(p):
'factor : LPAREN expression RPAREN'
p[0] = p[2]
# 语法错误处理规则
def p_error(p):
print("Syntax error in input!")
# 构建解析器
parser = yacc.yacc()
while True:
try:
s = input('calc > ')
except EOFError:
break
if not s:
continue
result = parser.parse(s)
print(result)
逐行解释
- 每个文法规则由一个函数定义,该函数的文档字符串包含相应的上下文无关文法规范。构成函数主体的语句实现了该规则的语义动作。每个函数接受一个参数 p,它是一个序列,包含相应规则中每个文法符号的值。p[i] 的值与文法符号的映射如下所示:
def p_expression_plus(p):
'expression : expression PLUS term'
# ^ ^ ^ ^
# p[0] p[1] p[2] p[3]
p[0] = p[1] + p[3]
- 对于分词,相应的 p[i] 的“值”与词法分析器模块中分配的 p.value 属性相同。因此,PLUS 的值将是 +。
- 对于非终结符,其值由放置在 p[0] 中的内容决定。如果没有放置任何内容,则值为 None。另外,p[-1] 与 p[3] 不同,因为 p 不是一个简单的列表(p[-1] 可以指定嵌入的动作(这里不讨论))。
注意,函数可以有任意名称,只要它以 p_ 开头即可。
- p_error(p) 规则被定义为捕获语法错误(与 yacc/bison 中的 yyerror 相同)。
- 如果产生式的结构相似,可以将多个文法规则合并到一个函数中,这是一个好主意。
def p_binary_operators(p):
'''expression : expression PLUS term
| expression MINUS term
term : term TIMES factor
| term DIVIDE factor'''
if p[2] == '+':
p[0] = p[1] + p[3]
elif p[2] == '-':
p[0] = p[1] - p[3]
elif p[2] == '*':
p[0] = p[1] * p[3]
elif p[2] == '/':
p[0] = p[1] / p[3]
- 可以使用字符字面量代替分词。
def p_binary_operators(p):
'''expression : expression '+' term
| expression '-' term
term : term '*' factor
| term '/' factor'''
if p[2] == '+':
p[0] = p[1] + p[3]
elif p[2] == '-':
p[0] = p[1] - p[3]
elif p[2] == '*':
p[0] = p[1] * p[3]
elif p[2] == '/':
p[0] = p[1] / p[3]
当然,字面量必须在词法分析器模块中指定。
- 空产生式的格式为 '''symbol : '''
- 要显式设置起始符号,请使用 start = 'foo',其中 foo 是某个非终结符。
- 可以使用 precedence 变量设置优先级和结合性。
precedence = (
('nonassoc', 'LESSTHAN', 'GREATERTHAN'), # Nonassociative operators
('left', 'PLUS', 'MINUS'),
('left', 'TIMES', 'DIVIDE'),
('right', 'UMINUS'), # Unary minus operator
)
分词按优先级从低到高排序。nonassoc 表示这些分词不结合。这意味着像 a < b < c 这样的表达式是非法的,而 a < b 仍然是合法的。
- parser.out 是一个调试文件,当 yacc 程序首次执行时会创建它。每当发生移入/归约冲突时,解析器总是选择移入。