正则表达式操作方法

Abstract

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

Introduction

re模块是在 Python 1.5 中添加的,提供了 Perl 样式的正则表达式模式。早期版本的 Python 带有regex模块,该模块提供 Emacs 样式的模式。 regex模块已在 Python 2.5 中完全删除。

正则表达式(称为 RE 或 regexe 或 regex 模式)本质上是一种嵌入在 Python 中的小型,高度专业化的编程语言,可passre模块使用。使用这种小语言,您可以为要匹配的可能字符串集指定规则。该集合可能包含英语句子,电子邮件地址,TeX 命令或您喜欢的任何内容。然后,您可以问诸如“此字符串是否与模式匹配?”或“该字符串中的任何位置是否与模式匹配?”之类的问题。您还可以使用 RE 修改字符串或以各种方式将其拆分。

正则表达式模式被编译成一系列字节码,然后由用 C 编写的匹配引擎执行。对于高级使用,可能有必要特别注意引擎将如何执行给定的 RE,并将 RE 写入到为了产生运行速度更快的字节码的某种方式。本文档未涵盖优化,因为优化需要您对匹配引擎的内部有很好的了解。

正则表达式语言相对较小且受限制,因此并非所有可能的字符串处理任务都可以使用正则表达式来完成。还有一些可以用正则表达式完成的任务,但是表达式却变得非常复杂。在这种情况下,您最好编写 Python 代码来进行处理;尽管 Python 代码比复杂的正则表达式要慢,但它也可能更易于理解。

Simple Patterns

我们将从了解最简单的正则表达式开始。由于正则表达式用于对字符串进行操作,因此我们将从最常见的任务开始:匹配字符。

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

Matching Characters

大多数字母和字符将完全匹配自己。例如,正则表达式test将完全匹配字符串test。 (您可以启用不区分大小写的模式,该模式也将使该 RE 也匹配TestTEST;稍后会对此进行更多介绍.)

该规则有 exception。有些字符是特殊的元字符,与自己不匹配。相反,它们表示某些不寻常的事物应该匹配,或者它们pass重复它们或更改其含义来影响 RE 的其他部分。本文档的大部分致力于讨论各种元字符及其作用。

这是元字符的完整列表。它们的含义将在本 HOWTO 的其余部分中讨论。

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

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

元字符在类内部无效。例如,[akm$]将与任何字符'a''k''m''$'匹配; '$'通常是一个元字符,但在字符类中却被剥夺了其特殊性质。

您可以pass“补充”集合来匹配未在类中列出的字符。pass包括'^'作为该类的第一个字符来表明这一点。例如,[^5]将匹配'5'以外的任何字符。如果插入符号出现在字符类的其他位置,则没有特殊含义。例如:[5^]将匹配'5''^'

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

'\'开头的一些特殊序列表示通常有用的 sched 义字符集,例如数字集,字母集或任何非空格的集。以下 sched 义的特殊序列是可用序列的子集。等效类适用于字节字符串模式。有关 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

能够匹配不同的字符集是正则表达式可以做到的第一件事,而这对于字符串可用的方法而言是不可能的。但是,如果这是正则表达式的唯一附加Function,那么它们就不会有太大的进步。另一个Function是,您可以指定 RE 的某些部分必须重复一定次数。

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

例如,ca*t将与ct(0 a个字符),cat(1 a),caaat(3 a个字符)匹配,依此类推。 RE 引擎由于 C 的int类型的大小而具有各种内部限制,这将阻止它匹配超过 20 亿个a字符;您可能没有足够的内存来构造那么大的字符串,因此您不应该遇到该限制。

诸如*的重复是* greedy *;重复 RE 时,匹配引擎会try将其重复多次。如果模式的后续部分不匹配,则匹配引擎将备份并以较少的重复次数再试一次。

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

StepMatchedExplanation
1aRE 中的a匹配。
2abcbd引擎会尽可能匹配[bcd]*,直到字符串的末尾。
3Failure引擎try匹配b,但是当前位置在字符串的末尾,因此失败。
4abcb备份,以便[bcd]*少匹配一个字符。
5Failure再次tryb,但是当前位置在最后一个字符'd'上。
6abc再次备份,以使[bcd]*仅与bc匹配。
6abcb重试b。这次,当前位置的字符是'b',因此成功。

