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 09 Python
详解python的几种标准输出重定向方式
Aug 15 Python
Python检测网络延迟的代码
May 15 Python
Python查找第n个子串的技巧分享
Jun 27 Python
对pandas中iloc,loc取数据差别及按条件取值的方法详解
Nov 06 Python
Python3中列表list合并的四种方法
Apr 19 Python
Python Request爬取seo.chinaz.com百度权重网站的查询结果过程解析
Aug 13 Python
深入浅析Python 中的sklearn模型选择
Oct 12 Python
Python爬虫工具requests-html使用解析
Apr 29 Python
Django正则URL匹配实现流程解析
Nov 13 Python
Python借助with语句实现代码段只执行有限次
Mar 23 Python
Python识别花卉种类鉴定网络热门植物并自动整理分类
Apr 08 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
《魔兽争霸3:重制版》更新 多项视觉效果调整
2020/05/04 魔兽争霸
改进的IP计数器
2006/10/09 PHP
采集邮箱的php代码(抓取网页中的邮箱地址)
2012/07/17 PHP
Symfony2开发之控制器用法实例分析
2016/02/05 PHP
新浪的图片新闻效果
2007/01/13 Javascript
FF IE兼容性的修改小结
2009/09/02 Javascript
JS 对象介绍
2010/01/20 Javascript
关于javascript中this关键字(翻译+自我理解)
2010/10/20 Javascript
jWiard 基于JQuery的强大的向导控件介绍
2011/10/28 Javascript
js下拉框二级关联菜单效果代码具体实现
2013/08/03 Javascript
Javscript调用iframe框架页面中函数的方法
2014/11/01 Javascript
JavaScript模拟push
2016/03/06 Javascript
用AngularJS的指令实现tabs切换效果
2016/08/31 Javascript
webpack+vue.js实现组件化详解
2016/10/12 Javascript
JS+HTML5 FileReader实现文件上传前本地预览功能
2020/03/27 Javascript
JS实现下拉菜单列表与登录注册弹窗效果
2017/08/10 Javascript
echarts学习笔记之图表自适应问题详解
2017/11/22 Javascript
Bootstrap popover 实现鼠标移入移除显示隐藏功能方法
2018/01/24 Javascript
JavaScript类型相关的常用操作总结
2019/02/14 Javascript
JavaScript封闭函数及常用内置对象示例
2019/05/13 Javascript
微信小程序实现电子签名并导出图片
2020/05/27 Javascript
Python中字符编码简介、方法及使用建议
2015/01/08 Python
python模拟表单提交登录图书馆
2018/04/27 Python
详解Django解决ajax跨域访问问题
2018/08/24 Python
Python常用爬虫代码总结方便查询
2019/02/25 Python
python中必要的名词解释
2019/11/20 Python
css3中flex布局宽度不生效的解决
2020/12/09 HTML / CSS
使用HTML5里的classList操作CSS类
2016/06/28 HTML / CSS
雅诗兰黛(Estee Lauder)英国官方网站:世界顶级化妆品牌
2016/12/29 全球购物
Ever New美国:澳大利亚领先的女装时尚品牌
2019/11/28 全球购物
2014年乡镇纪委工作总结
2014/12/19 职场文书
个人创业事迹材料
2014/12/30 职场文书
西游降魔篇观后感
2015/06/15 职场文书
《蚂蚁和蝈蝈》教学反思
2016/02/22 职场文书
关于公司年会的开幕词
2016/03/04 职场文书
XX部保密工作制度范本
2019/08/27 职场文书