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 Django批量导入数据
Mar 25 Python
Python的几个高级语法概念浅析(lambda表达式闭包装饰器)
May 28 Python
Django 登陆验证码和中间件的实现
Aug 17 Python
Python 创建新文件时避免覆盖已有的同名文件的解决方法
Nov 16 Python
用python一行代码得到数组中某个元素的个数方法
Jan 28 Python
超简单的Python HTTP服务
Jul 22 Python
Django实现web端tailf日志文件功能及实例详解
Jul 28 Python
Python三元运算与lambda表达式实例解析
Nov 30 Python
使用Keras实现Tensor的相乘和相加代码
Jun 18 Python
Python3 使用pip安装git并获取Yahoo金融数据的操作
Apr 08 Python
聊聊pytorch测试的时候为何要加上model.eval()
May 23 Python
Python图片验证码降噪和8邻域降噪
Aug 30 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 array_intersect()函数使用代码
2009/01/14 PHP
PHP采集腾讯微博的实现代码
2012/01/19 PHP
php cURL和Rolling cURL并发方式比较
2013/10/30 PHP
php常用字符串比较函数实例汇总
2014/11/24 PHP
PHP使用header()输出图片缓存实例
2014/12/09 PHP
用JavaScript调用WebService的示例
2008/04/07 Javascript
jquery实现图片左右间隔滚动特效(可自动播放)
2013/05/08 Javascript
JavaScript中的数组遍历forEach()与map()方法以及兼容写法介绍
2016/05/19 Javascript
学习使用bootstrap的modal和carousel
2016/12/09 Javascript
Vue.js2.0中的变化小结
2017/10/24 Javascript
node.js支持多用户web终端实现及安全方案
2017/11/29 Javascript
JavaScript实现创建自定义对象的常用方式总结
2018/07/09 Javascript
vue watch普通监听和深度监听实例详解(数组和对象)
2018/08/16 Javascript
vue同步父子组件和异步父子组件的生命周期顺序问题
2018/10/07 Javascript
JS将时间秒转换成天小时分钟秒的字符串
2019/07/10 Javascript
vue中如何实现后台管理系统的权限控制的方法步骤
2019/09/05 Javascript
用python + openpyxl处理excel2007文档思路以及心得
2014/07/14 Python
对Python 网络设备巡检脚本的实例讲解
2018/04/22 Python
python实现列表中由数值查到索引的方法
2018/06/27 Python
TensorFlow实现Logistic回归
2018/09/07 Python
Django框架创建mysql连接与使用示例
2019/07/29 Python
python多线程与多进程及其区别详解
2019/08/08 Python
python3.6.8 + pycharm + PyQt5 环境搭建的图文教程
2020/06/11 Python
python读取excel数据绘制简单曲线图的完整步骤记录
2020/10/30 Python
python SOCKET编程基础入门
2021/02/27 Python
KEETSA环保床垫:更好的睡眠,更好的生活!
2016/11/24 全球购物
Sneaker Studio捷克:购买运动鞋
2018/07/08 全球购物
New Balance比利时官方网站:购买鞋子和服装
2021/01/15 全球购物
体育课课后反思
2014/04/24 职场文书
新闻发布会活动策划方案
2014/09/15 职场文书
考研经验交流会策划书
2015/11/02 职场文书
导游词创作书写原则以及开场白技巧怎么学?
2019/09/25 职场文书
nginx搭建图片服务器的过程详解(root和alias的区别)
2021/03/31 Servers
Windows下redis下载、redis安装及使用教程
2021/06/02 Redis
ajax请求前端跨域问题原因及解决方案
2021/10/16 Javascript
python自动获取微信公众号最新文章的实现代码
2022/07/15 Python