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 按上述思路实现的一个扫描器,它以自己为源文件进行分词。将此文件下载下来,放在终端的当前目录,再在终端输入:

  1. $ python scan.py

将输出:

  1. TOKEN TYPE TOKEN VALUE
  2. --------------------------------------------------
  3. T_identifier single_char_operators_typeA
  4. T_= =
  5. T_{ {
  6. T_string ";"
  7. T_, ,
  8. T_string ","
  9. ...

该文件中的核心代码是 scan 函数,它对一行字符串进行分词,代码如下,读者可以粗略的看看这个函数的代码,基本就是按以上所描述的算法实现的。

  1. def scan(s):
  2. n, i = len(s), 0
  3. while i < n:
  4. ch, i = s[i], i + 1
  5.  
  6. if isWhiteSpace(ch):
  7. continue
  8.  
  9. if ch == "#":
  10. return
  11.  
  12. if ch in single_char_operators_typeA:
  13. yield Token(ch)
  14. elif ch in single_char_operators_typeB:
  15. if i < n and s[i] == "=":
  16. yield Token(ch + "=")
  17. else:
  18. yield Token(ch)
  19. elif isLetter(ch) or ch == "_":
  20. begin = i - 1
  21. while i < n and (isLetter(s[i]) or isDigit(s[i]) or s[i] == "_"):
  22. i += 1
  23. word = s[begin:i]
  24. if word in reservedWords:
  25. yield Token(word)
  26. else:
  27. yield Token("T_identifier", word)
  28. elif isDigit(ch):
  29. begin = i - 1
  30. aDot = False
  31. while i < n:
  32. if s[i] == ".":
  33. if aDot:
  34. raise Exception("Too many dot in a number!\n\tline:"+line)
  35. aDot = True
  36. elif not isDigit(s[i]):
  37. break
  38. i += 1
  39. yield Token("T_double" if aDot else "T_integer", s[begin:i])
  40. elif ord(ch) == 34: # 34 means '"'
  41. begin = i
  42. while i < n and ord(s[i]) != 34:
  43. i += 1
  44. if i == n:
  45. raise Exception("Non-terminated string quote!\n\tline:"+line)
  46. yield Token("T_string", chr(34) + s[begin:i] + chr(34))
  47. i += 1
  48. else:
  49. raise Exception("Unknown symbol!\n\tline:"+line+"\n\tchar:"+ch)

可以看出直接扫描法思路简单,代码量非常少,scan.py 不过 100 代码。但缺点是速度慢,对标识符类型的 token 需要进行至少 2 次扫描,且需进行字符串查找和比较。而且不容易扩展,只适用于语法简单的语言。目前一般的编译器都是采用基于正则表达式匹配的分词扫描法,以下介绍此方法。