现在已经到达 RE 的结尾,并且已经匹配abcb。这说明了匹配引擎最初是如何发挥最大作用的,如果找不到匹配项,它将逐步备份并一次又一次地重试 RE 的其余部分。它将一直备份,直到它try对[bcd]*进行零匹配为止,如果随后失败,引擎将得出结论,该字符串根本与 RE 不匹配。

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

还有两个重复的限定词。问号字符?匹配一次或零次;您可以将其视为将某些内容标记为可选内容。例如,home-?brew匹配homebrewhome-brew

最复杂的重复限定词是{m,n},其中* m n 是十进制整数。此限定符意味着必须至少 m 个重复,并且最多 n *个。例如,a/{1,3}b将匹配a/ba//ba///b。它与不带斜线的ab或不带斜线的a////b不匹配。

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

简化主义者的 Reader 可能会注意到,可以使用此符号来表达其他三个限定词。 {0,}*相同,{1,}等效于+,并且{0,1}?相同。尽可能使用*+?更好,这仅仅是因为它们更短且更易于阅读。

使用正则表达式

既然我们已经研究了一些简单的正则表达式,那么我们如何在 Python 中实际使用它们呢? re模块提供了正则表达式引擎的接口,使您可以将 RE 编译为对象,然后对其进行匹配。

编译正则表达式

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

>>> import re
>>> p = re.compile('ab*')
>>> p  
<_sre.SRE_Pattern object at 0x...>

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

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

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

将 RE 放在字符串中可以简化 Python 语言,但是有一个缺点,这是下一部分的主题。

反斜杠瘟疫

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

假设您要编写一个与字符串\section匹配的 RE,该字符串可能在 LaTeX 文件中找到。要弄清楚在程序代码中写什么,请从要匹配的所需字符串开始。接下来,您必须在反斜杠和其他元字符之前加上反斜杠,以转义字符串\\section,以转义它们。必须传递给re.compile()的结果字符串必须是\\section。但是,要将其表示为 Python 字符串 Literals,必须再次再次转义两个反斜杠。

CharactersStage
\section要匹配的文本字符串
\\sectionre.compile()的转义反斜杠
"\\\\section"字符串 Literals 的转义反斜杠

简而言之,要匹配 Literals 反斜杠,必须将'\\\\'作为 RE 字符串编写,因为正则表达式必须为\\,并且每个反斜杠必须在常规 Python 字符串 Literals 中表示为\\。在具有反斜杠重复Function的 RE 中,这会导致许多重复的反斜杠,并使生成的字符串难以理解。

解决方案是对正则表达式使用 Python 的原始字符串表示法。在带有'r'前缀的字符串 Literals 中,反斜杠不会以任何特殊方式进行处理,因此r"\n"是包含'\''n'的两个字符的字符串,而"\n"是包含换行符的一个单字符的字符串。正则表达式通常使用原始字符串表示法以 Python 代码编写。

Regular StringRaw string
"ab*"r"ab*"
"\\\\section"r"\\section"
"\\w+\\s+\\1"r"\w+\s+\1"

Performing Matches

一旦有了表示已编译正则表达式的对象,该如何处理?模式对象具有几种方法和属性。这里仅涵盖最重要的内容;请查阅re文档以获取完整列表。

Method/AttributePurpose
match()确定 RE 是否在字符串开头匹配。
search()扫描字符串,查找该 RE 匹配的任何位置。
findall()查找与 RE 匹配的所有子字符串,并将它们作为列表返回。
finditer()查找 RE 匹配的所有子字符串,并将它们作为iterator返回。

如果找不到匹配项,则match()search()返回None。如果成功,则返回match object实例,其中包含有关匹配的信息:匹配的开始和结束位置,匹配的子字符串等等。

您可以pass与re模块进行交互式实验来了解这一点。如果您有可用的 Tkinter,则可能还需要查看Tools/scripts/redemo.py,这是 Python 发行版中随附的演示程序。它允许您 ImportingRE 和字符串,并显示 RE 匹配还是失败。 redemo.py在try调试复杂的 RE 时非常有用。 Phil Schwartz 的Kodos也是用于开发和测试 RE 模式的交互式工具。

本 HOWTO 的示例使用标准的 Python 解释器。首先,运行 Python 解释器,导入re模块,然后编译 RE:

