Python标准库re:使用正则表达式

关于正则表达式的预备知识,可以参见:正则表达式简单入门

本文介绍正则表达式在Python中的应用,主要通过标准库 re 提供的函数实现。

使用 re 库的预备知识

字符转义问题

由于正则表达式使用反斜杠 \ 来转义字符,而 Python 也使用 \ 来转义字符。而在这种情况下,两者的转义可能发生冲突:

expr01 = '\bon'
expr02 = '\\bon'
target_string = 'turn on'

由于 Python 会先将正则表达式编译成一个字符串,再利用 re 库的正则表达式语法去匹配一个字符串如 target_string ,所以 Python 的转义级别要高于正则表达式的。因此在本例中:

  1. Python 会先将表达式 expr01 中的 "\b" 转义成一个退格符,再将 "退格符on" 作为一个正则表达式去匹配目标字符串,因此得到的结果是什么也匹配不到。
  2. Python 会先将表达式 expr02 中的 "\\" 转义成反斜杠 "\" ,再将 "\bon" 作为一个正则表达式,而正则表达式又将 "\b" 转义成单词的边界,即完整的表达式为 "单词的边界on" 去匹配目标字符串,因此匹配的结果是 "on"

在上例可以看出,Python 和正则表达式的转义混合会引起混乱。为了避免这种情况发生,强烈建议使用 Python 的原始字符串,它通过在字符串字面量加上 r 前缀来表明原始字符, 该前缀会忽略 Python 的转义,从而可以准确地表达一个正则表达式。因此,上例可以写成:

raw_expr = r'\bon'
target_string = 'turn on'

常用参数

要用上正则表达式,应该要提供一个正则表达式和待匹配的文本。re 库的函数通常都有以下三个常用的参数:pattern 、string 和 flag :

  • pattern :一个代表正则表达式的字符串,建议使用 r 不转义。
  • string :需要匹配的字符串,如果字符串过长(如 HTML 源码),一般使用变量指向该字符串。
  • flags :匹配模式。常见 flags 的取值及含义如下:
re.I
re.IGNORECAS
忽略大小写模式
re.M
re.MULTILINE
多行模式,使 ^ 和 $ 能够作用于每行的开始和结尾。该模式下便有了 ^ 和 \A 以及 $ 和 \Z 的区别。
re.S
re.DOTALL
单行模式,使 . 所匹配的任意字符包含换行符
re.X
re.VERBOSE
冗长模式,忽略空格,并允许用 # 号添加注释
re.A
re.ACSII
使用 ASCII 字符集中定义的 \w 、\W 、\b 、\B 、\s 、\S ,而不是默认的 Unicode 字符集。
re.L
re.LOCALE
本地化模式,使用当前本地化语言字符集中定义的 \w 、\W 、\b 、\B 、\s 、\S(用于多语言操作系统)
re.U
re.UNICODE
使用 Unicode 字符集中定义的 \w 、\W 、\b 、\B 、\s 、\S ,默认的情况

如果要同时设置多个匹配模式,使用按位或运算符 | 号将其隔开。例如 flags=re.M|re.X 。

基本用法:匹配和搜索

匹配

match(pattern, string, flags=0) 函数使用 pattern 传入的正则表达式按 flags 匹配模式,从字符串的起始位置逐一匹配字符串 string 。若匹配成功,返回一个 Match 对象;若匹配不成功,返回一个空值 None 。例如:

import re

target01 = '010-123456'
target02 = '010 123456'
regex01 = r'\d{3}-\d{3,6}'

result01 = re.match(pattern=regex01, string=target01)
result02 = re.match(pattern=regex01, string=target02)

print(result01)
print(result02)

结果为:

$ python -u regex.py <re.Match object; span=(0, 10), match='010-123456'> None

由于 Python 默认一个对象在条件判断时代表布尔值 True ,而空值代表布尔值 False ,为了防止匹配失败造成的影响,可以使用 if 语句来判断 match() 函数的匹配是否成功。

如果 match() 函数匹配成功,它将返回一个 Match 对象。对于这个 Match 对象,可以使用一些方法来获取正则表达式匹配的信息。

