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版
Dec 07 Python
Python类方法__init__和__del__构造、析构过程分析
Mar 06 Python
Python实现视频下载功能
Mar 14 Python
Python实现连接postgresql数据库的方法分析
Dec 27 Python
Python实现简易版的Web服务器(推荐)
Jan 29 Python
利用python实现在微信群刷屏的方法
Feb 21 Python
Pytorch之contiguous的用法
Dec 31 Python
pyinstaller还原python代码过程图解
Jan 08 Python
使用Python爬取弹出窗口信息的实例
Mar 14 Python
Pytorch环境搭建与基本语法
Jun 03 Python
Pycharm安装第三方库失败解决方案
Nov 17 Python
Python装饰器的练习题
Nov 23 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错误WARNING: SESSION_START() [FUNCTION.SESSION-START]解决方法
2014/05/04 PHP
浅析php适配器模式(Adapter)
2014/11/25 PHP
PHP 使用memcached简单示例分享
2015/03/05 PHP
sina的lightbox效果。
2007/01/09 Javascript
js 覆盖和重载 函数
2009/09/25 Javascript
Javascript Function对象扩展之延时执行函数
2010/07/06 Javascript
使用jQuery异步加载 JavaScript脚本解决方案
2014/04/20 Javascript
node.js中的fs.symlinkSync方法使用说明
2014/12/15 Javascript
Nodejs中读取中文文件编码问题、发送邮件和定时任务实例
2015/01/01 NodeJs
JS实现仿QQ效果的三级竖向菜单
2015/09/25 Javascript
简单谈谈JavaScript的同步与异步
2015/12/31 Javascript
EasyUI中在表单提交之前进行验证
2016/07/19 Javascript
关于javascript原型的修改与重写(覆盖)差别详解
2016/08/31 Javascript
javascript实现将数字转成千分位的方法小结【5种方式】
2016/12/11 Javascript
easyui 中的datagrid跨页勾选问题的实现方法
2017/01/18 Javascript
react-native之ART绘图方法详解
2017/08/08 Javascript
安装vue-cli报错 -4058 的解决方法
2017/10/19 Javascript
Jquery 动态添加元素并添加点击事件实现过程解析
2019/10/12 jQuery
[01:51]2014DOTA2国际邀请赛 这个赛场没有失败者VGTi5再见
2014/07/23 DOTA
Python交换变量
2008/09/06 Python
仅利用30行Python代码来展示X算法
2015/04/01 Python
Python中用Spark模块的使用教程
2015/04/13 Python
Python模块WSGI使用详解
2018/02/02 Python
Flask实现跨域请求的处理方法
2018/09/27 Python
一文带你了解Python中的字符串是什么
2018/11/20 Python
浅谈PySpark SQL 相关知识介绍
2019/06/14 Python
PyQt5图形界面播放音乐的实例
2019/06/17 Python
python实现文件的分割与合并
2019/08/29 Python
使用 pytorch 创建神经网络拟合sin函数的实现
2020/02/24 Python
matplotlib 对坐标的控制,加图例注释的操作
2020/04/17 Python
基于Python和C++实现删除链表的节点
2020/07/06 Python
python根据用户需求输入想爬取的内容及页数爬取图片方法详解
2020/08/03 Python
详解python百行有效代码实现汉诺塔小游戏(简约版)
2020/10/30 Python
学习心理学心得体会
2016/01/22 职场文书
python中print格式化输出的问题
2021/04/16 Python
Hive HQL支持2种查询语句风格
2022/06/25 数据库