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常用的日期时间处理方法示例
Feb 08 Python
使用Python写个小监控
Jan 27 Python
django1.8使用表单上传文件的实现方法
Nov 04 Python
详解Python最长公共子串和最长公共子序列的实现
Jul 07 Python
flask-socketio实现WebSocket的方法
Jul 31 Python
python3通过selenium爬虫获取到dj商品的实例代码
Apr 25 Python
Python安装与基本数据类型教程详解
May 29 Python
Python适配器模式代码实现解析
Aug 02 Python
python rsa实现数据加密和解密、签名加密和验签功能
Sep 18 Python
Pytorch 实现focal_loss 多类别和二分类示例
Jan 14 Python
解决matplotlib.pyplot在Jupyter notebook中不显示图像问题
Apr 22 Python
python导入库的具体方法
Jun 18 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伪造referer突破网盘禁止外连的代码
2008/06/15 PHP
apache+codeigniter 通过.htcaccess做动态二级域名解析
2012/07/01 PHP
PHP通过内置函数memory_get_usage()获取内存使用情况
2014/11/20 PHP
php实现计算百度地图坐标之间距离的方法
2016/05/05 PHP
jQuery 瀑布流 浮动布局(一)(延迟AJAX加载图片)
2012/05/23 Javascript
自己使用jquery写的一个无缝滚动的插件
2014/04/30 Javascript
Javascript基础教程之比较操作符
2015/01/18 Javascript
倾力总结40条常见的移动端Web页面问题解决方案
2016/05/24 Javascript
jQuery实现底部浮动窗口效果
2016/09/07 Javascript
浅谈JavaScript作用域和闭包
2017/09/18 Javascript
vue实现商城上货组件简易版
2017/11/27 Javascript
vue中使用cropperjs的方法
2018/03/01 Javascript
JavaScript HTML DOM元素 节点操作汇总
2019/07/29 Javascript
将RGB值转换为灰度值的简单算法
2019/10/09 Javascript
vue+elementui 对话框取消 表单验证重置示例
2019/10/29 Javascript
原生js+css调节音量滑块
2020/01/15 Javascript
深入webpack打包原理及loader和plugin的实现
2020/05/06 Javascript
JS端基于download.js实现图片、视频时直接下载而不是打开预览
2020/05/09 Javascript
vue scroll滚动判断的实现(是否滚动到底部、滚动方向、滚动节流、获取滚动区域dom元素)
2020/06/11 Javascript
[01:00:17]DOTA2-DPC中国联赛 正赛 SAG vs Dynasty BO3 第二场 1月25日
2021/03/11 DOTA
[01:48:04]DOTA2-DPC中国联赛 正赛 PSG.LGD vs Elephant BO3 第一场 2月7日
2021/03/11 DOTA
pytyon 带有重复的全排列
2013/08/13 Python
从零学python系列之数据处理编程实例(一)
2014/05/22 Python
简单介绍Python的Django框架的dj-scaffold项目
2015/05/30 Python
python爬虫正则表达式之处理换行符
2018/06/08 Python
Python获取时间戳代码实例
2019/09/24 Python
python3 通过 pybind11 使用Eigen加速代码的步骤详解
2020/12/07 Python
Python排序函数的使用方法详解
2020/12/11 Python
python使用matplotlib的savefig保存时图片保存不完整的问题
2021/01/08 Python
英国票务网站:Ticketmaster英国
2018/08/27 全球购物
如何判断一段程序是由C 编译程序还是由C++编译程序编译的
2013/08/04 面试题
大型活动组织方案
2014/05/10 职场文书
纪念九一八事变83周年国旗下讲话稿
2014/09/15 职场文书
三方股份合作协议书
2014/10/13 职场文书
刑事和解协议书范本
2014/11/19 职场文书
2019年七夕情人节浪漫祝福语大全!
2019/08/08 职场文书