Python的regex模块——更强大的正则表达式引擎

Python自带了正则表达式引擎(内置的re模块),但是不支持一些高级特性,比如下面这几个:

  • 固化分组    Atomic grouping
  • 占有优先量词    Possessive quantifiers
  • 可变长度的逆序环视    Variable-length lookbehind
  • 递归匹配    Recursive patterns
  • (起始/继续)位置锚\G    Search anchor

幸好,在2009年,Matthew Barnett写了一个更强大正则表达式引擎——regex模块这是一个Python的第三方模块。

除了上面那几个杠杠的高级特性,还有很多有趣、有用的东西。本文大致介绍一下,很多内容取自regex的文档。

 

无论是编程还是文本处理,regex模块都是一件利器。

用一个指标可以大致了解它的复杂性,re模块有4270行C语言代码,而regex模块有24513行C语言代码。

 

这个模块以后可能被收进Python标准库。目前(2015年)它还在不断发展,经常发布bug修正版,不过感觉用在一般生产环境应该没什么问题。

 

目录

一、安装regex

二、一些有趣的特性

三、模糊匹配

四、两种工作模式

五、Version 0模式和re模块不兼容之处

六、比较re、V0、V1的传动方式

 

一、安装regex

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种模糊匹配:

  • i,模糊插入
  • d,模糊删除
  • s,模糊替换

以及e,包括以上三种模糊

 

举个例子:

>>> regex.findall('(?:hello){s<=2}', 'hallo')
['hallo']

(?:hello){s<=2}的意思是:匹配hello,其中最多容许有两个字符的错误。

于是可以成功匹配hallo。

 

这里只简单介绍一下模糊匹配,详情还是参见文档吧。

 

四、两种工作模式

regex有Version 0Version 1两个工作模式,其中的Version 0基本兼容现有的re模块,以下是区别:

  Version 0  (基本兼容re模块) Version 1
启用方法

设置.VERSION0或.V0标志,或者在表达式里写上(?V0)。

设置.VERSION1或.V1标志,或者在表达式里写上(?V1)。

零宽匹配

像re模块那样处理:

.split 不能在零宽匹配处切割字符串。

.sub 在匹配零宽之后向前传动一个位置。

PerlPCRE那样处理

.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模块不兼容之处

上面说了“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、V0、V1的传动方式

这部分一般人不必看,因为用不到,但还是写下来吧。

上面提到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

你可能感兴趣的:(Python的regex模块——更强大的正则表达式引擎)