当匹配的表达式中有用到圆括号 ( ) 或 (?P<name>) 分组时,可以使用 .group() 方法用来获取各组匹配到的字符串。括号内传入的参数为代表组序号的整数,或代表组名称的字符串(只需要名称就够了,不需要别的修饰符)。如果不传入参数或传入 0 ,则代表获取整个匹配结果。如果传入的值没有对应的组,会发生 IndexError 错误。例如:

target03 = 'e is the base of the Natural Logarithms.'
regex02 = r'(.*) is the (?P<what>.*)\.$'
result03 = re.match(pattern=regex02,
                    string=target03)
print(result03.group())
print(result03.group(1))
print(result03.group('what'))

结果为:

$ python -u regex.py e is the base of the Natural Logarithms. e base of the Natural Logarithms

类似地,还可以使用 .groups() 方法得到分组匹配到的字符串元组,或者通过 .groupdict() 方法得到一个以自定义名称作为键、匹配结果作为值的字典。如果匹配的表达式中没有自定义名称的分组,后者返回一个空字典。

以下给出了这样一个示例:

target04 = 'www.python.org'
regex03 = r'(www)\.(?P<name>.+)\.(?P<domain>.+)'
result04 = re.match(pattern=regex03,
                    string=target04)
print(result04.groups())
print(result04.groupdict())

结果为:

$ python -u regex.py ('www', 'python', 'org') {'name': 'python', 'domain': 'org'}

该对象还有以下常用的方法:

  • .start(group) :返回对应组开始的匹配位置,整个字符串的开头位置为 0 向后类推。忽略 group 参数相当于返回整个匹配结果的表达式的开头位置。由于 match() 函数会从整个字符串的开头匹配,所以不带参数或者带参数 0 时会返回 0 。
  • .end(group) :返回对应组结尾的匹配位置,整个字符串的开头位置为 0 向后类推。忽略 group 参数相当于返回整个匹配结果的表达式的结尾位置。
  • .span(group) :以元组的形式返回对应组的开头和结尾的匹配位置,相当于 (.start(group), .stop(group))

它的一些属性可以反过来查找匹配时用到的信息:

  • re :匹配时使用的正则表达式对象
  • string :待匹配的文本
  • pos :正则表达式搜索文本的开始位置
  • endpos :正则表达式搜索文本的结束位置
  • lastindex :正则表达式最后的组序号
  • lastgroup :正则表达式最后的组名

fullmatch(pattern, string, flags=0) 函数和 match() 函数相似,只不过 fullmatch() 函数用来检查正则表达式和目标字符串是否完全匹配,而不是部分匹配。

如果完全匹配,则 fullmatch() 函数返回一个 Match 对象,否则便返回空值。

其参数、Match 对象的属性等都和 match() 函数几乎一致。

但是有一个例外:在懒惰匹配模式下,如果其完整的匹配结果是原字符串,则 fullmatch() 也能够成功匹配。这是由于懒惰匹配必须要先匹配完整的字符串,再回溯选取最少的结果。以下给出了一个简单的演示:

import re
target05 = 'Python is ...'
result05 = re.fullmatch(pattern=r'.+',
                        string=target05)
result06 = re.fullmatch(pattern=r'.{3,}?',
                        string=target05)
result07 = re.fullmatch(pattern=r'.{3,6}',
                        string=target05)
print(result05)
print(result06)
print(result07)

结果为:

$ python -u regex.py <_sre.SRE_Match object; span=(0, 13), match='Python is ...'> <_sre.SRE_Match object; span=(0, 13), match='Python is ...'> None

搜索

相比与匹配,搜索可能应用更多一些。search(pattern, string, flags=0) 函数也和 match() 函数相似,该函数会检索整个字符串来寻找目标表达式的匹配对象。

search() 函数和 match() 函数的区别在于配对位置。match() 函数只会匹配字符串的开始,如果字符串的开始不符合正则表达式,便匹配失败。而 search() 函数会检索整个字符串来判断里面是否有结果满足正则表达式,如果字符串的某一部分符合正则表达式,便匹配成功。

该函数也会得到一个 Match 对象。

regex04 = r'\b(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d\b'
target06 = 'Now it\'s 22:32:15.'
result08 = re.search(regex04, target06)
print(result08)

