词法分析器或者叫扫描器主要用来分析字符串的文本,然后把文本里组成的单词分析出来,识别为某一类型的属性。对于编写编译器或者解析器的第一步工作就是做这样的事情:词法分析。以前有很多种使用字符串搜索的办法,这里使用正则表达式来实现这个目的。
例子:
print("词法分析器") import collections import re Token = collections.namedtuple('Token', ['typ', 'value', 'line', 'column']) def tokenize(code): keywords = {'IF', 'THEN', 'ENDIF', 'FOR', 'NEXT', 'GOSUB', 'RETURN'} token_specification = [ ('NUMBER', r'\d+(\.\d*)?'), # Integer or decimal number ('ASSIGN', r':='), # Assignment operator ('END', r';'), # Statement terminator ('ID', r'[A-Za-z]+'), # Identifiers ('OP', r'[+\-*/]'), # Arithmetic operators ('NEWLINE', r'\n'), # Line endings ('SKIP', r'[ \t]+'), # Skip over spaces and tabs ('MISMATCH',r'.'), # Any other character ] tok_regex = '|'.join('(?P<%s>%s)' % pair for pair in token_specification) line_num = 1 line_start = 0 for mo in re.finditer(tok_regex, code): kind = mo.lastgroup value = mo.group(kind) if kind == 'NEWLINE': line_start = mo.end() line_num += 1 elif kind == 'SKIP': pass elif kind == 'MISMATCH': raise RuntimeError('%r unexpected on line %d' % (value, line_num)) else: if kind == 'ID' and value in keywords: kind = value column = mo.start() - line_start yield Token(kind, value, line_num, column) statements = ''' IF quantity THEN total := total + price * quantity; tax := price * 0.05; ENDIF; ''' for token in tokenize(statements): print(token)
结果输出如下:
词法分析器
Token(typ='IF', value='IF', line=2, column=4)
Token(typ='ID', value='quantity', line=2, column=7)
Token(typ='THEN', value='THEN', line=2, column=16)
Token(typ='ID', value='total', line=3, column=8)
Token(typ='ASSIGN', value=':=', line=3, column=14)
Token(typ='ID', value='total', line=3, column=17)
Token(typ='OP', value='+', line=3, column=23)
Token(typ='ID', value='price', line=3, column=25)
Token(typ='OP', value='*', line=3, column=31)
Token(typ='ID', value='quantity', line=3, column=33)
Token(typ='END', value=';', line=3, column=41)
Token(typ='ID', value='tax', line=4, column=8)
Token(typ='ASSIGN', value=':=', line=4, column=12)
Token(typ='ID', value='price', line=4, column=15)
Token(typ='OP', value='*', line=4, column=21)
Token(typ='NUMBER', value='0.05', line=4, column=23)
Token(typ='END', value=';', line=4, column=27)
Token(typ='ENDIF', value='ENDIF', line=5, column=4)
Token(typ='END', value=';', line=5, column=9)
在这个例子里,先从库collections导入namedtuple,以便可以记录每个单词(Token)的属性,在这里主要为4个属性:类型、值、行号、列号,因此创建一个命名的元组Token数据结构,为后面保存每一个单词属性提供了空间。
接着定义一个函数tokenize(),在函数里先定义关键字集合keywords,定义识别不同单词的正则表达式的字典token_specification,然后通过通过字符串join函数生成一个正则表达式:
(?P<NUMBER>\d+(\.\d*)?)|(?P<ASSIGN>:=)|(?P<END>;)|(?P<ID>[A-Za-z]+)|(?P<OP>[+\-*/])|(?P<NEWLINE>\n)|(?P<SKIP>[ \t]+)|(?P<MISMATCH>.)
通过上面正则表达式,就可匹配所有规则,只要匹配成功,就保存在最后一个分组里,因而使用了lastgroup来获取。具体来分析一个正则表达式(?P<NUMBER>\d+(\.\d*)?),外面大括号表示分组,?表示可以出现,P<NUMBER>表示分组的名称为NUMBER,\d+表示匹配所有数字字符,+(\.\d*)?表示小数点部分是否可以存在。
通过语句re.finditer(tok_regex, code)来匹配整个输入文本中所有单词,然后在for循环里得到所有匹配成功的单词,接着判断单词的类型,并根据换行符来增加行号,判断列号,有了单词的类型、行号、列表号,再把值保存起来就完成了整个词法分析过程。
值得注意的是,由于分析的文本长度不限,有可能达到1M,或者几M大小,因此不可能把所有单词全部保存在一起再返回,因而采用迭代子的方式进行返回,从而使用了关键字yield来返回。从上面看来,有了正则表达式的功能,再来写一个词法分析器是轻而易举的事情,不费吹灰之力,这是为“懒人”准备的好工具。