Python手写词法分析器:基于正则表达式的Tokenizer实现

清欢
清欢
发布时间:2026-10-24| 5人浏览过
原创

词法分析是编译和解析的第一步——把一串原始字符拆成有类型的记号(Token)。Python标准库的 re 模块提供了足够的正则能力,配合 re.compile 和命名分组,几十行代码就能写出一个可用的 Tokenizer。本文从 Token 的定义开始,逐步构建一个完整的手写词法分析器,最终用它处理一段简单的算术表达式源码。

一、词法分析器做什么

词法分析器(Lexer / Tokenizer)的输入是一段源代码字符串,输出是一个 Token 序列。每个 Token 至少包含两个信息:类型(比如 NUMBER、IDENTIFIER、PLUS)和值(对应源码中的原始文本)。有些实现还会记录行号和列号,方便后续报错。

以表达式 x = 3 + 42 * y 为例,词法分析器应该输出如下 Token 序列:

类型值
IDENTIFIERx
ASSIGN=
NUMBER3
PLUS+
NUMBER42
STAR*
IDENTIFIERy

空格和换行通常在这一步被丢弃(除非语言语法需要它们)。遇到无法识别的字符时,词法分析器应该报错并指出位置。

Python手写词法分析器:基于正则表达式的Tokenizer实现
图1:词法分析器将源代码字符串逐位置匹配,拆分为带类型的Token序列

二、用命名分组构建主正则

手写 Tokenizer 最直接的办法是:为每种 Token 写一条正则,然后用 |(或运算)把它们合并成一条大正则。Python 的命名分组 (?P<NAME>...) 能让我们在匹配成功后直接知道命中了哪种 Token。

需要注意的是,re 模块要求同一正则里命名分组不能重名。在合并时,每种 Token 类型的名字就是分组名,不会冲突。

定义 Token 规则

用一个列表存储每种 Token 的名称和对应正则。顺序很重要:正则引擎从左到右尝试匹配,把更长、更具体的模式放前面。比如 >= 必须排在 > 前面,关键字 if 要在普通标识符之前用 \b 限定边界。

import re
from typing import NamedTuple

# Token 数据结构
class Token(NamedTuple):
    type: str    # Token 类型
    value: str   # 原始文本
    line: int    # 行号
    column: int  # 列号

# Token 规则列表:(类型名, 正则表达式)
TOKEN_SPEC = [
    ('NUMBER',    r'\d+(\.\d*)?'),   # 整数或小数
    ('ASSIGN',    r'='),              # 赋值
    ('PLUS',      r'\+'),             # 加号
    ('MINUS',     r'-'),              # 减号
    ('STAR',      r'\*'),             # 乘号
    ('SLASH',     r'/'),              # 除号
    ('LPAREN',    r'\('),             # 左括号
    ('RPAREN',    r'\)'),             # 右括号
    ('IDENT',     r'[A-Za-z_]\w*'),   # 标识符
    ('NEWLINE',   r'\n'),             # 换行
    ('SKIP',      r'[ \t]+'),         # 空格和制表符(跳过)
    ('MISMATCH',  r'.'),              # 无法识别的字符
]

几点说明:

  • NUMBER 放在 IDENT 前面,因为 \d+ 不会和标识符冲突,但顺序清晰更易维护。
  • SKIP 用来吃掉空白,匹配后不生成 Token。
  • MISMATCH 放在最后,任何前面都没匹配上的字符会落到这里,触发报错。

合并正则

把所有规则用 | 拼接成一条带命名分组的正则:

tok_regex = '|'.join(
    f'(?P<{name}>{pattern})' for name, pattern in TOKEN_SPEC
)
master_pat = re.compile(tok_regex)

合并后的正则大致形如:

(?P<NUMBER>\d+(\.\d*)?)|(?P<ASSIGN>=)|(?P<PLUS>\+)|...  

re.finditer 会从左到右依次尝试每个分支,返回第一个成功的匹配。只要规则顺序正确,就能保证最长匹配优先。

Python手写词法分析器:基于正则表达式的Tokenizer实现
图2:各Token规则按优先级排列,合并为一条主正则供finditer遍历

三、完整的 Tokenizer 实现

把上面的准备工作整合成一个 tokenize 函数。它接收源代码字符串,返回 Token 生成器:

