一个简单的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. 序列化接口的id有什么用?

    反序列化Java对象时必须提供该对象的class文件,现在的问题是随着项目的升级,系统的class文件也会升级,Java如何保证两个class文件的兼容性? Java序列化机制允许为序列化类提供一个p ...

  2. 面试问题之C++语言:C++中指针和引用的区别

    转载于:https://blog.csdn.net/gcc2018/article/details/82285940 1.指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元:而引用 ...

  3. 可以直接调用 Thread 类的 run ()方法么?

    当然可以.但是如果我们调用了 Thread 的 run()方法,它的行为就会和普通的方 法一样,会在当前线程中执行.为了在新的线程中执行我们的代码,必须使用 Thread.start()方法.

  4. 并发包中automic类的原理

    提到同步,我们一般首先想到的是lock,synchronized,但java中有一套更加轻量级的同步方式即atomic类.java的并发原子包里面提供了很多可以进行原子操作的类,比如: AtomicI ...

  5. 机器学习 machine learn

    机器学习 机器学习 概述 什么是机器学习 机器学习是一门能够让编程计算机从数据中学习的计算机科学.一个计算机程序在完成任务T之后,获得经验E,其表现效果为P,如果任务T的性能表现,也就是用以衡量的P, ...

  6. 9_根轨迹_Part3_分离点/汇合点和根的性质

  7. MySQL索引机制(详细+原理+解析)

    MySQL索引机制 永远年轻,永远热泪盈眶 一.索引的类型与常见的操作 前缀索引 MySQL 前缀索引能有效减小索引文件的大小,提高索引的速度.但是前缀索引也有它的坏处:MySQL 不能在 ORDER ...

  8. CSS 常用的定位和布局方法汇总(已添加源码地址)

    CSS-Layout 旨在打造详尽的前端布局代码学习库(自从用了框架开发,CSS生疏了不少,所以开这个库练练手)SF不能正确解析含有中文的网址,所以某些预览链接无法跳转,请访问我的博客阅读此文 常见定 ...

  9. Django实现统一包装接口返回值数据格式

    前言 最近实在太忙了,开始了一个新的项目,为了快速形成产品,我选择了Django来实现后端,然后又拿起了之前我封装了项目脚手架「DjangoStarter」. 由于前段时间我写了不少.NetCore的 ...

  10. 如何在 Java 中实现无向环和有向环的检测

    无向环 一个含有环的无向图如下所示,其中有两个环,分别是 0-2-1-0 和 2-3-4-2: 要检测无向图中的环,可以使用深度优先搜索.假设从顶点 0 出发,再走到相邻的顶点 2,接着走到顶点 2 ...