Regular Expression HOWTO

作者:A.M. Kuchling <amk@amk.ca>

抽象

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

Introduction

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

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

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

Simple Patterns

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

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

Matching Characters

大多数字母和字符将简单地匹配自己。例如,正则表达式test将匹配字符串test(您可以启用一种不区分大小写的模式,让此RE与TestTEST匹配;稍后将对此进行详细说明。)

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

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

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

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

元字符在类中不活动。For example, [akm$] will match any of the characters 'a', 'k', 'm', or '$'; '$' is usually a metacharacter, but inside a character class it’s stripped of its special nature.

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

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

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

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

以下特殊序列列表不完整。有关Unicode字符串模式的序列和扩展类定义的完整列表,请参见标准库引用中Regular Expression Syntax的最后一部分。通常,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),它甚至可以匹配换行符。'.'常用于要匹配“任意字符”的位置。

Repeating Things

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

第一个重复我们要看的东西的元字符是**与字面值字符*不匹配;相反,它指定前一个字符可以匹配零次或更多次,而不是一次。

例如,ca*t会匹配ct(0 a个字符),cat(1 a),caaat(3 a个字符),等等。RE引擎具有源于C的int类型的大小的各种内部限制,这将阻止它匹配超过20亿个a

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

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

匹配说明
1aRE中的a匹配。
2abcbd引擎匹配[bcd]*,尽可能地,它是到字符串的结尾。
3失败引擎尝试匹配b,但当前位置在字符串的结尾,因此失败。
4abcb备份,以便[bcd]*匹配一个字符。
5失败再次尝试b,但当前位置是最后一个字符,即'd'
6abc再次备份,以便[bcd]*只匹配bc
6abcb再次尝试b这一次,当前位置的字符为'b',因此它成功。

RE的结束现已到达,它与abcb匹配。这演示了匹配引擎如何尽可能远地达到其最初,如果没有找到匹配,它将逐步备份,并一次又一次重试RE的其余部分。它将备份,直到它已经尝试了[bcd]*的零匹配,如果随后失败,引擎将断定字符串与RE不匹配。

另一个重复的元字符是+,其匹配一次或多次。注意*+之间的差异; *匹配或更多次,因此重复的任何内容可能不存在,而+至少需要t9>发生。To use a similar example, ca+t will match cat (1 a), caaat (3 a‘s), but won’t match 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()还接受可选的标志参数,用于启用各种特殊功能和语法变体。我们稍后将讨论可用的设置,但现在一个例子将:

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

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

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

反斜杠瘟疫

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

假设您想要写一个与字符串\section匹配的RE,它可能在LaTeX文件中找到。None接下来,您必须通过在前面加上反斜杠来转义任何反斜杠和其他元字符,从而产生字符串\\section必须传递到re.compile()的结果字符串必须是\\section然而,为了表示为Python字符串字面值,两个反斜杠必须再次转义

字符阶段
\section要匹配的文本字符串
\\sectionre.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如果成功,将返回match object实例,其中包含匹配的信息:开始和结束的位置,匹配的子字符串等。

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

这个HOWTO使用标准的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()将返回一个match object,因此您应该将结果存储在一个变量中供以后使用。

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

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

方法/属性目的
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)

在实际程序中,最常见的方式是将match object存储在变量中,然后检查是否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()方法将match object实例的序列作为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)

Module-Level Functions

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

>>> 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标志。

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

含义
ASCIIAMakes several escapes like \w, \b, \s and \d match only on ASCII characters with the respective property.
DOTALLS设为.匹配任何字符,包括换行符
IGNORECASEI不区分大小写的匹配
LOCALEL进行区域设置感知匹配
MULTILINEM多行匹配,影响^$
VERBOSEX(用于“扩展”)启用详细的RE,可以更干净和可理解地组织。
I
IGNORECASE

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

L
LOCALE

根据当前语言环境而不是根据当前语言环境,选择\w\W\b\B Unicode数据库。

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

M
MULTILINE

^$尚未说明;它们将在More Metacharacters中介绍。

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

S
DOTALL

使'.'特殊字符匹配任何字符,包括换行符;没有此标志,'.'将匹配除了换行符之外的任何

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的版本更难理解。

More Pattern Power

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

More Metacharacters

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

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

|

交替,或“或”操作符号。如果A和B是正则表达式,则A|B将匹配符合AB的任何字符串。|具有非常低的优先级,以便在交替使用多字符字符串时使其工作正常。Crow|Servo will match either Crow or Servo, not Cro, a 'w' or an 'S', and 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相反,仅当当前位置不在字边界时匹配。

Grouping

