Skip to main content

正则表达式HOWTO

Author:

上午。 Kuchling <amk@amk.ca>

抽象

本文档是在Python中使用 re 模块使用正则表达式的入门教程。它提供了比库参考中相应部分更温和的介绍。

介绍

正则表达式(称为RE,或正则表达式或正则表达式模式)本质上是嵌入在Python内部并通过 re 模块提供的一种小型,高度专用的编程语言。使用这个小语言,您可以指定要匹配的可能字符串集合的规则;此集合可能包含英语句子,电子邮件地址或TeX命令,或任何你喜欢的。然后,您可以提出诸如“此字符串是否匹配模式?”或“此字符串中任何位置的模式是否匹配”等问题。您还可以使用RE修改字符串或以各种方式拆分它。

正则表达式模式被编译为一系列字节码,然后由以C语言编写的匹配引擎执行。对于高级使用,可能需要注意引擎将如何执行给定的RE,并将RE写入以产生运行得更快的字节码。本文档不涉及优化,因为它需要您对匹配引擎的内部结构有很好的理解。

正则表达式语言相对较小和受限制,所以不是所有可能的字符串处理任务都可以使用正则表达式。还有一些任务,can 使用正则表达式,但表达式变得非常复杂。在这些情况下,你可能更喜欢编写Python代码来进行处理;而Python代码将比复杂的正则表达式慢,它也可能更容易理解。

简单模式

我们将从学习最简单的正则表达式开始。由于正则表达式用于操作字符串,我们将从最常见的任务开始:匹配字符。

有关计算机科学基础正则表达式(确定性和非确定性有限自动机)的详细解释,您可以参考几乎任何教科书上编写的编译器。

匹配的字符

大多数字母和字符将简单地匹配自己。例如,正则表达式 test 将精确匹配字符串 test。 (您可以启用一种不区分大小写的模式,让这个RE与 TestTEST 匹配;稍后再讨论这个模式。)

这个规则有例外;一些字符是特殊的 metacharacters,并且不匹配自己。相反,它们表示一些不寻常的东西应当匹配,或者通过重复它们或改变它们的含义来影响RE的其他部分。这个文档的大部分都致力于讨论各种元字符和他们做什么。

这里有一个完整的元字符列表;他们的意义将在本HOWTO的其余部分讨论。

. ^ $ * + ? { } [ ] \ | ( )

我们将看到的第一个元字符是 []。它们用于指定字符类,它是您要匹配的一组字符。字符可以单独列出,或者一个字符范围可以通过给出两个字符并用 '-' 分隔它们来指示。例如,[abc] 将匹配字符 abc 中的任何一个;这与 [a-c] 相同,后者使用范围来表示相同的字符集。如果你只想匹配小写字母,你的RE将是 [a-z]

元字符在类中不活动。例如,[akm$] 将匹配字符 'a''k''m''$' 中的任何一个; '$' 通常是一个元字符,但在字符类中它被剥夺了它的特殊性质。

您可以通过 complementing 匹配集合中未列出的字符。这通过包括 '^' 作为类的第一个字符来指示;字符类外的 '^' 将简单地匹配 '^' 字符。例如,[^5] 将匹配除 '5' 之外的任何字符。