import re
from typing import NamedTuple, Generator

class Token(NamedTuple):
    type: str
    value: str
    line: int
    column: int

TOKEN_SPEC = [
    ('NUMBER',    r'\d+(\.\d*)?'),
    ('ASSIGN',    r'='),
    ('PLUS',      r'\+'),
    ('MINUS',     r'-'),
    ('STAR',      r'\*'),
    ('SLASH',     r'/'),
    ('LPAREN',    r'\('),
    ('RPAREN',    r'\)'),
    ('IDENT',     r'[A-Za-z_]\w*'),
    ('NEWLINE',   r'\n'),
    ('SKIP',      r'[ \t]+'),
    ('MISMATCH',  r'.'),
]

def tokenize(code: str) -> Generator[Token, None, None]:
    tok_regex = '|'.join(
        f'(?P<{name}>{pattern})' for name, pattern in TOKEN_SPEC
    )
    master_pat = re.compile(tok_regex)

    line_num = 1
    line_start = 0

    for mo in master_pat.finditer(code):
        kind = mo.lastgroup        # 命中的分组名
        value = mo.group()         # 匹配到的文本
        column = mo.start() - line_start + 1

        if kind == 'NUMBER':
            # 如果包含小数点就转 float,否则 int
            # 这里只记录原始文本,类型转换留给后续阶段
            pass
        elif kind == 'NEWLINE':
            line_num += 1
            line_start = mo.end()
            continue
        elif kind == 'SKIP':
            continue
        elif kind == 'MISMATCH':
            raise RuntimeError(
                f'第 {line_num} 行第 {column} 列:'
                f'无法识别的字符 {value!r}'
            )

        yield Token(kind, value, line_num, column)

几个关键点:

  • mo.lastgroup 返回匹配成功的命名分组名称,直接就是 Token 类型。
  • 行号和列号通过记录每行起始位置来计算,不需要逐字符扫描。
  • NEWLINE 和 SKIP 匹配后执行 continue,不产出 Token。
  • 遇到 MISMATCH 抛异常,附带准确的位置信息。

四、测试运行

写一段测试代码验证 Tokenizer 的输出:

source = '''x = 3 + 42
result = x * (y - 1)
'''

print(f'{"类型":<12} {"值":<10} {"行":<4} {"列":<4}')
print('-' * 36)
for tok in tokenize(source):
    print(f'{tok.type:<12} {tok.value:<10} {tok.line:<4} {tok.column:<4}')

示例输出:

类型           值          行   列
------------------------------------
IDENT        x          1    1
ASSIGN       =          1    3
NUMBER       3          1    5
PLUS         +          1    7
NUMBER       42         1    9
IDENT        result     2    1
ASSIGN       =          2    8
IDENT        x          2    10
STAR         *          2    12
LPAREN       (          2    14
IDENT        y          2    15
MINUS        -          2    17
NUMBER       1          2    19
RPAREN       )          2    20

每个 Token 的类型、值、行号、列号都准确对应源码位置。如果源码中出现 # 或 @ 等未定义的字符,MISMATCH 规则会触发异常并报出具体位置。

五、如何扩展

上面的 Tokenizer 处理的是简单算术表达式。实际应用中,可以按需扩展规则。下面列出几个常见方向。

添加关键字识别

许多语言有保留关键字(如 if、while、return)。一种做法是先按 IDENT 匹配,再在生成 Token 时查表替换类型:

KEYWORDS = {'if', 'else', 'while', 'return', 'def'}

# 在 tokenize 函数中,匹配到 IDENT 后加入:
if kind == 'IDENT' and value in KEYWORDS:
    kind = value.upper()   # 'if' -> 'IF', 'while' -> 'WHILE'

这比为每个关键字写一条正则更简洁,也不容易因为顺序出错。

支持字符串字面量

在 TOKEN_SPEC 中添加字符串规则。双引号字符串的正则可以写成:

('STRING', r'"[^"\\]*(?:\\.[^"\\]*)*"'),

