正则表达式基础

正则表达式基础

作者:A.M. Kuchling <<amk@amk.ca>
原文网址:https://docs.python.org/2/howto/regex.html#regex-howto

摘要

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

介绍

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

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

正则表达式模式被编译成一系列字节码,然后由用C编写的匹配引擎执行。对于高级用途,可能需要特别注意引擎如何执行给定的RE,并将RE写入以某种方式生成运行速度更快的字节码。本文档不涉及优化,因为它要求您充分了解匹配引擎的内部结构。

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

简单模式

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

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

匹配字符

大多数字母和字符只会匹配自己。例如,正则表达式testtest完全匹配字符串。(您可以启用一个不区分大小写的模式,让这个RE匹配Test或者TEST 以后更多关于此的更多信息。)

这条规则有例外; 有些字符是特殊的 元字符,并且与自身不匹配。相反,它们表示应该匹配一些与众不同的东西,或者通过重复它们或改变它们的含义来影响RE的其他部分。本文档的大部分内容都致力于讨论各种元字符及其功能。

这是元字符的完整列表; 他们的意思将在本基础知识的其余部分讨论。

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

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

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

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

也许最重要的元字符是反斜杠,\。与Python字符串文字一样,反斜杠后面可以跟各种字符,以指示各种特殊序列。它也用于转义所有元字符,因此您仍然可以在模式中匹配它们; 例如,如果你需要匹配一个[ 或者 \,你可以在它们之前用反斜杠来删除它们的特殊含义:\[\\

一些以特征序列开头的特殊序列'\'表示通常有用的预定义字符集,例如数字集,字母集或任何非空格的集合。以下预定义的特殊序列是可用的特定序列的子集。等效类用于字节串模式。有关Unicode字符串模式的序列和扩展类定义的完整列表,请参阅正则表达式语法的最后一部分 。

  • \d

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

  • \D

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

  • \s

    匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。注意 Unicode 正则表达式会匹配全角空格符。

  • \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

步骤匹配内容说明
1aa在RE匹配。
2abcbd引擎匹配[bcd]*,尽可能地到达字符串abcd的末尾d
3匹配失败引擎尝试匹配 b,但当前位置位于字符串的末尾,因此失败。
4abcb返回第二个步骤,减少一个子表达式 [bcd]* 所匹配的字符。
5匹配失败再次匹配b一次,但当前位置是最后一个字符是一个'd'。匹配失败。
6abc再次匹配返回第二步骤,再次减少一个子表达式 [bcd]*这次所匹配 的是bc
6abcb再次匹配b一次。这次是当前位置的角色'b',所以它成功了。

现在已经达到RE的结束,并且它已经匹配abcb。这演示了匹配引擎最初是如何进行的,如果没有找到匹配,它将逐步匹配并一次又一次地重试RE的其余部分。直到它尝试零匹配为止 [bcd]*,如果随后失败,引擎将断定该字符串与RE完全不匹配。

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

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

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

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

喜欢简化的读者可能会注意到其他三个限定符都可以用这种表示法表达。 {0,}是相同的*{1,} 相当于+,和{0,1}是一样的?。最好使用*+或者?为什么? ,只是因为它们更短,更容易阅读。

使用正则表达式

现在我们已经看了一些简单的正则表达式,我们如何在Python中实际使用它们?该re模块提供了正则表达式引擎的接口,允许您将RE编译为对象,然后执行匹配。

编译正则表达式

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

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

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

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

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

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

反斜杠的烦恼

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

假设您要编写与字符串匹配的RE,该字符串\section可能位于LaTeX文件中。要找出在程序代码中写入的内容,就要匹配字符串。接下来,您必须通过在它们前面加上反斜杠来转义任何反斜杠和其他元字符,从而产生字符串\\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匹配的所有子字符串,并将它们作为迭代器返回。

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

您可以通过交互式试验re 模块来了解这一点。如果您有Tkinter可用,您可能还需要查看 Tools / scripts / redemo.py,这是Python发行版附带的演示程序。它允许您输入RE和字符串,并显示RE是匹配还是失败。redemo.py在尝试调试复杂的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...>

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

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

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

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

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

方法/属性目的
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 at 0x...>
>>> 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()方法返回一系列 匹配对象实例作为迭代器。[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)

模块级函数

您不必创建模式对象并调用其方法; 该 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 at 0x...>

在幕后,这些函数只是为您创建一个模式对象,并在其上调用适当的方法。它们还将编译对象存储在缓存中,因此使用相同RE的未来调用会更快。

你应该使用这些模块级函数,还是应该先获取模块再调用它的方法?该选择取决于RE的使用频率以及您的个人编码风格。如果RE仅在代码中的一个点使用,则模块功能可能更方便。如果程序包含许多正则表达式,或者在多个位置重用相同的表达式,则可能值得在一个地方收集所有定义,在一段代码中提前编译所有RE。要从标准库中获取示例,以下是不推荐使用的xmllib模块的摘录:

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

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

编译标志

编译标志允许您修改正则表达式的工作方式。re模块中的标志有两个名称,一个长名称,如 IGNORECASE一个短的单字母形式,如I。(如果你熟悉Perl的图案修饰,单字母形式使用同样的字母;短形式re.VERBOSE就是re.X。)多个标志可以通过按位或|指定。例如,设置标志:re.I | re.M``I``M

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

标志含义
DOTALLS使.匹配任何字符,包括换行符
IGNORECASEI不区分大小写的匹配
LOCALEL设置适应多国语言感知匹配
MULTILINEM多线匹配,影响^$
VERBOSEX启用详细的RE,可以更干净,更容易理解。
UNICODEU根据Unicode字符集进行转义如\w\b\s\d
  • I

  • IGNORECASE

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

  • L

  • LOCALE

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

  • M

  • MULTILINE

    ^$尚未解释,他们会在 更多的元字符中介绍。)通常^仅在字符串的开头匹配,$仅匹配字符串的末尾,并且紧跟在字符串末尾的换行符(如果有)之前。指定此标志^时,在字符串的开头和字符串中每行的开头匹配,紧跟在每个换行符之后。类似地,$元字符匹配字符串的结尾和每行的结尾(紧接在每个换行符之前)。

  • S

  • DOTALL

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

  • U

  • UNICODE

    \w\W\b\B\d\D\s\S 依赖于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

更多模式

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

 

更多元字符

我们还没有涉及到一些元字符。其中大部分内容将在本节中介绍。

要讨论的一些剩余元字符是零宽界定符。它们并不会使引擎在处理字符串时更快;相反,它们根本就没有对应任何字符,只是成功或失败。例如,\b 是一个在单词边界定位当前位置的界定符; 这个位置根本就不会被\b改变。这意味着零宽界定符永远不会重复,因为如果它们在给定位置匹配一次,它们显然可以无限次匹配。

  • |

    可选,或“or”运算符。如果A和B是正则表达式, A|B将匹配任何字符串AB|优先级非常低,以便在使用多字符字符串时使其选择适当的字符运行。Crow|Servo将匹配CrowServo不匹配Croa 'w'或an 'S',和ervo。要匹配文字'|',请使用\|或将其括在字符类中,如[|]

  • ^

    在行的开头匹配。除非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...>

    要匹配文字'$',请使用\$或在字符类中使用将其括起来,如 [$]

  • \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 at 0x...>

    >>> 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 at 0x...>

    其次,在字符类中,对此限定符没有用处, \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\b')
>>> p.search('Paris in the the spring').group()
'the the'

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

非捕获和命名组

精心设计的RE可以使用许多组,既可以捕获感兴趣的子串,也可以对RE本身进行分组和构建。在复杂的RE中,很难跟踪组号。有两个功能可以帮助解决这个问题。它们都使用常规表达式扩展的通用语法,所以我们先看一下。

Perl 5为标准正则表达式添加了几个附加功能,Python re模块支持其中的大多数。选择新的单键击元字符或新的特殊序列以\来表示新功能并不使Perl的正则表达式与标准RE容易混淆是很困难的。例如,如果您选择&作为新的元字符,那么旧的表达式将假设它&是一个普通字符,并且不会通过写入\&[&]转义它。

Perl开发人员选择的解决方案是(?...)用作扩展语法。 ?括号后立即出现语法错误,因为?没有任何重复,所以这不会引起任何兼容性问题。紧跟在? 之后的字符指出扩展的用途,因此(?=foo)是一回事(一个积极的先行界定符)而(?:foo)是另一回事(包含子表达式的非捕获组foo)。

Python 新增了一个扩展语法到 Perl 扩展语法中。如果问号后面的第一个字符是一个 P,则表示它是特定于Python的扩展名。目前有两种这样的扩展: (?P<name>...)定义一个命名组,而(?P=name)是对命名组的反向引用。如果Perl 5的未来版本使用不同的语法添加类似的功能,re则将更改模块以支持新语法,同时为了兼容性而保留特定Python的语法。

现在我们已经看了一般的扩展语法,我们可以回到简化复杂RE中的组处理的功能。由于组从左到右编号,并且复杂表达式可能使用许多组,因此很难跟踪正确的编号。修改这样一个复杂的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)')             #word是组名
>>> 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'

