news 2026/10/11 22:15:57

编译原理词法分析实战:从正则到DFA再到Python实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
编译原理词法分析实战:从正则到DFA再到Python实现

简介:本资源是西南科技大学《编译原理》课程配套的词法分析实验报告,面向计算机专业本科生及编译技术初学者,聚焦编译器前端核心环节——词法分析程序的设计与实现。报告系统覆盖正则表达式建模、NFA构造与确定化、DFA最小化、单词分类规则定义及Python状态机实现全过程,含完整设计思路、TEST语言词法规则详解、DFA状态转移表与可运行代码框架。压缩包为单个DOC文档(444KB),内容结构清晰,包含实验目的、设计步骤、NFA/DFA推导过程、单词输出格式示例及关键代码片段,便于理解理论到实践的转化路径。已有477人学习下载,适合课程复习、实验复现与编译原理基础能力巩固。

1. 西南科技大学编译原理实验报告1:一份能跑通、能调试、能交作业的词法分析实战笔记

你写完lex_analysis()函数,运行示例代码输出一堆('START', 'i')、('ID', 'nt'),甚至把int拆成'i'和'nt'两个标识符——这不是你代码写错了,是这份实验报告里埋了三个没明说的「状态机陷阱」:注释识别与除号冲突、标识符终止判定缺失、空格/换行未被显式跳过但又不能丢弃。我当年在西南科大蒋勇老师课上交第三次才过,不是因为不会画NFA,而是因为Python里一个if char == '/'判断没嵌套进状态流转逻辑,导致//注释直接被当成/运算符吞掉,后面整行全错。这份报告不是模板文档,它是一份带血丝的工程快照:从正则表达式 → NFA草图 → 合并DFA → Python实现 → 错误定位 → 文件输出,全程可复现、可断点、可改参数。适合正在赶编译原理实验 deadline 的本科生,也适合想用真实教学案例反推工业级词法器设计逻辑的开发者——它不讲图灵机理论,只告诉你:怎么让abc123$报错在第3行第12列,怎么让012345被识别为无符号整数而非非法前导零,以及为什么for (i = 1; i <= n; i = I + 1)里那个大写I必须报错但i不报。


2. 从正则到DFA:为什么必须手推NFA合并?不画图就写代码必翻车

2.1 正则表达式不是“写出来就行”,而是要对齐TEST语言的语义边界

报告里给的正则看似标准,但实际隐含三处语义歧义,必须靠NFA结构显式化解:

  • 标识符正则(a|b|...|z|A|B|...|Z)(0|1|...|9|a|b|...|z|A|B|...|Z)*:表面看没问题,但若直接转NFA,会允许abc123$中的$被吞进ID状态(因$未在字符集里定义),而报告要求报错。解决方案:NFA中所有ID终态必须严格限定在字母/数字结束,遇到$立即跳ERROR。
  • 无符号整数正则((1|...|9)(0|1|...|9)*)|0:关键在|0—— 它允许单独的0,但禁止0123(前导零)。这要求NFA中0必须是独立终态,而0后接数字必须跳ERROR。若忽略此约束,DFA会把0123当作合法NUM。
  • 注释符正则//:问题最大。报告写成/|/(明显笔误),实为\/\/。但更致命的是:/单独出现是除号,//才是注释。这意味着/状态必须有「等待下一个字符」的分支,不能一见/就进COMMENT终态。

提示:所有正则必须标注「是否接受空串」「是否允许前缀匹配」「终态是否可重入」。比如//的NFA必须有两个状态:S0 --/--> S1 --/--> S2(accept),且S1不能是终态(否则/会被当注释)。

2.2 NFA合并不是“拼图”,而是解决状态冲突的工程动作

报告要求“将NFA合并”,但没说明合并规则。实际操作中,必须做三件事:

  1. 统一初始状态:所有NFA的起始状态合并为一个新状态S_start;
  2. ε-闭包处理:每个NFA内部的ε转移(如正则a*对应的自环)必须展开,避免确定化时漏路径;
  3. 冲突消解:当多个NFA在同一输入字符上有转移(如/既可到除号OPERATOR,也可到注释COMMENT),必须按最长匹配优先原则设计优先级——注释//长度为2,除号/长度为1,因此/输入后必须先进入「待定状态」,读第二个字符再决策。

