模型介绍
有限状态自动机(Finite Automaton, FA)是计算理论里最简洁、也最具代表性的抽象计算模型之一。它把“计算”看成一台只能处于有限个内部状态、并根据输入符号在这些状态之间转移的装置。其核心思想可以概括为:
当前状态 + 当前输入 → 下一状态
有限状态自动机常被用到词法分析当中,词法分析(Lexical Analysis)可被理解为将字符(String)序列转换为单词(Token)序列的过程,词法分析器则是以函数的形式存在。
应用举例
Java分析的词法可分为以下几例:
保留词:abstract、byte、case、catch、char、class、if、finally、new、native、void…
算数运算符:+、-、、/、%、++、–、+=、-=、/=、=
关系运算符:==、!=、>、<、>=、<=
位运算符:&、|、^、~、<<、>>、>>>
逻辑运算符:&&、||、!
赋值运算符:=、+=、-=、*=、/=、%=、<<=、>>=、&=、^=、|=
条件运算符:?
词法分析案例
下面是一段简单的 Java 代码,其功能是计算并输出一个数组中所有元素的平均值
public class AverageCalculator {
public static void main(String[] args) {
int[] numbers = {1, 2, 3, 4, 5};
double sum = 0;
for (int num : numbers) {
sum += num;
}
double average = sum / numbers.length;
System.out.println("The average of the array is: " + average);
}
}
词法分析器的输出结果通常以表格形式呈现。对于上述 Java 代码,词法分析器的部分输出结果如下表所示:
编号 | 所在行 | 单词 | 类型 | 单词 ID |
---|---|---|---|---|
1 | 1 | public | 保留字 | 35 |
2 | 1 | class | 保留字 | 9 |
3 | 1 | AverageCalculator | 标示符 | 1 |
4 | 1 | { | 界符 | 1 |
5 | 2 | public | 保留字 | 35 |
6 | 2 | static | 保留字 | 38 |
7 | 2 | void | 保留字 | 48 |
8 | 2 | main | 标示符 | 2 |
9 | 2 | ( | 界符 | 5 |
10 | 2 | String | 标示符 | 3 |
11 | 2 | [] | 界符 | 4 |
12 | 2 | args | 标示符 | 4 |
13 | 2 | ) | 界符 | 6 |
14 | 2 | { | 界符 | 1 |
15 | 3 | int | 保留字 | 27 |
16 | 3 | [] | 界符 | 4 |
17 | 3 | numbers | 标示符 | 5 |
18 | 3 | = | 运算符 | 21 |
19 | 3 | { | 界符 | 1 |
20 | 3 | 1 | 数字 | 1 |
21 | 3 | , | 界符 | 7 |
22 | 3 | 2 | 数字 | 2 |
23 | 3 | , | 界符 | 7 |
24 | 3 | 3 | 数字 | 3 |
25 | 3 | , | 界符 | 7 |
26 | 3 | 4 | 数字 | 4 |
27 | 3 | , | 界符 | 7 |
28 | 3 | 5 | 数字 | 5 |
29 | 3 | } | 界符 | 2 |
30 | 3 | ; | 界符 | 8 |
31 | 4 | double | 保留字 | 30 |
32 | 4 | sum | 标示符 | 6 |
33 | 4 | = | 运算符 | 21 |
34 | 4 | 0 | 数字 | 6 |
35 | 4 | ; | 界符 | 8 |
36 | 5 | for | 保留字 | 21 |
37 | 5 | ( | 界符 | 5 |
38 | 5 | int | 保留字 | 27 |
39 | 5 | num | 标示符 | 7 |
40 | 5 | : | 界符 | 9 |
41 | 5 | numbers | 标示符 | 5 |
42 | 5 | ) | 界符 | 6 |
43 | 5 | { | 界符 | 1 |
44 | 6 | sum | 标示符 | 6 |
45 | 6 | += | 运算符 | 22 |
46 | 6 | num | 标示符 | 7 |
47 | 6 | ; | 界符 | 8 |
48 | 7 | } | 界符 | 2 |
49 | 8 | double | 保留字 | 30 |
50 | 8 | average | 标示符 | 8 |
51 | 8 | = | 运算符 | 21 |
52 | 8 | sum | 标示符 | 6 |
53 | 8 | / | 运算符 | 23 |
54 | 8 | numbers | 标示符 | 5 |
55 | 8 | . | 界符 | 10 |
56 | 8 | length | 标示符 | 9 |
57 | 8 | ; | 界符 | 8 |
58 | 9 | System | 标示符 | 10 |
59 | 9 | . | 界符 | 10 |
60 | 9 | out | 标示符 | 11 |
61 | 9 | . | 界符 | 10 |
62 | 9 | println | 标示符 | 12 |
63 | 9 | ( | 界符 | 5 |
64 | 9 | "The average of the array is: " | 字符串 | 13 |
65 | 9 | + | 运算符 | 24 |
66 | 9 | average | 标示符 | 8 |
67 | 9 | ) | 界符 | 6 |
68 | 9 | ; | 界符 | 8 |
69 | 10 | } | 界符 | 2 |
70 | 11 | } | 界符 | 2 |
词法分析算法通常可以用有限状态自动机(Finite State Automaton,FSA)来实现。有限状态自动机是一种数学模型,它通过状态的转移来识别输入的字符序列是否属于某种模式。
在词法分析中,每个单词的识别可以看作是一个状态转移的过程。例如:
- 初始状态:开始扫描字符。
- 当遇到字母或下划线时,进入标识符或保留字的识别状态。
- 当遇到数字时,进入数字的识别状态。
- 当遇到界符或运算符时,进入相应的识别状态。
每个状态转移都对应着对输入字符的判断和处理,最终根据到达的状态确定单词的类型。
实际上,这种状态转移完全可以通过嵌套的 if-else 语句或者 switch-case 语句来实现。
# 简单词法分析器实现(基于循环+多层if-else)
def lexical_analyzer(code):
# 定义保留字、界符和运算符
reserved_words = {"if", "else", "while", "for", "int", "float", "return"}
delimiters = {",", ";", "(", ")", "{", "}", "[", "]"}
operators = {"+", "-", "*", "/", "=", "==", "!=", "<", ">", "<=", ">="}
# 预处理:去除注释和多余空格
lines = []
for line in code.split('\n'):
# 去除单行注释
comment_index = line.find('//')
if comment_index != -1:
line = line[:comment_index]
# 去除前后空格
line = line.strip()
if line: # 只保留非空行
lines.append(line)
tokens = [] # 存储识别出的词法单元
token_id = 0
# 逐行处理
for line_num, line in enumerate(lines, 1):
pos = 0 # 当前字符位置
length = len(line)
# 循环扫描当前行的每个字符
while pos < length:
# 跳过空格
if line[pos].isspace():
pos += 1
continue
# 判断是否是字母或下划线开头(可能是保留字或标识符)
if line[pos].isalpha() or line[pos] == '_':
# 读取完整的单词
start = pos
while pos < length and (line[pos].isalnum() or line[pos] == '_'):
pos += 1
word = line[start:pos]
# 判断是保留字还是标识符
if word in reserved_words:
token_type = "保留字"
else:
token_type = "标识符"
token_id += 1
tokens.append({
"id": token_id,
"value": word,
"type": token_type,
"line": line_num
})
# 判断是否是数字开头(可能是整数或浮点数)
elif line[pos].isdigit() or line[pos] == '.':
start = pos
has_dot = False
# 读取完整的数字
while pos < length:
if line[pos].isdigit():
pos += 1
elif line[pos] == '.':
# 已经有一个小数点了,不能再出现
if has_dot:
break
has_dot = True
pos += 1
else:
break
number = line[start:pos]
token_id += 1
tokens.append({
"id": token_id,
"value": number,
"type": "数字",
"line": line_num
})
# 判断是否是运算符或界符
else:
# 先尝试识别双字符运算符(如==、!=等)
if pos + 1 < length and line[pos:pos+2] in operators:
symbol = line[pos:pos+2]
pos += 2
token_type = "运算符"
# 识别单字符运算符或界符
elif line[pos] in operators:
symbol = line[pos]
pos += 1
token_type = "运算符"
elif line[pos] in delimiters:
symbol = line[pos]
pos += 1
token_type = "界符"
# 未知符号
else:
symbol = line[pos]
pos += 1
token_type = "未知符号"
token_id += 1
tokens.append({
"id": token_id,
"value": symbol,
"type": token_type,
"line": line_num
})
return tokens
# 测试代码
if __name__ == "__main__":
test_code = """
// 这是一个测试代码
int main() {
int a = 10;
float b = 3.14;
if (a > b) {
return 0;
} else {
return 1;
}
}
"""
# 进行词法分析
result = lexical_analyzer(test_code)
# 输出结果
print("词法分析结果:")
print(f"{'ID':<5} {'值':<10} {'类型':<6} {'行号'}")
print("-" * 35)
for token in result:
print(f"{token['id']:<5} {token['value']:<10} {token['type']:<6} {token['line']}")
词法分析结果如下:
词法分析结果:
ID 值 类型 行号
-----------------------------------
1 int 保留字 1
2 main 标识符 1
3 ( 界符 1
4 ) 界符 1
5 { 界符 1
6 int 保留字 2
7 a 标识符 2
8 = 运算符 2
9 10 数字 2
10 ; 界符 2
11 float 保留字 3
12 b 标识符 3
13 = 运算符 3
14 3.14 数字 3
15 ; 界符 3
16 if 保留字 4
17 ( 界符 4
18 a 标识符 4
19 > 运算符 4
20 b 标识符 4
21 ) 界符 4
22 { 界符 4
23 return 保留字 5
24 0 数字 5
25 ; 界符 5
26 } 界符 6
27 else 保留字 6
28 { 界符 6
29 return 保留字 7
30 1 数字 7
31 ; 界符 7
32 } 界符 8
33 } 界符 9