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

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

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


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

相关推荐

NUS邵林团队发布DexSinGrasp基于强化学习实现物体分离与抓取统一

本文的作者均来自新加坡国立大学LinSLab。本文的共同第一作者为新加坡国立大学实习生许立昕和博士生刘子轩,主要研究方向为机器人学习和灵巧操纵,其余作者分别为硕士生桂哲玮、实习生郭京翔、江泽宇以及...

「PLC进阶」如何通过编写SCL语言程序实现物料分拣?

01、前言SCL作为IEC61131-3编程语言的一种,由于其高级语言的特性,特别适合复杂运算、复杂数学函数应用的场合。本文以FactoryIO软件中的物料分拣案例作为硬件基础,介绍如何通过SCL来实...

zk源码—5.请求的处理过程一(http1.1请求方法)

大纲1.服务器的请求处理链...

自己动手从0开始实现一个分布式 RPC 框架

前言为什么要自己写一个RPC框架,我觉得从个人成长上说,如果一个程序员能清楚的了解RPC框架所具备的要素,掌握RPC框架中涉及的服务注册发现、负载均衡、序列化协议、RPC通信协议、Socket通信、异...

MLSys’25 | 极低内存消耗:用SGD的内存成本实现AdamW的优化性能

AIxiv专栏是机器之心发布学术、技术内容的栏目。过去数年,机器之心AIxiv专栏接收报道了2000多篇内容,覆盖全球各大高校与企业的顶级实验室,有效促进了学术交流与传播。如果您有优秀的工作想要分享,...

线程池误用导致系统假死(线程池会自动销毁吗)

背景介绍在项目中,为了提高系统性能使用了RxJava实现异步方案,其中异步线程池是自建的。但是当QPS稍微增大之后却发现系统假死、无响应和返回,调用方出现大量超时现象。但是通过监控发现,系统线程数正常...

大型乘用车工厂布局规划(六大乘用车基地)

乘用车工厂的布局规划直接影响生产效率、物流成本、安全性和未来扩展能力。合理的布局应确保生产流程顺畅、物流高效、资源优化,并符合现代化智能制造和绿色工厂的要求。以下是详细的工厂布局规划要点:1.工厂布...

西门子 S7-200 SMART PLC 连接Factory IO的方法

有很多同学不清楚如何西门子200smart如何连接FactoryIO,本教程为您提供了如何使用西门子S7-200SMARTPLC连接FactoryIO的说明。设置PC和PLC之间的...

西门子博图高级仿真软件的应用(西门子博途软件仿真)

1.博图高级仿真软件(S7-PLCSIMAdvancedV2.0)S7-PLCSIMAdvancedV2.0包含大量仿真功能,通过创建虚拟控制器对S7-1500和ET200SP控制器进行仿真...

PLC编程必踩的6大坑——请对号入座,评论区见

一、缺乏整体规划:面条式代码问题实例:某快递分拣线项目初期未做流程图设计,工程师直接开始编写传送带控制程序。后期增加质检模块时发现I/O地址冲突,电机启停逻辑与传感器信号出现3处死循环,导致项目延期2...

统信UOS无需开发者模式安装软件包
统信UOS无需开发者模式安装软件包

原文链接:统信UOS无需开发者模式安装软件包...

2025-05-05 14:55 csdh11

100个Java工具类之76:数据指纹DigestUtils

为了提高数据安全性,保证数据的完整性和真实性,DigestUtils应运而生。正确恰当地使用DigestUtils的加密算法,可以实现数据的脱敏,防止数据泄露或篡改。...

麒麟KYLINIOS软件仓库搭建02-软件仓库添加新的软件包

#秋日生活打卡季#原文链接:...

Java常用工具类技术文档(java中工具类的作用)

一、概述Java工具类(UtilityClasses)是封装了通用功能的静态方法集合,能够简化代码、提高开发效率。本文整理Java原生及常用第三方库(如ApacheCommons、GoogleG...

软路由的用法(自动追剧配置)(软路由教学)

本内容来源于@什么值得买APP,观点仅代表作者本人|作者:值友98958248861环境和需求...