简介本资源为南京邮电大学编译原理实验一的词法分析器构造实验报告面向计算机相关专业正在学习编译原理课程的学生尤其适合需要完成词法分析实验、理解单词识别与编码原理的读者。资源包内共1个doc文档压缩包大小约7.69MB内容涵盖实验目的、实验环境、设计概要、实现分析与完整代码解析。报告以C语言子集为分析对象详细定义了关键字、运算符、界限符、整型常数与标识符的正规文法并给出单词编码方案与状态转换图。代码部分展示了关键字检测、界限符与运算符识别、字母数字判断及保留字表查询等核心函数的实现逻辑可直接参考调试。目前已有196人学习适合作为实验报告撰写模板与词法分析器实现的对照参考帮助读者快速掌握编译原理中词法分析阶段的关键技术。1. 南京邮电大学编译原理实验一词法分析到底在做什么如果你在南邮的编译原理课上拿到实验一大概率会先愣一下老师给了一段类似 Pascal 或 C 的源码片段要求你输出一串二元组格式是(种别码, 属性值)。很多同学第一反应是「这不就是字符串处理吗」然后随手写个split就交了结果一跑测试用例注释没跳过、浮点数被切成两段、关键字和标识符混在一起直接翻车。词法分析器Lexer是编译器流水线的第一道关卡它的任务不是「理解」代码而是把连续的字符流切分成有意义的词素Token并给每个词素打上类别标签。南邮实验一通常要求你手工实现一个扫描器而不是直接调lex或flex目的就是让你亲手处理「最长匹配」「回退」「前瞻」这些看起来简单、写起来全是边界问题的逻辑。这篇文章面向正在做这个实验、或者想搞懂词法分析器到底怎么落地的人从状态机设计讲到代码实现再到调试时那些让人抓狂的坑一步步把实验一做扎实。2. 词法分析器的核心机制从正则到状态机2.1 为什么不能只用正则表达式一把梭很多同学第一想法是用正则表达式匹配所有 Token写个re.findall就完事。理论上正则文法确实能描述词法规则但实验里通常要求你输出每个 Token 的行号和列号还要处理「最长匹配」原则。举个例子输入ifif正则if|identifier如果按顺序匹配会先命中if剩下if再匹配一次得到两个关键字但正确的词法规则应该是把ifif整体识别为一个标识符。这就是最长匹配Maximal Munch原则当多个规则都能匹配时选那个匹配长度最长的。正则引擎默认的回溯行为不一定符合这个原则你得手动控制匹配顺序和长度比较。更麻烦的是实验要求你处理注释//和/* */注释不是 Token但必须被正确跳过而且/* */不能嵌套遇到未闭合的注释要报错。这些逻辑用纯正则写会非常别扭所以常见做法是手写一个确定性有限自动机DFA用while循环加switch或if-else驱动状态转移。2.2 手写扫描器的状态转移设计一个典型的词法分析器主循环长这样维护一个指针pos指向当前字符每次循环先跳过空白和注释然后根据当前字符判断进入哪个 Token 的识别分支。识别标识符/关键字时一直往后读直到遇到非字母数字下划线识别数字时要处理整数、小数、科学计数法识别运算符时要处理双字符运算符如、、!。下面是一个简化但可运行的核心框架用 Python 写方便你直接改成 C 或 Java 版本。# 词法分析器核心扫描逻辑简化版 KEYWORDS {if, else, while, for, int, float, return} def tokenize(source): tokens [] pos 0 line 1 col 1 n len(source) while pos n: ch source[pos] # 跳过空白字符更新行列号 if ch in \t\r: pos 1 col 1 continue if ch \n: pos 1 line 1 col 1 continue # 跳过单行注释 if ch / and pos 1 n and source[pos1] /: while pos n and source[pos] ! \n: pos 1 continue # 跳过块注释注意未闭合情况 if ch / and pos 1 n and source[pos1] *: pos 2 closed False while pos 1 n: if source[pos] * and source[pos1] /: pos 2 closed True break if source[pos] \n: line 1 col 1 pos 1 if not closed: raise SyntaxError(fUnclosed comment at line {line}) continue # 识别标识符和关键字 if ch.isalpha() or ch _: start pos start_col col while pos n and (source[pos].isalnum() or source[pos] _): pos 1 col 1 word source[start:pos] if word in KEYWORDS: tokens.append((KEYWORD, word, line, start_col)) else: tokens.append((IDENTIFIER, word, line, start_col)) continue # 识别数字整数、小数、科学计数法 if ch.isdigit(): start pos start_col col while pos n and source[pos].isdigit(): pos 1 col 1 if pos n and source[pos] .: pos 1 col 1 while pos n and source[pos].isdigit(): pos 1 col 1 if pos n and source[pos] in eE: pos 1 col 1 if pos n and source[pos] in -: pos 1 col 1 while pos n and source[pos].isdigit(): pos 1 col 1 num source[start:pos] tokens.append((NUMBER, num, line, start_col)) continue # 识别双字符运算符 two_char_ops {, , , !, , ||} if pos 1 n and source[pos:pos2] in two_char_ops: tokens.append((OPERATOR, source[pos:pos2], line, col)) pos 2 col 2 continue # 单字符运算符和界符 if ch in -*/!|: tokens.append((OPERATOR, ch, line, col)) pos 1 col 1 continue if ch in (){}[];,: tokens.append((DELIMITER, ch, line, col)) pos 1 col 1 continue # 无法识别的字符 raise SyntaxError(fUnexpected character {ch} at line {line}, col {col}) return tokens这段代码的逻辑说明主循环每次处理一个字符先做「预处理」——跳过空白和注释然后进入 Token 识别。标识符分支用isalpha判断首字符后续用isalnum加下划线数字分支处理了小数点后的位数和科学计数法的指数部分运算符分支先检查双字符组合再回退到单字符。参数方面line和col是必须维护的因为实验通常要求输出 Token 的位置信息而且报错时没有行列号你根本找不到问题在哪。KEYWORDS集合可以根据实验给定的语言子集调整南邮实验一一般要求支持if、else、while、for、int、float、return这几个。2.3 种别码的设计与输出格式实验通常要求输出(种别码, 属性值)的二元组。种别码可以自己定义比如关键字用 1标识符用 2数字用 3运算符用 4界符用 5。但更常见的做法是给每个关键字单独一个种别码比如if是 1else是 2这样语法分析阶段处理起来更方便。属性值对于关键字和运算符可以是空或者原字符串对于标识符和数字就是实际的值。下面是一个输出格式的示例# 输出二元组种别码映射表 TOKEN_CODE { KEYWORD: 1, IDENTIFIER: 2, NUMBER: 3, OPERATOR: 4, DELIMITER: 5 } def print_tokens(tokens): for tok_type, value, line, col in tokens: code TOKEN_CODE[tok_type] print(f({code}, {value}) # line {line}, col {col})这里把种别码统一映射方便你后续改成每个关键字独立编码。注意实验报告里通常要求你列出所有 Token 的种别码表这个表要和你代码里的定义一致否则助教检查时会对不上。3. 从零实现一个可跑通的词法分析器3.1 实验环境准备与输入处理南邮实验一一般会给你一个test.txt或者直接在命令行输入源码。我建议你先把输入文件读进来统一处理换行符。Windows 下换行是\r\nLinux 下是\n如果你不处理\r会被当成非法字符报错。常见做法是在读文件时用open(file, r, encodingutf-8)然后source f.read().replace(\r\n, \n)。另外实验可能要求你从标准输入读取那就用sys.stdin.read()。环境方面C/C 用 GCC 或 Clang 都行Java 用 JDK 8 以上Python 用 3.6 即可。不需要额外装 lex 或 flex实验一就是让你手写。3.2 完整代码结构与关键函数拆分把上面的核心逻辑拆成几个函数skip_whitespace_and_comments、read_identifier、read_number、read_operator。这样主循环更清晰也方便你单独测试每个函数。下面是一个更工程化的结构class Lexer: def __init__(self, source): self.source source self.pos 0 self.line 1 self.col 1 self.n len(source) def peek(self, offset0): idx self.pos offset if idx self.n: return self.source[idx] return \0 def advance(self): ch self.source[self.pos] self.pos 1 if ch \n: self.line 1 self.col 1 else: self.col 1 return ch def skip_whitespace(self): while self.pos self.n and self.peek() in \t\r\n: self.advance() def skip_comment(self): if self.peek() / and self.peek(1) /: while self.pos self.n and self.peek() ! \n: self.advance() return True if self.peek() / and self.peek(1) *: self.advance() self.advance() while self.pos self.n: if self.peek() * and self.peek(1) /: self.advance() self.advance() return True self.advance() raise SyntaxError(fUnclosed comment at line {self.line}) return False def next_token(self): self.skip_whitespace() while self.skip_comment(): self.skip_whitespace() if self.pos self.n: return None ch self.peek() start_line self.line start_col self.col if ch.isalpha() or ch _: return self.read_identifier(start_line, start_col) if ch.isdigit(): return self.read_number(start_line, start_col) return self.read_operator(start_line, start_col) def read_identifier(self, line, col): start self.pos while self.pos self.n and (self.peek().isalnum() or self.peek() _): self.advance() word self.source[start:self.pos] if word in KEYWORDS: return (KEYWORD, word, line, col) return (IDENTIFIER, word, line, col) def read_number(self, line, col): start self.pos while self.pos self.n and self.peek().isdigit(): self.advance() if self.peek() .: self.advance() while self.pos self.n and self.peek().isdigit(): self.advance() if self.peek() in eE: self.advance() if self.peek() in -: self.advance() while self.pos self.n and self.peek().isdigit(): self.advance() return (NUMBER, self.source[start:self.pos], line, col) def read_operator(self, line, col): two self.source[self.pos:self.pos2] if two in {, , , !, , ||}: self.advance() self.advance() return (OPERATOR, two, line, col) ch self.advance() if ch in -*/!|: return (OPERATOR, ch, line, col) if ch in (){}[];,: return (DELIMITER, ch, line, col) raise SyntaxError(fUnexpected character {ch} at line {line}, col {col})这个类把状态维护在self里peek和advance分离方便你处理前瞻。skip_comment返回布尔值表示是否跳过了注释主循环里用while反复调用因为可能连续多个注释。read_number里对.和e的处理要小心如果输入是123.后面没有数字按实验要求可能报错也可能当成整数加小数点这个要看具体实验说明。3.3 测试用例与输出验证写完之后用几组边界用例测一下。第一组int a 10;应该输出(1, int)、(2, a)、(4, )、(3, 10)、(5, ;)。第二组if (a b) { return a; }检查双字符运算符是否被正确识别。第三组// comment\nint x;检查注释跳过和行号是否正确。第四组/* unclosed应该报错。第五组123.45e-6检查科学计数法。你可以写个简单的测试脚本def test_lexer(): cases [ (int a 10;, 5), (if (a b) { return a; }, 10), (// comment\nint x;, 3), (123.45e-6, 1), ] for src, expected_count in cases: lexer Lexer(src) tokens [] while True: tok lexer.next_token() if tok is None: break tokens.append(tok) assert len(tokens) expected_count, fFailed: {src}, got {len(tokens)} print(fPASS: {src} - {tokens})跑通这些用例实验一的基本分就拿到了。如果助教还要求输出到文件把print改成写文件即可。4. 避坑与排查词法分析实验里最容易翻车的五个点4.1 注释跳过不干净导致后续 Token 错位现象输入里有/* comment */ int a;结果int被识别成了标识符或者直接报错。原因块注释的结束判断写成了source[pos] * and source[pos1] /但循环条件没处理好导致*/后面的字符被多跳过一个。解决在skip_comment里用while找到*/后pos要精确停在*/之后不要多走一步。建议单独写个测试输入/**/int看输出是不是从int开始。4.2 最长匹配原则被忽略ifif被拆成两个关键字现象输入ifif输出两个if。原因识别标识符时读到if就停了没有继续往后读。解决标识符识别必须一直读到非字母数字下划线为止然后再去查关键字表。关键字表查询是在完整单词之后做的不是边读边查。4.3 浮点数的小数点处理不当1.2.3被吞掉现象输入1.2.3输出一个数字1.2.3或者报错。原因read_number里对.的处理没有限制只能出现一次。解决加个标志位has_dot遇到.时如果已经有过小数点就停止把后面的.3留给下一个 Token。实验里通常不会出现1.2.3这种非法输入但如果你不处理遇到1..2就会出问题。4.4 行号和列号在注释和换行后错乱现象报错信息里的行号对不上或者 Token 的列号从 1 开始但实际不是。原因advance里更新col的逻辑在遇到\n时重置为 1但跳过注释时如果注释里有换行没有调用advance而是直接pos 1导致行号没更新。解决所有字符移动都必须走advance不要直接改pos。如果为了性能想直接跳也要手动更新line和col。4.5 双字符运算符的前瞻越界现象输入末尾是程序报IndexError。原因peek(1)在pos1 n时返回了\0但判断source[pos:pos2]时切片不会越界可如果你用source[pos1]直接索引就会崩。解决统一用peek函数它内部做了边界检查。或者用切片source[pos:pos2]Python 切片越界不报错但 C 里就得小心。5. 进阶技巧把词法分析器改成可配置的如果你想让实验一做得更漂亮可以把关键字表、运算符表做成外部配置用 JSON 或简单的文本文件定义。这样换一个语言子集不用改代码。另外可以加一个--verbose模式输出每个 Token 的详细位置和匹配规则方便调试。我一般还会写个dump_tokens函数把 Token 流格式化成表格直接贴进实验报告。最后一个技巧用enum代替魔法数字做种别码代码可读性会好很多。这些改动不大但能让你的实验从「能跑」变成「好维护」助教一看就知道你花了心思。希望帮到你。本文还有配套的精品资源点击获取