这条正则处理了转义字符的情况(如 \"),需要放在 MISMATCH 之前。

支持注释

行注释通常匹配到行末:

('COMMENT', r'#[^\n]*'),

把 COMMENT 和 SKIP 一样处理——匹配后跳过,不生成 Token。

Python手写词法分析器:基于正则表达式的Tokenizer实现
图3:在基础Tokenizer上可扩展关键字、字符串、注释等多种规则

六、常见问题与注意事项

规则顺序导致错误匹配

如果把 IDENT 放在 NUMBER 前面,像 3abc 这样的输入会先把 3 匹配为 IDENT 的一部分吗?不会——因为 [A-Za-z_]\w* 要求首字符是字母或下划线。但如果规则之间有真正的前缀冲突,顺序就至关重要。比如 == 和 =,如果 = 在前,== 永远不会被匹配到,因为引擎在第一个 = 处就成功返回了。

通用规则:更长的模式放前面,关键字用 \b 约束词边界。

性能考虑

re.finditer 在 CPython 中由 C 实现的正则引擎驱动,对于几千行的源码足够快。如果源码规模达到数万行,可以考虑:

  • 预编译正则(本文已经使用 re.compile)。
  • 使用 re.Scanner(非公开API,但功能类似,一些老项目在用)。
  • 对性能要求极高的场景,换用 C 扩展或 mmap 读取文件。

与 tokenize 标准库的区别

Python 自带 tokenize 模块,可以对 Python 源码做词法分析。它的规则完整覆盖了 Python 语法,包括缩进 Token(INDENT / DEDENT)、f-string 等。手写 Tokenizer 的意义在于:理解原理、为自定义语言或 DSL 构建解析器的第一步。

七、完整代码汇总

把上面所有部分合并成一个可以直接运行的脚本:

import re
from typing import NamedTuple, Generator

class Token(NamedTuple):
    type: str
    value: str
    line: int
    column: int

TOKEN_SPEC = [
    ('NUMBER',    r'\d+(\.\d*)?'),
    ('ASSIGN',    r'='),
    ('PLUS',      r'\+'),
    ('MINUS',     r'-'),
    ('STAR',      r'\*'),
    ('SLASH',     r'/'),
    ('LPAREN',    r'\('),
    ('RPAREN',    r'\)'),
    ('IDENT',     r'[A-Za-z_]\w*'),
    ('NEWLINE',   r'\n'),
    ('SKIP',      r'[ \t]+'),
    ('MISMATCH',  r'.'),
]

KEYWORDS = {'if', 'else', 'while', 'return', 'def'}

def tokenize(code: str) -> Generator[Token, None, None]:
    tok_regex = '|'.join(
        f'(?P<{name}>{pattern})' for name, pattern in TOKEN_SPEC
    )
    master_pat = re.compile(tok_regex)
    line_num = 1
    line_start = 0

    for mo in master_pat.finditer(code):
        kind = mo.lastgroup
        value = mo.group()
        column = mo.start() - line_start + 1

        if kind == 'NUMBER':
            pass
        elif kind == 'IDENT' and value in KEYWORDS:
            kind = value.upper()
        elif kind == 'NEWLINE':
            line_num += 1
            line_start = mo.end()
            continue
        elif kind == 'SKIP':
            continue
        elif kind == 'MISMATCH':
            raise RuntimeError(
                f'第 {line_num} 行第 {column} 列:'
                f'无法识别的字符 {value!r}'
            )

        yield Token(kind, value, line_num, column)

# ---------- 测试 ----------
if __name__ == '__main__':
    source = '''x = 3 + 42
result = x * (y - 1)
if x return y
'''
    print(f'{"类型":<12} {"值":<10} {"行":<4} {"列":<4}')
    print('-' * 36)
    for tok in tokenize(source):
        print(f'{tok.type:<12} {tok.value:<10} {tok.line:<4} {tok.column:<4}')

这段代码可以保存为 tokenizer.py,直接用 python tokenizer.py 运行,输出全部 Token 及其位置。

Python手写词法分析器:基于正则表达式的Tokenizer实现
图4:手写词法分析器的五个实现步骤,从数据结构到扩展验证

整个 Tokenizer 的核心不到 50 行。理解了这个模式之后,无论是为模板引擎写解析器、为配置文件定义格式,还是为自制编程语言搭建前端,都可以从这里开始,逐步添加规则、扩展功能。正则负责字符层面的匹配,命名分组负责分类,finditer 负责推进位置——三者配合,就是一个完整的词法分析器。

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。