结果为:

$ python -u regex.py <_sre.SRE_Match object; span=(9, 17), match='22:32:15'>

可以看到,得到的结果可以通过 .span() 方法来得到匹配字符串的位置,或者通过 .group() 方法得到完整的匹配字符串。

注意:search() 函数虽然可以搜索整个字符串,但是它只会返回第一个匹配成功的结果,例如:

result09 = re.search(r'ca.+?\b', 'A cat catches a cap.')
print(result09)

结果为:

$ python -u regex.py <_sre.SRE_Match object; span=(2, 5), match='cat'>

尽管上例按照正则表达式的匹配方式来说,"cat"、"catches"、"cab" 都可以成功匹配。


如果想到获取所有的匹配结果,可以使用 findall(pattern, string, flags=0) 函数。该函数可以在字符串中找到正则表达式所匹配的所有子串,并返回一个包含所有结果的列表。如果没有找到匹配的结果,则返回空列表;若 pattern 中包含组,则返回各组匹配结果的列表。

以下给出了这样一个示例:

target07 = '''
    <link rel="stylesheet" href="/static/style.css">
    <link rel="stylesheet" href="/static/font.css">
    <script src="/static/jquery.js"></script>
    <script src="/static/doctools.js"></script>
    <script src="/static/locale.js"></script>'''
print(re.findall(r'/static/(.+\.js)', target07))

结果为:

$ python -u regex.py ['jquery.js', 'doctools.js', 'locale.js']

对于以上这种每个正则表达式只包含一个分组时的情况,findall() 返回的列表中的每个元素都是该唯一分组匹配到的字符串。如果包含两个或以上分组,那么每个分组匹配到的字符串就以元组的形式排开了。

finditer(pattern, string, flags=0) 函数的作用与 ffindall() 函数类似。只不过当被匹配的对象很长(如一个大型网站的 HTML 源码),可能会匹配出非常多的结果。这个时候不希望直接返回一个很长的列表,便需要用到 finditer() 函数返回一个生成器来存储匹配结果,其中每个迭代元素都是 Match 对象。

以下给出了一个这样的示例:

import requests
html = requests.get('https://www.python.org/').text
result = re.finditer(r'https://[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]', html)
print(result)
print(next(result))
print(next(result))

结果为:

$ python -u regex.py <callable_iterator object at 0x018690D0> <re.Match object; span=(1303, 1368), match='https://media.ethicalads.io/media/client/v1.4.0/e> <re.Match object; span=(3759, 3815), match='https://www.python.org/static/opengraph-icon-200x>

替换与分隔

替换

正则表达式可以用来替换字符串中不符合要求的部分。使用 sub(pattern, repl, string, count=0, flags=0) 函数可以实现该效果。以下是该函数的参数说明:

  • pattern :一个符合正则表达式的要替换字符串
  • repl :用来替换的字符串,也可为一个函数
  • string :被查找并替换的原始字符串
  • count :模式匹配后从前向后替换的最大次数,默认为 0 ,表示替换所有的匹配
  • flags :编译时用的匹配模式

该函数返回替换后的结果。以下给出了这样一个示例:

code_segment = '''/* hello world application */
    int main(/* command line arguments */int argc, char* argv[]) {
        printf("Hello, world!"); /* print function */
    } '''
result = re.sub(r'/\*.*?\*/', '', code_segment)
print(result)

结果为:

$ python -u regex.py    int main(int argc, char* argv[]) {        printf("Hello, world!");    }

一种常见的需求就是对捕获到的结果做一些小改动,而不是完全替换为一个毫不相干的结果。这可以通过引用组实现。repl 参数内,可以通过 \g<group> 的形式引用一个分组的匹配结果。

以下给出了这样的一个示例:

name_list = ''' name: John ... name: Smith ....
    name: Peter .. name: James ...'''
result = re.sub(r'name: (?P<name>\w+)', '\g<name>@example.mail', name_list)
print(result)

结果为:

$ python -u regex.py John@example.com ... Smith@example.com ....    Peter@example.com .. James@example.com ...

