如何Python使用re模块实现okenizer


Posted in Python onApril 30, 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模块实现okenizer的文章就介绍到这了!


Tags in this post...

Python 相关文章推荐
python三元运算符实现方法
Dec 17 Python
python支持断点续传的多线程下载示例
Jan 16 Python
深入理解Python 代码优化详解
Oct 27 Python
python实现支持目录FTP上传下载文件的方法
Jun 03 Python
python3 与python2 异常处理的区别与联系
Jun 19 Python
一份python入门应该看的学习资料
Apr 11 Python
python之线程通过信号pyqtSignal刷新ui的方法
Jan 11 Python
Python深拷贝与浅拷贝用法实例分析
May 05 Python
Python英文文章词频统计(14份剑桥真题词频统计)
Oct 13 Python
Python通过文本和图片生成词云图
May 21 Python
python中scrapy处理项目数据的实例分析
Nov 22 Python
python 基于wx实现音乐播放
Nov 24 Python
如何使用python包中的sched事件调度器
Apr 30 #Python
详解OpenCV获取高动态范围(HDR)成像
详解OpenCV曝光融合
python使用shell脚本创建kafka连接器
Apr 29 #Python
python中pycryto实现数据加密
Apr 29 #Python
Python如何快速找到多个字典中的公共键(key)
Apr 29 #Python
Python如何让字典保持有序排列
Apr 29 #Python
You might like
学习php分页代码实例
2013/10/24 PHP
php文件后缀不强制为.php的实操方法
2019/09/18 PHP
laravel利用中间件防止未登录用户直接访问后台的方法
2019/09/30 PHP
JS面向对象基础讲解(工厂模式、构造函数模式、原型模式、混合模式、动态原型模式)
2014/08/16 Javascript
一款由jquery实现的整屏切换特效
2014/09/15 Javascript
Javascript基础教程之数据类型 (字符串 String)
2015/01/18 Javascript
javascript关于open.window子页面执行完成后刷新父页面的问题分析
2015/04/27 Javascript
JavaScript实现向右伸出的多级网页菜单效果
2015/08/25 Javascript
如何动态加载外部Javascript文件
2015/12/02 Javascript
JS使用post提交的两种方式
2015/12/03 Javascript
jQuery 3.0中存在问题及解决办法
2016/07/15 Javascript
JS判断键盘是否按的回车键并触发指定按钮点击操作的方法
2017/02/13 Javascript
多个上传文件用js验证文件的格式和大小的方法(推荐)
2017/03/09 Javascript
浅析Angular2子模块以及异步加载
2017/04/24 Javascript
web前端开发中常见的多列布局解决方案整理(一定要看)
2017/10/15 Javascript
详解如何在angular2中获取节点
2017/11/23 Javascript
vue webpack打包后图片路径错误的完美解决方法
2018/12/07 Javascript
mpvue小程序循环动画开启暂停的实现方法
2019/05/15 Javascript
微信小程序接入腾讯云验证码的方法步骤
2020/01/07 Javascript
浅谈vue生命周期共有几个阶段?分别是什么?
2020/08/07 Javascript
[05:20]2018DOTA2亚洲邀请赛主赛事第三日战况回顾 LGD率先挺进胜者组决赛
2018/04/06 DOTA
Python的Tornado框架实现异步非阻塞访问数据库的示例
2016/06/30 Python
python实现感知器
2017/12/19 Python
flask-socketio实现WebSocket的方法
2018/07/31 Python
python清除字符串前后空格函数的方法
2018/10/21 Python
python 实现GUI(图形用户界面)编程详解
2019/07/17 Python
美体小铺英国官网:The Body Shop英国
2017/01/24 全球购物
UNIONBAY官网:美国青少年服装品牌
2019/03/26 全球购物
化工专业推荐信范文
2013/11/28 职场文书
2014年元旦促销活动方案
2014/02/22 职场文书
安全月宣传标语
2014/10/07 职场文书
2014年高中教师工作总结
2014/12/19 职场文书
2015年幼儿园中班下学期工作总结
2015/05/22 职场文书
建党伟业观后感
2015/06/01 职场文书
导游词之介休绵山
2019/12/31 职场文书
nginx反向代理配置去除前缀案例教程
2021/07/26 Servers