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中的__SLOTS__属性使用示例
Feb 18 Python
Python合并多个装饰器小技巧
Apr 28 Python
Python中判断输入是否为数字的实现代码
May 26 Python
替换python字典中的key值方法
Jul 06 Python
对python产生随机的二维数组实例详解
Dec 13 Python
利用Python正则表达式过滤敏感词的方法
Jan 21 Python
Python自动化之数据驱动让你的脚本简洁10倍【推荐】
Jun 04 Python
Python使用import导入本地脚本及导入模块的技巧总结
Aug 07 Python
Pytorch: 自定义网络层实例
Jan 07 Python
Python sys模块常用方法解析
Feb 20 Python
python安装和pycharm环境搭建设置方法
May 27 Python
使用pandas实现筛选出指定列值所对应的行
Dec 13 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无限分类的类
2007/01/02 PHP
Php Image Resize图片大小调整的函数代码
2011/01/17 PHP
php实现俄罗斯乘法实例
2015/03/07 PHP
基于Jquery的跨域传输数据(JSONP)
2011/03/10 Javascript
在Html中使用Requirejs进行模块化开发实例详解
2016/04/15 Javascript
jQuery表单验证插件解析(推荐)
2016/07/21 Javascript
AngularJS基础 ng-selected 指令简单示例
2016/08/03 Javascript
JavaScript定时器setTimeout()和setInterval()详解
2017/08/18 Javascript
weex里Vuex state使用storage持久化详解
2017/09/09 Javascript
Vue 中批量下载文件并打包的示例代码
2017/11/20 Javascript
angular 实时监听input框value值的变化触发函数方法
2018/08/31 Javascript
JS重学系列之聊聊new操作符
2019/03/04 Javascript
使用 node.js 模仿 Apache 小部分功能
2019/07/07 Javascript
JS如何实现手机端输入验证码效果
2020/05/13 Javascript
Openlayers学习之地图比例尺控件
2020/09/28 Javascript
[06:04]DOTA2国际邀请赛纪录片:Just For LGD
2013/08/11 DOTA
Python的Bottle框架中实现最基本的get和post的方法的教程
2015/04/30 Python
Python for Informatics 第11章之正则表达式(二)
2016/04/21 Python
Django Admin 实现外键过滤的方法
2017/09/29 Python
Python 最大概率法进行汉语切分的方法
2018/12/14 Python
python 计算数据偏差和峰度的方法
2019/06/29 Python
详解torch.Tensor的4种乘法
2020/09/03 Python
python super()函数的基本使用
2020/09/10 Python
纯CSS3打造动感漂亮时尚的扇形菜单
2014/03/18 HTML / CSS
英国假发网站:Hothair
2018/02/23 全球购物
Abbacino官网:包、钱包和女士配饰
2019/04/15 全球购物
匈牙利超级网上商店和优惠:Alza.hu
2019/12/17 全球购物
得到Class的三个过程是什么
2012/08/10 面试题
统计每一学生的平均成绩
2014/06/06 面试题
会计学生自我鉴定
2014/02/06 职场文书
汽车质检员岗位职责
2015/04/08 职场文书
培训班通知
2015/04/25 职场文书
起诉书格式范文
2015/05/20 职场文书
校园开放日新闻稿
2015/07/17 职场文书
村主任当选感言
2015/08/01 职场文书
党员电教片《信仰》心得体会
2016/01/15 职场文书