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中的死锁、可重入锁和互斥锁
Nov 05 Python
Python中对元组和列表按条件进行排序的方法示例
Nov 10 Python
全面了解python字符串和字典
Jul 07 Python
修改默认的pip版本为对应python2.7的方法
Nov 06 Python
django用户登录验证的完整示例代码
Jul 21 Python
基于python进行抽样分布描述及实践详解
Sep 02 Python
给keras层命名,并提取中间层输出值,保存到文档的实例
May 23 Python
pytorch 限制GPU使用效率详解(计算效率)
Jun 27 Python
Flask中sqlalchemy模块的实例用法
Aug 02 Python
Python解析微信dat文件的方法
Nov 30 Python
pytho matplotlib工具栏源码探析一之禁用工具栏、默认工具栏和工具栏管理器三种模式的差异
Feb 25 Python
python自动化之如何利用allure生成测试报告
May 02 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程序?
2006/12/08 PHP
在JavaScript中调用php程序
2009/03/09 PHP
php发送post请求的三种方法
2014/02/11 PHP
PHP网页游戏学习之Xnova(ogame)源码解读(四)
2014/06/23 PHP
PHP导出带样式的Excel示例代码
2016/08/28 PHP
让Laravel API永远返回JSON格式响应的方法示例
2018/09/05 PHP
jquery获取tr中控件值并操作tr实现思路
2013/03/27 Javascript
JavaScript的21条基本知识点
2014/03/04 Javascript
JavaScript数据类型转换的注意事项
2016/07/31 Javascript
DOM事件探秘篇
2017/02/15 Javascript
Angular实现搜索框及价格上下限功能
2018/01/19 Javascript
vue解决使用webpack打包后keep-alive不生效的方法
2018/09/01 Javascript
详解使用element-ui table组件的筛选功能的一个小坑
2018/11/02 Javascript
Android 自定义view仿微信相机单击拍照长按录视频按钮
2019/07/19 Javascript
layui禁用侧边导航栏点击事件的解决方法
2019/09/25 Javascript
vue Treeselect 树形下拉框:获取选中节点的ids和lables操作
2020/08/15 Javascript
vue项目配置 webpack-obfuscator 进行代码加密混淆的实现
2021/02/26 Vue.js
[02:08]什么藏在DOTA2 TI9“小紫本”里?斧王历险记告诉你!
2019/05/17 DOTA
python使用pandas处理excel文件转为csv文件的方法示例
2019/07/18 Python
Python3将数据保存为txt文件的方法
2019/09/12 Python
Python读写文件模式和文件对象方法实例详解
2019/09/17 Python
python查看数据类型的方法
2019/10/12 Python
pygame实现烟雨蒙蒙下彩虹雨
2019/11/11 Python
解决tensorflow训练时内存持续增加并占满的问题
2020/01/19 Python
Django def clean()函数对表单中的数据进行验证操作
2020/07/09 Python
Python Process创建进程的2种方法详解
2021/01/25 Python
美体小铺英国官网:The Body Shop英国
2017/01/24 全球购物
校园餐饮创业计划书
2014/01/10 职场文书
挂职自我鉴定
2014/02/26 职场文书
安全生产网格化管理实施方案
2014/03/01 职场文书
《回乡偶书》教学反思
2014/04/12 职场文书
2014国庆节演讲稿:祖国在我心中(400字)
2014/09/25 职场文书
初中生散播谣言检讨书
2014/11/17 职场文书
接待员岗位职责范本
2015/04/15 职场文书
2015小学教育教学工作总结
2015/07/21 职场文书
Python如何用re模块实现简易tokenizer
2022/05/02 Python