因为词法规则可以使用递归,所以词法解析器在技术上和语法解析器一样强大。那意味着我们甚至可以在词法分析器中匹配语法结构。或者,在另一个极端,我们可以把字符当作记号,使用语法分析器去把语法结构应用到字符流(这种被称为无扫描语法分析器)。这导致什么在词法分析器中匹配和什么在语法分析器中匹配的界线在哪里并不是很明显。幸运的是,有几条经验法则可以让我们做出判断:
想象下现在需要处理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协议命令字符串的内容,我们可以遵循相同的思考过程。如果不需要检查字符串的部分,那么词法分析器可以把整个字符串作为一个单独的记号传递给语法分析器。如果我们需要抽出各种不同的部分,最好就是让词法分析器去识别那些部分后再把这些匹配出的构件传递给语法分析器。