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 18 Python
python+selenium实现自动抢票功能实例代码
Nov 23 Python
Python中如何导入类示例详解
Apr 17 Python
对python tkinter窗口弹出置顶的方法详解
Jun 14 Python
python实现动态创建类的方法分析
Jun 25 Python
python实现H2O中的随机森林算法介绍及其项目实战
Aug 29 Python
Python 3.8正式发布重要新功能一览
Oct 17 Python
浅谈Python类中的self到底是干啥的
Nov 11 Python
python3 tkinter实现添加图片和文本
Nov 26 Python
Python实现随机取一个矩阵数组的某几行
Nov 26 Python
python sitk.show()与imageJ结合使用常见的问题
Apr 20 Python
python中random.randint和random.randrange的区别详解
Sep 20 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漏洞小结
2012/02/05 PHP
Yii实现复选框批量操作实例代码
2017/03/15 PHP
拖动一个HTML元素
2006/12/22 Javascript
禁止F5等快捷键的JS代码
2007/03/06 Javascript
FormValid0.5版本发布,带ajax自定义验证例子
2007/08/17 Javascript
JavaScript下利用fso判断文件是否存在的代码
2010/12/11 Javascript
js函数模拟显示桌面.scf程序示例
2014/04/20 Javascript
jquery+css实现绚丽的横向二级下拉菜单-附源码下载
2015/08/23 Javascript
jQuery实现点击弹出背景变暗遮罩效果实例代码
2016/06/24 Javascript
AngularJS监听路由的变化示例代码
2016/09/23 Javascript
EasyUI框架 使用Ajax提交注册信息的实现代码
2017/09/27 Javascript
Vue项目pdf(base64)转图片遇到的问题及解决方法
2018/10/19 Javascript
jQuery实现ajax的嵌套请求案例分析
2019/02/16 jQuery
nodejs微信开发之授权登录+获取用户信息
2019/03/17 NodeJs
送你43道JS面试题(收藏)
2019/06/17 Javascript
详解微信小程序支付流程与梳理
2019/07/16 Javascript
使用typescript构建Vue应用的实现
2019/08/26 Javascript
VUE 实现动态给对象增加属性,并触发视图更新操作示例
2019/11/29 Javascript
javascript设计模式 ? 中介者模式原理与用法实例分析
2020/04/20 Javascript
从表单校验看JavaScript策略模式的使用详解
2020/10/17 Javascript
[39:02]DOTA2亚洲邀请赛 3.31 小组赛 B组 Mineski vs VGJ.T
2018/04/01 DOTA
理解Python中的With语句
2015/02/02 Python
Python性能优化技巧
2015/03/09 Python
Python读取指定日期邮件的实例
2019/02/01 Python
Python实现的删除重复文件或图片功能示例【去重】
2019/04/23 Python
Python 解决OPEN读文件报错 ,路径以及r的问题
2019/12/19 Python
python第三方库学习笔记
2020/02/07 Python
pycharm中使用request和Pytest进行接口测试的方法
2020/07/31 Python
Python selenium如何打包静态网页并下载
2020/08/12 Python
Python Merge函数原理及用法解析
2020/09/16 Python
python 删除系统中的文件(按时间,大小,扩展名)
2020/11/19 Python
波兰最大的儿童服装连锁店之一:5.10.15.
2018/02/11 全球购物
干部作风整顿自我剖析材料和整改措施
2014/09/18 职场文书
安全教育培训制度
2015/08/06 职场文书
初中生活随笔
2015/08/15 职场文书
Java图书管理系统,课程设计必用(源码+文档)
2021/06/30 Java/Android