也许最重要的元字符是反斜杠,\。和Python字符串一样,反斜杠后面可以跟着各种字符来表示各种特殊序列。它也用于转义所有的元字符,所以你仍然可以匹配他们的模式;例如,如果您需要匹配 [\,您可以在它们前面加上反斜杠,以删除其特殊含义:\[\\

'\' 开头的一些特殊序列表示通常有用的预定义字符集,例如数字集,字母集或不是空格的任何集合。

让我们举个例子:\w 匹配任何字母数字字符。如果正则表达式模式以字节表示,这相当于类 [a-zA-Z0-9_]。如果正则表达式模式是字符串,则 \w 将匹配 unicodedata 模块提供的Unicode数据库中标记为字母的所有字符。通过在编译正则表达式时提供 re.ASCII 标志,可以在字符串模式中使用 \w 的更受限制的定义。

以下特殊序列列表不完整。有关Unicode字符串模式的序列和扩展类定义的完整列表,请参阅标准库参考中的 正则表达式语法 的最后一部分。通常,Unicode版本匹配Unicode数据库中适当类别中的任何字符。

\d

匹配任何十进制数字;这相当于 [0-9] 类。

\D

匹配任何非数字字符;这相当于 [^0-9] 类。

\s

匹配任何空格字符;这相当于 [ \t\n\r\f\v] 类。

\S

匹配任何非空白字符;这相当于 [^ \t\n\r\f\v] 类。

\w

匹配任何字母数字字符;这相当于 [a-zA-Z0-9_] 类。

\W

匹配任何非字母数字字符;这相当于 [^a-zA-Z0-9_] 类。

这些序列可以包含在字符类中。例如,[\s,.] 是将匹配任何空格字符,或 ',''.' 的字符类。

本节中的最终元字符是 .。它匹配任何除了换行符之外的任何东西,并且有一个替换模式(re.DOTALL),它甚至可以匹配换行符。 '.' 通常用于要匹配“任何字符”的位置。

重复的事情

能够匹配不同的字符集是正则表达式可以做的第一件事,即字符串上可用的方法是不可能的。然而,如果这是正则表达式的唯一额外的能力,他们不会是很大的进步。另一个功能是您可以指定RE的某些部分必须重复一定次数。

我们将重点讨论的第一个元字符是 ** 与文字字符 * 不匹配;相反,它指定前一个字符可以匹配零次或多次,而不是一次。

例如,ca*t 将匹配 ct (0个 a 字符),cat (1个 a),caaat (3个 a 字符)等等。 RE引擎有各种内部限制,源自C的 int 类型的大小,将阻止它匹配超过20亿的 a 字符;模式通常不会被写入以匹配那么多的数据。

例如 * 的重复是 greedy;当重复RE时,匹配引擎将尝试重复它尽可能多的次数。如果模式的稍后部分不匹配,则匹配引擎将然后备份并以较少的重复再次尝试。

逐步的例子将使这一点更加明显。让我们考虑表达式 a[bcd]*b。这与字母 'a',来自类 [bcd] 的零个或多个字母匹配,并且最终以 'b' 结束。现在想象这个RE与字符串 abcbd 匹配。

匹配

说明

1

a

RE中的 a 匹配。

2

abcbd

引擎匹配 [bcd]*,尽可能地,它是字符串的结尾。

3

Failure

引擎尝试匹配 b,但当前位置在字符串的结尾,因此失败。

4

abcb

备份,以便 [bcd]* 匹配一个较少的字符。

5

Failure

再次尝试 b,但当前位置是最后一个字符,这是一个 'd'

6

abc

备份,以便 [bcd]* 只匹配 bc

6

abcb

再次尝试 b。这一次,当前位置的字符是 'b',因此它成功。

RE的结束现在已经到达,它已经匹配 abcb。这演示了匹配引擎如何首先达到它,并且如果没有找到匹配,则它将逐步备份,并且一次又一次地重试RE的剩余部分。它将备份直到它已经尝试了 [bcd]* 的零匹配,并且如果随后失败,则引擎将断定该字符串根本不匹配RE。

另一个重复的元字符是 +,其匹配一次或多次。注意 *+ 之间的区别; * 匹配 zero 或更多次,因此重复的任何内容可能不存在,而 + 至少需要 one 出现。为了使用类似的例子,ca+t 将匹配 cat (1 a),caaat (3 a),但不匹配 ct

还有两个重复限定符。问号字符 ? 匹配一次或零次;你可以认为它是标记为可选的东西。例如,home-?brew 匹配 homebrewhome-brew

最复杂的重复限定符是 {m,n},其中 mn 是十进制整数。这个限定符意味着必须至少有 m 重复,最多只有 n。例如,a/{1,3}b 将匹配 a/ba//ba///b。它不匹配 ab,它没有斜线,或 a////b,有四个。

您可以省略 mn;在这种情况下,为缺失值假定一个合理的值。省略 m 被解释为下限0,而省略 n 导致无穷大的上限 - 实际上,上限是前面提到的20亿的极限,但也可以是无穷大。

读者可以注意到,其他三个限定词都可以使用这个符号表示。 {0,}* 相同,{1,} 等同于 +{0,1}? 相同。最好使用 *+?,当你可以,只是因为他们更短,更容易阅读。

使用正则表达式

现在我们看了一些简单的正则表达式,我们如何在Python中使用它们? re 模块提供了一个到正则表达式引擎的接口,允许你将RE编译成对象,然后与它们进行匹配。

编译正则表达式

正则表达式被编译成模式对象,其具有用于各种操作的方法,例如搜索模式匹配或执行字符串替换。

>>> import re
>>> p = re.compile('ab*')
>>> p
re.compile('ab*')

re.compile() 还接受可选的 flags 参数,用于启用各种特殊功能和语法变体。我们稍后将讨论可用的设置,但现在只需要一个示例:

>>> p = re.compile('ab*', re.IGNORECASE)

RE作为字符串传递给 re.compile()。 RE作为字符串处理,因为正则表达式不是核心Python语言的一部分,并且没有为表达它们创建特殊的语法。 (有些应用程序根本不需要RE,所以没有必要通过包含它们来扩展语言规范。)而是,re 模块只是一个包含在Python中的C扩展模块,就像 socketzlib 模块一样。

将RE放在字符串中使Python语言更简单,但有一个缺点,这是下一节的主题。

反斜线瘟疫

如前所述,正则表达式使用反斜杠字符('\')来表示特殊形式或允许使用特殊字符而不调用其特殊含义。这与Python在字符串文字中用于相同目的的相同字符的使用冲突。

假设您想要写一个与字符串 \section 匹配的RE,这可能在LaTeX文件中找到。要找出在程序代码中要写什么,从要匹配的所需字符串开始。接下来,您必须通过在前面加上反斜杠来转义任何反斜杠和其他元字符,从而生成字符串 \\section。必须传递给 re.compile() 的结果字符串必须是 \\section。但是,为了将其表达为Python字符串文字,两个反斜杠都必须转义为 again

字符

阶段

\section

要匹配的文本字符串

\\section

re.compile() 的转义反斜杠

"\\\\section"

用于字符串文字的转义反斜杠

简而言之,为了匹配字面反斜杠,必须将 '\\\\' 写为RE字符串,因为正则表达式必须是 \\,并且每个反斜杠必须在常规Python字符串文字中表示为 \\。在反复出现反斜杠的RE中,这会导致大量重复的反斜杠,并使得生成的字符串难以理解。

解决方案是使用Python的原始字符串符号表示正则表达式;在以 'r' 为前缀的字符串文字中,反斜杠不以任何特殊方式处理,因此 r"\n" 是包含 '\''n' 的双字符字符串,而 "\n" 是包含换行符的单字符字符串。正则表达式通常使用这个原始字符串符号在Python代码中编写。

常规字符串

原始字符串

"ab*"

r"ab*"

"\\\\section"

r"\\section"

"\\w+\\s+\\1"

r"\w+\s+\1"

执行匹配

一旦你有一个对象表示一个编译的正则表达式,你用它做什么?模式对象有几种方法和属性。只有最重要的将在这里覆盖;请查阅 re 文档以获取完整的列表。

方法/属性

目的

match()

确定RE是否匹配字符串的开头。

search()

扫描字符串,查找此RE匹配的任何位置。

findall()

查找RE匹配的所有子字符串,并将它们作为列表返回。

finditer()

查找RE匹配的所有子字符串,并将它们作为 iterator 返回。

如果没有找到匹配,match()search() 返回 None。如果它们成功,将返回一个 匹配对象 实例,其中包含有关匹配的信息:开始和结束的位置,匹配的子字符串等。

您可以通过交互式实验 re 模块来了解这一点。如果你有 tkinter 可用,你可能还想看看 Tools/demo/redemo.py,一个包括Python分发版的演示程序。它允许您输入RE和字符串,并显示RE是匹配还是失败。 redemo.py 在尝试调试复杂的RE时可能非常有用。

这个如何使用标准的Python解释器的例子。首先,运行Python解释器,导入 re 模块,并编译一个RE:

>>> import re
>>> p = re.compile('[a-z]+')
>>> p
re.compile('[a-z]+')

现在,你可以尝试匹配RE [a-z]+ 的各种字符串。空字符串不应该匹配,因为 + 表示“一个或多个重复”。在这种情况下,match() 应该返回 None,这将导致解释器不打印输出。您可以显式打印 match() 的结果以使此清除。

>>> p.match("")
>>> print(p.match(""))
None

现在,让我们在它应该匹配的字符串(如 tempo)上尝试。在这种情况下,match() 将返回一个 匹配对象,因此您应该将结果存储在一个变量中供以后使用。

>>> m = p.match('tempo')
>>> m  
<_sre.SRE_Match object; span=(0, 5), match='tempo'>

现在您可以查询 匹配对象 以获取有关匹配字符串的信息。 匹配对象 实例也有几种方法和属性;最重要的是:

方法/属性

目的

group()

返回RE匹配的字符串

start()

返回匹配的起始位置

end()

返回匹配的结束位置

span()

返回包含匹配的(开始,结束)位置的元组

尝试这些方法很快就会弄清楚它们的含义:

>>> m.group()
'tempo'
>>> m.start(), m.end()
(0, 5)
>>> m.span()
(0, 5)

group() 返回由RE匹配的子字符串。 start()end() 返回匹配的开始和结束索引。 span() 在单个元组中返回开始和结束索引。由于 match() 方法仅检查RE是否在字符串的开头匹配,所以 start() 将始终为零。然而,模式的 search() 方法扫描字符串,因此在这种情况下匹配可能不从零开始。

>>> print(p.match('::: message'))
None
>>> m = p.search('::: message'); print(m)  
<_sre.SRE_Match object; span=(4, 11), match='message'>
>>> m.group()
'message'
>>> m.span()
(4, 11)

在实际程序中,最常见的风格是将 匹配对象 存储在变量中,然后检查它是否为 None。这通常看起来像:

p = re.compile( ... )
m = p.match( 'string goes here' )
if m:
    print('Match found: ', m.group())
else:
    print('No match')

两个模式方法返回模式的所有匹配。 findall() 返回匹配字符串的列表:

>>> p = re.compile('\d+')
>>> p.findall('12 drummers drumming, 11 pipers piping, 10 lords a-leaping')
['12', '11', '10']

findall() 必须创建整个列表,然后才能作为结果返回。 finditer() 方法返回 匹配对象 实例的序列作为 iterator:

>>> iterator = p.finditer('12 drummers drumming, 11 ... 10 ...')
>>> iterator  
<callable_iterator object at 0x...>
>>> for match in iterator:
...     print(match.span())
...
(0, 2)
(22, 24)
(29, 31)

模块级函数

您不必创建模式对象并调用其方法; re 模块还提供称为 match()search()findall()sub() 等的顶级功能。这些函数采用与相应的模式方法相同的参数,并将RE字符串作为第一个参数添加,并仍然返回 None匹配对象 实例。

>>> print(re.match(r'From\s+', 'Fromage amk'))
None
>>> re.match(r'From\s+', 'From amk Thu May 14 19:12:10 1998')  
<_sre.SRE_Match object; span=(0, 5), match='From '>

在引擎盖下,这些函数只是为你创建一个模式对象,并调用它上面的相应方法。它们还将编译对象存储在缓存中,因此使用相同RE的未来调用不需要再次解析模式。

你应该使用这些模块级函数,还是应该得到模式并自己调用它的方法?如果你正在一个循环中访问一个正则表达式,预编译它将保存一些函数调用。在循环外,由于内部缓存没有太大的区别。

编译标志

编译标志允许您修改正则表达式如何工作的一些方面。标志在 re 模块中有两个名称,诸如 IGNORECASE 的长名称以及诸如 I 的短的单字母形式。 (如果你熟悉Perl的模式修饰符,单字母形式使用相同的字母; re.VERBOSE 的短形式例如是 re.X。)多个标志可以通过按位或它们来指定;例如,re.I | re.M 设置 IM 标志。

这里有一个可用标志的表,后面是每个更详细的解释。

含义

ASCIIA

使 \w\b\s\d 等多个转义符仅与具有相应属性的ASCII字符匹配。

DOTALLS

使 . 匹配任何字符,包括换行符

IGNORECASEI

不区分大小写的匹配

LOCALEL

执行区域设置感知匹配

MULTILINEM

多线匹配,影响 ^$

VERBOSEX (用于’扩展’)

启用详细的RE,可以更干净和可理解地组织。

I
IGNORECASE

执行不区分大小写的匹配;字符类和字符串将通过忽略大小写来匹配字母。例如,[A-Z] 也将匹配小写字母,Spam 将匹配 SpamspamspAM。这个小写不考虑当前语言环境;如果你也设置了 LOCALE 标志,它会。

L
LOCALE

根据当前区域设置而不是Unicode数据库,创建 \w\W\b\B

语言环境是C库的一个功能,旨在帮助编写考虑到语言差异的程序。例如,如果你正在处理法语文本,你想要能够写 \w+ 匹配单词,但 \w 只匹配字符类 [A-Za-z];它将不匹配 'é''ç'。如果系统配置正确并且选择了法语语言环境,某些C函数将告诉程序 'é' 也应被视为字母。在编译正则表达式时设置 LOCALE 标志将导致生成的编译对象使用 \w 的这些C函数;这是更慢,但也使 \w+ 能够匹配法语单词,正如你所期望的。

M
MULTILINE

^$ 尚未解释;它们将在 更多元字符 部分介绍。)

通常 ^ 仅在字符串的开头匹配,$ 仅匹配字符串的末尾,并且在字符串末尾的换行符(如果有)之前。当指定此标志时,^ 在字符串的开头和字符串中每行的开头匹配,紧跟在每个换行之后。类似地,$ 元字符在字符串的末尾和每行的末尾(紧接在每个换行符之前)匹配。

S
DOTALL

使 '.' 特殊字符匹配任何字符,包括换行符;没有这个标志,'.' 会匹配任何 except 换行符。

A
ASCII

使 \w\W\b\B\s\S 执行纯ASCII匹配,而不是完全Unicode匹配。这仅对Unicode模式有意义,对于字节模式将被忽略。

X
VERBOSE

此标志允许您编写更加可读的正则表达式,为您提供更大的灵活性,如何格式化它们。当指定此标志时,忽略RE字符串中的空格,除非空格位于字符类中或前面有未转义的反斜杠;这使您可以更清晰地组织和缩进RE。此标志还允许您在RE中放置将被引擎忽略的注释;注释由 '#' 标记,该 '#' 既不在字符类中,也不在非转义反斜杠之前。

例如,这里是一个使用 re.VERBOSE 的RE;看看它是多么容易阅读?

charref = re.compile(r"""
 &[#]                # Start of a numeric entity reference
 (
     0[0-7]+         # Octal form
   | [0-9]+          # Decimal form
   | x[0-9a-fA-F]+   # Hexadecimal form
 )
 ;                   # Trailing semicolon
""", re.VERBOSE)

没有详细设置,RE将如下所示:

charref = re.compile("&#(0[0-7]+"
                     "|[0-9]+"
                     "|x[0-9a-fA-F]+);")

在上面的例子中,Python的自动连接字符串文字已经被用来将RE分解成更小的部分,但它仍然比使用 re.VERBOSE 的版本更难理解。

更多模式功率

到目前为止,我们只覆盖了正则表达式的一部分功能。在本节中,我们将介绍一些新的元字符,以及如何使用组来检索匹配的文本部分。

更多元字符

有一些元字符,我们还没有覆盖。其中大部分将在本节中讨论。

一些待讨论的剩余元字符是 zero-width assertions。它们不会使发动机前进通过琴弦;相反,它们根本不使用字符,而只是成功或失败。例如,\b 是当前位置位于字边界的断言;位置不被 \b 改变。这意味着零宽度断言不应该重复,因为如果它们在给定位置匹配一次,则它们显然可以无限次地匹配。

|

交替,或“或”运算符。如果A和B是正则表达式,A|B 将匹配符合 AB 的任何字符串。 | 具有非常低的优先级,以便在交替使用多字符字符串时使其工作正常。 Crow|Servo 将匹配 CrowServo,而不匹配 Cro'w''S'ervo

要匹配文本 '|',请使用 \|,或将其包含在字符类中,如在 [|] 中。

^

在行的开头匹配。除非已设置 MULTILINE 标志,否则只会在字符串的开头匹配。在 MULTILINE 模式下,这也在字符串中每个换行之后立即匹配。

例如,如果希望仅在行的开头匹配单词 From,则要使用的RE是 ^From

>>> print(re.search('^From', 'From Here to Eternity'))  
<_sre.SRE_Match object; span=(0, 4), match='From'>
>>> print(re.search('^From', 'Reciting From Memory'))
None
$

在行的结尾处匹配,它被定义为字符串的结尾或任何位置后跟换行符。

>>> print(re.search('}$', '{block}'))  
<_sre.SRE_Match object; span=(6, 7), match='}'>
>>> print(re.search('}$', '{block} '))
None
>>> print(re.search('}$', '{block}\n'))  
<_sre.SRE_Match object; span=(6, 7), match='}'>

要匹配字面 '$',请使用 \$ 或将其放在字符类中,如在 [$] 中。

\A

仅匹配字符串的开头。当不处于 MULTILINE 模式时,\A^ 实际上是相同的。在 MULTILINE 模式下,它们是不同的:\A 仍仅匹配字符串的开头,但 ^ 可以匹配在字符串内的任何位置,换行符之后。

\Z

仅匹配字符串的结尾。

\b

词边界。这是一个零宽度断言,仅在字的开头或结尾匹配。词定义为字母数字字符序列,因此词的结尾由空格或非字母数字字符表示。

以下示例仅在它是完整字词时匹配 class;它将不匹配,当它包含在另一个单词。

>>> p = re.compile(r'\bclass\b')
>>> print(p.search('no class at all'))  
<_sre.SRE_Match object; span=(3, 8), match='class'>
>>> print(p.search('the declassified algorithm'))
None
>>> print(p.search('one subclass is'))
None

当使用这个特殊的序列时,你应该记住两个细微之处。首先,这是Python的字符串字面量和正则表达式序列之间的最大冲突。在Python的字符串字面量中,\b 是退格字符,ASCII值为8.如果不使用原始字符串,那么Python会将 \b 转换为退格符,并且您的RE将不会如您所期望的那样匹配。以下示例看起来与我们以前的RE相同,但在RE字符串前面省略了 'r'

>>> p = re.compile('\bclass\b')
>>> print(p.search('no class at all'))
None
>>> print(p.search('\b' + 'class' + '\b'))  
<_sre.SRE_Match object; span=(0, 7), match='\x08class\x08'>

第二,在一个字符类中,没有用于这个断言,\b 表示退格符,与Python的字符串文字兼容。

\B

另一个零宽度断言,这是与 \b 的相反,仅当当前位置不在字边界时匹配。

分组

通常你需要获得更多的信息,而不仅仅是RE是否匹配。正则表达式通常用于通过将RE分成几个子组来分解字符串,这些子组匹配不同的感兴趣的组件。例如,RFC-822报头行被分为报头名称和值,由 ':' 分隔,像这样:

From: author@example.com
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: editor@example.com

这可以通过编写与整个标题行匹配的正则表达式来处理,并且有一个组匹配头名称,另一个组匹配头值。

组由 '('')' 元字符标记。 '('')' 具有与它们在数学表达式中大致相同的含义;它们将它们中包含的表达式组合在一起,您可以使用重复的限定符重复组的内容,例如 *+?{m,n}。例如,(ab)* 将匹配零个或多个重复的 ab

>>> p = re.compile('(ab)*')
>>> print(p.match('ababababab').span())
(0, 10)

'('')' 指示的组还捕获它们匹配的文本的开始和结束索引;这可以通过传递参数到 group()start()end()span() 来检索。组从0开始编号。组0总是存在;它是整个RE,所以 匹配对象 方法都有组0作为它们的默认参数。后面我们将看到如何表达不捕获它们匹配的文本跨度的组。

>>> p = re.compile('(a)b')
>>> m = p.match('ab')
>>> m.group()
'ab'
>>> m.group(0)
'ab'

子组从左到右编号,从1开始编号。组可以嵌套;确定数字,只是计算开始的括号字符,从左到右。

>>> p = re.compile('(a(b)c)d')
>>> m = p.match('abcd')
>>> m.group(0)
'abcd'
>>> m.group(1)
'abc'
>>> m.group(2)
'b'

group() 可以一次传递多个组号,在这种情况下,它将返回包含这些组的相应值的元组。

>>> m.group(2,1,2)
('b', 'abc', 'b')

groups() 方法返回一个包含所有子组的字符串的元组,从1到很多。

>>> m.groups()
('abc', 'b')

模式中的反向引用允许您指定先前捕获组的内容也必须在字符串中的当前位置找到。例如,如果组1的确切内容可以在当前位置找到,则 \1 将成功,否则将失败。记住Python的字符串字面量也使用反斜杠,后跟数字以允许在字符串中包含任意字符,因此在RE中合并反向引用时,请务必使用原始字符串。

例如,以下RE检测字符串中的双字。

>>> p = re.compile(r'(\b\w+)\s+\1')
>>> p.search('Paris in the the spring').group()
'the the'

像这样的后向引用对于仅仅搜索字符串通常不是有用的 - 有很少的文本格式以这种方式重复数据—但是你很快就会发现,当执行字符串替换时,它们是 very 有用的。

非捕获和命名组

详细的RE可以使用许多组,既捕获感兴趣的子串,又分组和结构RE本身。在复杂的RE中,变得难以跟踪组号。有两个功能可以帮助解决这个问题。两者都使用正则表达式扩展的常见语法,因此我们将首先查看。

Perl 5因其对标准正则表达式的强大添加而众所周知。对于这些新特性,Perl开发人员无法选择新的单键击元字符或以 \ 开头的新特殊序列,而不会使Perl的正则表达式与标准RE混淆不同。如果他们选择 & 作为新的元字符,例如,旧的表达式将假设 & 是正规字符,并且不会通过编写 \&[&] 来逃脱它。

Perl开发人员选择的解决方案是使用 (?...) 作为扩展语法。 ? 后的括号是一个语法错误,因为 ? 没有什么可重复,所以这没有引入任何兼容性问题。紧跟在 ? 之后的字符指示正在使用什么扩展,因此 (?=foo) 是一个东西(正前瞻断言),(?:foo) 是其他东西(包含子表达 foo 的非捕获组)。

Python支持几个Perl的扩展,并在Perl的扩展语法中添加了扩展语法。如果问号后面的第一个字符是 P,那么您知道它是Python特有的扩展。

现在我们已经了解了通用扩展语法,我们可以返回简化在复杂RE中使用组的功能。

有时候,你会想要使用一个组来表示正则表达式的一部分,但不想检索组的内容。您可以通过使用非捕获组:(?:...) 使您能够使用任何其他正则表达式替换 ...

>>> m = re.match("([abc])+", "abc")
>>> m.groups()
('c',)
>>> m = re.match("(?:[abc])+", "abc")
>>> m.groups()
()

除了您无法检索组匹配的内容之外,非捕获组的行为与捕获组的行为完全相同;你可以把任何内容,重复它与重复元字符,如 *,并将其嵌套在其他组(捕获或非捕获)。 (?:...) 在修改现有模式时特别有用,因为您可以添加新组而不更改所有其他组的编号。应该提到的是,在捕获和非捕获组之间的搜索没有性能差异;两种形式都不比另一种快。

更重要的功能是命名组:而不是通过数字引用它们,可以使用名称引用组。

命名组的语法是Python特定的扩展之一:(?P<name>...)name 显然是组的名称。命名组的行为与捕获组的行为完全相同,并且还将名称与组关联。处理捕获组的 匹配对象 方法都接受通过数字引用组的整数或包含所需组名称的字符串。命名组仍然是给定的数字,因此您可以通过两种方式检索组的信息:

>>> p = re.compile(r'(?P<word>\b\w+\b)')
>>> m = p.search( '(((( Lots of punctuation )))' )
>>> m.group('word')
'Lots'
>>> m.group(1)
'Lots'

命名组是方便的,因为他们让你使用容易记住的名字,而不必记住数字。这里是来自 imaplib 模块的示例RE:

InternalDate = re.compile(r'INTERNALDATE "'
        r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-'
        r'(?P<year>[0-9][0-9][0-9][0-9])'
        r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
        r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
        r'"')

显然更容易检索 m.group('zonem'),而不必记住检索组9。

表达式中的反向引用的语法(如 (...)\1)是指组的编号。当然有一个变体使用组名而不是数字。这是另一个Python扩展:(?P=name) 表示名为 name 的组的内容应该再次在当前点匹配。找到双字的正则表达式,(\b\w+)\s+\1 也可以写成 (?P<word>\b\w+)\s+(?P=word):

>>> p = re.compile(r'(?P<word>\b\w+)\s+(?P=word)')
>>> p.search('Paris in the the spring').group()
'the the'

前瞻断言

另一个零宽度断言是前瞻断言。 lookahead断言可用正面和负面形式,看起来像这样:

(?=...)

正前瞻断言。如果包含的正则表达式(在此由 ... 表示)在当前位置成功匹配,则成功,否则失败。但是,一旦尝试了包含的表达式,匹配引擎根本不前进;其余的模式是在断言开始的地方尝试。

(?!...)

负前瞻断言。这是正面的断言的相反;如果包含的表达式 doesn’t 在字符串中的当前位置匹配,则它成功。

为了具体化,让我们来看一个前景是有用的情况。考虑一个简单的模式来匹配文件名,并将它分为基本名称和扩展名,由 . 分隔。例如,在 news.rc 中,news 是基本名称,rc 是文件名的扩展名。

匹配的模式很简单:

.*[.].*$

注意,. 需要被特别处理,因为它是一个元字符,所以它在一个字符类中只匹配那个特定的字符。还注意到尾随 $;这是添加以确保所有其余的字符串必须包括在扩展中。此正则表达式匹配 foo.barautoexec.bat 以及 sendmail.cfprinters.conf

现在,考虑使问题复杂一点;如果你想匹配文件名,其中扩展名不是 bat?一些不正确的尝试:

.*[.][^b].*$ 上面的第一个尝试尝试通过要求扩展的第一个字符不是 b 来排除 bat。这是错误的,因为模式也不匹配 foo.bar

.*[.]([^b]..|.[^a].|..[^t])$

当您尝试通过要求下列情况之一匹配第一个解决方案时,表达式变得更加混乱:扩展的第一个字符不是 b;第二个字符不是 a;或第三个字符不是 t。它接受 foo.bar 并拒绝 autoexec.bat,但它需要一个三字母扩展名,不接受带两个字母扩展名的文件名,例如 sendmail.cf。我们将复杂的模式,以努力解决它。

.*[.]([^b].?.?|.[^a]?.?|..?[^t]?)$

在第三次尝试中,第二和第三个字母都是可选的,以便允许匹配的扩展短于三个字符,例如 sendmail.cf

模式变得非常复杂,这使得它很难阅读和理解。更糟糕的是,如果问题发生了变化,并且您希望将 batexe 作为扩展排除,则模式会变得更加复杂和混乱。

一个消极的前瞻性解决了所有这些混乱:

.*[.](?!bat$)[^.]*$ 负前瞻意味着:如果表达式 bat 在这一点不匹配,请尝试其余的模式;如果 bat$ 匹配,整个模式将失败。需要后面的 $ 来确保诸如 sample.batch 之类的东西,其中扩展名仅以 bat 开头,将被允许。当文件名中有多个点时,[^.]* 确保模式起作用。

排除另一个文件扩展名现在很容易;只需将其添加为断言内的替代。以下模式排除以 batexe 结尾的文件名:

.*[.](?!bat$|exe$)[^.]*$

修改字符串

到目前为止,我们只是对一个静态字符串执行搜索。正则表达式也通常用于以各种方式修改字符串,使用以下模式方法:

方法/属性

目的

split()

将字符串拆分为列表,将其拆分到RE匹配的任何地方

sub()

查找RE匹配的所有子字符串,并用不同的字符串替换它们

subn()

做与 sub() 相同的事情,但返回新的字符串和替换的数量

分割字符串

模式的 split() 方法在RE匹配的任何地方拆分字符串,返回一个列表。它类似于字符串的 split() 方法,但在可以拆分的分隔符中提供了更多的通用性;字符串 split() 仅支持以空格或固定字符串拆分。正如你所期望的,还有一个模块级 re.split() 功能。

.split(string[, maxsplit=0])

通过正则表达式的匹配拆分 string。如果在RE中使用捕获括号,则它们的内容也将作为结果列表的一部分返回。如果 maxsplit 不为零,则最多执行 maxsplit 拆分。

您可以通过传递 maxsplit 的值来限制分割的数量。当 maxsplit 非零时,最多会进行 maxsplit 拆分,并且返回字符串的其余部分作为列表的最后一个元素。在以下示例中,分隔符是任何非字母数字字符序列。

>>> p = re.compile(r'\W+')
>>> p.split('This is a test, short and sweet, of split().')
['This', 'is', 'a', 'test', 'short', 'and', 'sweet', 'of', 'split', '']
>>> p.split('This is a test, short and sweet, of split().', 3)
['This', 'is', 'a', 'test, short and sweet, of split().']

有时你不仅对分隔符之间的文本感兴趣,而且还需要知道分隔符是什么。如果在RE中使用捕获括号,则它们的值也将作为列表的一部分返回。比较以下调用:

>>> p = re.compile(r'\W+')
>>> p2 = re.compile(r'(\W+)')
>>> p.split('This... is a test.')
['This', 'is', 'a', 'test', '']
>>> p2.split('This... is a test.')
['This', '... ', 'is', ' ', 'a', ' ', 'test', '.', '']

模块级函数 re.split() 添加要用作第一个参数的RE,但在其他方面是相同的。

>>> re.split('[\W]+', 'Words, words, words.')
['Words', 'words', 'words', '']
>>> re.split('([\W]+)', 'Words, words, words.')
['Words', ', ', 'words', ', ', 'words', '.', '']
>>> re.split('[\W]+', 'Words, words, words.', 1)
['Words', 'words, words.']

搜索和替换

另一个常见的任务是找到一个模式的所有匹配,并用不同的字符串替换它们。 sub() 方法接受替换值,可以是字符串或函数,以及要处理的字符串。

.sub(replacement, string[, count=0])

返回通过用替换 replacement 替换 string 中的RE的最左侧的不重叠出现而获得的字符串。如果未找到模式,则 string 将保持不变。

可选参数 count 是要替换的模式出现的最大数量; count 必须是非负整数。默认值为0表示替换所有出现。

这里有一个使用 sub() 方法的简单示例。它用颜色名称替换为 colour:

>>> p = re.compile('(blue|white|red)')
>>> p.sub('colour', 'blue socks and red shoes')
'colour socks and colour shoes'
>>> p.sub('colour', 'blue socks and red shoes', count=1)
'colour socks and red shoes'

subn() 方法执行相同的工作,但返回包含新字符串值和执行的替换数量的2元组:

>>> p = re.compile('(blue|white|red)')
>>> p.subn('colour', 'blue socks and red shoes')
('colour socks and colour shoes', 2)
>>> p.subn('colour', 'no colours at all')
('no colours at all', 0)

只有当空匹配与之前的匹配不相邻时,才会替换它们。

>>> p = re.compile('x*')
>>> p.sub('-', 'abxd')
'-a-b-d-'

如果 replacement 是字符串,则会处理其中的任何反斜杠转义。也就是说,\n 转换为单个换行符,\r 转换为回车符,依此类推。未知的转义,例如 \& 被遗忘。反向引用,例如 \6,被替换为RE中相应组匹配的子字符串。这允许您在生成的替换字符串中合并原始文本的部分。

此示例匹配单词 section,后跟一个包含在 {} 中的字符串,并将 section 更改为 subsection:

>>> p = re.compile('section{ ( [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First} section{second}')
'subsection{First} subsection{second}'

还有一种语法用于引用由 (?P<name>...) 语法定义的命名组。 \g<name> 将使用由名为 name 的组匹配的子字符串,\g<number> 使用相应的组号。因此,\g<2> 等效于 \2,但在替换字符串(例如 \g<2>0)中不含歧义。 (\20 将被解释为对组20的引用,而不是对组2的引用,后面是文字字符 '0'。)以下替换都是等效的,但使用替换字符串的所有三个变体。

>>> p = re.compile('section{ (?P<name> [^}]* ) }', re.VERBOSE)
>>> p.sub(r'subsection{\1}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<1>}','section{First}')
'subsection{First}'
>>> p.sub(r'subsection{\g<name>}','section{First}')
'subsection{First}'

replacement 也可以是一个功能,它给你更多的控制。如果 replacement 是函数,则对于 pattern 的每个非重叠的发生,调用该函数。在每次调用时,函数都会传递一个 匹配对象 参数用于匹配,并可以使用此信息计算所需的替换字符串并返回。

在以下示例中,替换函数将小数转换为十六进制:

>>> def hexrepl(match):
...     "Return the hex string for a decimal number"
...     value = int(match.group())
...     return hex(value)
...
>>> p = re.compile(r'\d+')
>>> p.sub(hexrepl, 'Call 65490 for printing, 49152 for user code.')
'Call 0xffd2 for printing, 0xc000 for user code.'

当使用模块级 re.sub() 函数时,模式作为第一个参数传递。模式可以作为对象或字符串提供;如果需要指定正则表达式标志,则必须使用模式对象作为第一个参数,或者在模式字符串中使用嵌入的修饰符。 sub("(?i)b+", "x", "bbbb BBBB") 返回 'x x'

常见问题

正则表达式对于某些应用程序来说是一个强大的工具,但在某些方面,它们的行为不直观,有时它们的行为方式不如您期望的。本节将指出一些最常见的陷阱。

使用字符串方法

有时使用 re 模块是一个错误。如果您匹配固定字符串或单个字符类,并且您没有使用任何 re 功能(如 IGNORECASE 标志),则可能不需要正则表达式的全部功能。字符串有几种使用固定字符串执行操作的方法,并且它们通常更快,因为实现是为了目的而优化的单个小C循环,而不是大的,更通用的正则表达式引擎。

一个示例可能是用另一个替换单个固定字符串;例如,您可以用 deed 替换 wordre.sub() 似乎是用于此的函数,但考虑 replace() 方法。注意,replace() 也将替换 word 在字中,将 swordfish 转换为 sdeedfish,但是天真的RE word 也会这样做。 (为了避免对单词的部分执行替换,模式必须是 \bword\b,以便要求 word 在任一侧具有单词边界,这需要超过 replace() 的能力)。

另一个常见任务是从字符串中删除单个字符的每个出现,或者用另一个单个字符替换它。你可以使用像 re.sub('\n', ' ', S) 这样的东西,但是 translate() 能够执行这两个任务,并且会比任何正则表达式操作都快。

简而言之,在转向 re 模块之前,请考虑您的问题是否可以使用更快更简单的字符串方法解决。

贪婪与非贪婪

当重复正则表达式时,如在 a* 中,产生的动作是尽可能多地消费模式。当你试图匹配一对平衡分隔符,例如HTML标记周围的尖括号时,这个事实通常会让你感到困扰。用于匹配单个HTML标签的天真模式不工作,因为 .* 的贪婪性质。

>>> s = '<html><head><title>Title</title>'
>>> len(s)
32
>>> print(re.match('<.*>', s).span())
(0, 32)
>>> print(re.match('<.*>', s).group())
<html><head><title>Title</title>

RE与 <html> 中的 '<' 匹配,.* 使用字符串的其余部分。在RE中还有更多的剩余,然而,> 不能在字符串的末尾匹配,所以正则表达式引擎必须逐个字符回溯,直到找到一个匹配的 >。最终的比赛从 <html>'<' 延伸到 </title>'>',这不是你想要的。

在这种情况下,解决方案是使用非贪心限定符 *?+???{m,n}?,它们尽可能匹配为 little 文本。在上述示例中,在第一 '<' 匹配之后立即尝试 '>',并且当它失败时,引擎每次前进一个字符,在每一步重试 '>'。这产生正确的结果:

>>> print(re.match('<.*?>', s).group())
<html>

(注意,使用正则表达式解析HTML或XML是很痛苦的,快速而又脏的模式将处理常见的情况,但是HTML和XML有特殊的情况会破坏明显的正则表达式;在你编写正则表达式处理所有可能的情况,模式将是 very 复杂的。对于这样的任务使用HTML或XML解析器模块。)

使用re.VERBOSE

现在你可能已经注意到,正则表达式是一个非常紧凑的符号,但它们不是非常可读。适度复杂的RE可能成为反斜杠,括号和元字符的冗长集合,使得它们难以阅读和理解。

对于这样的RE,在编译正则表达式时指定 re.VERBOSE 标志可能很有帮助,因为它允许您更清楚地格式化正则表达式。

re.VERBOSE 标志有几个效果。字符类中的 isn’t 被忽略的正则表达式中的空格。这意味着诸如 dog | cat 的表达式等同于较不可读的 dog|cat,但是 [a b] 将仍然匹配字符 'a''b' 或空格。此外,您还可以在RE中放置注释;注释从 # 字符扩展到下一个换行符。当与三引号字符串一起使用时,这使得RE可以更整齐地格式化:

pat = re.compile(r"""
 \s*                 # Skip leading whitespace
 (?P<header>[^:]+)   # Header name
 \s* :               # Whitespace, and a colon
 (?P<value>.*?)      # The header's value -- *? used to
                     # lose the following trailing whitespace
 \s*$                # Trailing whitespace to end-of-line
""", re.VERBOSE)

这比可读性要好得多:

pat = re.compile(r"\s*(?P<header>[^:]+)\s*:(?P<value>.*?)\s*$")

反馈

正则表达式是一个复杂的主题。此文档是否帮助您了解它们?有没有部分不清楚,或者你遇到的问题,没有在这里覆盖?如果是,请向作者提出改进建议。

关于正则表达式的最完整的书几乎肯定是由O’Reilly出版的Jeffrey Friedl的Mastering Regular Expressions。不幸的是,它只专注于Perl和Java的正则表达式的风格,并且根本不包含任何Python材料,因此它不会作为Python中编程的参考。 (第一版涵盖了Python现在删除的 regex 模块,它不会帮助你。)考虑从你的库中检出它。