Python自带了正则表达式引擎(内置的re模块),但是不支持一些高级特性,比如下面这几个:
幸好,在2009年,Matthew Barnett写了一个更强大正则表达式引擎——regex模块,这是一个Python的第三方模块。
除了上面那几个杠杠的高级特性,还有很多有趣、有用的东西。本文大致介绍一下,很多内容取自regex的文档。
无论是编程还是文本处理,regex模块都是一件利器。
用一个指标可以大致了解它的复杂性,re模块有4270行C语言代码,而regex模块有24513行C语言代码。
这个模块以后可能被收进Python标准库。目前(2015年)它还在不断发展,经常发布bug修正版,不过感觉用在一般生产环境应该没什么问题。
目录
一、安装regex
二、一些有趣的特性
三、模糊匹配
四、两种工作模式
五、Version 0模式和re模块不兼容之处
六、比较re、V0、V1的传动方式
regex支持Python 2.5+和Python 3.1+,用pip命令安装:
pip install regex
regex基本兼容re模块,现有的程序可以很容易切换到regex模块:
import regex as re
完整的Unicode支持
1,支持最新的Unicode标准,这一点经常比Python本身还及时。
2,支持Unicode代码属性,包括scripts和blocks。
如:\p{Cyrillic}表示西里尔字符,\p{InCyrillic}表示西里尔区块。
3,支持完整的Unicode字符大小写匹配,详见此文。
如:ss可匹配ß;cliff(这里的ff是一个字符)可匹配CLIFF(FF是两个字符)。
不需要的话可以关闭此特性。目前尚不支持Unicode组合字符与单一字符的大小写匹配。
4,regex.WORD标志开启后:
作用1:\b、\B采用Unicode的分界规则,详见此文。
如:开启后\b.+?\b可搜索到3.4;关闭后小数点.成为分界符,于是只能搜到['3', '.', '4']。
作用2:采用Unicode的换行符。除了传统的\r、\n,Unicode还有一些换行符,开启后作用于.MULTILINE和.DOTALL模式。
5,\X匹配Unicode的单个字符。
Unicode有时用多个字符组合在一起作为一个字符,详见此文。
\X可以匹配一个组合字符,也可以匹配一个普通字符。
如:^\X$可以匹配'\u0041\u0308'。
单词起始位置、单词结束位置
\b是单词分界位置,但不能区分是起始还是结束位置。
regex用\m表示单词起始位置,用\M表示单词结束位置。
(?|...|...)
重置分支匹配中的捕获组编号。
>>> regex.match(r"(?|(first)|(second))", "first").groups() ('first',) >>> regex.match(r"(?|(first)|(second))", "second").groups() ('second',)
两次匹配都是把捕获到的内容放到编号为1捕获组中,在某些情况很方便。
(?flags-flags:...)
局部范围的flag控制。在re模块,flag只能作用于整个表达式,现在可以作用于局部范围了:
>>> regex.search(r"<B>(?i:good)</B>", "<B>GOOD</B>") <regex.Match object; span=(0, 11), match='<B>GOOD</B>'>
在这个例子里,忽略大小写模式只作用于标签之间的单词。
(?i:)是打开忽略大小写,(?-i:)则是关闭忽略大小写。
如果有多个flag挨着写既可,如(?is-f:),减号左边的是打开,减号右边的是关闭。
除了局部范围的flag,还有全局范围的flag控制,如 (?si)<B>good</B>
re模块也支持这个,可以参见Python文档。
把flags写进表达式、而不是以函数参数的方式声明,方便直观且不易出错。
(?(DEFINE)...)
定义可重复使用的子句
>>> regex.search(r'(?(DEFINE)(?P<quant>\d+)(?P<item>\w+))(?&quant) (?&item)', '5 elephants') <regex.Match object; span=(0, 11), match='5 elephants'>
此例中,定义之后,(?&quant)表示\d+,(?&item)表示\w+。如果子句很复杂,能省不少事。
partial matches
部分匹配。可用于验证用户输入,当输入不合法字符时,立刻给出提示。
除了以上这些,还有很多新特性(匹配控制、便利方法等等),这里就不介绍了,请自行查阅文档。
regex有模糊匹配(fuzzy matching)功能,提供了3种模糊匹配:
以及e,包括以上三种模糊
举个例子:
>>> regex.findall('(?:hello){s<=2}', 'hallo') ['hallo']
(?:hello){s<=2}的意思是:匹配hello,其中最多容许有两个字符的错误。
于是可以成功匹配hallo。
这里只简单介绍一下模糊匹配,详情还是参见文档吧。
regex有Version 0和Version 1两个工作模式,其中的Version 0基本兼容现有的re模块,以下是区别:
Version 0 (基本兼容re模块) | Version 1 | |
启用方法 | 设置.VERSION0或.V0标志,或者在表达式里写上(?V0)。 |
设置.VERSION1或.V1标志,或者在表达式里写上(?V1)。 |
零宽匹配 | 像re模块那样处理: .split 不能在零宽匹配处切割字符串。 .sub 在匹配零宽之后向前传动一个位置。 |
像Perl和PCRE那样处理: .split 可以在零宽匹配处切割字符串。 .sub 采用正确的行为。 |
内联flag | 内联flag只能作用于整个表达式,不可关闭。 | 内联flag可以作用于局部表达式,可以关闭。 |
字符组 | 只支持简单的字符组。 | 字符组里可以有嵌套的集合,也可以做集合运算(并集、交集、差集、对称差集)。 |
大小写匹配 | 默认支持普通的Unicode字符大小写,如Й可匹配й。 这与Python3里re模块的默认行为相同。 |
默认支持完整的Unicode字符大小写,如ss可匹配ß。 如果不需要这个可以在表达式里写上(?-f)关闭此特性。 |
如果什么设置都不做,会采用regex.DEFAULT_VERSION指定的模式。在目前,regex.DEFAULT_VERSION的默认值是regex.V0。
如果想默认使用V1模式,这样就可以了:
import regex regex.DEFAULT_VERSION = regex.V1
其中零宽匹配的替换操作差异比如明显。大多数正则引擎采用的是Perl流派的作法,于是Version 1也改过去了。
>>> # Version 0 behaviour (like re) >>> regex.sub('(?V0).*', 'x', 'test') 'x' >>> regex.sub('(?V0).*?', '|', 'test') '|t|e|s|t|' >>> # Version 1 behaviour (like Perl) >>> regex.sub('(?V1).*', 'x', 'test') 'xx' >>> regex.sub('(?V1).*?', '|', 'test') '|||||||||'
re模块对零宽匹配的实现可能是有误的(见issue1647489);
而V0零宽匹配的搜索和替换会出现不一致的行为(搜索采用V1的方式,替换采用re模块的方式);
所以如果用正则表达式做特别严肃的事,个人建议用Version 1模式,并注意其间的差异。
详见本文第六部分。
说着挺吓人的,在实际使用中re、V0、V1之间极少出现不兼容的现象。
上面说了“Version 0基本兼容re模块”,说说不兼容的地方:
1、对零宽匹配的处理。
regex修复了re模块的搜索bug(见issue1647489),但是也带来了不兼容的问题。
在re中,用".*?"搜索"test"返回:['', '', '', '', ''],也就是:最前、字母之间的3个位置、最后,总共5个位置。
在regex中,则返回:['', 't', '', 'e', '', 's', '', 't', '']
在实际使用中,这个问题几乎不会造成不兼容的情况,所以基本可以忽略此差异。
2、\s的范围。
在re中,\s在这一带的范围是0x09 ~ 0x0D,0x1C ~ 0x1E。
在regex中,\s采用的是Unicode 6.3+标准的\p{Whitespace},在这一带的范围有所缩小,只有:0x09 ~ 0x0D。
十六进制 | 十进制 | 英文说明 | 中文说明 |
0x09 | 9 | HT (horizontal tab) | 水平制表符 |
0x0A | 10 | LF (NL line feed, new line) | 换行键 |
0x0B | 11 | VT (vertical tab) | 垂直制表符 |
0x0C | 12 | FF (NP form feed, new page) | 换页键 |
0x0D | 13 | CR (carriage return) | 回车键 |
... | ... | ... | ... |
0x1C | 28 | FS (file separator) | 文件分割符 |
0x1D | 29 | GS (group separator) | 分组符 |
0x1E | 30 | RS (record separator) | 记录分离符 |
除此之外,可能还有未知的不兼容之处。
这部分一般人不必看,因为用不到,但还是写下来吧。
上面提到re有bug(见issue1647489),V1是最合宜的。这是由于它们采用了不同的传动方式,这里具体说说。
用以下代码分别测试三种模式:
# 分别测试re、V0、V1 import regex as re re.DEFAULT_VERSION = re.V1 # 测试 .*? 的搜索和替换 search = re.findall(r'.*?', 'test') # 搜索 print(search) replace = re.sub(r'.*?', 'x', 'test') # 替换 print(replace) # 测试 .* 的搜索和替换 search = re.findall(r'.*', 'test') # 搜索 print(search) replace = re.sub(r'.*', 'x', 'test') # 替换 print(replace)
观察输出,存在两个问题:
1、在.*?匹配一个空位置后,是否需要进一步匹配一个字符?
2、在.*匹配所有字符后,是否需要进一步匹配一个空位置?
以下是根据输出整理的3张表格:
re模块:
.*? | .* | |
搜索 | 否 | 是 |
替换 | 否 | 否 |
V0模式:
.*? | .* | |
搜索 | 是 | 是 |
替换 | 否 | 否 |
V1模式:
.*? | .* | |
搜索 | 是 | 是 |
替换 | 是 | 是 |
无论如何,在同一张表格中,相同颜色(纵列)应该是一致的。这表示搜索和替换采用了一致的行为。
可以看到:
re的行为混乱。表格左上的那个否造成了搜索bug(issue1647489)。
V0虽然没有那个搜索bug(issue1647489),但搜索和替换采用了不一致的行为,替换操作依然存在那个bug。
V1的行为一致,并且搜索和替换都没有bug(issue1647489)。