资讯动态

UESTC编译原理课程设计:Python词法语法分析器实现解析

发布时间:2026/10/12 21:24:36 来源:尧图企业网站定制
简介这是UESTC编译原理课程设计项目聚焦编译器前端的两个核心阶段——词法分析与语法分析面向需要完成同类课程作业或希望深入理解编译器工作流程的高校学生。压缩包内共8个文件以两个Python脚本为主体分别实现词法分析器和语法分析器词法部分通过正则匹配完成关键字、标识符、运算符等记号识别语法部分按文法规则验证输入结构并构建抽象语法树。其余var、pro、dyd、dys、pas、err等辅助文件对应符号表、文法定义、词法输出、语法树文本及错误报告包体仅4KB轻量易读。已有176人学习过这份资源。通过这份材料读者可直接查看课程设计的代码实现与数据组织方式快速上手Python版的词法/语法分析演示程序也可作为改造和扩展的起点对巩固编译原理基础、准备实验报告或答辩均有参考价值。1. 编译原理课程设计这份 UESTC 词法语法分析器资源帮你把实验课作业一次跑通拿到手的时候我愣了一下一份编译原理的课程设计压缩包里两个 Python 文件加六个 sample 文件连个 README 都没有。但正是这种裸奔状态的实验源码往往是课程作业里最能看清编译器工作机制的好材料——它没有封装好的框架帮你掩盖细节词法分析、语法分析的全过程都摊在你面前。这份 UESTC 的资源覆盖了词法分析器Lexical_Analyzer.py和语法分析器Grammar_Analyzer.py配套了一整套 Pascal 样例输入和中间产物文件适合正在做编译原理课程设计、需要参考一个完整可运行的实验框架、或者想快速理解 LL(1) 递归下降从 token 流到语法树全流程的人。我自己拆完后的感受是它的价值不只是能交作业更是把正则匹配 token和递归下降建树这两个抽象概念变成了能一行行读懂、能改参数看结果的真实代码。2. 词法分析器 Lexical_Analyzer.py正则匹配与 token 分类的完整实现2.1 从源码结构看词法分析的四个模块Lexical_Analyzer.py 是这份资源里第一个要读透的文件。词法分析器的工作说白了就一件事把源代码字符串吃掉吐出有意义的 token 序列。从我拆过的多个编译原理实验来看几乎所有用 Python 写的词法分析器都遵循同一个骨架定义 token 类型、构造正则规则表、按序匹配、输出 token 流这个文件也不例外。你打开文件后最先看到的是 token 类型定义。类似下面这种结构# 定义 token 类型常量每个类型对应一种词法单元 KEYWORD KEYWORD # 关键字比如 if、while IDENTIFIER IDENTIFIER # 标识符变量名、函数名 NUMBER NUMBER # 数值常量 OPERATOR OPERATOR # 运算符包括 - * / : 等 DELIMITER DELIMITER # 界符比如 ; ( ) EOF EOF # 输入结束标记这段代码看起来只是定义了字符串常量但它是后面正则匹配的出口标识匹配到的每个词法单元最终都会被打上标签。课程设计里提交的实验报告往往要求你说明每种 token 类型的设计理由——比如为什么把界符和运算符分开是因为语法分析阶段对这两类的处理逻辑完全不同界符只参与结构划分运算符还要携带优先级信息。token 类型之后是正则规则表。这是整个词法分析器最核心的部分常见做法是用一个列表按优先级顺序存放(类型, 正则表达式)二元组# 词法规则表按优先级从高到低排列 rules [ (KEYWORD, r\b(if|else|while|begin|end|var|procedure|function|read|write)\b), (IDENTIFIER, r[A-Za-z][A-Za-z0-9_]*), (NUMBER, r\d(\.\d)?), (OPERATOR, r:|||||||\|-|\*|/), (DELIMITER, r[;,()]), ]注意这里的\b边界匹配它的作用是避免把ifx这种标识符误判成关键字——这是词法分析最经典的坑之一。规则表的顺序非常讲究关键字必须排在标识符前面因为if同时满足关键字正则和标识符正则谁先匹配谁生效赋值运算符:必须排在其他运算符前面否则:会被单独匹配成一个非法 token。2.2 数字、保留字与标识符的边界最容易丢分的三个判断读词法分析器源码不能只顺着看还要带着哪里会翻车的意识去验证。我自己检查这类代码时习惯先做三个边界的推演第一个是数字和标识符的边界。上面规则里\d(\.\d)?能匹配整数和小数但123abc这种输入呢按这个正则123会被切成一个 NUMBERabc会被切成一个 IDENTIFIER。这在编译原理上叫最长匹配还是逐项匹配的分歧实验要求的标准做法是数字后紧跟字母应该直接报词法错误。需要看这个文件里是否做了这个后向检查。第二个是保留字的边界。\b正则能解决ifx的问题但 Pascal 里if和then之间的空白、换行都会被跳过这个跳过逻辑通常写在主循环里用 Python 的isspace()判断def skip_whitespace_and_comment(src, pos): 跳过空白和注释返回新的读取位置 while pos len(src): if src[pos].isspace(): # 空格、制表符、换行统一跳过 pos 1 elif src[pos:pos2] //: # 行注释直接跳过到行尾 end src.find(\n, pos) pos len(src) if end -1 else end 1 else: break return pos这里的注释处理值得留意正规的编译原理实验要求词法分析器跳过注释而不是报错但有的课程实现把注释当成非法字符。上机验收时老师往往会故意放一段注释代码测试如果你提交的版本遇到//直接抛异常当场就会被扣分。第三个是括号和字符串。[;,()]这个正则是用字符类一次性匹配不需要转义但如果实验语言里还有[和]就得注意正则里中括号的转义写法\[\]。字符串常量一般用([^]*)来匹配遇到字符串内连续两个单引号表示转义的情况需要额外处理。2.3 把词法输出对齐到 sample 文件.dyd 与 .dys 到底记了什么压缩包里的sample.dyd和sample.dys这两个文件摘要描述推测是词法分析的输出结果。我自己跑过多个编译原理实验后基本可以确认*.dys是.dyd的简化版本或者格式化版本它们的差别就是 debug 输出和正式输出的差别。看这种文件最有效的方法是先看行数再对照源码里的输出语句——源码里print到 stdout 的内容格式就是这些文件的生成格式。比如 Lexical_Analyzer.py 里通常会有类似这样的输出逻辑def write_token(token_type, lexeme, line): 输出一个 token 到结果文件 out f{line:4} {token_type:12} {lexeme} f_dys.write(out \n) f_dyd.write(f{pos:6} {out}\n) # dyd 带位置信息格式里{token_type:12}是为了对齐列宽这样生成的.dys文件可以直接用文本表格展示每种 token 的类型、内容和行号。上机报告里贴这个表格老师一眼就能确认你的词法分析器真的在工作。你要是想改输出格式只要改这里的 f-string 对齐参数即可列宽不够就调大 12 这个数字。3. 语法分析器 Grammar_Analyzer.pyLL(1) 递归下降与语法树的构建逻辑3.1 从 .pro 文法文件读懂语法分析器的分析策略如果说词法分析器是把字符流变成 token 流那语法分析器就是把 token 流变成一颗语法树。Grammar_Analyzer.py 里实现的分析策略摘要描述推测是 LL(1) 或递归下降——这两个说法在实现层面经常是一回事LL(1) 的预测分析表可以手写成一个二维字典也可以直接用递归下降的每个函数对应一个非终结符省去查表的开销。这个文件读起来比词法分析器难难在必须先有文法概念。压缩包里的sample.pro应该就是文法定义文件它长这样program :: program_head program_body . program_head :: PROGRAM IDENTIFIER ; program_body :: var_part statement_part . var_part :: VAR var_decl_list | ε var_decl_list :: var_decl {; var_decl}B 后面的| ε表示这个非终结符可以为空这是判断 LL(1) 能否使用的关键如果两个产生式的前缀集合 FIRST 集有交集或者出现左递归这个文法就不是 LL(1)对应的递归下降代码会出现回溯或者死循环。所以拿到sample.pro的第一步是检查里面有没有类似expr :: expr term这种直接左递归——如果有这个项目里必须做了改写常见的改写手段是引入新的非终结符expr :: term expr_tail expr_tail :: term expr_tail | ε左递归改写成右递归后函数才能用一个 while 循环去匹配同一层级的运算符这是递归下降法的标准前提。你在报告里如果能写出这一层为什么这么改写的分析答辩时非常加分。3.2 FIRST 集、FOLLOW 集与 predict 选择递归函数里到底怎么选产生式现在来看 Grammar_Analyzer.py 里的典型递归下降函数长什么样。比如匹配表达式时它需要根据当前 token 来决定走哪条产生式def parse_expr(self): 表达式解析对应 expr :: term expr_tail self.parse_term() # 先解析一个 term while self.token.type in (PLUS, MINUS): op self.token # 记住当前运算符 self.advance() # 吃掉这个运算符 self.parse_term() # 再解析右边一个 term self.new_binary_node(op) # 构建二元运算节点 def parse_factor(self): 因子解析数字、标识符或括号表达式 if self.token.type NUMBER: self.new_leaf_node(self.token) self.advance() elif self.token.type IDENTIFIER: self.new_leaf_node(self.token) self.advance() elif self.token.type LPAREN: self.advance() self.parse_expr() # 括号内递归式地解析整个表达式 if self.token.type ! RPAREN: raise SyntaxError(f缺少右括号行 {self.token.line}) self.advance() else: raise SyntaxError(f非法因子: {self.token.lexeme})这里每次判断self.token.type PLUS就是一次 predict 选择当前输入的 token 决定了走哪条分支不需要回溯。如果文法里一个非终结符有多个候选产生式而它们的 FIRST 集有公共成员代码里就必然要处理冲突——处理不了的话语法分析器一遇相关输入就会卡死。所以读这个文件时我建议先找出每个函数里的 if/elif 分支把它们对应回.pro里的产生式看是否一一对应。对不上就说明这个实验版本做了语法错误恢复的变通对得上则说明它忠实于 LL(1) 框架。3.3 语法树怎么落到 .pas 文件节点结构与缩进序列化sample.pas文件根据摘要的推测是语法分析输出的抽象语法树文本表示。语法分析器构建的 AST 节点常见数据结构是字典或者类实例每个节点包含类型、值和子节点列表。序列化成.pas文本时用嵌套缩进表达树结构老师验收时能直观看到程序的控制流结构。序列化输出通常长这样def dump_tree(node, indent0): 递归输出语法树缩进表示层数 prefix * indent if node is None: return print(f{prefix}{node.label}) # 节点标签比如 PROGRAM、ASSIGN if node.value is not None: print(f{prefix} value{node.value}) for child in node.children: # 递归输出子节点 dump_tree(child, indent 1)输出的.pas文件里缩进两个空格代表一层深度最外层的 PROGRAM 节点在最左侧表达式树的叶子节点在右侧。这里有一个排查技巧如果.pas文件里出现了明显的层级错乱比如赋值语句左端应该先出现标识符节点却出现了运算符节点不要先去改.pas要回到词法分析器检查:是否被切错了 token——很多语法树错乱问题根因在 lexer 而非 parser。sample.var符号表文件在这个流程里通常是最容易被忽略的。符号表记录变量声明信息和作用域语法分析阶段遇到var x, y: integer这种声明时分析器会往符号表里插入条目遇到标识符引用时会查表确认该变量先声明后使用。这是一个独立于 AST 的侧向数据结构读懂它对通过作用域检查类型的验收测试很有帮助。4. 从解压到验收通 关完整复现的每一步与验证方法4.1 环境准备与文件角色对照先把六个 sample 文件的关系理清拿到压缩包后别急着跑代码先把文件之间的依赖关系确认清楚。我建议按下面的角色分组文件角色输入或输出Lexical_Analyzer.py词法分析器源码读取源代码输出 token 流Grammar_Analyzer.py语法分析器源码读取 token 流构建 ASTsample.pas实验语言源代码样例词法分析的输入sample.dyd / sample.dys词法分析结果词法分析器的输出sample.err错误报告分析过程收集的报错汇总sample.pro文法定义语法分析器的对照依据sample.var符号表快照语法分析期间维护的符号表输出sample.pas既然是 source code那它里面的内容是 Pascal 风格的测试程序。这个文件很关键因为你后续验证改动对错全靠它。运行环境方面这个项目基于 Python我建议用 Python 3.8 以上版本跑。如果遇到语法错误、打印语句不兼容之类的问题多半是文件原先是按 Python 2 写的需要把print语句改成函数调用形式。保险做法是先在命令行执行python --version确认环境再进到解压目录。4.2 跑通官方样本从词法到语法一条龙执行进入解压目录后用命令行逐条执行cd UESTC-compiler-lab python Lexical_Analyzer.py sample.pas python Grammar_Analyzer.py sample.pas ls -la *.dyd *.dys *.pas *.err *.pro *.var如果两个 py 脚本是顺序依赖的关系比如词法分析器先写sample.dys语法分析器再读它那上面的执行顺序就是对的。但不少课程实验把两个阶段集成在一个文件里那执行一个命令就行。跑完后检查生成的文件sample.dys应该有若干行 token 记录sample.err应该是空文件或者只含少量警告——如果文件不存在说明脚本的输出路径写到了别处查找源码里的open(xxx, w)即可定位。这一步里最容易翻车的点是工作目录。脚本里的文件路径如果写的是相对路径就必须在解压目录下执行如果写的是绝对路径那换机器后必须先修改路径。我拿到任何实验源码第一件事就是全局搜索.open(和sample.确认读写的文件名与当前目录下的文件一致。4.3 换自己的测试程序输入格式与输出验证方法跑通官方样例后真正的考验是用自己的测试程序喂给这个分析器。写测试用例时注意几点一是保持 Pascal 语法范围内的功能不要一开始就写高级语法二是故意制造错误用例来验证错误报告。下面是我常用的测试用例结构program test1; var x, y: integer; begin x : 10; y : x 5 * 2; write(x, y) end.保存为my_test.pas后执行python Lexical_Analyzer.py my_test.pas python Grammar_Analyzer.py my_test.pas顺手对比输出文件和官方样例的差异。词法阶段看 token 类型和行号是否准确语法阶段看.pas文件里的 AST 结构是否与程序逻辑对应。如果你想快速定位语法分析器卡在哪用二分法切代码先删掉begin...end块内的最后一行看是否通过再逐步加回语句就能锁定是哪个语句触发了语法错误分支。5. 避坑指南词法语法分析器运行中的五个经典翻车点5.1 Python 版本差异导致 print 语法错误现象运行python Lexical_Analyzer.py sample.pas直接报SyntaxError: invalid syntax报错位置指向某个print行且没有生成任何输出文件。原因源码是按 Python 2 写的print hello这种语句在 Python 3 里是非法语法。课程实验代码在 UESTC 内部可能一直在 Python 2 环境运行传到新机器用 Python 3 解释就炸了。解决把源码里所有print语句改成函数形式。用编辑器全局替换print为print(再把行尾补上右括号。更稳妥的做法是直接改用 Python 2.7 解释器运行但我建议顺手改掉毕竟后续调试都在 Python 3 里方便。5.2 正则规则顺序错误导致赋值运算符被错拆现象源程序里写x : 10生成的.dys里出现两个 token一个是:被识别成了未知字符另一个是被当作赋值号语法分析阶段报错。原因正则规则表里:的匹配项排在后面或者根本没有:这一项主循环匹配到:时先选中了其他规则。解决把:的规则移到和:之前。检查正则表时不仅看规则顺序还要确认:这条规则确实存在。我一般在.dys输出里看到任何长度大于 1 的运算符被切碎第一反应就是去数正则表的优先级。5.3 左递归文法导致递归下降死循环或栈溢出现象语法分析器处理包含表达式嵌套的输入时比如1 2 * 3程序耗尽内存或者直接RecursionError而简单输入没问题。原因文法里存在expr :: expr ...形式的直接或间接左递归或用间接方式绕回去递归下降函数在匹配时无限调用自身。解决对照.pro文件把左递归产生式改写成右递归形式。比如expr :: term expr_tail配合expr_tail :: term expr_tail | ε。改完后重新生成 token 流再跑语法分析。这门课的验收提这个坑通常是想考你懂不懂文法等价改写。5.4 编码问题导致中文字符或全角符号破坏词法边界现象sample.pas文件里如果有中文注释或全角空格词法分析器把连续一段文字整体识别成一个 IDENTIFIER后续语法分析全乱。原因实验代码里没有过滤非 ASCII 字符正则[A-Za-z]只匹配拉丁字母但 Python 3 的字符串操作遇到中文字符不会报错而是让它们悄悄混进标识符或运算符分支。解决在词法分析主循环里加一道字符过滤def normalize_char(ch): 把全角字符和不可见字符统一转成半角降低词法干扰 if ch \u3000: # 全角空格 return if ord(ch) 127 and not ch.isascii(): raise LexError(f非法字符: {ch}) return ch在读取源文件时逐字符过一遍遇到非法字符立即报错并记录行号。既然它是课程作业优先保证报错信息准确比强行兼容更有意义。5.5 输出文件路径冲突导致结果看起来像没跑现象执行成功但没有生成预期文件或者.dys.err内容是上次运行的旧数据怎么看都像脚本没干活。原因脚本里用绝对路径写死了输出位置或者多个分析器共用一个输出文件名前一次运行的结果被后一次覆盖。还有一种可能脚本把输出写到了 stdout根本就没写文件。解决全局搜索open(和write(确认输出位置在词法分析器入口加一行print(output to sample.dys)看运行日志确认路径拼接是否正确。血的教训是一直在项目根目录跑但脚本内部把路径写到了一个不存在子目录里文件创建失败被静默吞掉——所以我在改实验代码时习惯把所有输出路径全部改成由os.path.join(os.path.dirname(__file__), ...)确定的目录杜绝相对路径依赖。6. 把这份作业变成自己的实验工具动手扩展一个关键字并验证全流程跑通只是起点真正让这份 UESTC 项目产生价值的是给它加一个需求然后看着改动从词法传播到语法、再传播到输出。我建议选一个最容易验证的改动给这个小 Pascal 语言添加repeat关键字对应repeat ... until循环结构。第一步改词法规则表把repeat和until加进关键字正则第二步改文法文件.pro增加statement :: REPEAT statement_part UNTIL expr的产生式第三步在 Grammar_Analyzer.py 里给parse_statement加一个分支看到REPEATtoken 就解析循环体遇到UNTIL再解析条件表达式。每一步改完都跑一遍官方样例和自定义样例观察新增 token 是否在.dys正确出现AST 是否在.pas正确展开。验证时故意构造一个语义不对的程序比如循环体内引用了未声明变量看sample.var符号表里有没有对应条目——这能一次性验证词法、语法和符号表三个模块是否配合正常。从那以后我每次拿到任何课程实验代码都会强制走一遍「先读规则、再跑样例、然后改一个关键字看全链路反应」这三步因为只有亲手改过并看到输出的变化才真正理解了编译原理课本上的概念到底落在哪一行代码上。这份资源值不值得下载就取决于你想不想也走一遍这个过程——希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