7.2 直接扫描法
直接扫描法的思路非常简单,每轮扫描,根据第一个字符判断属于哪种类型的 token ,然后采取不同的策略扫描出一个完整的 token ,再接着进行下一轮扫描。例如 TinyC 中,若仅考虑一些简单的情况,按 token 的第一个字符,可以将所有类别的 token 分为以下 7 大类:
(1)A型单字符运算符
包括:+, -, *, /, %, 这种 token 只有一个字符,若本轮扫描的第一个字符为上述字符,则立即返回此字符所代表的 token ,然后移到下一个字符开始下一轮扫描。
(2)B型单字符运算符和双字符运算符
B型单字符运算符包括: < > = ! ,双字符运算符包括: <=, >=, ==, != 。 若本轮扫描的第一个字符为B型单字符运算符时,先查看下一个字符是否是 “=” ,如果是,则返回这两个字符代表的 token ,如果否,则返回这个字符代表的 token 。例如,如果扫描到 “>” ,则查看下一个字符是否是 “=” ,是则返回 T_GREATEEQUAL ,否则返回 T_GREATTHAN 。
(3)关键词和标识符
关键词和标识符都是以字母或下划线开始、且只有字母、下划线或数字组成。若本轮扫描的第一个字符为字母或下划线时,则一直向后扫描,直到遇到第一个既不是字母、也不是下划线或数字的字符,此时一个完整的词就被扫描出来了,然后,查看这个词是不是为关键字,如果是,则返回关键字代表的 token ,如果不是,则返回 T_IDENTIFIER 以及这个词的字面值。
(4)整数常量
整数常量以数字开始,若本轮扫描的第一个字符为数字,则一直向后扫描,直到遇到第一个非数字字符,然后返回 T_INTEGERCONSTANT 和这个数字。
(5)字符串常量
字符串常量以双引号开始和结束,若本轮扫描的第一个字符为双引号,则一直向后扫描,直到遇到第一个双引号,然后返回 T_STRINGCONSTANT 和这个字符串。
(6)空格
若本轮扫描的第一个字符为为空格,则跳过此字符。
(7)注释
注释仅考虑以 # 开始的情况,若本轮扫描的第一个字符为 #,则直接跳过此行字符流。
以上算法的实现也很简单,scan.py 是用 python 按上述思路实现的一个扫描器,它以自己为源文件进行分词。将此文件下载下来,放在终端的当前目录,再在终端输入:
- $ python scan.py
将输出:
- TOKEN TYPE TOKEN VALUE
- --------------------------------------------------
- T_identifier single_char_operators_typeA
- T_= =
- T_{ {
- T_string ";"
- T_, ,
- T_string ","
- ...
该文件中的核心代码是 scan 函数,它对一行字符串进行分词,代码如下,读者可以粗略的看看这个函数的代码,基本就是按以上所描述的算法实现的。
- def scan(s):
- n, i = len(s), 0
- while i < n:
- ch, i = s[i], i + 1
- if isWhiteSpace(ch):
- continue
- if ch == "#":
- return
- if ch in single_char_operators_typeA:
- yield Token(ch)
- elif ch in single_char_operators_typeB:
- if i < n and s[i] == "=":
- yield Token(ch + "=")
- else:
- yield Token(ch)
- elif isLetter(ch) or ch == "_":
- begin = i - 1
- while i < n and (isLetter(s[i]) or isDigit(s[i]) or s[i] == "_"):
- i += 1
- word = s[begin:i]
- if word in reservedWords:
- yield Token(word)
- else:
- yield Token("T_identifier", word)
- elif isDigit(ch):
- begin = i - 1
- aDot = False
- while i < n:
- if s[i] == ".":
- if aDot:
- raise Exception("Too many dot in a number!\n\tline:"+line)
- aDot = True
- elif not isDigit(s[i]):
- break
- i += 1
- yield Token("T_double" if aDot else "T_integer", s[begin:i])
- elif ord(ch) == 34: # 34 means '"'
- begin = i
- while i < n and ord(s[i]) != 34:
- i += 1
- if i == n:
- raise Exception("Non-terminated string quote!\n\tline:"+line)
- yield Token("T_string", chr(34) + s[begin:i] + chr(34))
- i += 1
- else:
- raise Exception("Unknown symbol!\n\tline:"+line+"\n\tchar:"+ch)
可以看出直接扫描法思路简单,代码量非常少,scan.py 不过 100 代码。但缺点是速度慢,对标识符类型的 token 需要进行至少 2 次扫描,且需进行字符串查找和比较。而且不容易扩展,只适用于语法简单的语言。目前一般的编译器都是采用基于正则表达式匹配的分词扫描法,以下介绍此方法。