Python如何用re模块实现简易tokenizer


Posted in Python onMay 02, 2022

一个简单的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的文章就介绍到这了!


Tags in this post...

Python 相关文章推荐
Python实现二维有序数组查找的方法
Apr 27 Python
Python编程实现的图片识别功能示例
Aug 03 Python
Python3解决棋盘覆盖问题的方法示例
Dec 07 Python
Django中间件基础用法详解
Jul 18 Python
利用python计算windows全盘文件md5值的脚本
Jul 27 Python
Pytorch 定义MyDatasets实现多通道分别输入不同数据方式
Jan 15 Python
基于matplotlib xticks用法详解
Apr 16 Python
详解Pandas 处理缺失值指令大全
Jul 30 Python
Python代码注释规范代码实例解析
Aug 14 Python
python 窃取摄像头照片的实现示例
Jan 08 Python
分析Python list操作为什么会错误
Nov 17 Python
利用Python脚本写端口扫描器socket,python-nmap
Jul 23 Python
Python实现简单得递归下降Parser
使用Python开发贪吃蛇游戏 SnakeGame
Apr 30 #Python
使用Python开发冰球小游戏
详解Python中的for循环
Python采集壁纸并实现炫轮播
Apr 30 #Python
Python循环之while无限迭代
如何Python使用re模块实现okenizer
Apr 30 #Python
You might like
PHP fastcgi模式上传大文件(大约有300多K)报错
2014/09/28 PHP
PHP数组内存利用率低和弱类型详细解读
2017/08/10 PHP
php获取ajax的headers方法与内容实例
2017/12/27 PHP
PHP设计模式(三)建造者模式Builder实例详解【创建型】
2020/05/02 PHP
原生js实现改变随意改变div属性style的名称和值的结果
2013/09/26 Javascript
javascript实现根据身份证号读取相关信息
2014/12/17 Javascript
AngularJS ui-router (嵌套路由)实例
2017/03/10 Javascript
使用命令行工具npm新创建一个vue项目的方法
2017/12/27 Javascript
antd组件Upload实现自己上传的实现示例
2018/12/18 Javascript
详解webpack编译速度提升之DllPlugin
2019/02/05 Javascript
[28:48]《真视界》- 2017年国际邀请赛
2017/09/27 DOTA
[01:28]一分钟告诉你DOTA2 TI9不朽宝藏Ⅱ中有什么!
2019/07/09 DOTA
[54:18]DOTA2-DPC中国联赛 正赛 PSG.LGD vs LBZS BO3 第一场 1月22日
2021/03/11 DOTA
Python实现测试磁盘性能的方法
2015/03/12 Python
Python判断Abundant Number的方法
2015/06/15 Python
用python制作游戏外挂
2018/01/04 Python
python3实现公众号每日定时发送日报和图片
2018/02/24 Python
python reverse反转部分数组的实例
2018/12/13 Python
python tkinter canvas 显示图片的示例
2019/06/13 Python
这可能是最好玩的python GUI入门实例(推荐)
2019/07/19 Python
如何利用python给图片添加半透明水印
2019/09/06 Python
Python 获取项目根路径的代码
2019/09/27 Python
Python使用QQ邮箱发送邮件报错smtplib.SMTPAuthenticationError
2019/12/20 Python
python+adb命令实现自动刷视频脚本案例
2020/04/23 Python
keras实现调用自己训练的模型,并去掉全连接层
2020/06/09 Python
python利用appium实现手机APP自动化的示例
2021/01/26 Python
荣耀俄罗斯官网:HONOR俄罗斯
2020/10/31 全球购物
Oracle里面常用的数据字典有哪些
2014/02/14 面试题
电子商务专业毕业生工作推荐信
2013/11/17 职场文书
女儿十岁生日答谢词
2014/01/27 职场文书
班主任对学生的评语
2014/04/26 职场文书
法学自荐信
2014/06/20 职场文书
2014年校长工作总结
2014/12/11 职场文书
中小学生安全教育观后感
2015/06/17 职场文书
Go语言基础切片的创建及初始化示例详解
2021/11/17 Golang
Python实现GIF动图以及视频卡通化详解
2021/12/06 Python