词法分析器和语法分析器的界线
因为词法规则可以使用递归,所以词法解析器在技术上和语法解析器一样强大。那意味着我们甚至可以在词法分析器中匹配语法结构。或者,在另一个极端,我们可以把字符当作记号,使用语法分析器去把语法结构应用到字符流(这种被称为无扫描语法分析器)。这导致什么在词法分析器中匹配和什么在语法分析器中匹配的界线在哪里并不是很明显。幸运的是,有几条经验法则可以让我们做出判断:
- 在词法分析器中匹配和丢弃任何语法分析器根本不需要见到的东西。例如,在词法分析器中识别和扔掉像空格和注释这些东西。否则,语法分析器必须经常查看是否有空格或注释在记号间。
- 在词法分析器中匹配诸如标志符、关键字、字符串和数字这样的常用记号。语法分析器比词法分析器有更多的开销,因此我们不必让语法分析器承受把数字放在一起识别成整数的负担。
- 把那些语法分析器不需要去辨别的词法结构合并成一个单独的记号类型。例如,如果我们的应用把整数和浮点数当作同一事物对待,然后把它们合并成记号类型NUMBER,那么就没必要向语法分析器发送单独的记号类型。
- 合并能被语法分析器视为一个单独实体的任何东西。例如,如果语法分析器不在乎XML标签里的内容,词法分析器可以把尖括号中的任何东西合并成一个单独的被称为TAG的记号类型。
- 如果语法分析器需要先分开一小块文本后才能去处理它,那么词法分析器应该传递独立的构件作为记号给语法分析器。例如,如果语法分析器需要处理一个IP地址的元素,词法分析器应该发送IP构件(整数和点)的独立的记号。
想象下现在需要处理Web服务器上的日志文件,每一行表示一条记录。让我们假设每条记录都有一个请求IP地址、HTTP协议命令和结果代码。这里是一个日志条目的示例:
192.168.209.85 "GET /download/foo.html HTTP/1.0" 200
如果想要统计文件中有多少行,那么我们可以忽略掉任何东西除了换行字符的序列:
file : NL+ ; // 匹配换行(NL)序列的语法规则
STUFF : ~'\n'+ -> skip ; // 匹配和丢弃除'\n'外的任何东西
NL : '\n' ; // 返回NL给语法分析器或调用代码
词法分析器不必识别太多的结构,语法分析器会匹配换行记号的序列。
接下来,我们需要从日志文件中收集一系列的IP地址。这意味着我们需要一条规则去识别IP地址的词法结构。并且我们也可以提供其它记录元素的词法规则:
IP : INT '.' INT '.' INT '.' INT ; // 192.168.209.85
INT : [0-9]+ ; // 匹配IP八位组或者HTTP结果代码
STRING: '"' .*? '"' ; // 匹配HTTP协议命令
NL : '\n' ; // 匹配日志文件记录终结符
WS : ' ' -> skip ; // 忽略空格
拥有一套完整的记号后,我们可以让语法规则匹配日志文件中的记录:
file : row+ ; // 匹配日志文件中行的语法规则
row : IP STRING INT NL ; // 匹配日志文件记录
更进一步,我们需要把文本IP地址转换成32位的数字。使用便利的库函数split('.'),我们可以把IP地址作为字符串传递给语法分析器让它去处理。但是,更好的做法是让词法分析器匹配IP地址的词法结构,然后把匹配出的构件作为记号传递给语法分析器。
file : row+ ; // 匹配日志文件中行的语法规则
row : ip STRING INT NL ; // 匹配日志文件记录
ip : INT '.' INT '.' INT '.' INT ; // 在语法分析器中匹配IP地址
INT : [0-9]+ ; // 匹配IP八位组或者HTTP结果代码
STRING: '"' .*? '"' ; // 匹配HTTP协议命令
NL : '\n' ; // 匹配日志文件记录终结符
WS : ' ' -> skip ; // 忽略空格
把词法规则IP切换成语法规则ip显示了我们可以多么轻易地移动这条分界线。
如果要求处理HTTP协议命令字符串的内容,我们可以遵循相同的思考过程。如果不需要检查字符串的部分,那么词法分析器可以把整个字符串作为一个单独的记号传递给语法分析器。如果我们需要抽出各种不同的部分,最好就是让词法分析器去识别那些部分后再把这些匹配出的构件传递给语法分析器。