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 相关文章推荐
Eclipse中Python开发环境搭建简单教程
Mar 23 Python
Python实现信用卡系统(支持购物、转账、存取钱)
Jun 24 Python
深入理解python中的select模块
Apr 23 Python
一文总结学习Python的14张思维导图
Oct 17 Python
利用python实现简单的邮件发送客户端示例
Dec 23 Python
如何优雅地改进Django中的模板碎片缓存详解
Jul 04 Python
一行代码让 Python 的运行速度提高100倍
Oct 08 Python
python opencv判断图像是否为空的实例
Jan 26 Python
python调用c++传递数组的实例
Feb 13 Python
详解Python爬取并下载《电影天堂》3千多部电影
Apr 26 Python
Python利用sqlacodegen自动生成ORM实体类示例
Jun 04 Python
django restframework serializer 增加自定义字段操作
Jul 15 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
盘点被央视点名过的日本动画电影 一部比一部强
2020/03/08 日漫
JpGraph php柱状图使用介绍
2011/08/23 PHP
js的参数有长度限制吗?发现不能超过2083个字符
2014/04/20 Javascript
JavaScript常用脚本汇总(一)
2015/03/04 Javascript
jquery实现鼠标滑过小图查看大图的方法
2015/07/20 Javascript
浅谈javascript函数式编程
2015/09/06 Javascript
ajax在兼容模式下失效的快速解决方法
2016/03/22 Javascript
防止Node.js中错误导致进程阻塞的办法
2016/08/11 Javascript
让DIV的滚动条自动滚动到最底部的3种方法(推荐)
2016/09/24 Javascript
基于jquery实现弹幕效果
2016/09/29 Javascript
vue.js学习笔记之绑定style样式和class列表
2016/10/31 Javascript
原生js实现节日时间倒计时功能
2017/01/18 Javascript
javascript事件的传播基础实例讲解(35)
2017/02/14 Javascript
Node.JS更改Windows注册表Regedit的方法小结
2017/08/18 Javascript
JavaScript变量声明var,let.const及区别浅析
2018/04/23 Javascript
vue根据值给予不同class的实例
2018/09/29 Javascript
在Python中使用poplib模块收取邮件的教程
2015/04/29 Python
Python3访问并下载网页内容的方法
2015/07/28 Python
Python3安装Scrapy的方法步骤
2017/11/23 Python
Random 在 Python 中的使用方法
2018/08/09 Python
python利用re,bs4,requests模块获取股票数据
2019/07/29 Python
如何使用selenium和requests组合实现登录页面
2020/02/03 Python
Tensorflow 1.0之后模型文件、权重数值的读取方式
2020/02/12 Python
python使用隐式循环快速求和的实现示例
2020/09/11 Python
python 5个实用的技巧
2020/09/27 Python
Django基于Models定制Admin后台实现过程解析
2020/11/11 Python
Python3+SQLAlchemy+Sqlite3实现ORM教程
2021/02/16 Python
详解HTML5如何使用可选样式表为网站或应用添加黑暗模式
2020/04/07 HTML / CSS
英国独特的时尚和生活方式品牌:JOY
2018/03/17 全球购物
中间件分为哪几类
2016/09/18 面试题
私营公司诉讼代理委托书范本
2014/09/13 职场文书
大学校园招聘会感想
2015/08/10 职场文书
分析Python感知线程状态的解决方案之Event与信号量
2021/06/16 Python
详解Spring Security中的HttpBasic登录验证模式
2022/03/17 Java/Android
vue @click.native 绑定原生点击事件
2022/04/22 Vue.js
python index() 与 rindex() 方法的使用示例详解
2022/12/24 Python