Python使用re模块实现Tokenizer(表达式分词器)

Miles
Miles
发布时间:2026-10-16 | 4人浏览过
原创

本文介绍如何使用Python的re模块构建一个简单的表达式分词器(Tokenizer)。通过定义命名捕获组的正则表达式模式,利用scanner和match方法将字符串解析为标记流。文章还讲解了如何过滤空格标记,并重点分析了正则表达式匹配顺序对子串匹配的影响,提供了正确的模式排列建议。

一个简单的tokenizer

分词(tokenization)是Python字符串处理中极为常见的任务。本文将讲解如何利用正则表达式构建一个简单的表达式分词器(tokenizer),该工具能够将表达式字符串从左至右解析为标记(tokens)序列。

假设给定如下表达式字符串:

text = 'foo = 12 + 5 * 6'

我们的目标是将其转换为以下以序列对形式呈现的分词结果:

tokens = [('NAME', 'foo'), ('EQ', '='), ('NUM', '12'), ('PLUS', '+'),\
    ('NUM', '5'), ('TIMES', '*'), ('NUM', '6')]

要实现此类分词操作,首先需定义所有可能的标记模式(即pattern,用于描述或匹配特定句法规则的字符串,此处使用正则表达式作为模式)。需注意,模式中必须包含空格(whitespace),否则当字符串中出现模式中未定义的字符时,扫描过程将提前终止。此外,为了赋予标记如NAME、EQ等名称,我们采用正则表达式中的命名捕获组来实现。

import re
NAME = r'(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)' 
# 这里?P<NAME>表示模式名称,()表示一个正则表达式捕获组,合在一起即一个命名捕获组
EQ = r'(?P<EQ>=)'
NUM = r'(?P<NUM>\d+)' #\d表示匹配数字,+表示任意数量
PLUS = r'(?P<PLUS>\+)' #需要用\转义
TIMES = r'(?P<TIMES>\*)' #需要用\转义
WS = r'(?P<WS>\s+)' #\s表示匹配空格, +表示任意数量
master_pat = re.compile("|".join([NAME, EQ, NUM, PLUS, TIMES, WS]))  # | 用于选择多个模式,表示"或"

接下来,利用模式对象中的scanner()方法完成分词操作,该方法用于创建一个扫描对象:

scanner = master_pat.scanner(text)

随后,可以使用match()方法获取单次匹配结果,每次匹配一个模式:

scanner = master_pat.scanner(text)
m = scanner.match() 
print(m.lastgroup, m.group()) # NAME foo
m = scanner.match()
print(m.lastgroup, m.group()) # WS

虽然逐次调用可行,但略显繁琐。我们可以利用迭代器进行批量调用,并将单次迭代结果存储为具名元组形式。

Token = namedtuple('Token', ['type', 'value'])
def generate_tokens(pat, text):
    scanner = pat.scanner(text)
    for m in iter(scanner.match, None):
        #scanner.match做为迭代器每次调用的方法,
        #None为哨兵的默认值,表示迭代到None停止
        yield Token(m.lastgroup, m.group())
for tok in generate_tokens(master_pat, "foo = 42"):
    print(tok)

最终,表达式串"foo = 12 + 5 * 6"的tokens流显示如下:

Token(type='NAME', value='foo')
Token(type='WS', value=' ')
Token(type='EQ', value='=')
Token(type='WS', value=' ')
Token(type='NUM', value='12')
Token(type='WS', value=' ')
Token(type='PLUS', value='+')
Token(type='WS', value=' ')
Token(type='NUM', value='5')
Token(type='WS', value=' ')
Token(type='TIMES', value='*')
Token(type='WS', value=' ')
Token(type='NUM', value='6')

过滤tokens流

接下来,我们希望过滤掉空格标记,使用生成器表达式即可实现:

tokens = (tok for tok in generate_tokens(master_pat, "foo = 12 + 5 * 6")
          if tok.type != 'WS')
for tok in tokens:
    print(tok)

可以看到,空格已成功被过滤:

Token(type='NAME', value='foo')
Token(type='EQ', value='=')
Token(type='NUM', value='12')
Token(type='PLUS', value='+')
Token(type='NUM', value='5')
Token(type='TIMES', value='*')
Token(type='NUM', value='6')

注意子串匹配陷阱

tokens在正则表达式(即"|".join([NAME, EQ, NUM, PLUS, TIMES, WS]))中的顺序至关重要。因为在匹配过程中,re模块会按照指定的顺序对模式进行匹配。因此,如果某个模式恰好是另一个较长模式的子串,必须确保较长模式排在前面以优先匹配。以下分别展示了正确的和错误的匹配方法:

LT = r'(?P<LT><)'
LE = r'(?P<LE><=)'
EQ = r'(?P<EQ>>=)'
master_pat = re.compile("|".join([LE, LT, EQ]))  # 正确的顺序
master_pat = re.compile("|".join([LT, LE, EQ]))  # 错误的顺序

第二种顺序的错误之处在于,这样会将'<='文本匹配为LT('<')紧跟着EQ('='),而没有正确匹配为单独的LE(<=)。

对于“有可能”形成子串的模式也要格外小心,例如下面这种情况:

PRINT = r'(?P<PRINT>print)'
NAME = r'(?P<NAME>[a-zA-Z_][a-zA-Z_0-9]*)'
master_pat = re.compile("|".join([PRINT, NAME]))  # 正确的顺序
for tok in generate_tokens(master_pat, "printer"):
    print(tok)

可以看到,print实际上成为了另一个模式的子串,导致另一个模式的匹配出现问题:

# Token(type='PRINT', value='print')
# Token(type='NAME', value='er')

对于更高级的语法分词,建议采用如PyParsing或PLY等包。特别地,对于英文自然语言文章的分词,通常集成在各类NLP包中(一般分为按空格拆分、处理前后缀、去掉停用词三个步骤)。对于中文自然语言处理分词,也有丰富的工具(例如jieba分词工具包)。

引用

[1] Martelli A, Ravenscroft A, Ascher D. Python cookbook[M]. " O'Reilly Media, Inc.", 2015. 数学是符号的艺术,音乐是上界的语言。

到此这篇关于Python使用re模块实现Tokenizer的文章就介绍到这了,更多相关Python Tokenizer内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表