词法分析到底在做什么
词法分析是编译器的第一个阶段,核心任务是把源程序的字符流拆成一个个有意义的“单词”(Token)。打个比方,就像我们读英文文章时,会自动把连续的字母分成单词——词法分析器就是编译器的“分词器”,但它的规则更严谨。

要理解词法分析,得先分清三个关键概念,用表格对比更清楚:
术语 | 定义 | 例子 |
---|---|---|
词素(Lexeme) | 源程序中实际出现的字符序列,是Token的“原材料” | int 、123 、== |
模式(Pattern) | 描述一类词素的规则(比如“整数”的模式是“连续的数字”),通常用正则表达式表示 | 整数模式:[0-9]+ |
Token | 词素的抽象表示(包含“类型”和“值”),是编译器后续阶段(语法分析)的输入 | (INT_KEYWORD, “int”)、(INT_LITERAL, “123”) |
举个例子:源程序里的int a = 123;
,词法分析器会输出4个Token:
1. (INT_KEYWORD, “int”)——关键字
2. (IDENTIFIER, “a”)——标识符
3. (ASSIGN, “=”)——赋值运算符
4. (INT_LITERAL, “123”)——整数字面量
词法分析的本质,就是将“字符序列”映射到“Token序列”,帮编译器屏蔽源程序的“字符级细节”,聚焦于“语法结构”。
如何定义词法规则:正则表达式+DFA
词法规则是词法分析的“法律”——所有词素都必须匹配某个规则。而描述规则的最佳工具是正则表达式,但正则表达式是“声明式”的(说“要什么”),词法分析器需要“命令式”的执行逻辑(说“怎么做”),这就需要把正则表达式转换成确定有限自动机(DFA)。
步骤1:用正则表达式描述模式
首先,你需要为每种Token定义正则表达式。比如:
– 关键字(if/else):if|else
(确保是完整单词,避免把
ifx
误判为关键字)
– 标识符:[a-zA-Z_][a-zA-Z0-9_]*
(字母或下划线开头,后面跟字母、数字或下划线)
– 整数:[0-9]+
(连续数字)
– 比较运算符:==|!=|<|>|<=|>=
(长运算符优先,比如先匹配==
再匹配=
)
步骤2:正则→NFA→DFA
正则表达式无法直接执行,需要先转成非确定有限自动机(NFA),再转成DFA(因为DFA的状态转移是确定的,执行效率更高)。这里用一个简单例子演示流程:
假设我们要处理“整数或浮点数”的模式:[0-9]+(.[0-9]+)?
(比如123
或123.45
)。
- 正则→NFA:用Thompson构造法,把正则表达式拆成基本单元(单字符、连接、选择、闭包),逐步构建NFA。比如:
- 单个字符
[0-9]
对应一个NFA(状态0→状态1,输入0-9) +
(至少一次)对应“循环”:状态1→状态0的ε转移(ε表示“空输入”)-
(.[0-9]+)?
(可选的小数部分)对应“选择”:状态2→状态3(输入.
)→状态4→状态5(输入0-9)→状态4的ε转移(循环),然后状态2有ε转移到终点(表示没有小数部分)。 -
NFA→DFA:用子集构造法,把NFA的“状态集合”当作DFA的“单个状态”。比如:
- 初始状态:NFA初始状态的ε-闭包(所有能通过ε转移到达的状态)
-
对每个输入字符,计算当前状态集合的“转移后的ε-闭包”,作为DFA的下一个状态。
-
DFA最小化:去掉冗余状态(比如两个状态对所有输入的转移都相同,且都是接受状态或都不是),减少DFA的复杂度。比如如果有两个状态S1和S2,输入
0-9
都转移到S3,输入.
都转移到S4,且都是接受状态,那么可以合并成一个状态。
最终的DFA,就是词法分析器的“匹配引擎”——每读一个字符,就按照DFA的状态转移规则移动,直到到达“接受状态”(匹配成功)或“错误状态”(匹配失败)。
词法分析器的核心流程拆解
词法分析器的工作流程,可以拆解成4个核心步骤,每一步都有细节要注意:
1. 字符读取:处理“向前看”(Lookahead)
词法分析器需要逐个读取字符,但有时候需要“先看一眼下一个字符”再决定怎么处理——这叫“向前看”。比如识别==
时:
– 先读第一个=
,此时不确定是=
(赋值)还是==
(等于);
– 再读下一个字符,如果是=
,就组成==
;
– 如果不是,就把第二个字符“退回去”(ungetc),只处理第一个=
。
几乎所有词法分析器都需要这个逻辑——比如C语言中的<=
(小于等于)、++
(自增),都需要向前看1个字符。
2. 词法扫描:跳过无关字符
源程序中的空格、换行、注释都是“无关字符”(不影响语法结构),词法分析器需要跳过它们。比如:
– 空格/换行:直接读取下一个字符;
– 注释://
(单行)需要读到换行符为止,/* */
(多行)需要读到*/
为止(注意处理嵌套注释?不,C语言不允许嵌套注释,所以直接找*/
就行)。
跳过无关字符的目的,是让后续阶段(语法分析)不用处理“字符级的噪音”。
3. 模式匹配:最长匹配原则
当多个模式都能匹配当前字符序列时,要选最长的那个——这叫“最长匹配原则”。比如:
– 源程序中的123a
,如果有两个模式:[0-9]+
(整数)和[a-zA-Z_][a-zA-Z0-9_]*
(标识符),那么最长匹配是123
(整数)+a
(标识符),而不是1
+23a
。
另一个例子:ifx
,如果关键字if
的模式是if
(带单词边界),那么ifx
会被匹配为标识符([a-zA-Z_][a-zA-Z0-9_]*
),而不是关键字if
+x
——这就是为什么关键字的正则要加。
4. 生成Token:记录类型和值
匹配到词素后,需要生成Token——包含“类型”(比如INT_KEYWORD、IDENTIFIER)和“值”(比如int
、a
)。Token的类型是枚举值(方便语法分析器判断),值是词素的原始字符串(方便后续阶段获取具体内容,比如语义分析中的变量名)。
常见问题与避坑技巧
词法分析看起来简单,但实际实现中会遇到很多“坑”,这里列几个高频问题及解决方法:
问题1:关键字与标识符冲突
比如int
是关键字,但用户可能写int int = 123;
——这时候第一个int
是关键字,第二个int
是标识符?不对,因为关键字是“保留字”,不能作为标识符。解决方法:把关键字放到“优先匹配列表”——词法分析器先查关键字表,如果当前词素是关键字,就生成关键字Token,否则生成标识符Token。
比如Python中的关键字表:['if', 'else', 'for', 'while', ...]
,词法分析器遇到if
时,先查这个表,匹配就生成(KEYWORD, ‘if’),否则生成(IDENTIFIER, ‘if’)。
问题2:转义字符的处理
字符串中的转义字符(比如
、"
)需要特殊处理——比如源程序中的"hello
,词素是
world""hello
,但实际存储的是
world"hello
+换行+world
。解决方法:在词法分析时解析转义字符,把
转换成ASCII的换行符(0x0A),"
转换成双引号(0x22)。
问题3:多行注释的嵌套
比如C语言中的/* /* comment */ */
,这其实是非法的(C语言不允许嵌套注释),但有些编译器会支持。解决方法:用计数器记录注释层数——遇到/*
时计数器+1,遇到*/
时计数器-1,直到计数器为0时结束注释。
问题4:浮点数的歧义
比如123.
(末尾的点),是整数还是浮点数?根据C语言的规则,123.
是浮点数(double类型),但有些词法分析器会把123.
误判为整数123
+点运算符。解决方法:调整正则表达式的顺序——把浮点数的正则放到整数前面,比如先匹配
[0-9]+(.[0-9]+)?
(浮点数),再匹配[0-9]+
(整数),这样123.
会先匹配浮点数模式。
用Python实现一个简单词法分析器
说了这么多理论,不如动手写一个——我们用Python实现一个处理“简单表达式”的词法分析器,支持:
– 关键字:if
、else
– 标识符:字母或下划线开头
– 整数:连续数字
– 运算符:+
、-
、*
、/
、==
、=
步骤1:定义词法规则和Token类型
首先,我们用re
模块(Python的正则表达式库)来处理模式匹配,规则列表要按“长匹配优先”排序(比如==
要在=
前面):
import re
# Token类型枚举(用字符串代替枚举,方便调试)
TOKEN_TYPES = {
'KEYWORD': 'KEYWORD',
'IDENTIFIER': 'IDENTIFIER',
'INT_LITERAL': 'INT_LITERAL',
'OPERATOR': 'OPERATOR',
'WHITESPACE': 'WHITESPACE' # 跳过
}
# 词法规则:(Token类型, 正则表达式),长规则优先
token_specs = [
(TOKEN_TYPES['KEYWORD'], r'if|else'),
(TOKEN_TYPES['IDENTIFIER'], r'[a-zA-Z_][a-zA-Z0-9_]*'),
(TOKEN_TYPES['INT_LITERAL'], r'd+'),
(TOKEN_TYPES['OPERATOR'], r'==|[-+*/=]'),
(TOKEN_TYPES['WHITESPACE'], r's+'),
]
步骤2:编译正则表达式
用re.finditer
来遍历源程序中的所有匹配项,注意正则表达式的分组命名(用(?P<name>...)
):
# 编译正则表达式:拼接所有规则,每个规则用命名分组
token_regex = '|'.join(f'(?P<{name}>{regex})' for name, regex in token_specs)
步骤3:实现词法分析函数
遍历匹配项,生成Token序列,跳过空格:
def lex(source_code):
tokens = []
# 遍历所有匹配项
for match in re.finditer(token_regex, source_code):
# 获取匹配的Token类型和值
token_type = match.lastgroup
token_value = match.group(token_type)
# 跳过空格
if token_type == TOKEN_TYPES['WHITESPACE']:
continue
# 添加Token(类型,值)
tokens.append((token_type, token_value))
return tokens
步骤4:测试
我们用一个简单的源程序测试:
# 测试源程序
source = '''
if x == 42:
y = x + 10
else:
y = 0
'''
# 生成Token
tokens = lex(source)
# 打印结果
for token in tokens:
print(token)
输出结果(部分):
('KEYWORD', 'if')
('IDENTIFIER', 'x')
('OPERATOR', '==')
('INT_LITERAL', '42')
('OPERATOR', ':')
('IDENTIFIER', 'y')
('OPERATOR', '=')
('IDENTIFIER', 'x')
('OPERATOR', '+')
('INT_LITERAL', '10')
...
这个词法分析器虽然简单,但覆盖了词法分析的核心逻辑——你可以在此基础上扩展:比如添加浮点数、字符串字面量、注释处理,或者用DFA代替正则表达式(更高效)。
最后:词法分析的“边界”
词法分析是编译器的“第一个关卡”,但它的职责是“拆单词”,而不是“检查语法”——比如int 123 = a;
,词法分析器会生成正确的Token(INT_KEYWORD、INT_LITERAL、ASSIGN、IDENTIFIER),但语法分析器会报错(因为变量名不能是整数)。
记住:词法分析器的任务是“正确拆分Token”,而不是“检查语义”——不要让词法分析器做超出职责的事,否则会增加复杂度。
原创文章,作者:,如若转载,请注明出处:https://zube.cn/archives/346