repl 参数有一种特殊情况就是它为一个函数对象。这时这个函数需要有且仅有这样一个参数,代表匹配结果的 Match 对象。因此可以将需要将要替换的字符分组,再用 Match 对象的 .group() 方法取出需要替换的字符串的组,并在函数内部运算。例如:

from random import choice
cards = [a + b for a in '♠♥♣♦' for b in 'A23456789JQK']
this_round = '''... got ♠6 . ♥4 and ♣K ... ♥9 ♦5 .. ♥A ♣4'''
def get_random_card(before):
    after = choice(cards)
    if after != before.group():
        return after
    else:
        return get_random_card(before)
result = re.sub(r'([♠♥♣♦][A2-9JQK])', get_random_card, this_round)
print(result)

结果为:

$ python -u regex.py ... got ♣3 . ♦2 and ♦9 ... ♠7 ♠5 .. ♥5 ♣6

这种使用函数来处理如何完成替换的方式非常灵活。


subn(pattern, repl, string, count=0, flags=0) 函数和 sub() 函数功能类似,唯一不同的是 subn() 函数的返回结果是一个包含 (result, count) 的元组,即可以同时获取正则替换的完整结果与替换次数。

分割

Python 内置了一个 .split() 方法,可以用来分割字符串:

>>> data = '0.2682, 0.1266, 0.0942, 0.5178, 0.9459' >>> data.split(', ') ['0.2682', '0.1266', '0.0942', '0.5178', '0.9459']

但是该方法有一个缺点,那就是对于复杂情况下的分割效果不够理想:

>>> row_data = '0.2682 0.1266 0.0942 0.5178 0.9459' >>> row_data.split(' ') ['0.2682', '0.1266', '', '', '0.0942', '', '0.5178', '', '', '', '0.9459']

上例中,.split() 方法无法识别出多个空格,导致多个空格的分割出现了未达到预期的效果。

这个时候,便可以使用 re 库中的分割函数 split(pattern, string, maxsplit=0, flags=0) ,该函数的效果是将 pattern 代表的正则表达式为分割标识,将 string 位于分割标识两侧的字符串拆分成列表。通俗地说,就是相比内置的 .split 方法,该函数可以用正则表达式作为分割标识了。因此相比内置的 .split() 方法,re 库中的 split() 函数分割更加的灵活。

例如,以上使用内置 .split() 分割失败的情况,可以使用 split() 函数这么处理:

>>> from re import split >>> split(r'\s+', row_data) ['0.2682', '0.1266', '0.0942', '0.5178', '0.9459']

可以看到正则表达式很完美地识别了多个空格并完成了拆分。

其它内容

正则表达式对象

当在 Python 中使用正则表达式匹配时,re 模块会执行两个步骤:

  1. 将输入的正则表达式编译成一个正则表达式对象。如果正则表达式的字符串本身不合法,会产生 re.error 错误。
  2. 用编译后的正则表达式去匹配字符串。

因此当要匹配大量数据时,需要重复使用一个正则表达式几百上千次。此时,出于效率的考虑,可以先预编译该正则表达式为一个正则表达式对象,再利用正则表达式对象的方法去匹配字符串,这样便省略了步骤 1 ,很好地提升了效率。

使用 re.compile(pattern, flags=0) 函数可以生成一个正则表达式对象:

import re
pattern = re.compile(r'^\d{6}$')
print(pattern, type(pattern))

结果为:

$ python -u regex.py re.compile('^\\d{6}$') <class 're.Pattern'>

对于一个已经预编译完成的正则表达式对象,可以使用对象的一些方法来完成匹配。正则表达式对象方法有:

  • .match(string, pos, endpos)
  • .fullmatch(string, pos, endpos)
  • .search(string, pos, endpos)
  • .finall(string, pos, endpos)
  • .finditer(string, pos, endpos)
  • .sub(repl, string, count)
  • .subn(repl, string, count)
  • .split(string, maxsplit)

其中 pos 和 endpos 参数可以指定正则表达式的搜索位置。除此之外,这些方法与各自对应的函数的使用方法基本一致。

附录

参考资料

https://docs.python.org/3/library/re.html

Python3 re库官方文档

京ICP备2021034974号
contact me by hello@frozencandles.fun