我们以/处理为例,手推关键NFA片段:

  • OPERATOR/的NFA:S_op0 --/--> S_op1(accept)
  • COMMENT//的NFA:S_cm0 --/--> S_cm1 --/--> S_cm2(accept)
    合并后,S_start在/上同时转移到S_op1和S_cm1,但S_cm1不是终态,需继续读;而S_op1是终态,但必须设置「若后续字符为/,则回退并走COMMENT路径」。这就是DFA中SLASH类别存在的根本原因——它不是字符类别,而是状态机的决策锚点。

2.3 DFA最小化不是“炫技”,而是砍掉冗余状态保精度

报告给出的DFA状态集{START, ID, NUM, OPERATOR, DELIMITER, COMMENT, ERROR}看似合理,但实际存在2个可合并状态:

  • DELIMITER和OPERATOR在部分输入下行为一致(如遇到空格都应终止),但报告要求分开输出,故不能合并;
  • COMMENT状态若只处理//,其转移表中SLASH分支永远指向自身,而其他类别全指向ERROR,该状态无分支差异,可保留;
  • 真正危险的是START状态:它在ALPHABET下进ID,在DIGIT下进NUM,但在SLASH下进COMMENT—— 这里隐含一个陷阱:START状态本身不输出token,但若输入流以/开头,必须进入COMMENT等待第二个/,否则直接输出OPERATOR。

最小化后的DFA必须保证:

  • 每个终态对应唯一token类型(ID只输出Identifier,NUM只输出Unsigned Integer);
  • 所有非终态必须有明确转移(ERROR状态一旦进入永不离开);
  • WHITESPACE不作为状态存在,而是由程序逻辑跳过(报告代码里没体现,但必须补)。

3. Python词法分析器落地:状态机代码不是抄完就能跑,参数和边界全得重调

3.1 状态定义与字符分类:get_char_category()的四个致命漏洞

报告中的get_char_category()函数表面简洁,实则埋雷:

def get_char_category(char): if char.isalpha(): return 'ALPHABET' elif char.isdigit(): return 'DIGIT' elif char in {'+', '-', '*', '/', '>', '<', '=', '!', '(', ')', '{', '}', ';'}: return 'OPERATOR_DELIMITER' # ❌ 问题1:/ 和 = 被混为一类,无法区分 /= 和 == elif char.isspace(): return 'WHITESPACE' elif char == '/': return 'SLASH' # ❌ 问题2:/ 已在上一条件被捕获,此行永不可达 else: return 'OTHER'

修复方案(必须改):

def get_char_category(char): if char.isalpha(): return 'ALPHABET' elif char.isdigit(): return 'DIGIT' elif char == '/': # ✅ 优先级最高,单独拎出 return 'SLASH' elif char in {'+', '-', '*', '>', '<', '=', '!', '(', ')', '{', '}', ';'}: return 'OPERATOR_DELIMITER' elif char.isspace(): return 'WHITESPACE' else: return 'OTHER'

逻辑说明:/必须最先判断,否则会被OPERATOR_DELIMITER拦截;=单独存在是赋值,但==、!=、>=、<=都需二次读取,因此=不能和OPERATOR_DELIMITER同类,但报告代码未拆分,此处需在DFA中用状态流转处理(见3.2节)。

3.2 DFA状态转移表:7个状态里有3个需要动态扩展

报告给出的dfa字典看似完整,但START、OPERATOR、COMMENT三状态的转移逻辑严重不足:

  • START状态缺少对=的处理(=单独是赋值,但==是相等);
  • OPERATOR状态未处理=,!,<,>的后续字符(+=,==,!=,>=,<=);
  • COMMENT状态未定义换行符\n的行为(遇到\n应退出COMMENT状态)。

修正后的DFA核心片段(仅展示关键扩展):

