# pip install ply
基本例子
PLY 很魔法的一点是它使用到了模块内部反射. 也就是说在产生一个词法分析器时, 并不是把词法规则传递给 PLY 的接口, 而是依次将一些指定名字的变量或函数定义在 py 文件中.下面给出第一个例子, 从文本中抓出十进制数值.
import ply.lex
tokens = (
'NUMBER',
)
t_NUMBER = r'\d*[.]?\d+'
def t_error(t):
t.lexer.skip(1)
ply.lex.lex()
ply.lex.input('''
The Chinese mathematician Zu Chongzhi, around 480 AD, calculated that
pi ~= 355/113
(a fraction that goes by the name Milv in Chinese), using Liu Hui's
algorithm applied to a 12288-sided polygon. With a correct value for its
seven first decimal digits, this value of 3.141592920... remained the most
accurate approximation of pi available for the next 800 years.
<From Wikipedia:Pi>
''')
for token in iter(ply.lex.token, None):
print token.type, token.value
NUMBER 480
NUMBER 355
NUMBER 113
NUMBER 12288
NUMBER 3.141592920
NUMBER 800
- 文件中定义了
tokens
表示可能的词元类型; 在官方例子中, 其中的取值通常以全大写的形式出现 - 定义词元规则
t_NUMBER
, 其名字是token
变量中的成员'NUMBER'
加上前缀t_
; 在构造词法分析器时, PLY 会将以t_
开头的所有定义收集起来 - 词元规则
t_NUMBER
的取值是正则表达式, 用来匹配所有的数值 - 定义
t_error
函数, 如果什么奇怪的东西混进来, 这个函数会被调用; 不过现在只是抓取数值, 无视其它符号, 所以实现只是跳过一个字符 (skip
的参数是字符数量) - 调用
ply.lex.lex
构造词法分析器 - 调用
ply.lex.input
喂一些输入进去 - 从
ply.lex.token
获得词法分析的结果
利用分词输出
从这个基本例子迈向一个用于简单表达式计算的词法分析器并不困难. 如果按照之前一篇文章里所演示的那个功能 (请将该文章中最后一段代码保存在 expr_eval.py 中以供这次使用), 需要至少以下这些词元类型- 二元算符 */^
- 二元或一元算符 +-
- 括号
- 整数
t_ignore
来实现.并且, 这次不是将结果打印出来完事, 而是要构造一个能用的词元序列. 因此需要一个将 PLY 词元转换为在 expr_eval.py 中定义的
Token
类实例的函数. Token
对象构造时, 除了词元字符串值参数外, 还需要两个值表示是否为数值以及是否可能是前置一元运算符, 这两个参数都可以根据词元类型获取.下面是对应的代码
import ply.lex
import expr_eval
tokens = (
'BINARY_OP', 'UNARY_OR_BINARY_OP', 'OPEN_PAREN', 'CLOSE_PAREN', 'INTEGER',
)
t_BINARY_OP = r'[*/^]'
t_UNARY_OR_BINARY_OP = r'-|\+'
t_INTEGER = r'\d+'
t_OPEN_PAREN = r'('
t_CLOSE_PAREN = r')'
t_ignore = ' '
def t_error(t):
raise ValueError('invalid character: ' + t.value[0])
def create_token(token):
if token.type in ['OPEN_PAREN', 'CLOSE_PAREN', 'BINARY_OP']:
return expr_eval.Token(token.value, False, False)
if token.type in ['UNARY_OR_BINARY_OP']:
return expr_eval.Token(token.value, False, True)
return expr_eval.Token(token.value, True, False)
ply.lex.lex()
if __name__ == '__main__':
ply.lex.input('''-(6 * 5) + (4 ^ 3) ^ 2 - 1''')
expr = expr_eval.buildExpression([
create_token(token) for token in iter(ply.lex.token, None)
])
print expr.evaluate()
print -(6 * 5) + (4 ** 3) ** 2 - 1
t_ignore
取值并不是一个正则表达式 (并非 r'[ ]+'
); 这是个坑, 如果不慎写成了这种形式, 输入字符串中所有的加号也会被忽略在获取词元时转换
create_token
的实现显得非常直截了当, 但并不很优雅. 而实际上 PLY 提供其它的机制令开发者绕开这种笨拙的映射, 这一机制的实现是将词元规则定义为函数而不是一个字符串. 比如def t_UNARY_OR_BINARY_OP(token):
r'-|\+'
token.value = expr_eval.Token(token.value, False, True)
return token
token
的 value
属性被修改为了期望的词元对象, 因此, 在 create_token
中, 对应类型的词元不需再次转换了def create_token(token):
if token.type in ['OPEN_PAREN', 'CLOSE_PAREN', 'BINARY_OP']:
return expr_eval.Token(token.value, False, False)
if token.type in ['UNARY_OR_BINARY_OP']:
return token.value
return expr_eval.Token(token.value, True, False)
create_token
这个赘余就不再需要了, 也就是说代码被修改成这样import ply.lex
import expr_eval
tokens = (
'BINARY_OP', 'UNARY_OR_BINARY_OP', 'OPEN_PAREN', 'CLOSE_PAREN', 'INTEGER',
)
def t_BINARY_OP(token):
r'[*/^]'
token.value = expr_eval.Token(token.value, False, False)
return token
def t_UNARY_OR_BINARY_OP(token):
r'-|\+'
token.value = expr_eval.Token(token.value, False, True)
return token
def t_INTEGER(token):
r'\d+'
token.value = expr_eval.Token(token.value, True, False)
return token
def t_OPEN_PAREN(token):
r'('
token.value = expr_eval.Token(token.value, False, False)
return token
def t_CLOSE_PAREN(token):
r')'
token.value = expr_eval.Token(token.value, False, False)
return token
t_ignore = ' '
def t_error(t):
raise ValueError('invalid character: ' + t.value[0])
ply.lex.lex()
if __name__ == '__main__':
ply.lex.input('''-(6 * 5) + (4 ^ 3) ^ 2 - 1''')
expr = expr_eval.buildExpression([
token.value for token in iter(ply.lex.token, None)
])
print expr.evaluate()
print -(6 * 5) + (4 ** 3) ** 2 - 1