摘要 :Python 有个第三方正则表达式模块 regex, 它兼容官方内置的 re 模块,在满足大部分常规使用的同时,又新增了一系列有趣和有用的高级特性。

一、什么是正则表达式

正则表达式(RE,Regular Expression),又称规则表达式,在代码中常简写为regexregexpre。 它是计算机科学的一个概念,通常被用来检索、替换那些符合某个模式(规则)的文本。

与之类似还有用于文件查找的通配符(Wildcard),比如*, ?, {foo,bar}。它只能进行简单的匹配,而正则表达式可以进行复杂的匹配。

python-regex.png

二、正则基本语法

我们通过一张图表来对正则表达式的基本进行一个回顾:

Single Char(单字) Quantifiers(数量) Position(位置)
\d数字(即[0-9] ?0 个或一个,最多一个 ^一行的开头
\D非数字(即[^0-9] *0 个或更多 ^一行的开头
\wWord(即[0-9a-zA-Z] +1 个或更多,至少 1 个 $一行的结尾
\W非 Word(即[^0-9a-zA-Z] X{min,max}X 出现次数在范围内 \b单词边界(Bounds)
\sWhite 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)匹配foobar

详细教程可以参考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 支持

  1. 支持最新的 Unicode 标准,这一点经常比 Python 本身还及时。

  2. 支持 Unicode 代码属性,包括 scripts 和 blocks。

    如:\p {Cyrillic} 表示西里尔字符(scripts),\p {InCyrillic} 表示西里尔区块(blocks)。

  3. 支持完整的 Unicode 字符大小写匹配,详见 Casemap Charprop FAQ

    如:ss 可匹配 ßcliff(这里的 是一个字符)可匹配 CLIFFFF 是两个字符)。

    不需要的话可以关闭此特性。不支持 Unicode 组合字符与单一字符的大小写匹配,所以感觉这个特性不太实用。

  4. regex.WORD 标志开启后:

    作用 1:\b\B 采用 Unicode 的分界规则,详见 此文

    如:开启后 \b.+?\b 可搜索到 3.4;关闭后小数点 . 成为分界符,于是只能搜到 ['3', '.', '4']

    作用 2:采用 Unicode 的换行符。除了传统的 \r\nUnicode 还有一些换行符,开启后作用于 .MULTILINE.DOTALL 模式。

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

  1. i 模糊插入
  2. d 模糊删除
  3. s 模糊替换 以及 e,包括以上三种模糊。

举个例子:

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

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

于是可以成功匹配 hallo。

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

两种工作模式

regexVersion 0Version 1两个工作模式,其中的Version 0基本兼容 Python 内置的re模块,以下是具体区别:

Version 0 Version 1
启用方法 设置.VERSION0.V0标志,或者在表达式里写上(?V0) 设置.VERSION1.V1标志,或者在表达式里写上(?V1)
零宽匹配 re模块那样处理:
.split不能在零宽匹配处切割字符串。
.sub在匹配零宽之后向前传动一个位置。
PerlPCRE 那样处理:
.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+ 环境下,reregex模块的行为同时做了改变,据称是采用了 “正确” 的方式处理零宽匹配:总是返回第一个匹配(字符串或零宽), 但是如果是零宽并且之前匹配的还是零宽则忽略。这和 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 ~ 0x0D0x1C ~ 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) 记录分离符

除此之外,可能还有未知的不兼容之处。

七、扩展资料