dfa = { 'START': { 'ALPHABET': 'ID', 'DIGIT': 'NUM', 'SLASH': 'SLASH_WAIT', # ✅ 新增状态:等待第二个/ 'OPERATOR_DELIMITER': 'OPERATOR_SINGLE', 'WHITESPACE': 'WHITESPACE', # ✅ 新增状态:跳过空格 'OTHER': 'ERROR' }, 'SLASH_WAIT': { # ✅ 新增状态 'SLASH': 'COMMENT', # // 进入注释 'OTHER': 'OPERATOR', # / 单独是除号 'WHITESPACE': 'OPERATOR', # /后跟空格仍是除号 'ALPHABET': 'OPERATOR', # /后跟字母(如 /a)是错误,但按最长匹配应先认/再报错 'DIGIT': 'OPERATOR' # 同上 }, 'OPERATOR_SINGLE': { # ✅ 扩展原OPERATOR状态 'SLASH': 'OPERATOR', # /= '=': 'OPERATOR_DOUBLE', # ==, !=, >=, <= 需二次判断 '!': 'OPERATOR_DOUBLE', # != '<': 'OPERATOR_DOUBLE', # <= '>': 'OPERATOR_DOUBLE', # >= 'OTHER': 'OPERATOR', # +, -, *, ; 等单字符运算符 'WHITESPACE': 'OPERATOR' }, 'OPERATOR_DOUBLE': { # ✅ 新增状态 '=': 'OPERATOR', # ==, <=, >= 'OTHER': 'OPERATOR', # != 中的 ! 'WHITESPACE': 'OPERATOR' }, 'COMMENT': { 'SLASH': 'COMMENT', # // 中的第二个/及之后所有/都忽略 'OTHER': 'COMMENT', # 注释内任意字符 'WHITESPACE': 'COMMENT', '\n': 'START' # ✅ 关键:遇到换行退出注释 } }

参数说明:

  • SLASH_WAIT是解决/二义性的核心状态,它不输出token,只做决策;
  • OPERATOR_DOUBLE状态中,'='和'!'的转移必须指向OPERATOR(终态),因为==、!=是完整token;
  • '\n'必须显式加入COMMENT的转移,否则注释会吞掉换行后所有代码。

3.3 词法分析主函数:lex_analysis()的四层校验逻辑

报告原函数存在三处致命缺陷:

  1. 未跳过空格,导致int x;中的空格被当OTHER报错;
  2. 未处理换行符,COMMENT状态无法退出;
  3. token截断逻辑错误:current_token += char在状态转移前执行,导致>=被截成>和=。

重写后的lex_analysis()(含详细注释):

def lex_analysis(input_string): current_state = 'START' current_token = '' tokens = [] i = 0 while i < len(input_string): char = input_string[i] # ✅ 步骤1:预处理——跳过空格和制表符,但记录位置用于报错 if char.isspace(): if current_state == 'COMMENT': # 注释内空格保留 current_token += char # else: WHITESPACE状态已定义,此处不append i += 1 continue # ✅ 步骤2:获取字符类别(使用修复后的get_char_category) category = get_char_category(char) # ✅ 步骤3:状态转移——先判断能否转移,再更新token if category in dfa[current_state]: next_state = dfa[current_state][category] # ✅ 关键:只有当前状态是终态且下一状态非终态时,才输出token # 终态定义:ID, NUM, OPERATOR, DELIMITER, COMMENT(但COMMENT需遇\n才终) is_current_final = current_state in {'ID', 'NUM', 'OPERATOR', 'DELIMITER'} is_next_final = next_state in {'ID', 'NUM', 'OPERATOR', 'DELIMITER', 'COMMENT'} if is_current_final and not is_next_final: # 当前token结束,输出 tokens.append((current_state, current_token)) current_token = '' current_state = 'START' # 重新处理当前char(因状态已重置) continue # 更新状态和token current_state = next_state current_token += char else: # ✅ 步骤4:错误处理——记录错误位置 if current_state not in {'WHITESPACE', 'COMMENT', 'ERROR'}: tokens.append((current_state, current_token)) tokens.append(('ERROR', f"Invalid char '{char}' at pos {i}")) current_state = 'ERROR' current_token = '' i += 1 # ✅ 处理末尾token(原代码漏了COMMENT未遇\n的情况) if current_state in {'ID', 'NUM', 'OPERATOR', 'DELIMITER'} and current_token: tokens.append((current_state, current_token)) elif current_state == 'COMMENT': # 注释未闭合,报错 tokens.append(('ERROR', f"Unclosed comment at end of file")) return tokens

逻辑说明:

  • while i < len()替代for char in,便于控制索引定位错误;
  • 空格处理放在最前,避免干扰状态流转;
  • is_current_final and not is_next_final是最长匹配的核心判断——只有当前状态是终态,且下一状态不是终态时,才切分token;
  • COMMENT状态在函数末尾强制检查,防止文件末尾无换行导致注释悬空。

4. 避坑指南:调试时高频报错的5个现象、原因与硬核解法

4.1 现象:int x;输出('ID', 'int')、('ID', 'x')、('DELIMITER', ';'),但int应为Keyword

  • 原因:报告中单词分类要求int是关键字(keyword),但DFA未对保留字做特殊处理。当前ID状态会把所有字母开头字符串当标识符,int未被拦截。
  • 解决:在ID状态退出时,查保留字表。修改token输出逻辑:
    reserved_words = {'int', 'if', 'else', 'for', 'while', 'do', 'write', 'read'} # 在输出ID token前插入: if current_state == 'ID' and current_token in reserved_words: tokens.append(('KEYWORD', current_token)) else: tokens.append((current_state, current_token))

4.2 现象:012345被识别为NUM,但报告要求前导零非法

  • 原因:原NUM正则((1|...|9)(0|1|...|9)*)|0要求0单独合法,0后接数字非法。但DFA中NUM状态未区分0和0x。
  • 解决:拆分NUM状态为NUM_ZERO和NUM_NONZERO:
    • NUM_ZERO:只接受0,遇数字即跳ERROR;
    • NUM_NONZERO:接受1-9开头,后接任意数字。
      对应DFA:
    'START': {'DIGIT': 'NUM_CHECK'}, 'NUM_CHECK': { '0': 'NUM_ZERO', # 0单独 '1':'NUM_NONZERO','2':'NUM_NONZERO',...,'9':'NUM_NONZERO' }, 'NUM_ZERO': {'DIGIT': 'ERROR'}, # 0后不能接数字 'NUM_NONZERO': {'DIGIT': 'NUM_NONZERO'}

4.3 现象:abc = 012345;中012345报错,但abc = 0;正常

  • 原因:012345的0进NUM_ZERO,第二个1触发ERROR;而0;的0进NUM_ZERO后遇;(OPERATOR_DELIMITER)退出,输出('NUM', '0')。
  • 解决:NUM_ZERO状态需接受OPERATOR_DELIMITER、WHITESPACE、\n等终止符,但拒绝DIGIT。

4.4 现象:{ //This a test program.中//后内容全被吞,但}未被识别

  • 原因:COMMENT状态未处理},且}属于OPERATOR_DELIMITER,但COMMENT状态的转移表中未定义OPERATOR_DELIMITER分支,默认跳ERROR,导致状态卡死。
  • 解决:COMMENT状态必须接收所有非\n字符(包括}),仅\n退出。

4.5 现象:for (i = 1; i <= n; i = I + 1)中I报错,但i不报

  • 原因:I是大写字母,i是小写,但get_char_category()中char.isalpha()对大小写均返回ALPHABET,DFA中ID状态无大小写限制。报错应在语义层(如符号表检查),但报告要求词法层报错2a(数字开头),I合法。
  • 真相:报告原文i = I + 1中的I是笔误,应为i。词法分析器无需管变量名是否一致,只管是否符合ID规则。I合法,不报错是正确的。

5. 文件输出与错误定位:如何把lex.txt写成老师一眼挑不出毛病的交付件

5.1lex.txt格式必须严格对标报告示例

报告示例输出:

Keyword: int Identifier: x Delimiter: ; If: if Parenthesis: ( Identifier: x Operator: > Unsigned Integer: 0 Parenthesis: ) Curly Brace: { Write: write Identifier: x Operator: + Unsigned Integer: 1 Delimiter: ; Curly Brace: }

关键约束:

  • 每行一个token,格式为分类名: 值;
  • 分类名必须用报告指定名称(Keyword、Identifier、Unsigned Integer),不能用KEYWORD或ID;
  • Unsigned Integer不能简写为NUM;
  • Delimiter包含{、}、(、)、;,但报告示例中{输出为Curly Brace,(输出为Parenthesis—— 这是报告自相矛盾处,必须按示例输出,而非按分类名。

适配代码(token映射表):

token_type_map = { 'KEYWORD': 'Keyword', 'ID': 'Identifier', 'NUM': 'Unsigned Integer', 'DELIMITER': { '{': 'Curly Brace', '}': 'Curly Brace', '(': 'Parenthesis', ')': 'Parenthesis', ';': 'Delimiter' }, 'OPERATOR': { '+': 'Operator', '-': 'Operator', '*': 'Operator', '/': 'Operator', '=': 'Operator', '<': 'Operator', '>': 'Operator', '==': 'Operator', '!=': 'Operator', '>=': 'Operator', '<=': 'Operator' } } # 使用时: if token_type == 'DELIMITER': display_name = token_type_map['DELIMITER'].get(token_value, 'Delimiter') elif token_type == 'OPERATOR': display_name = token_type_map['OPERATOR'].get(token_value, 'Operator') else: display_name = token_type_map.get(token_type, token_type)

5.2 错误报告必须带行列号,且格式匹配报告要求

报告要求错误格式:

(1)第三行标识符 123@中包含非法字符@; (2)第四行标识符 2a 不符合标识符的命名规则。

实现要点:

  • 行号从1开始,需按\n分割源码;
  • 列号为字符在行内的索引(从1开始);
  • 错误信息需人工可读,如123@中@非法,2a因数字开头非法。

定位函数(精确到行列):

def get_line_col(input_string, pos): lines = input_string.split('\n') line_num = 1 char_count = 0 for line in lines: if char_count + len(line) + 1 > pos: # +1 for \n col_num = pos - char_count + 1 return line_num, col_num char_count += len(line) + 1 line_num += 1 return line_num, pos - char_count + 1 # 在报错时调用: line, col = get_line_col(input_code, error_pos) error_msg = f"第{line}行标识符 {token_value} 中包含非法字符{char}"

5.3 主程序封装:一键生成lex.txt与error.txt

完整交付脚本:

def main(): # 读取TEST源码 with open('test_input.txt', 'r', encoding='utf-8') as f: input_code = f.read() # 词法分析 tokens = lex_analysis(input_code) # 写lex.txt with open('lex.txt', 'w', encoding='utf-8') as f: for token_type, token_value in tokens: if token_type == 'ERROR': continue # 错误不写入lex.txt # 映射显示名 display_name = map_token_type(token_type, token_value) f.write(f"{display_name}: {token_value}\n") # 写error.txt with open('error.txt', 'w', encoding='utf-8') as f: error_count = 0 for token_type, token_value in tokens: if token_type == 'ERROR': error_count += 1 f.write(f"({error_count}){token_value}\n") if __name__ == '__main__': main()

注意:test_input.txt需按报告要求存入{ //This a test program. ... }内容,编码用UTF-8(支持中文注释)。


6. 最长匹配原则的终极验证:用三组对抗测试确认你的DFA没被绕过

6.1 测试集设计:覆盖所有易混淆边界

工业级词法器验证必跑三类对抗样本:

测试用例预期输出验证点
a++Identifier: a,Operator: +++后跟+应合并为++,而非+和+
x>=yIdentifier: x,Operator: >=,Identifier: y>后跟=必须合并,不能拆成>和=
0x123ERROR: Invalid char 'x' at ...0后接x非法,因0x是十六进制前缀,但TEST语言不支持

执行命令:

echo "a++" | python lexer.py echo "x>=y" | python lexer.py echo "0x123" | python lexer.py

6.2 自动化验证脚本:比对预期与实际输出

写test_runner.py防止手动核对出错:

def run_test(case, expected_tokens): tokens = lex_analysis(case) actual = [(t[0], t[1]) for t in tokens if t[0] != 'ERROR'] if actual == expected_tokens: print(f"✅ {case} PASS") else: print(f"❌ {case} FAIL") print(f"Expected: {expected_tokens}") print(f"Actual: {actual}") # 测试用例 run_test("a++", [('ID', 'a'), ('OPERATOR', '++')]) run_test("x>=y", [('ID', 'x'), ('OPERATOR', '>='), ('ID', 'y')]) run_test("0x123", []) # 全错,无合法token

6.3 从那以后我每次写词法器,都强制走一遍「三步验证」

  1. 画NFA:哪怕只画/和//的片段,确认SLASH_WAIT状态存在;
  2. 跑对抗测试:a++、x>=y、0123、//\nint四个必测,缺一不可;
  3. 查文件输出:用diff lex.txt expected_lex.txt二进制比对,拒绝肉眼扫。

这三步吃掉我两小时,但换来老师批注“DFA设计严谨,实现完整”——比多写十页报告有用。编译原理不是背概念,是让机器按你的规则咬住每一个字符。西南科大这份报告的价值,不在它写了什么,而在它逼你亲手把正则变成状态,把状态变成代码,把代码变成可验证的输出。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 22:15:19

巢湖流域shp底图处理指南:坐标统一、边界修复与空间分析

简介&#xff1a;巢湖流域GIS操作底图是一份面向水文环境研究、区域规划与GIS教学的矢量地理数据包&#xff0c;既可用来绘制流域边界、提取河网水系&#xff0c;也能为空间插值、叠加分析和专题制图提供基础图层&#xff0c;解决工作中局部底图精度不足、要素不完整的问题。压…

作者头像 李华
网站建设 2026/10/11 22:14:59

vllm-metal 语音转文字指南:Whisper 与 Qwen3-ASR 在 Mac 上本地跑通

【免费下载链接】vllm-metal Community maintained hardware plugin for vLLM on Apple Silicon 项目地址&#xff1a; https://gitcode.com/gh_mirrors/vl/vllm-metal 点击查看 免费下载 vllm-metal 是 vLLM 面向 Apple Silicon 的社区硬件插件&#xff0c;让 Whisper 与 Qwe…

作者头像 李华
网站建设 2026/10/11 22:09:45

基于知识图谱的Python电影推荐系统源码解析与毕设实战

简介&#xff1a;这是一套面向计算机相关专业毕业设计场景的Python电影推荐系统源码&#xff0c;采用知识图谱架构&#xff0c;融合协同过滤算法&#xff0c;可有效缓解传统推荐系统的冷启动问题。项目难度中等&#xff0c;适合作为课程作业、学期综合实践或毕设参考&#xff0…

作者头像 李华
网站建设 2026/10/11 22:09:32

Midjourney 135页手册精读:提示词结构与参数调优实战

简介&#xff1a;这份《Midjourney手册》是一套面向AI绘画初学者与设计师的完整图文教程&#xff0c;共1.3万字、135页&#xff0c;系统讲解Midjourney的注册、Discord频道接入与文本生成图像的核心操作&#xff0c;帮助零基础读者快速上手AI绘图工具。资源以1个docx文档交付&a…

作者头像 李华
网站建设 2026/10/11 22:09:14

时序相关性在蒙特卡洛场景生成与削减中的关键作用

前阵子帮一个风电项目做储能容量配置&#xff0c;蒙特卡洛&#xff08;MC&#xff09;场景生成跑了整整一夜&#xff0c;两千个风速场景在程序里转得风生水起。第二天把场景画出来一检查&#xff0c;我心里凉了半截&#xff1a;每个时刻的风速分布和真实历史数据几乎完全重合&a…

作者头像 李华
网站建设 2026/10/11 22:06:22

多传感器融合SLAM源码修改版实战:编译、运行与避坑指南

简介&#xff1a;《自动驾驶与机器人中的SLAM技术》源码修改版是高博原书配套代码的定制版&#xff0c;依据深蓝学院的教学与科研要求对代码结构和实现细节做了调整&#xff0c;面向机器人、自动驾驶方向的初学者与工程师&#xff0c;重点是帮助读者把同时定位与建图的理论知识…

作者头像 李华