前向界定符

另一个零宽度界定符是前向界定符。包括前向肯定界定符和前项否定界定符,看起来像这样:

  • (?=...)

    前向肯定界定符。如果包含的正则表达式(在此处的...)在当前位置成功匹配,则成功,否则失败。但是,一旦尝试了包含的表达式,匹配引擎就不会前进; 模式的其余部分是在界定符开始时尝试的。

  • (?!...)

    前项否定界定符。这与前向肯定界定符相反; 如果包含的表达式在字符串中的当前位置匹配,则成功。

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

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

.*[.].*$

请注意,.需要特别对待,因为它是元字符; 我把它放在一个字符类里面。还要注意尾部$; 添加此项以确保扩展名中的所有其余字符串都必须包含在扩展名中。这个正则表达式匹配foo.barautoexec.batsendmail.cfprinters.conf

现在,考虑使问题复杂化; 如果你想匹配扩展名不在(bat)的文件名怎么办?一些错误的尝试:

.*[.][^b].*$ 上面的第一次尝试尝试bat通过要求扩展的第一个字符不是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$)[^.]*$

修改字符串

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

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

拆分字符串

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

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

    通过正则表达式的匹配拆分字符串。如果在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()方法用于替换值,可以是字符串或函数,也可以是要处理的字符串。

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

    返回通过替换符串中* RE的最左边非重叠事件而获得的字符串。如果未找到模式,则返回字符串不变。可选参数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-'

