Python 正则表达式引擎 Regex vs RE
/ / 阅读数:5127摘要 :Python 有个第三方正则表达式模块 regex, 它兼容官方内置的 re 模块,在满足大部分常规使用的同时,又新增了一系列有趣和有用的高级特性。
一、什么是正则表达式
正则表达式(RE,Regular Expression),又称规则表达式,在代码中常简写为regex
、regexp
或re
。
它是计算机科学的一个概念,通常被用来检索、替换那些符合某个模式(规则)的文本。
与之类似还有用于文件查找的通配符(Wildcard),比如*
, ?
, {foo,bar}
。它只能进行简单的匹配,而正则表达式可以进行复杂的匹配。
二、正则基本语法
我们通过一张图表来对正则表达式的基本进行一个回顾:
Single Char(单字) | Quantifiers(数量) | Position(位置) |
---|---|---|
\d 数字(即[0-9] ) |
? 0 个或一个,最多一个 |
^ 一行的开头 |
\D 非数字(即[^0-9] ) |
* 0 个或更多 |
^ 一行的开头 |
\w Word(即[0-9a-zA-Z] ) |
+ 1 个或更多,至少 1 个 |
$ 一行的结尾 |
\W 非 Word(即[^0-9a-zA-Z] ) |
X{min,max} X 出现次数在范围内 |
\b 单词边界(Bounds) |
\s White Space(空格、Tab 等) |
X{min,} X 出现次数大于等于 min |
|
\S 非 White Space(空格、Tab 等) |
X{,max} X 出现次数小于等于 max |
|
[0-9ABC] 数字或 A、B、C |
X{n} X 出现 n 次 |
|
. 任何字符 |
(foo|bar) 匹配foo 或bar |
详细教程可以参考https://www.w3cschool.cn/zhengzebiaodashi/regexp-tutorial.html 。
三、常用正则表达式
- 两位以上汉字:
^[\u4e00-\u9fa5]{2,}$
- 字母开头的 32 数字、小写字母、下划线组合:
^[a-z][a-z0-9_]{,31}$
- 邮箱地址:
^.+@.+\..+$
- 链接地址:
^(https?:)?//.+\..+$
- IPv4 地址:
^(?:(?:25 [0-5]|2 [0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25 [0-5]|2 [0-4][0-9]|[01]?[0-9][0-9]?)$
- 手机号码:
^1\d {10}$
- 身份证号(15 位或 18 位):
^\d {15}|\d {18}|\d {17}[xX]$
- 出生日期:
^\d {4}-\d {1,2}-\d {1,2}$
推荐一个在线验证正则表达式的网站:http://www.pyregex.com 。
使用 Python 编写正则匹配时有个小技巧,先对正则表达式compile
,然后再使用:
import re age_re = re.compile('\d+') for i in range(100): # 每次都要先 compile 再 search re.search('\d+', 'i am 18 years old').group() # 避免多次 compile,效率更高 age_re.search('i am 18 years old').group() |
四、Python 正则比较 Regex vs RE
具体对比可以参考这篇文章:https://www.cnblogs.com/animalize/p/4949219.html,以下对原文进行了适当排版和更新:
Python 自带了正则表达式引擎(内置的re
模块),它能满足大部分常规使用,但是不支持一些高级特性,比如下面这几个:
- 固化分组 Atomic grouping
- 占有优先量词 Possessive quantifiers
- 可变长度的逆序环视 Variable-length lookbehind
- 递归匹配 Recursive patterns
- (起始 / 继续)位置锚
\G
Search anchor
幸好,在 2009 年,Matthew Barnett 写了一个更强大第三方正则表达式引擎 —— regex 模块 。
除了上面这几个高级特性,还有很多有趣、有用的东西,本文大致介绍一下,很多内容取自regex
的文档。
五、Regex 注意内存 "泄露" 问题
目前(2019 年)它还在不断更新和发展,我们已经在线上生产环境使用了几年时间。
不过这期间也遇到一个 其内部缓存 导致内存 "泄露" 的问题:
问题原因:regex 正则库在执行 compile 函数时,会将编译后的正则式缓存到本地的_cache 中(最多缓存 500 条)。 所以在经常变化正则表达式且正则比较大的情况下(比如经常更新敏感词库),会有正则表达式对象没被释放, 导致内存 "泄漏",持续升高问题。
解决办法: 在每次重载关键词库后,调用 regex 的 purge 函数清空掉缓存。
# regex/regex_2/regex.py def _compile(pattern, flags=0, kwargs={}): "Compiles a regular expression to a PatternObject." ... if not debugging: if (info.flags & LOCALE) == 0: pattern_locale = None args_needed = frozenset(args_needed) # Store this regular expression and named list. pattern_key = (pattern, type(pattern), flags, args_needed, DEFAULT_VERSION, pattern_locale) _cache[pattern_key] = compiled_pattern # <---- CACHE HERE ---- # Store what keyword arguments are needed. _named_args[args_key] = args_needed return compiled_pattern |
六、Regex 介绍
安装 regex
regex
支持 Python 2.7 + 和 Python 3.5+,可以用pip
命令安装:
pip install regex |
regex
基本兼容re
模块,现有的程序可以很容易切换到regex
模块:
import regex as re |
下面介绍它的一些有趣的特性。
完整的 Unicode 支持
支持最新的 Unicode 标准,这一点经常比 Python 本身还及时。
支持 Unicode 代码属性,包括 scripts 和 blocks。
如:
\p {Cyrillic}
表示西里尔字符(scripts),\p {InCyrillic}
表示西里尔区块(blocks)。支持完整的 Unicode 字符大小写匹配,详见 Casemap Charprop FAQ 。
如:
ss
可匹配ß
;cliff
(这里的ff
是一个字符)可匹配CLIFF
(FF
是两个字符)。不需要的话可以关闭此特性。不支持 Unicode 组合字符与单一字符的大小写匹配,所以感觉这个特性不太实用。
regex.WORD
标志开启后:作用 1:
\b
、\B
采用 Unicode 的分界规则,详见 此文 。如:开启后
\b.+?\b
可搜索到3.4
;关闭后小数点.
成为分界符,于是只能搜到['3', '.', '4']
。作用 2:采用 Unicode 的换行符。除了传统的
\r
、\n
,Unicode
还有一些换行符,开启后作用于.MULTILINE
和.DOTALL
模式。\X
匹配 Unicode 的单个字形(Grapheme)。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 捕获组中,在某些情况很方便。
局部范围的 flag 控制
语法:(?flags-flags:...)
在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-f)<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)
可用于验证用户输入,当输入不合法字符时,立刻给出提示。
可以 pickle 编译后的正则表达式对象
如果正则表达式很复杂或数量很多,编译需要较长时间。
这时可以把编译好的正则式用 pickle 存储到文件里,下次使用直接pickle.load()
就不必再次编译了。
除了以上这些,还有很多新特性(匹配控制、便利方法等等),这里就不介绍了,请自行查阅文档。
Python 3 支持 Timeout
regex
中的匹配方法支持超时设置,超时(单位为秒)应用于整个操作:
>>> from time import sleep >>> >>> def fast_replace(m): ... return 'X' ... >>> def slow_replace(m): ... sleep(0.5) ... return 'X' ... >>> regex.sub(r'[a-z]', fast_replace, 'abcde', timeout=2) 'XXXXX' >>> regex.sub(r'[a-z]', slow_replace, 'abcde', timeout=2) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "C:\Python37\lib\site-packages\regex\regex.py", line 276, in sub endpos, concurrent, timeout) TimeoutError: regex timed out |
模糊匹配
regex
有模糊匹配(Fuzzy Matching)功能,能针对字符进行模糊匹配,提供了 3 种模糊匹配:
i
模糊插入d
模糊删除s
模糊替换 以及e
,包括以上三种模糊。
举个例子:
>>> regex.findall('(?:hello){s<=2}', 'hallo') ['hallo'] |
(?:hello){s<=2}
的意思是:匹配hello
,其中最多容许有两个字符的错误。
于是可以成功匹配 hallo。
这里只简单介绍一下模糊匹配,详情还是参见文档吧。
两种工作模式
regex
有Version 0
和Version 1
两个工作模式,其中的Version 0
基本兼容 Python 内置的re
模块,以下是具体区别:
Version 0 | Version 1 | |
---|---|---|
启用方法 | 设置.VERSION0 或.V0 标志,或者在表达式里写上(?V0) 。 |
设置.VERSION1 或.V1 标志,或者在表达式里写上(?V1) 。 |
零宽匹配 | 像re 模块那样处理:.split 不能在零宽匹配处切割字符串。.sub 在匹配零宽之后向前传动一个位置。 |
像 Perl 和 PCRE 那样处理:.split 可以在零宽匹配处切割字符串。.sub 采用正确的行为。 |
内联 flag | 内联 flag 只能作用于整个表达式,不可关闭。 | 内联 flag 可以作用于局部表达式,可以关闭。 |
字符组 | 只支持简单的字符组。 | 字符组里可以有嵌套的集合,也可以做集合运算(并集、交集、差集、对称差集)。 |
大小写匹配 | 默认支持普通的 Unicode 字符大小写,如Й 可匹配й 。这与 Python3 里 re 模块的默认行为相同。 |
默认支持完整的 Unicode 字符大小写,如ss 可匹配ß 。可惜不支持 Unicode 组合字符与单一字符的大小写匹配,所以感觉这个特性不太实用。可以在表达式里写上 (?-f) 关闭此特性。 |
如果什么设置都不做,会采用regex.DEFAULT_VERSION
指定的模式。在目前,regex.DEFAULT_VERSION
的默认值是regex.V0
。
如果想默认使用 V1 模式,这样就可以了:
import regex regex.DEFAULT_VERSION = regex.V1 |
V1 模式默认开启.FULLCASE
(完整的忽略大小写匹配)。
通常用不上这个,所以在忽略大小写匹配时用(?-f)
关闭.FULLCASE
即可,这样速度更快一点,例如:(?i-f)tag
其中零宽匹配的替换操作差异比如明显。绝大多数正则引擎采用的是 Perl 流派的作法,于是Version 1
也朝着 Perl 的方向改过去了。
>>> # 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
模块的方式);
在 Python 3.7+ 环境下,re
和regex
模块的行为同时做了改变,据称是采用了 “正确” 的方式处理零宽匹配:总是返回第一个匹配(字符串或零宽),
但是如果是零宽并且之前匹配的还是零宽则忽略。这和 3.6- 的re
模块、regex
的 V0 模式、V1 模式都略有不同。
说着挺吓人的,在实际使用中 3.6-re
、3.7+ re
、V0、V1 之间极少出现不兼容的现象。
Version 0
模式和re
模块不兼容之处
上面说了 “Version 0
基本兼容re
模块”,说说不兼容的地方:
对零宽匹配的处理
regex
修复了re
模块的搜索 bug(见 issue1647489 ),但是也带来了不兼容的问题。
在re
中,用".*?"
搜索"test"
返回:['', '', '', '', '']
,也就是:最前、字母之间的 3 个位置、最后,总共 5 个位置。
在regex
中,则返回:['', 't', '', 'e', '', 's', '', 't', '']
在实际使用中,这个问题几乎不会造成不兼容的情况,所以基本可以忽略此差异。
空白字符\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) | 记录分离符 |
除此之外,可能还有未知的不兼容之处。