通常你需要获得更多的信息,而不仅仅是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

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

组由'('')'元字符标记。'(' and ')' have much the same meaning as they do in mathematical expressions; they group together the expressions contained inside them, and you can repeat the contents of a group with a repeating qualifier, such as *, +, ?, or {m,n}. 例如,(ab)*将匹配零或多个重复的ab

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

'('')'指示的组还捕获它们匹配的文本的开始和结束索引;这可以通过向group()start()end()span()组编号从0开始。组0总是存在;它是整个RE,因此match object方法都将组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'

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

Non-capturing and Named Groups

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

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

Perl开发人员选择的解决方案是使用(?...)作为扩展语法。?紧接在括号之后是语法错误,因为?将没有什么可重复,所以这没有引入任何兼容性问题。紧接在?之后的字符 indicate what extension is being used, so (?=foo) is one thing (a positive lookahead assertion) and (?:foo) is something else (a non-capturing group containing the subexpression 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显然是组的名称。命名组的行为与捕获组完全相同,并且还将名称与组关联。处理捕获组的match object方法都接受通过数字引用组的整数或包含所需组名称的字符串。命名组仍然是给定的数字,因此您可以通过两种方式检索有关组的信息:

>>> 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的组的内容应该再次在当前点匹配。The regular expression for finding doubled words, (\b\w+)\s+\1 can also be written as (?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 Assertions

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

(?=...)
正前瞻断言。如果包含的正则表达式(此处由...表示)在当前位置成功匹配,则会成功,否则失败。但是,一旦已经尝试了包含的表达式,匹配引擎根本不前进;其余的模式是在断言开始的地方尝试。
(?!...)
负前瞻断言。这是正面的断言的相反;如果包含的表达式在字符串中的当前位置匹配,则它成功。

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

匹配的模式很简单:

.*[.].*$

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

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

.*[.][^b].*$ The first attempt above tries to exclude bat by requiring that the first character of the extension is not a b. 这是错误的,因为模式也不匹配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$)[^.]*$

Modifying Strings

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

方法/属性目的
split()将字符串拆分成一个列表,拆分它RE在任何地方匹配
sub()查找RE匹配的所有子字符串,并用不同的字符串替换它们
subn()sub()相同,但返回新的字符串和替换的数量

Splitting Strings

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

。 t>> 分割 string [maxsplit = 0 t5 > ]

通过正则表达式的匹配拆分字符串如果在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.']

Search and Replace

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

sub 替换string count = 0 ]

返回通过替换string中RE的最左侧不重叠的出现次数替换替换获得的字符串。如果未找到该模式,则会不加改变地返回string

可选参数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-'

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

This example matches the word section followed by a string enclosed in {, }, and changes section to 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}'

替换也可以是一个函数,这给了你更多的控制。如果替换是函数,则对于模式的每个不重叠的发生,调用该函数。在每次调用时,函数都会传递一个match object参数,并且可以使用此信息计算所需的替换字符串并返回。

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

>>> 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'

Common Problems

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

Use String Methods

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

一个示例可能是用另一个替换单个固定字符串;例如,您可以用deed替换wordre.sub()似乎是用于此的函数,但考虑replace()方法。Note that replace() will also replace word inside words, turning swordfish into sdeedfish, but the naive RE word would have done that, too. (为了避免对字的部分执行替换,模式将必须是\bword\b,以便要求word在任一侧具有字边界。这需要超越replace()的能力。)

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

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

Greedy versus Non-Greedy

当重复正则表达式(如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中还有更多的剩余,但是,>在字符串的末尾不能匹配,所以正则表达式引擎必须逐个字符回溯,直到找到匹配的>The final match extends from the '<' in <html> to the '>' in </title>, which isn’t what you want.

在这种情况下,解决方案是使用非贪婪限定符*?+???{m,n}?,它们尽可能匹配文本。In the above example, the '>' is tried immediately after the first '<' matches, and when it fails, the engine advances a character at a time, retrying the '>' at every step. 这产生正确的结果:

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

(请注意,使用正则表达式解析HTML或XML是很痛苦的。快速和脏的模式将处理常见的情况,但HTML和XML有特殊的情况,将打破明显的正则表达式;在您编写处理所有可能情况的正则表达式时,模式将非常复杂。对此类任务使用HTML或XML解析器模块。)

Using re.VERBOSE

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

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

re.VERBOSE标志有几个效果。在字符类中不是的正则表达式中的空格将被忽略。This means that an expression such as dog | cat is equivalent to the less readable dog|cat, but [a b] will still match the characters 'a', 'b', or a space. 此外,您还可以在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*$")

Feedback

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

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