如果替换的是一个字符,任何在其中的反斜杠都会被处理。也就是说,\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的引用,而不是对后面跟着一个字母 "0" 的组 2 的引用。)以下替换都是等效的,但使用替换字符串的所有三种变体。

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

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

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

>>> 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循环,而不是大型,更通用的正则表达式引擎。

举个用一个固定字符串替换另一个的例子; 例如,您可以替换worddeedre.sub()看起来像是用于此的功能,但请考虑该replace()方法。请注意, replace()这也将取代word内部单词,swordfish 变成sdeedfish,但天真的RE word也会这样做。(为了避免对单词的部分进行替换,必须使用模式\bword\b,以便要求word在任何一方都有一个单词边界。这使得工作超出了 replace()能力。)

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

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

match()与search()

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)

有时你可能倾向于使用re.match(),只需添加.*`` 到你的RE前面。请尽量不要这么做,最好采用 re.search() 代替之。正则表达式编译器对RE进行一些分析,以加快寻找匹配的过程。一个那样的分析机会指出匹配的第一个字符是什么; 例如,模式Crow必须从 "C"开始匹配。分析机可以让引擎快速扫描字符串以找到开始字符,并只在"C" 被发现后才开始全部匹配。

添加.*会使此优化失败,需要扫描到字符串的末尾,然后回溯以找到RE的其余部分的匹配项。请 re.search()改用。

贪婪与非贪婪

当重复一个正则表达式时,如用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}?,尽可能匹配小的文本。在上面的例子中,'>'在第一次'<'匹配之后立即尝试,并且当它失败时,引擎一次增加一个字符,并在每步重试">"。这个处理将得到正确的结果:

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

(请注意,使用正则表达式解析HTML或XML很痛苦。变化混乱的模式将处理常见情况,但HTML和XML有特殊情况会破坏明显的正则表达式;当您编写正则表达式去处理所有可能的情况,模式将非常复杂。使用HTML或XML解析器模块来执行此类任务。)

使用re.VERBOSE

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

对于此类RE,re.VERBOSE在编译正则表达式时指定标志可能会有所帮助,因为它允许你可以编辑正则表达式的格式使之更清楚。

re.VERBOSE标志有几个方面的影响。包含在字符类中的正则表达式中的空格将被忽略。这就意味着象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模块,对您没有多大帮助。)考虑从您的库中查看它。

脚注

[1]在Python 2.2.2中引入。
  

[2]^[^指定字符串] 之间的区别:

^ 指的是匹配字符串开始的位置

[^指定字符串] 指的是除指定字符串以外的其他字符串

(^[0-9])+     //匹配有一至多个数字的字符串组合
[^[0-9]]+  // 匹配有一至多个不含数字的字符串组合

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值