一个简单的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(<=)。

对英文文章进行分词可以按照括号拆分,然后处理前后缀和停用词,这一般被集成在了各类NLP工具中,此处不做展开。

我们对于“有可能”形成子串的模式也要小心,比如下面这样:

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的更多相关文章

  1. Python技法:实现简单的递归下降Parser

    1. 算术运算表达式求值 在上一篇博文<Python技法:用re模块实现简易tokenizer>中,我们介绍了用正则表达式来匹配对应的模式,以实现简单的分词器.然而,正则表达式不是万能的, ...

  2. Python(五)模块

    本章内容: 模块介绍 time & datetime random os sys json & picle hashlib XML requests ConfigParser logg ...

  3. [转载]Python中的sys模块

    #!/usr/bin/python # Filename: cat.py import sys def readfile(filename): '''Print a file to the stand ...

  4. Python安装包或模块的多种方式汇总

    windows下安装python第三方包.模块汇总如下(部分方式同样适用于其他平台): 1. windows下最常见的*.exe,*msi文件,直接运行安装即可: 2. 安装easy_install, ...

  5. Python 五个常用模块资料 os sys time re built-in

    1.os模块   os模块包装了不同操作系统的通用接口,使用户在不同操作系统下,可以使用相同的函数接口,返回相同结构的结果.   os.name:返回当前操作系统名称('posix', 'nt', ' ...

  6. Python中的random模块,来自于Capricorn的实验室

    Python中的random模块用于生成随机数.下面介绍一下random模块中最常用的几个函数. random.random random.random()用于生成一个0到1的随机符点数: 0 < ...

  7. python函数和常用模块(三),Day5

    递归 反射 os模块 sys模块 hashlib加密模块 正则表达式 反射 python中的反射功能是由以下四个内置函数提供:hasattr.getattr.setattr.delattr,改四个函数 ...

  8. Python基础之--常用模块

    Python 模块 为了实现对程序特定功能的调用和存储,人们将代码封装起来,可以供其他程序调用,可以称之为模块. 如:os 是系统相关的模块:file是文件操作相关的模块:sys是访问python解释 ...

  9. Python自动化之常用模块

    1 time和datetime模块 #_*_coding:utf-8_*_ __author__ = 'Alex Li' import time # print(time.clock()) #返回处理 ...

随机推荐

  1. Java 中用到的线程调度算法是什么?

    计算机通常只有一个 CPU,在任意时刻只能执行一条机器指令,每个线程只有获得 CPU 的使用权才能执行指令.所谓多线程的并发运行,其实是指从宏观上看,各个线 程轮流获得 CPU 的使用权,分别执行各自 ...

  2. 什么是feigin?它的优点是什么?

    1.feign采用的是基于接口的注解2.feign整合了ribbon,具有负载均衡的能力3.整合了Hystrix,具有熔断的能力使用:1.添加pom依赖.2.启动类添加@EnableFeignClie ...

  3. 什么是sql注入?如何有效防止sql注入?

    一.什么是sql注入 利用程序员的代码bug,将输入的参数绕过校验并在系统中当做代码运行,从而攻击系统. 二.如何避免sql注入 1.对sql语句进行预编译 PreparedStatement类可以对 ...

  4. Thymeleaf集成Shiro,shiro权限使用el表达式

    如果是Thymeleaf集成Shiro的话, 如果使用Shiro在页面上权限字符串需使用thymeleaf的表达式的话, 如果权限字符串在实例级别的话, 可以使用这种方式进行权限字符串的动态实例控制 ...

  5. Java_lambda表达式之"stream流学习,Map学习,collect学习,Conllectors工具类学习"

    Lambda表达式学习 对List<Integer> userIdList = UserList.stream().map(User::getUserId).collect(Collect ...

  6. javascript 理解和使用回调函数

    在javascript中,function是内置的类对象,也就是说它是一种类型的对象,可以和其他String.Array.Number.Objec类的对象一样用于内置对象的管理.因为function实 ...

  7. HTML5打造原生应用——Ionic框架简介与Ionic Hello World

    试了试用Ionic框架打造了两个应用,然后在Google Play上架了. 程序语言答人 教你设计物联网 更有意思的是这是在一周的业余时间内完成的三个应用中的两个,接着让我们看看这个框架如何实现高效地 ...

  8. CSRF浅析

    概念 CSRF,Cross Site Request Forgery,跨站请求伪造. 为什么跨站的请求需要伪造? 因为浏览器实现了同源策略,这里可以将站和源视为同一个概念. 同源策略 The same ...

  9. 关于 video 播放的新探索

    前端同学要使用 HTML5 播放器视频,必然会使用 video 标签,不过大多数同学只是使用了较简单的功能,其实它本身拥有不凡之力有待我们发现. 首先,我们先来看下 video 最基础的用法: 使用 ...

  10. javaweb图书管理系统之不同用户跳转不同页面

    关于分级自测题,我们知道该系统一共分为两个角色,一个是读者,一个是管理员,我们需要根据不同用户去到不同的页面,所以我们需要写一个登陆界面. 本文先写这个功能的实现,该功能主要在servlet里面实现. ...