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

0
Share

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

本文介绍正则表达式在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 库的函数通常都有以下三个常用的参数:patternstringflag

  • 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>) 进行分组时,可以使用 方法用来获取各组匹配到的字符串。括号内传入的参数为代表组序号的整数,或代表组名称的字符串(只需要名称就够了,不需要别的修饰符)。如果不传入参数或传入 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)

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

附录

参考资料

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

Python3 re库官方文档

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