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 相关文章推荐
asyncio 的 coroutine对象 与 Future对象使用指南
Sep 11 Python
Python语言描述连续子数组的最大和
Jan 04 Python
修复CentOS7升级Python到3.6版本后yum不能正确使用的解决方法
Jan 26 Python
解决pycharm每次新建项目都要重新安装一些第三方库的问题
Jan 17 Python
python画图--输出指定像素点的颜色值方法
Jul 03 Python
python配置文件写入过程详解
Oct 19 Python
pytorch使用 to 进行类型转换方式
Jan 08 Python
PyCharm汉化安装及永久激活详细教程(靠谱)
Jan 16 Python
完美解决keras保存好的model不能成功加载问题
Jun 11 Python
Tensorflow使用Anaconda、pycharm安装记录
Jul 29 Python
Python unittest装饰器实现原理及代码
Sep 08 Python
Python常用断言函数实例汇总
Nov 30 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
使用php4加速网络传输
2006/10/09 PHP
PHP 将图片按创建时间进行分类存储的实现代码
2010/01/05 PHP
php htmlspecialchars加强版
2010/02/16 PHP
PHP设置一边执行一边输出结果的代码
2013/09/30 PHP
php将字符串转换成16进制的方法
2015/03/17 PHP
PHP递归遍历指定目录的文件并统计文件数量的方法
2015/03/24 PHP
在PHP中输出JS语句以及乱码问题的解决方案
2019/02/13 PHP
PHP+百度AI OCR文字识别实现了图片的文字识别功能
2019/05/08 PHP
JavaScript 内置对象属性及方法集合
2010/07/04 Javascript
jquery实现点击消失的代码
2014/03/03 Javascript
jquery让返回的内容显示在特定div里(代码少而精悍)
2014/06/23 Javascript
分享2个jQuery插件--jquery.fileupload与artdialog
2014/12/26 Javascript
原生javascript实现DIV拖拽并计算重复面积
2015/01/02 Javascript
纯JavaScript 实现flappy bird小游戏实例代码
2016/09/27 Javascript
AngularJS定时器的使用与移除操作方法【interval与timeout】
2016/12/14 Javascript
JS实现仿UC浏览器前进后退效果的实例代码
2017/07/17 Javascript
解读ES6中class关键字
2017/11/20 Javascript
浅谈Vuejs中nextTick()异步更新队列源码解析
2017/12/31 Javascript
Vue中render函数的使用方法
2018/01/31 Javascript
React父子组件间的传值的方法
2018/11/13 Javascript
js中比较两个对象是否相同的方法示例
2019/09/02 Javascript
[01:02:30]Mineski vs Secret 2019国际邀请赛淘汰赛 败者组 BO3 第三场 8.22
2019/09/05 DOTA
Python中文竖排显示的方法
2015/07/28 Python
python编码最佳实践之总结
2016/02/14 Python
python 动态加载的实现方法
2017/12/22 Python
Django安装配置mysql的方法步骤
2018/10/15 Python
python找出完数的方法
2018/11/12 Python
python利用openpyxl拆分多个工作表的工作簿的方法
2019/09/27 Python
解决TensorFlow训练模型及保存数量限制的问题
2021/03/03 Python
澳大利亚运动鞋零售商:The Athlete’s Foot
2018/11/04 全球购物
Servlet的实例是在生命周期什么时候创建的?配置servlet最重要的是什么?
2012/05/30 面试题
人事档案接收函
2014/01/12 职场文书
村抢险救灾方案
2014/05/09 职场文书
习总书记三严三实学习心得体会
2014/10/13 职场文书
本溪关门山导游词
2015/02/09 职场文书
React列表栏及购物车组件使用详解
2021/06/28 Javascript