Python 2.2.2 (#1, Feb 10 2003, 12:57:01)
>>> import re
>>> p = re.compile('[a-z]+')
>>> p  #doctest: +ELLIPSIS
<_sre.SRE_Pattern object at 0x...>

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

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

现在,让我们在它应该匹配的字符串上try,例如tempo。在这种情况下,match()将返回match object,因此您应将结果存储在变量中以备后用。

>>> m = p.match('tempo')
>>> m  
<_sre.SRE_Match object at 0x...>

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

Method/AttributePurpose
group()返回与 RE 匹配的字符串
start()返回 match 的开始位置
end()返回 match 的结束位置
span()返回包含匹配项(开始,结束)位置的 Tuples

try这些方法将很快阐明它们的含义:

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

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

>>> print p.match('::: message')
None
>>> m = p.search('::: message'); print m  
<_sre.SRE_Match object at 0x...>
>>> 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[1]

>>> 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()等的顶级Function。这些函数采用与相应模式方法相同的参数,并添加 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 at 0x...>

这些Function在后台简单地为您创建了一个模式对象,并在其上调用适当的方法。它们还将已编译的对象存储在高速缓存中,因此以后使用相同 RE 的调用会更快。

您应该使用这些模块级别的函数,还是应该自己获取模式并调用其方法?该选择取决于 RE 的使用频率以及您的个人编码风格。如果仅在代码中的某一点使用 RE,则模块Function可能更方便。如果程序包含大量正则表达式,或在多个位置重复使用相同的正则表达式,那么可能值得在一个位置收集所有定义,这是一段代码,可以提前编译所有 RE。以标准库为例,以下是不赞成使用的xmllib模块的摘录:

ref = re.compile( ... )
entityref = re.compile( ... )
charref = re.compile( ... )
starttagopen = re.compile( ... )

我通常更喜欢使用编译后的对象,即使是一次性使用,但很少有人会像我这样纯粹。

Compilation Flags

编译标志使您可以修改正则表达式工作方式的某些方面。 re模块中的标志有两个名称,一个长名称(如IGNORECASE)和一个简短的单字母形式(如I)。 (如果您熟悉 Perl 的模式修饰符,则一个字母的形式使用相同的字母;例如re.VERBOSE的缩写形式为re.X。)可以pass按位或对其进行指定来指定多个标志;例如,re.I | re.M设置IM标志。

这是可用标志的表格,然后是每个标志的更详细说明。

FlagMeaning
DOTALL , S使.匹配任何字符,包括换行符
IGNORECASE , I进行不区分大小写的匹配
LOCALE , L进行语言环境匹配
MULTILINE , M多行匹配,影响^$
VERBOSE , X启用详细的 RE,这些 RE 可以组织得更加整洁和易于理解。
UNICODE , U根据 Unicode 字符数据库进行多个转义,例如\w\b\s\d
  • I

  • IGNORECASE

    • 执行不区分大小写的匹配;字符类和 Literals 字符串将pass忽略大小写来匹配字母。例如,[A-Z]也将匹配小写字母,而Spam将匹配SpamspamspAM。该小写字母未考虑当前的语言环境;如果您还设置了LOCALE标志,它将进行设置。
  • L

  • LOCALE

    • 使\w\W\b\B取决于当前语言环境。

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

通常^仅在字符串的开头匹配,而$仅在字符串的末尾和字符串末尾的换行符(如果有)之前匹配。指定此标志时,^在字符串的开头和字符串中每一行的开头(紧随每个换行符之后)匹配。同样,$元字符在字符串的末尾和每行的末尾(紧接在每个换行符之前)匹配。

  • S

  • DOTALL

    • 使'.'特殊字符完全匹配任何字符,包括换行符;如果没有此标志,则'.'将匹配*除换行符之外的所有内容。
  • U

  • UNICODE

    • 使\w\W\b\B\d\D\s\S取决于 Unicode 字符属性数据库。
  • X

  • VERBOSE

    • 该标志使您可以灵活地设置正则表达式的格式,从而编写更具可读性的正则表达式。指定此标志后,RE 字符串中的空格将被忽略,除非该空格在字符类中或在其前带有未转义的反斜杠;这使您可以更清晰地组织和缩进 RE。此标志还允许您将 Comments 放入 RE 中,而引擎会忽略该 Comments。Comments 用'#'标记,该字符既不在字符类中,也不在其前加反斜杠。

例如,这是一个使用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 的字符串 Literals 自动连接将 RE 分成较小的部分,但是与使用re.VERBOSE的版本相比,它仍然更难于理解。

更多图案效果

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

More Metacharacters

有一些我们尚未介绍的元字符。本节将介绍其中的大多数内容。

尚待讨论的一些元字符是“零宽度 assert”。它们不会导致引擎pass琴弦前进。相反,它们根本不消耗任何字符,只是成功或失败。例如,\b是当前位置位于单词边界的 assert;位置完全不受\b的影响。这意味着零宽度的 assert 永远不应重复,因为如果它们在给定位置匹配一次,显然可以匹配无数次。

  • |

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

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

  • ^

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

例如,如果您只想在行首匹配单词From,则要使用的 RE 是^From

>>> print re.search('^From', 'From Here to Eternity')  
<_sre.SRE_Match object at 0x...>
>>> print re.search('^From', 'Reciting From Memory')
None
  • $

    • 在一行的末尾匹配,该末尾定义为字符串的末尾,或后跟换行符的任何位置。
>>> print re.search('}$', '{block}')  
<_sre.SRE_Match object at 0x...>
>>> print re.search('}$', '{block} ')
None
>>> print re.search('}$', '{block}\n')  
<_sre.SRE_Match object at 0x...>

要匹配 Literals'$',请使用\$或将其包含在字符类中,如[$]所示。

  • \A

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

    • 仅在字符串末尾匹配。
  • \b

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

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

>>> p = re.compile(r'\bclass\b')
>>> print p.search('no class at all')  
<_sre.SRE_Match object at 0x...>
>>> print p.search('the declassified algorithm')
None
>>> print p.search('one subclass is')
None

使用此特殊序列时,您应该记住两个细节。首先,这是 Python 的字符串 Literals 和正则表达式序列之间最严重的冲突。在 Python 的字符串 Literals 中,\b是退格字符,ASCII 值 8.如果您不使用原始字符串,则 Python 会将\b转换为退格,并且您的 RE 将与您期望的不匹配。以下示例与我们之前的 RE 相同,但是Ellipsis了 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 at 0x...>

其次,在字符类中,该 assert 没有用,\b表示退格字符,以与 Python 的字符串 Literals 兼容。

  • \B

    • 另一个零宽度的 assert,与\b相反,仅当当前位置不在单词边界时才匹配。

Grouping

通常,您需要获得的信息不仅仅是 RE 是否匹配。正则表达式通常用于pass编写 RE 来剖析字符串,该 RE 分为几个子组,这些子组与感兴趣的不同部分匹配。例如,RFC-822Headers 行分为 Headers 名称和值,以':'分隔,如下所示:

From: [email protected]
User-Agent: Thunderbird 1.5.0.9 (X11/20061227)
MIME-Version: 1.0
To: [email protected]

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

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

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

'('')'表示的组还捕获了它们匹配的文本的开始和结束索引。可以pass将参数传递给group()start()end()span()来检索。组从 0 开始编号。组 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()可以一次传递多个组号,在这种情况下,它将返回一个 Tuples,其中包含这些组的相应值。

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

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

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

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

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

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

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

非捕获组和命名组

精心制作的 RE 可以使用许多组,既可以捕获感兴趣的子字符串,又可以对 RE 本身进行分组和结构化。在复杂的 RE 中,很难跟踪组号。有两个Function可以帮助解决此问题。两者都对正则表达式扩展使用了通用语法,因此我们首先来看一下。

Perl 5 在标准正则表达式中添加了一些附加Function,而 Python re模块则支持其中的大多数Function。在不使 Perl 的正则表达式与标准 RE 混淆的情况下,选择新的单键元字符或以\开头的新特殊序列来表示新Function将很困难。例如,如果选择&作为新的元字符,则旧表达式将假定&是常规字符,并且不会pass编写\&[&]对其进行转义。

Perl 开发人员选择的解决方案是使用(?...)作为扩展语法。括号后的?是语法错误,因为?无需重复,因此不会带来任何兼容性问题。 ?之后的字符表示正在使用什么 extensions,因此(?=foo)是一回事(正向超前 assert),而(?:foo)是另一回事(不包含子表达式foo的非捕获组)。

Python 在 Perl 的扩展语法中添加了扩展语法。如果问号后的第一个字符是P,那么您将知道它是特定于 Python 的 extensions。当前有两个此类 extensions:(?P<name>...)定义命名组,并且(?P=name)是对命名组的反向引用。如果 Perl 5 的将来版本使用不同的语法添加类似的Function,则re模块将更改为支持新语法,同时为了兼容起见,保留了 Python 特定的语法。

现在,我们已经研究了通用扩展语法,我们可以返回简化在复杂 RE 中使用组的Function。由于组是从左到右编号的,并且一个复杂的表达式可能会使用许多组,因此很难跟踪正确的编号。修改这样一个复杂的 RE 也很烦人:在开始处插入一个新组,然后更改其后的所有内容的数量。

有时,您可能希望使用组来收集正则表达式的一部分,但对检索组的内容不感兴趣。您可以pass使用非捕获组(?:...)来使这一事实明确,您可以在其中将...替换为任何其他正则表达式。

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

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

一个更重要的Function是命名组:组可以用名称来引用,而不是用数字来引用。

命名组的语法是特定于 Python 的扩展之一:(?P<name>...)。 * name *显然是组的名称。命名组的行为也与捕获组完全相同,并且还将名称与组相关联。处理捕获组的match object方法全部接受pass数字引用组的整数或包含所需组名称的字符串。命名的组仍具有编号,因此您可以pass两种方式检索有关组的信息:

>>> 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\b的正则表达式也可以写成\b(?P<word>\w+)\s+(?P=word)\b

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

Lookahead Assertions

另一个零宽度 assert 是超前 assert。前瞻性 assert 可以使用肯定形式也可以使用否定形式,如下所示:

  • (?=...)

    • 积极的前瞻性 assert。如果包含的正则表达式(在此由...表示)在当前位置成功匹配,则成功,否则失败。但是,一旦try了包含的表达式,匹配引擎将根本无法前进。模式的其余部分在 assert 开始的地方try。
  • (?!...)

    • 否定超前 assert。这与肯定的主张相反。如果所包含的表达式*与字符串的当前位置不匹配,则会成功。

为了使这一点具体,让我们看一个先行有用的情况。考虑一个简单的模式来匹配文件名,并将其拆分为基本名称和 extensions,并以.分隔。例如,在news.rc中,news是基本名称,而rc是文件名的 extensions。

与此匹配的模式非常简单:

.*[.].*$

请注意,.是元字符,因此需要特殊对待。我把它放在一个字符类中。还要注意尾随的$;这样做是为了确保 extensions 中必须包含其余所有字符串。此正则表达式匹配foo.barautoexec.bat以及sendmail.cfprinters.conf

现在,考虑使问题复杂化一些;如果要匹配 extensions 不是bat的文件名怎么办?一些不正确的try:

.*[.][^b].*$上面的第一次trypass要求 extensions 的第一个字符不是b来排除bat。这是错误的,因为模式也与foo.bar不匹配。

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

当您trypass匹配以下情况之一来修补第一个解决方案时,表达式变得更混乱:extensions 的第一个字符不是b;第二个字符不是a;或第三个字符不是t。它接受foo.bar并拒绝autoexec.bat,但是它需要三个字母的 extensions,并且不接受带有两个字母的 extensions 的文件名,例如sendmail.cf。我们将再次使该模式复杂化,以进行修复。

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

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

该模式现在变得非常复杂,这使其难以阅读和理解。更糟糕的是,如果问题发生了变化,并且您想同时排除batexe作为 extensions,则该模式将变得更加复杂和混乱。

负面的前瞻消除了所有这些困惑:

.*[.](?!bat$)[^.]*$否定的超前表示意味着:如果此时bat表达式不匹配,则try其余模式。如果bat$匹配,则整个模式将失败。必须使用尾随$来确保允许使用类似sample.batch的 extensions(其中 extensions 仅以bat开头)。 [^.]*确保文件名中包含多个点时该模式有效。

现在,排除另一个文件 extensions 很容易;只需将其添加为 assert 内的替代项即可。以下模式不包括以batexe结尾的文件名:

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

Modifying Strings

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

Method/AttributePurpose
split()将字符串拆分为列表,在 RE 匹配的任何位置拆分
sub()查找与 RE 匹配的所有子字符串,并用其他字符串替换
subn()做与sub()相同的操作,但返回新字符串和替换次数

Splitting Strings

模式的split()方法在 RE 匹配的任何地方将字符串分开,返回片段列表。它类似于split()字符串方法,但是在分隔符中提供了更多通用性; split()仅支持按空格或固定字符串分割。如您所料,还有模块级的re.split()函数。

  • . split(* string * [,* maxsplit = 0 *])

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

您可以pass传递* 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 *])

    • 返回pass用替换* replacement 替换 string 中最左边的 RE 出现的 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()方法执行相同的工作,但是返回一个包含新字符串值和已执行的替换次数的 2Tuples:

>>> 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被转换为回车,依此类推。诸如\j之类的未知转义字符被单独保留。反向引用(例如\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 的引用,后跟 Literals 字符'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 是一个函数,则对于 pattern *的每一次非重叠出现都会调用该函数。在每次调用时,都会为该函数传递一个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

对于某些应用程序而言,正则表达式是一个Function强大的工具,但是在某些方面,它们的行为不直观,有时甚至无法按照您期望的方式运行。本节将指出一些最常见的陷阱。

使用字符串方法

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

一个示例可能是将一个固定字符串替换为另一个。例如,您可以将word替换为deedre.sub()似乎是用于此Function的函数,但请考虑replace()方法。请注意,replace()还将替换单词中的word,将swordfish转换为sdeedfish,但是幼稚的 RE word也会这样做。 (为避免对单词的某些部分执行替换,模式必须为\bword\b,以要求word在两侧具有单词边界.这使工作超出replace()的能力.)

另一个常见的任务是从字符串中删除每个出现的单个字符或将其替换为另一个单个字符。您可以使用re.sub('\n', ' ', S)之类的方法来执行此操作,但是translate()可以完成这两项任务,并且比任何正则表达式操作都快。

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

match()函数仅检查 RE 是否在字符串的开头匹配,而search()将向前搜索字符串以查找匹配项。请记住这一点,这一点很重要。请记住,match()仅会报告从 0 开始的成功匹配;如果匹配不是从零开始,则match()不会*报告。

>>> print re.match('super', 'superstition').span()
(0, 5)
>>> print re.match('super', 'insuperable')
None

另一方面,search()将向前搜索该字符串,并报告找到的第一个匹配项。

>>> print re.search('super', 'superstition').span()
(0, 5)
>>> print re.search('super', 'insuperable').span()
(2, 7)

有时,您会很想 continue 使用re.match(),而只是将.*添加到 RE 的前面。抵制这种诱惑,改用re.search()。正则表达式编译器会对 RE 进行一些分析,以加快查找匹配项的过程。一个这样的分析指出了 match 的第一个字符是什么。例如,以Crow开头的模式必须与以'C'开头的模式匹配。pass分析,引擎可以快速扫描字符串以查找起始字符,仅在找到'C'的情况下才try完全匹配。

贪婪与不贪婪

当像a*一样重复一个正则表达式时,结果是要消耗尽可能多的模式。当您试图匹配Pair平衡的定界符(例如,围绕 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 中还有更多剩余空间,并且>在字符串末尾无法匹配,因此正则表达式引擎必须逐字符回退,直到找到与>匹配的字符。finally匹配从<html>中的'<'扩展到</title>中的'>',这不是您想要的。

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

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

(请注意,使用正则表达式解析 HTML 或 XML 是很痛苦的.快捷模式可以处理常见的情况,但是 HTML 和 XML 具有特殊的情况,它们会破坏明显的正则表达式;在编写正则表达式时处理所有可能的情况,这些模式将非常复杂.请使用 HTML 或 XML 解析器模块执行此类任务.)

Using re.VERBOSE

到目前为止,您可能已经注意到,正则表达式是一种非常紧凑的表示法,但是可读性却不高。中等复杂度的 RE 可能会成为反斜杠,括号和元字符的冗长集合,使其难以阅读和理解。

对于此类 RE,在编译正则表达式时指定re.VERBOSE标志可能会有所帮助,因为它使您可以更清晰地格式化正则表达式。

re.VERBOSE标志具有多种作用。正则表达式中在字符类中不是*的空格将被忽略。这意味着诸如dog | cat之类的表达式等效于可读性较差的dog|cat,但[a b]仍将与字符'a''b'或空格匹配。另外,您还可以在 RE 中放入 Comments;Comments 从#字符扩展到下一个换行符。与三引号字符串一起使用时,这可使 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

正则表达式是一个复杂的 Topic。本文档是否有助于您理解它们?是否存在不清楚的部分,或者您遇到的未在此处解决的问题?如果是这样,请向作者发送改进建议。

关于正则表达式最完整的书几乎可以肯定是 O'Reilly 出版的 Jeffrey Friedl 的 Mastering Regular Expressions。不幸的是,它只专注于 Perl 和 Java 的正则表达式风格,并且根本不包含任何 Python 材料,因此它不能用作 Python 编程的参考。 (第一版介绍了 Python 现在已删除的regex模块,该模块对您无济于事.)请考虑从您的库中检出它。

Footnotes

  • [1]
    • 在 Python 2.2.2 中引入。