3.2.5.9 写一个词法分析器

词法分析器或者叫扫描器主要用来分析字符串的文本,然后把文本里组成的单词分析出来,识别为某一类型的属性。对于编写编译器或者解析器的第一步工作就是做这样的事情:词法分析。以前有很多种使用字符串搜索的办法,这里使用正则表达式来实现这个目的。

例子:

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来返回。从上面看来,有了正则表达式的功能,再来写一个词法分析器是轻而易举的事情,不费吹灰之力,这是为“懒人”准备的好工具。

 


蔡军生  微信号:shenzhencai  深圳

你可能感兴趣的:(python,正则表达式,编译器,milang)