百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术教程 > 正文

python散装笔记——191: Python Lex-Yacc

csdh11 2025-03-25 12:16 6 浏览


PLY 是 lex 和 yacc 这两种流行的编译器构造工具的纯 Python 实现。

Section 191.1: 开始使用 PLY

要在您的机器上为 Python 2/3 安装 PLY,请按照以下步骤操作:

  1. 从这里下载源代码。
  2. 解压下载的 zip 文件。
  3. 进入解压后的 ply-3.10 文件夹。
  4. 在终端中运行以下命令: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 解析器时使用它。

逐行解释

  1. 使用 import ply.lex 导入模块。
  2. 所有词法分析器都必须提供一个名为 tokens 的列表,该列表定义了词法分析器可以产生的所有分词名称。此列表始终是必需的。
 tokens = [
   'NUMBER',
   'PLUS',
   'MINUS',
   'TIMES',
   'DIVIDE',
   'LPAREN',
   'RPAREN',
 ]

tokens 也可以是一个字符串元组(而不是字符串),其中每个字符串表示一个分词,如前所述。

  1. 每个字符串的正则表达式规则可以定义为字符串或函数。在任何情况下,变量名称都应以 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)具有以下属性:

  1. t.type,即分词类型(作为字符串)(例如:'NUMBER'、'PLUS' 等)。默认情况下,t.type 被设置为 t_ 前缀后面的名称。
  2. t.value,即词素(实际匹配的文本)。
  3. t.lineno,即当前行号(词法分析器不知道行号,因此需要手动更新)。使用名为 t_newline 的函数更新 lineno
 def t_newline(t):
   r'\n+'
   t.lexer.lineno += len(t.value)
  1. t.lexpos,即分词相对于输入文本开头的位置。
  • 如果从正则表达式规则函数中没有返回任何内容,则丢弃该分词。如果您想丢弃一个分词,您也可以在正则表达式规则变量中添加 t_ignore_ 前缀,而不是为相同规则定义一个函数。
 def t_COMMENT(t):
   r'\#.*'
   pass
   # 没有返回值。分词被丢弃

... 这与以下内容相同:

 t_ignore_COMMENT = r'\#.*'

当然,如果在看到注释时执行某些操作,则使用函数来定义正则表达式规则。

如果您尚未为某些字符定义分词,但仍然希望忽略它们,请使用 t_ignore = "<要忽略的字符>"(这些前缀是必需的):

 t_ignore_COMMENT = r'\#.*'
 t_ignore = ' \t' # 忽略空格和制表符
  • 在构建主正则表达式时,lex 会按照以下顺序添加文件中指定的正则表达式:
  1. 由函数定义的分词按照它们在文件中出现的顺序添加。
  2. 由字符串定义的分词按照定义该分词的正则表达式字符串长度的降序添加。

如果您在同一个文件中匹配 ===,请利用这些规则。

  • 字面量是直接返回的分词。t.typet.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 个字符。

  1. 最后的准备:

使用 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 程序首次执行时会创建它。每当发生移入/归约冲突时,解析器总是选择移入。

相关推荐

探索Java项目中日志系统最佳实践:从入门到精通

探索Java项目中日志系统最佳实践:从入门到精通在现代软件开发中,日志系统如同一位默默无闻却至关重要的管家,它记录了程序运行中的各种事件,为我们排查问题、监控性能和优化系统提供了宝贵的依据。在Java...

用了这么多年的java日志框架,你真的弄懂了吗?

在项目开发过程中,有一个必不可少的环节就是记录日志,相信只要是个程序员都用过,可是咱们自问下,用了这么多年的日志框架,你确定自己真弄懂了日志框架的来龙去脉嘛?下面笔者就详细聊聊java中常用日志框架的...

物理老师教你学Java语言(中篇)(物理专业学编程)

第四章物质的基本结构——类与对象...

一文搞定!Spring Boot3 定时任务操作全攻略

各位互联网大厂的后端开发小伙伴们,在使用SpringBoot3开发项目时,你是否遇到过定时任务实现的难题呢?比如任务调度时间不准确,代码报错却找不到方向,是不是特别头疼?如今,随着互联网业务规模...

你还不懂java的日志系统吗 ?(java的日志类)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

谈谈枚举的新用法--java(java枚举的作用与好处)

问题的由来前段时间改游戏buff功能,干了一件愚蠢的事情,那就是把枚举和运算集合在一起,然后运行一段时间后buff就出现各种问题,我当时懵逼了!事情是这样的,做过游戏的都知道,buff,需要分类型,且...

你还不懂java的日志系统吗(javaw 日志)

一、背景在java的开发中,使用最多也绕不过去的一个话题就是日志,在程序中除了业务代码外,使用最多的就是打印日志。经常听到的这样一句话就是“打个日志调试下”,没错在日常的开发、调试过程中打印日志是常干...

Java 8之后的那些新特性(三):Java System Logger

去年12月份log4j日志框架的一个漏洞,给Java整个行业造成了非常大的影响。这个事情也顺带把log4j这个日志框架推到了争议的最前线。在Java领域,log4j可能相对比较流行。而在log4j之外...

Java开发中的日志管理:让程序“开口说话”

Java开发中的日志管理:让程序“开口说话”日志是程序员的朋友,也是程序的“嘴巴”。它能让程序在运行过程中“开口说话”,告诉我们它的状态、行为以及遇到的问题。在Java开发中,良好的日志管理不仅能帮助...

吊打面试官(十二)--Java语言中ArrayList类一文全掌握

导读...

OS X 效率启动器 Alfred 详解与使用技巧

问:为什么要在Mac上使用效率启动器类应用?答:在非特殊专业用户的环境下,(每天)用户一般可以在系统中进行上百次操作,可以是点击,也可以是拖拽,但这些只是过程,而我们的真正目的是想获得结果,也就是...

Java中 高级的异常处理(java中异常处理的两种方式)

介绍异常处理是软件开发的一个关键方面,尤其是在Java中,这种语言以其稳健性和平台独立性而闻名。正确的异常处理不仅可以防止应用程序崩溃,还有助于调试并向用户提供有意义的反馈。...

【性能调优】全方位教你定位慢SQL,方法介绍下!

1.使用数据库自带工具...

全面了解mysql锁机制(InnoDB)与问题排查

MySQL/InnoDB的加锁,一直是一个常见的话题。例如,数据库如果有高并发请求,如何保证数据完整性?产生死锁问题如何排查并解决?下面是不同锁等级的区别表级锁:开销小,加锁快;不会出现死锁;锁定粒度...

看懂这篇文章,你就懂了数据库死锁产生的场景和解决方法

一、什么是死锁加锁(Locking)是数据库在并发访问时保证数据一致性和完整性的主要机制。任何事务都需要获得相应对象上的锁才能访问数据,读取数据的事务通常只需要获得读锁(共享锁),修改数据的事务需要获...