IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    [原]3.2.5.9 写一个词法分析器

    caimouse发表于 2015-09-13 09:06:34
    love 0

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

    例子:

    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  深圳


沪ICP备19023445号-2号
友情链接