Skip to content

Latest commit

 

History

History
1099 lines (750 loc) · 41.2 KB

File metadata and controls

1099 lines (750 loc) · 41.2 KB

二、Python 正则表达式

在上一章中,我们已经了解了泛型正则表达式是如何工作的。在本章中,我们将向您介绍 Python 提供的所有使用正则表达式的操作,以及 Python 如何处理正则表达式。

要做到这一点,我们将看到该语言在处理正则表达式时的一些怪癖,不同类型的字符串,它通过RegexObjectMatchObject类提供的 API,我们可以通过许多示例深入处理它们的每个操作,以及用户通常面临的一些问题。最后,我们将看到 Python 和其他正则表达式引擎之间以及 Python 2 和 Python 3 之间的细微差别。

简介

自 1.5 版以来,Python 提供了一个 Perl 风格的正则表达式,其中有一些细微的异常,我们将在后面看到。要搜索的模式和字符串可以是Unicode字符串,也可以是 8 位字符串(ASCII

提示

Unicode 是一种通用编码,包含超过 110.00 个字符和 100 个脚本,用于表示世界上所有的活字符,甚至历史脚本。您可以将其视为数字或被称为代码点的代码点与字符之间的映射。所以,我们可以用一个数字来表示每一个字符,不管用哪种语言。例如,字符A brief introduction是数字 26159,在 Python 中表示为\u662f(十六进制)。

re模块支持正则表达式。因此,与 Python 中的所有模块一样,我们只需要导入它就可以开始使用它们。为此,我们需要使用以下代码行启动 Python 交互式 shell:

>>> import re

导入模块后,我们可以开始尝试匹配模式。为此,我们需要编译一个模式,将其转换为字节码,如下面的代码行所示。这个字节码稍后将由用 C 编写的引擎执行。

>>> pattern = re.compile(r'\bfoo\b')

提示

字节码是一种中间语言。它是由语言生成的输出,稍后将由解释器进行解释。JVM 解释的 Java 字节码可能是最有名的例子。

一旦获得了编译后的模式,我们可以尝试将其与字符串匹配,如下代码所示:

>>> pattern.match("foo bar")
<_sre.SRE_Match at 0x108acac60>

正如我们在前面的示例中提到的,我们编译了一个模式,然后搜索该模式是否与文本foo bar匹配。

在命令行中使用 Python 和正则表达式非常容易,可以执行快速测试。您只需要启动 python 解释器并导入前面提到的re模块。但是,如果您喜欢使用 GUI 来测试正则表达式,可以通过以下链接下载一个用 Python 编写的 GUI:

http://svn.python.org/view/签出/python/trunk/Tools/scripts/redemo.py?内容类型=文本%2fplane

有许多在线工具,如上的工具 https://pythex.org/ 以及我们将在第 5 章正则表达式性能中介绍的桌面程序,如 RegexBuddy。

在这一点上,最好使用口译员与他们取得流利的交流,并获得直接的反馈。

字符串文字中的反斜杠

正则表达式不是核心 Python 语言的一部分。因此,它们没有特殊的语法,因此它们被当作任何其他字符串处理。正如我们在第一章引入正则表达式中看到的,反斜杠字符\用于表示正则表达式中的元字符或特殊形式。反斜杠也在字符串中用于转义特殊字符。换句话说,它在 Python 中有一个特殊的含义。因此,如果我们需要使用\字符,我们必须转义它:\\。这将为反斜杠赋予字符串字面意义。但是,为了在正则表达式内部匹配,我们应该避开反斜杠,有效地编写四个反斜杠:\\\\

举个例子,让我们编写一个正则表达式来匹配\

>>> pattern = re.compile("\\\\")
>>> pattern.match("\\author")
<_sre.SRE_Match at 0x104a88e68>

正如您所看到的,当模式很长时,这是乏味且难以理解的。

Python 提供了原始字符串符号r,反斜杠被视为普通字符。所以,r"\b"不再是退格;只是角色\ 和角色b,而r"\n"也是如此。

Python2.x 和 Python3.x 对字符串的处理方式不同。在 Python2 中,有两种类型的字符串,8 位字符串和 Unicode 字符串;在 Python3 中,我们有文本和二进制数据。文本始终为 Unicode,编码的 Unicode 表示为二进制数据(http://docs.python.org/3.0/whatsnew/3.0.html#text-vs-data-instead-unicode-vs-8 位

字符串有特殊的符号表示我们使用的类型。

字符串 Python 2.x

|

类型

|

前缀

|

描述

| | --- | --- | --- | | 一串 |   | 字符串字面值。它们是使用默认编码(在我们的例子中是 UTF-8)自动编码的。反斜杠是转义有意义字符所必需的。

>>>"España \n"
'Espa\xc3\xb1a \n'

| | 原始字符串 | rR | 除了反斜杠(被视为普通字符)之外,它们与文本字符串相同。

>>>r"España \n"
'Espa\xc3\xb1a \\n'

| | Unicode 字符串 | uU | 这些字符串使用 Unicode 字符集(ISO 10646)。

>>>u"España \n"
u'Espa\xf1a \n'

| | Unicode 原始字符串 | urUR | 它们是 Unicode 字符串,但将反斜杠视为正常的原始字符串。

>>>ur"España \n"
u'Espa\xf1a \\n'

|

转到Python 3 中的新功能部分,了解 Python 3 中的符号是如何使用的

使用原始字符串是 Python 官方文档之后推荐的选项,在本书中我们将使用 Python2.7。因此,考虑到这一点,我们可以将正则表达式重写为如下:

>>> pattern = re.compile(r"\\")
>>> pattern.match(r"\author")
<_sre.SRE_Match at 0x104a88f38>

Python 正则表达式的构建块

在 Python 中,有两个不同的对象处理正则表达式:

  • RegexObject:也称为图案对象。它表示已编译的正则表达式
  • MatchObject:表示匹配的图案

RegexObject

为了开始匹配模式,我们必须编译正则表达式。Python 为我们提供了一个接口来实现这一点,正如我们前面所看到的。结果将是一个模式对象或RegexObject。此对象有几种方法用于正则表达式的典型操作。正如我们稍后将看到的,re模块为每个操作提供了一个速记,因此我们可以避免先编译它。

>>> pattern = re.compile(r'fo+')

正则表达式的编译生成一个可重用的模式对象,该对象提供了所有可以执行的操作,例如匹配模式和查找与特定正则表达式匹配的所有子字符串。因此,例如,如果我们想知道字符串是否以<HTML>开头,我们可以使用以下代码:

>>> pattern = re.compile(r'<HTML>')
>>> pattern.match("<HTML>")
   <_sre.SRE_Match at 0x108076578>

匹配模式和执行与正则表达式相关的操作有两种方式。我们可以编译一个模式,给我们一个RegexObject,或者我们可以使用模块操作。让我们在下面的示例中比较这两种不同的机制。

如果我们想重复使用正则表达式,可以使用以下代码:

>>> pattern = re.compile(r'<HTML>')
>>> pattern.match("<HTML>")

另一方面,我们可以使用以下代码行直接在模块上执行操作:

>>> re.match(r'<HTML>', "<HTML>")

re模块为RegexObject中的每个操作提供包装。您可以将它们视为快捷方式。

在内部,这些包装器创建RegexObject,然后调用相应的方法。您可能想知道,是否每次调用其中一个包装器时,它都会首先编译正则表达式。答案是否定的。re模块缓存已编译的模式,以便在将来的调用中不必再次编译。

注意程序的内存需求。当您使用模块操作时,您无法控制缓存,因此最终可能会占用大量内存。您始终可以使用re.purge清除缓存,但这是性能的折衷。使用编译后的模式可以对内存消耗进行细粒度控制,因为您可以决定何时清除它们。

不过,这两种方式之间存在一些差异。使用RegexObject,可以限制将在其中搜索模式的区域,例如限制索引 2 和 20 处的字符之间的模式搜索。除此之外,您还可以使用模块中的操作在每次调用中设置flags。但是要小心,;每次更改标志时,都会编译和缓存一个新模式。

让我们深入了解可以使用模式对象完成的最重要的操作。

搜索

让我们看看我们必须在字符串中寻找模式的操作。注意 python 有两个操作,匹配和搜索;许多其他语言都有一个匹配项。

匹配(字符串[,位置[,结束位置]])

此方法尝试仅在字符串开头匹配已编译的模式。如果存在匹配项,则返回一个MatchObject。例如,让我们尝试匹配字符串是否以<HTML>开头:

>>> pattern = re.compile(r'<HTML>')
>>> pattern.match("<HTML><head>")
<_sre.SRE_Match at 0x108076578>

在前面的示例中,我们首先编译了模式,然后在<HTML><head>字符串中找到了匹配项。

让我们看看当字符串不以<HTML>开头时会发生什么,如以下代码行所示:

>>> pattern.match("⇢<HTML>")
    None

正如你所看到的,没有对手。记住我们之前说过的,match尝试在字符串开头匹配。与模式不同,字符串以空格开头。注意以下示例中与search的区别:

>>> pattern.search("⇢<HTML>")
<_sre.SRE_Match at 0x108076578>

正如所料,我们有一场比赛。

可选的pos参数指定从何处开始搜索,如下代码所示:

>>> pattern = re.compile(r'<HTML>')
>>> pattern.match("⇢ ⇢ <HTML>")
    None
>>> pattern.match("⇢ ⇢ <HTML>", 2)
 <_sre.SRE_Match at 0x1043bc850>

在突出显示的代码中,我们可以看到模式如何匹配,即使字符串中有两个空格。这是可能的,因为我们已经将位置设置为2,所以匹配操作开始在该位置搜索。

请注意,pos大于 0 并不意味着字符串从该索引开始,例如:

>>> pattern = re.compile(r'^<HTML>')
>>> pattern.match("<HTML>")
   <_sre.SRE_Match at 0x1043bc8b8>
>>> pattern.match("⇢ ⇢ <HTML>",  2)
    None

在前面的代码中,我们创建了一个模式来匹配字符串,其中“开始”后的第一个字符后跟<HTML>。之后,我们尝试匹配从第二个字符<开始的字符串<HTML>。没有匹配,因为模式试图首先匹配位于2位置的^ 元字符。

提示

锚字符提示

字符^$分别表示字符串的开始和结束。您既不能在字符串中看到它们,也不能写入它们,但它们始终存在,并且是正则表达式引擎的有效字符。

请注意,如果我们将字符串分为两个位置,结果会有所不同,如下代码所示:

>>> pattern.match("⇢ ⇢ <HTML>"[2:])
   <_sre.SRE_Match at 0x1043bca58>

切片给了我们一个新的字符串;因此,其中有一个^ 元字符。相反,pos只是将索引移动到字符串中搜索的起点。

第二个参数endpos设置模式在字符串中尝试匹配的距离。在以下情况下,它相当于切片:

>>> pattern = re.compile(r'<HTML>')
>>> pattern.match("<HTML>"[:2]) 
    None
>>> pattern.match("<HTML>", 0, 2) 
    None

所以,在下面的例子中,位置没有提到的问题。即使使用了$元字符,也存在匹配:

>>> pattern = re.compile(r'<HTML>$')
>>> pattern.match("<HTML>⇢", 0,6)
<_sre.SRE_Match object at 0x1007033d8>
>>> pattern.match("<HTML>⇢"[:6])
<_sre.SRE_Match object at 0x100703370>

如所示,切片与endpos之间没有区别。

搜索(字符串[,位置[,结束位置]])

此操作类似于许多语言的匹配,例如 Perl。它尝试在字符串的任何位置匹配模式,而不仅仅是在开头。如果存在匹配项,则返回一个MatchObject

>>> pattern = re.compile(r"world")
>>> pattern.search("hello⇢world")
   <_sre.SRE_Match at 0x1080901d0>
>>> pattern.search("hola⇢mundo ")
    None

posendpos参数的含义与match操作中的含义相同。

注意使用MULTILINE标志,^符号在字符串的开头和每行的开头匹配(稍后我们将看到更多关于该标志的内容)。因此,它改变了search的行为。

在下面的示例中,第一个search匹配<HTML>,因为它位于字符串的开头,但第二个search不匹配,因为字符串以空格开头。最后,在第三个search中,由于re.MULTILINE,我们在新线之后找到了一个匹配的<HTML>

>>> pattern = re.compile(r'^<HTML>', re.MULTILINE)
>>> pattern.search("<HTML>")
   <_sre.SRE_Match at 0x1043d3100>
>>> pattern.search("⇢<HTML>")
   None
>>> pattern.search("⇢ ⇢\n<HTML>")
   <_sre.SRE_Match at 0x1043bce68>

因此,只要的pos参数小于或等于新行,就会有一个匹配。

>>> pattern.search("⇢ ⇢\n<HTML>",  3)
  <_sre.SRE_Match at 0x1043bced0>
>>> pattern.search('</div></body>\n<HTML>', 4)
  <_sre.SRE_Match at 0x1036d77e8>
>>> pattern.search("  \n<HTML>", 4)
   None

findall(字符串[,位置[,结束位置]])

之前的操作一次只能处理一场比赛。相反,在本例中,它返回一个列表,其中包含一个模式的所有非重叠出现,而不是像searchmatch这样的MatchObject

在下面的示例中,我们将查找字符串中的每个单词。因此,我们得到一个列表,其中的每一项都是找到的模式,在本例中是一个单词。

>>> pattern = re.compile(r"\w+")
>>> pattern.findall("hello⇢world")
    ['hello', 'world']

请记住,空匹配是结果的一部分:

>>> pattern = re.compile(r'a*')
>>> pattern.findall("aba")
    ['a', '', 'a', '']

我打赌你一定想知道这里发生了什么?技巧来自*量词,它允许前面的正则表达式重复 0 次或更多次;同样的情况也发生在?量词上。

>>> pattern = re.compile(r'a?')
>>> pattern.findall("aba")
    ['a', '', 'a', '']

基本上,它们都匹配表达式,即使找不到前面的正则表达式:

findall(string[, pos[, endpos]])

芬德尔匹配过程

首先,正则表达式匹配字符a,然后跟在b后面。由于*量词,即空字符串,存在匹配。之后,它匹配另一个a,最后尝试匹配$。正如我们前面提到的,即使您看不到$,它也是正则表达式引擎的有效字符。正如发生在b上一样,它与*量词匹配。

我们已经在第一章中深入了解了量词,介绍了正则表达式

如果模式中有组,它们将作为元组返回。字符串是从左到右扫描的,因此组的返回顺序与找到组的顺序相同。

下面的示例尝试匹配由两个单词组成的模式,并为每个单词创建一个组。这就是为什么我们有一个元组列表,其中每个元组有两个组。

>>> pattern = re.compile(r"(\w+) (\w+)")
>>> pattern.findall("Hello⇢world⇢hola⇢mundo")
    [('Hello', 'world'), ('hola', 'mundo')]

findall操作和groups是另一件让很多人困惑的事情。在第 3 章中,我们专门用一个完整的章节来解释这个复杂的主题。

查找器(字符串[,位置[,结束位置]])

它的工作本质上与findall相同,但它返回一个迭代器,其中每个元素都是MatchObject,所以我们可以使用这个对象提供的操作。因此,当您需要每个匹配的信息时,它非常有用,例如子字符串的匹配位置。好几次,我发现自己用它来理解findall中发生了什么。

让我们回到我们最初的一个例子。匹配每两个单词并捕捉它们:

>>> pattern = re.compile(r"(\w+) (\w+)")
>>> it = pattern.finditer("Hello⇢world⇢hola⇢mundo")
>>> match = it.next()
>>> match.groups()
    ('Hello', 'world')
>>> match.span()
    (0, 11)

在前面的示例中,我们可以看到如何获得包含所有匹配项的迭代器。对于迭代器中的每个元素,我们得到一个MatchObject,因此我们可以在模式中看到捕获的组,在本例中为两个。我们还将获得比赛的位置。

>>> match = it.next()
>>> match.groups()
    ('hola', 'mundo')
>>> match.span()
    (12, 22)

现在,我们使用迭代器中的另一个元素并执行与前面相同的操作。因此,我们得到下一个匹配,它的组,以及匹配的位置。我们和第一场比赛一样:

>>> match = it.next()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

最后,我们尝试使用另一个匹配项,但在本例中会抛出一个StopIteration 异常。这是表示没有更多元素的正常行为。

修改字符串

在这个部分中,我们将看到修改字符串的操作,例如一个分割字符串的操作和另一个替换部分的操作。

拆分(字符串,maxsplit=0)

在几乎所有的语言中,您都可以在字符串中找到split操作。最大的区别在于re模块中的拆分功能更强大,因此您可以使用正则表达式。因此,在本例中,将根据模式的匹配情况拆分字符串。与往常一样,理解它的最佳方法是使用示例,因此让我们将字符串拆分为几行:

>>> re.split(r"\n", "Beautiful⇢is better⇢than⇢ugly.\nExplicit⇢is⇢better⇢than⇢implicit.")

['Beautiful⇢is⇢better⇢than⇢ugly.', 'Explicit⇢is⇢better⇢than⇢implicit.']

在上例中,匹配为\n;因此,使用字符串作为分隔符来拆分字符串。让我们看一个更复杂的示例,说明如何获取字符串中的单词:

>>> pattern = re.compile(r"\W")
>>> pattern.split("hello⇢world")
['Hello', 'world']

在前面的示例中,我们定义了一个模式来匹配任何非字母数字字符。因此,在这种情况下,匹配发生在空白处。这就是为什么字符串被拆分为单词。让我们看另一个例子来更好地理解它:

>>> pattern = re.compile(r"\W")
>>> pattern.findall("hello⇢world")
['⇢']

请注意,匹配的是空格。

maxsplit参数指定最多可以进行多少次拆分,并返回结果中的剩余部分:

>>> pattern = re.compile(r"\W")
>>> pattern.split("Beautiful is better than ugly", 2)
['Beautiful', 'is', 'better than ugly']

如您所见,只有两个单词被拆分,其他单词是结果的一部分。

您是否意识到不包括匹配的图案?请看一下本节中的每个示例。如果我们也想捕获模式,我们可以做什么?

答案是使用组:

>>> pattern = re.compile(r"(-)")
>>> pattern.split("hello-word")
['hello', '-', 'word']

这是因为拆分操作总是返回捕获的组。

请注意,当一个组匹配字符串的开头时,结果将包含空字符串作为第一个结果:

>>> pattern = re.compile(r"(\W)")
>>> pattern.split("⇢hello⇢word")
['', '⇢', 'hello', '⇢', 'word']

sub(repl,string,count=0)

此操作将原始字符串中的匹配模式替换为替换后返回生成的字符串。如果找不到模式,则返回原始字符串。例如,我们将用-(破折号)替换字符串中的数字:

>>> pattern = re.compile(r"[0-9]+")
>>> pattern.sub("-", "order0⇢order1⇢order13")
  'order-⇢order-⇢order-'

基本上,正则表达式匹配 1 个或更多数字,并将匹配的模式0113替换为-(破折号)。

请注意,它将替换阵列最左侧的不重叠引用。让我们看另一个例子:

 >>> re.sub('00', '-', 'order00000')
   'order--0'

在前面的例子中,我们用 2 替换 0。所以,前两个被匹配,然后被替换,接下来的两个零也被匹配,然后被替换,最后一个零保持不变。

repl参数也可以是函数,在这种情况下,它接收 MatchObject 作为参数,返回的字符串是替换字符串。例如,假设您有一个遗留系统,其中有两种订单。有些以破折号开头,有些以字母开头:

  • -1234
  • A193、B123、C124

您必须将其更改为以下内容:

  • A1234
  • B193、B123、B124

简言之,以破折号开头的应该以 a 开头,其余的应该以 B 开头。

>>>def normalize_orders(matchobj):
       if matchobj.group(1) == '-': return "A"
       else: return "B"

>>> re.sub('([-|A-Z])', normalize_orders, '-1234⇢A193⇢ B123')
'A1234⇢B193⇢B123'

正如前面提到的一样,对于每个匹配的模式,normalize_orders函数被调用。所以,如果第一个匹配的组是一个,那么我们返回一个A;在任何其他情况下,我们返回 B

注意,在代码中,我们得到了索引为 1 的第一个组;请看一下group操作以了解原因。

sub还提供了一个强大的功能。我们将在下一章深入了解它们。基本上,它所做的是用相应的组替换反向引用。例如,假设您希望将标记转换为 HTML,为了保持示例简短,只需将文本加粗:

>>> text = "imagine⇢a⇢new⇢*world*,⇢a⇢magic⇢*world*"
>>> pattern = re.compile(r'\*(.*?)\*')
>>> pattern.sub(r"<b>\g<1><\\b>", text)
'imagine⇢a⇢new⇢<b>world<\\b>,⇢a⇢magic⇢<b>world<\\b>'

与往常一样,前面的示例首先编译模式,该模式匹配两个*之间的每个单词,此外,它还捕获单词。注意,多亏了?元字符,模式是非贪婪的。

请注意,\g<number>的存在是为了避免文字数字的歧义,例如,假设您需要在组后面添加“1”:

>>> pattern = re.compile(r'\*(.*?)\*')
>>> pattern.sub(r"<b>\g<1>1<\\b>", text)
   'imagine⇢a⇢new⇢<b>world1<\\b>,⇢a⇢magic⇢<b>world1<\\b>'

如您所见,行为与预期一致。让我们看看在没有<>的情况下使用符号会发生什么:

>>> text = "imagine⇢a⇢new⇢*world*,⇢a⇢magic⇢*world*"
>>> pattern = re.compile(r'\*(.*?)\*')
>>> pattern.sub(r"<b>\g1
1<\\b>", text)
 error: bad group name

在前面的示例中,该组被突出显示以消除歧义并帮助我们看到它,而这正是正则表达式引擎所面临的问题。这里,正则表达式引擎尝试使用不存在的组号 11。因此,存在\g<group>符号。

sub需要记住的另一件事是,替换字符串中转义的每个反斜杠都将被处理。正如你在<\\b>中所看到的,如果你想避免它,你需要逃离它们。

您可以使用可选的计数参数限制替换的数量。

子网(repl,string,count=0)

它是与sub基本相同的操作,您可以将其视为sub之上的一个实用程序。它返回一个包含新字符串和替换次数的元组。让我们使用与前面相同的示例来了解工作原理:

>>> text = "imagine⇢a⇢new⇢*world*,⇢a⇢magic⇢*world*"
>>> pattern = re.compile(r'\*(.*?)\*')
>>> pattern.subn(r"<b>\g<1><\\b>", text)
('imagine⇢a⇢new⇢<b>world<\\b>,⇢a⇢magic⇢<b>world<\\b>', 2)

这是一个很长的部分。我们探索了使用re模块和RegexObject类可以完成的主要操作,并给出了示例。让我们继续看比赛后得到的物体。

匹配对象

该对象表示匹配的模式;每次执行以下操作之一时,您都会得到一个:

  • 火柴
  • 搜索
  • 芬迪特

此对象为我们提供了一组操作,用于处理捕获的组、获取有关匹配位置的信息等。让我们看看最重要的操作。

组(【第 1 组,】)

group操作将为您提供匹配的子组。如果调用时没有参数或零,则返回整个匹配;而如果传递了一个或多个组标识符,则返回对应组的匹配项。

让我们用一个例子来看看它们:

>>> pattern = re.compile(r"(\w+) (\w+)")
>>> match = pattern.search("Hello⇢world")

该模式匹配整个字符串并捕获两个组,Helloworld。比赛结束后,我们可以看到以下具体情况:

  • 如果没有参数或为零,则返回整个匹配项。

    >>> match.group()
    'Hello⇢world'
    
    >>> match.group(0)
    'Hello⇢world'
  • group1大于 0 时,返回对应的组。

    >>> match.group(1)
    'Hello'
    
    >>> match.group(2)
    'world'
  • 如果该组不存在,将抛出一个IndexError

    >>> match.group(3)
    …
    IndexError: no such group
  • With multiple arguments, it returns the corresponding groups.

    >>> match.group(0, 2)
       ('Hello⇢world', 'world')

    在这种情况下,我们需要整个模式和第二组,这就是为什么我们通过02

组可以命名,我们将在下一章深入了解;它有一个特殊的符号。如果模式具有命名组,则可以使用以下名称或索引访问这些组:

>>> pattern = re.compile(r"(?P<first>\w+) (?P<second>\w+)")

在前面的示例中,我们编译了一个模式来捕获两个组:第一个组名为first,第二个组名为second

>>> match = pattern.search("Hello⇢world")
>>> match.group('first')
'Hello'

通过这种方式,我们可以通过名称获得一个组。请注意,使用命名组,我们仍然可以通过组的索引获取组,如以下代码所示:

>>> match.group(1)
'Hello'

我们甚至可以使用这两种类型:

>>> match.group(0, 'first', 2)
('Hello⇢world', 'Hello', 'world')

组(【默认】)

groups操作与之前的操作类似。但是,在本例中,它返回一个包含匹配中所有子组的元组,而不是提供一个或一些组。让我们通过上一节中使用的示例来了解它:

>>> pattern = re.compile("(\w+) (\w+)")
>>> match = pattern.search("Hello⇢World")
>>> match.groups()
   ('Hello', 'World')

正如我们在上一节中所做的,我们有两个组HelloWorld,这正是groups给我们的。在本例中,您可以将groups视为group(1, lastGroup)

如果有不匹配的组,则返回默认参数。如果未指定默认参数,则使用None,例如:

>>> pattern = re.compile("(\w+) (\w+)?")
>>> match = pattern.search("Hello⇢")
>>> match.groups("mundo")
   ('Hello', 'mundo')
>>> match.groups()
   ('Hello', None)

上例中的模式试图匹配由一个或多个字母数字字符组成的两组。第二个是可选的;所以我们只得到一个字符串为Hello的组。获得匹配后,我们调用groups,将default设置为mundo,使其返回mundo作为第二组。请注意,在下面的调用中,我们没有设置默认值,因此返回None

groupdict(【默认】)

groupdict方法用于已使用命名组的情况。它将返回一个包含找到的所有组的字典:

>>> pattern = re.compile(r"(?P<first>\w+) (?P<second>\w+)")
>>> pattern.search("Hello⇢world").groupdict()
{'first': 'Hello', 'second': 'world'}

在前面的示例中,我们使用了一种类似于前面几节中看到的模式。它捕获了两个名为firstsecond的组。所以,groupdict在字典中返回它们。请注意,如果没有命名组,则返回一个空字典。

如果你不太了解这里发生的事情,不要担心。如前所述,我们将在第 3 章中看到所有与组相关的内容。

启动(【组】)

有时,知道模式匹配的索引很有用。与所有与组相关的操作一样,如果参数组为零,则该操作与匹配的整个字符串一起工作:

>>> pattern = re.compile(r"(?P<first>\w+) (?P<second>\w+)?")
>>> match = pattern.search("Hello⇢")
>>> match.start(1)
0

如果有组不匹配,则返回-1

>>> math = pattern.search("Hello⇢")
>>> match..start(2)
-1

结束(【组】)

end操作的行为与start完全相同,只是返回组匹配的子字符串的结尾:

>>> pattern = re.compile(r"(?P<first>\w+) (?P<second>\w+)?")
>>> match = pattern.search("Hello⇢")
>>> match.end (1)
5

跨度(【组】)

这是一个操作,它给您一个元组,其中包含startend中的值。此操作通常在文本编辑器中用于定位和突出显示搜索。以下代码是此操作的示例:

>>> pattern = re.compile(r"(?P<first>\w+) (?P<second>\w+)?")
>>> match = pattern.search("Hello⇢")
>>> match.span(1)
(0, 5)

展开(模板)

此操作在模板字符串中用反向引用替换后返回字符串。类似于sub

继续上一节中的示例:

>>> text = "imagine⇢a⇢new⇢*world*,⇢a⇢magic⇢*world*"
>>> match = re.search(r'\*(.*?)\*', text)
>>> match.expand(r"<b>\g<1><\\b>")
  '<b>world<\\b>'

模块操作

让我们看看模块中的两个有用操作。

逃生()

它逃避可能出现在表达式中的文字。

>>> re.findall(re.escape("^"), "^like^")
['^', '^']

吹扫()

它清除正则表达式缓存。我们已经谈过了;当您通过模块使用操作时,需要使用此选项以释放内存。请记住,有一个与性能的权衡;释放缓存后,必须再次编译和缓存每个模式。

很好,您已经了解了使用re模块可以执行的主要操作。在此之后,您可以在项目中开始使用正则表达式,而不会出现很多问题。

现在,我们将看到如何更改模式的默认行为。

编译标志

当将模式字符串编译成模式对象时,可以修改模式的标准行为。为了做到这一点,我们必须使用编译标志。可以使用位或“|”组合这些参数。

|

旗帜

|

python

|

描述

| | --- | --- | --- | | re.IGNORECASEre.I | 2.x3.x | 模式将匹配小写和大写。 | | re.MULTILINEre.M | 2.x3.x | 此标志更改两个元字符的行为:

  • ^:现在在字符串的开头和每一新行的开头匹配。
  • $:在这种情况下,它在字符串末尾和每行末尾匹配。具体地说,它在换行符之前匹配。

| | re.DOTALLre.S | 2.x3.x | 元字符“.”将匹配任何字符,即使是换行符。 | | re.LOCALEre.L | 2.x3.x | 此标志使\w、\w、\b、\b、\s 和\s 依赖于当前区域设置。“re.LOCALE 只是将字符传递给底层 C 库。它实际上只对每个字符有 1 个字节的 ByTestRing 有效。UTF-8 将 ASCII 范围之外的代码点编码为每个代码点多个字节,并且 re 模块将这些字节视为单独的字符。”(见 http://www.gossamer-threads.com/lists/python/python/850772 )请注意,当同时使用re.Lre.U时(re.L|re.U,仅使用 Locale)。另外,请注意,在 Python3 中不鼓励使用此标志;有关更多信息,请参阅文档。 | | re.VERBOSEre.X | 2.x3.x | 它允许编写更易于阅读和理解的正则表达式。为此,它以一种特殊的方式处理某些字符:

  • 空白被忽略,除非它在字符类中或前面有反斜杠
  • 除非#前面有反斜杠或在字符类中,否则#右侧的所有字符都会像注释一样被忽略。

| | re.DEBUG | 2.x3.x | 它提供有关编译模式的信息。 | | re.UNICODEre.U | 2.x | 它使\w、\w、\b、\b、\d、\d、\s 和\s 依赖于 Unicode 字符属性数据库。 | | re.ASCIIre.A(仅限 Python 3) | 3.x | 它使\w、\w、\b、\b、\d、\d、\s 和\s 仅执行 ASCII 匹配。这是有意义的,因为在 Python3 中,默认情况下匹配是 Unicode 的。您可以在Python3的新增功能部分找到更多信息。 |

让我们看一些最重要的标志的例子。

re.IGNORECASE 或 re.I

正如您所看到的,以下模式匹配,即使字符串以开头,而不是以 A 开头。

>>> pattern = re.compile(r"[a-z]+", re.I)
>>> pattern.search("Felix")
<_sre.SRE_Match at 0x10e27a238>
>>> pattern.search("felix")
<_sre.SRE_Match at 0x10e27a510>

re.MULTILINE 或 re.M

在下面的示例中,模式与换行后的日期不匹配,因为我们没有使用该标志:

>>> pattern = re.compile("^\w+\: (\w+/\w+/\w+)")
>>> pattern.findall("date: ⇢12/01/2013 \ndate: 11/01/2013")
['12/01/2013']

但是,在使用Multiline标志时,它匹配两个日期:

>>> pattern = re.compile("^\w+\: (\w+/\w+/\w+)", re.M)
>>> pattern.findall("date: ⇢12/01/2013⇢\ndate: ⇢11/01/2013")
  ['12/01/2013', '12/01/2013']

这不是捕捉日期的最佳方式。

re.DOTALL 或 re.S

让我们尝试匹配数字后的任何内容:

>>> re.findall("^\d(.)", "1\ne")
   []

我们可以在前面的示例中看到,具有默认行为的字符类.与换行符不匹配。让我们看看使用该标志会发生什么:

>>> re.findall("^\d(.)", "1\ne", re.S)
['\n']

正如所料,在上使用DOTALL标志,它与新行完全匹配。

关于区域设置或关于

在下面的示例中,我们得到前 256 个字符,然后我们尝试查找字符串中的每个字母数字字符,因此我们得到预期的字符,如下所示:

>>> chars = ''.join(chr(i) for i in xrange(256))
>>> " ".join(re.findall(r"\w", chars))
'0 1 2 3 4 5 6 7 8 9 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z _ a b c d e f g h i j k l m n o p q r s t u v w x y z'   

将区域设置设置为系统区域设置后,我们可以再次尝试获取每个字母数字字符:

>>> locale.setlocale(locale.LC_ALL, '')
'ru_RU.KOI8-R'  

在本例中,我们根据新的区域设置获得更多字符:

>>> " ".join(re.findall(r"\w", chars, re.LOCALE))
'0 1 2 3 4 5 6 7 8 9 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z _ a b c d e f g h i j k l m n o p q r s t u v w x y z \xa3 \xb3 \xc0 \xc1 \xc2 \xc3 \xc4 \xc5 \xc6 \xc7 \xc8 \xc9 \xca \xcb \xcc \xcd \xce \xcf \xd0 \xd1 \xd2 \xd3 \xd4 \xd5 \xd6 \xd7 \xd8 \xd9 \xda \xdb \xdc \xdd \xde \xdf \xe0 \xe1 \xe2 \xe3 \xe4 \xe5 \xe6 \xe7 \xe8 \xe9 \xea \xeb \xec \xed \xee \xef \xf0 \xf1 \xf2 \xf3 \xf4 \xf5 \xf6 \xf7 \xf8 \xf9 \xfa \xfb \xfc \xfd \xfe \xff'

re.UNICODE 或 re.U

让我们尝试查找字符串中的所有字母数字字符:

>>> re.findall("\w+", "this⇢is⇢an⇢example")
['this', 'is', 'an', 'example']

但是如果我们想对其他语言做同样的事情会发生什么?字母数字字符取决于语言,因此我们需要将其指示给正则表达式引擎:

>>> re.findall(ur"\w+", u"这是一个例子", re.UNICODE)
  [u'\u8fd9\u662f\u4e00\u4e2a\u4f8b\u5b50']
>>> re.findall(ur"\w+", u"هذا مثال", re.UNICODE)
   [u'\u0647\u0630\u0627', u'\u0645\u062b\u0627\u0644']

re.VERBOSE 或 re.X

在下面的模式中,我们使用了几个⇢; 第一个被忽略,因为它不在字符类中或前面有反斜杠,第二个是模式的一部分。我们还使用了#三次,第一次和第三次被忽略,因为它们前面没有反斜杠,第二次是模式的一部分。

>>> pattern = re.compile(r"""[#|_] + #comment
              \ \# #comment
              \d+""", re.VERBOSE)
>>> pattern.findall("#⇢#2")
['#⇢#2']

重新调试

>>>re.compile(r"[a-f|3-8]", re.DEBUG)
  in
    range (97, 102)
    literal 124
    range (51, 56)

Python 和 regex 特殊注意事项

在本节中,我们将回顾与其他风格的差异,如何处理 Unicode,以及 Python2.x 和 Python3 之间在re模块中的差异。

Python 与其他口味的区别

正如我们在本书开头提到的,模块具有 Perl 风格的正则表达式。然而,这并不意味着 Python 支持 Perl 引擎的所有特性。

有太多的差异,无法在这样一本短小的书中涵盖,如果你想在这里深入了解它们,你有两个很好的起点:

Unicode

当使用 Python2.x 并希望匹配 Unicode 时,正则表达式必须是 Unicode 转义。例如:

>>> re.findall(r"\u03a9", u"adeΩa")
[]
>>> re.findall(ur"\u03a9", u"adeΩa")
[u'\u03a9']

请注意,如果使用 Unicode 字符,但所使用的字符串类型不是 Unicode,python 会自动使用默认编码对其进行编码。例如,在我的例子中,我有 UTF-8:

>>> u"Ω".encode("utf-8")
'\xce\xa9'
>>> "Ω"
'\xce\xa9'

因此,在混合类型时必须小心:

>>> re.findall(r'Ω', "adeΩa")
['\xce\xa9']

这里,您不匹配 Unicode,而是匹配默认编码中的字符:

>>> re.findall(r'\xce\xa9', "adeΩa")
['\xce\xa9']

所以,如果您在其中任何一种模式中使用 Unicode,您的模式都不会匹配任何内容:

>>> re.findall(r'Ω', u"adeΩa")
[]

另一方面,您可以在两侧使用 Unicode,并且它将按照预期匹配:

>>> re.findall(ur'Ω', u"adeΩa")
   [u'\u03a9']

re模块不进行 Unicode 大小写折叠,因此不区分大小写在 Unicode 上不起作用:

>>> re.findall(ur"ñ" ,ur"Ñ", re.I)
[]

Python 3 的新增功能

Python 3 中有一些影响正则表达式行为的更改,并且re模块中添加了新特性。首先,让我们回顾一下字符串符号是如何改变的。

|

类型

|

前缀

|

描述

| | --- | --- | --- | | 一串 |   | 它们是字符串文本。它们是 Unicode。反斜杠是转义有意义字符所必需的。

>>>"España \n"
'España \n'

| | 原始字符串 | rR | 除了反斜杠(被视为普通字符)之外,它们与文本字符串相同。

>>>r"España \n"
'España \\n'

| | 字节字符串 | bB | 以字节表示的字符串。它们只能包含 ASCII 字符;如果字节大于 128,则必须对其进行转义。

>>> b"Espa\xc3\xb1a \n"
b'Espa\xc3\xb1a \n'

我们可以通过以下方式转换为 Unicode:

>>> str(b"Espa\xc3\xb1a \n", "utf-8")
'España \n'

反斜杠是转义有意义字符所必需的。 | | 字节原始字符串 | rR | 它们类似于字节字符串,但反斜杠是转义的。

>>> br"Espa\xc3\xb1a \n"
b'Espa\\xc3\\xb1a \\n'

因此,用于转义字节的反斜杠将再次转义,这使其转换为 Unicode 变得复杂:

>>> str(br"Espa\xc3\xb1a \n", "utf-8")
'Espa\\xc3\\xb1a \\n'

| | 统一码 | rU | 在早期版本的 Python3 中删除了u前缀,并在版本 3.3 中恢复,语法再次被接受。它们等于弦。 |

在 Python3 中,文本字符串默认为 Unicode,这意味着不再需要使用 Unicode 标志。

>>> re.findall(r"\w+", "这是一个例子")
  ['这是一个例子']

Python 3.3(http://docs.python.org/dev/whatsnew/3.3.html 增加了更多与相关的特性,以及 Unicode 在语言中的处理方式。例如,它增加了对完整范围的代码点的支持,包括非 BMP(http://en.wikipedia.org/wiki/Plane_(Unicode)。例如:

  • 在 Python 2.7 中:

    >>> re.findall(r".", u'\U0010FFFF')
    [u'\udbff', u'\udfff'] 
  • 在 Python 3.3.2 中:

    >>> re.findall(r".", u'\U0010FFFF')
    ['\U0010ffff']

正如我们在编译标志部分中看到的,添加了 ASCII 标志。

使用 Python 3 时需要注意的另一个重要方面与元字符有关。由于默认情况下字符串是 Unicode 的,因此元字符也是 Unicode 的,除非您使用 8 位模式或使用 ASCII 标志。

>>> re.findall(r"\w+", "هذا⇢مثال")
['هذا', 'مثال'] 
>>> re.findall(r"\w+", "هذا⇢مثال word", re.ASCII)
['word']

在前面的示例中,将忽略非 ASCII 字符。

考虑到 Unicode 模式和 8 位模式不能混合使用。

在下面的示例中,我们尝试将 8 位模式与 Unicode 字符串相匹配,这就是引发异常的原因(请记住,它在 Python 2.x 中可以工作):

>>> re.findall(b"\w+", b"hello⇢world")
[b'hello', b'world']
>>> re.findall(b"\w+", "hello world")
….
TypeError: can't use a bytes pattern on a string-like object

总结

这是一个漫长的篇章!我们已经在里面介绍了很多材料。我们从 Python 中字符串的工作方式以及 Python 2.x 和 Python 3.x 中字符串的不同符号开始。之后,我们研究了如何构建正则表达式,re模块提供给我们处理正则表达式的对象和接口,以及搜索和修改字符串的最重要操作。我们还学习了如何通过MatchObject从模式中提取信息,例如匹配的位置或组。我们还学习了如何使用编译标志修改某些字符类和元字符的默认行为。最后,我们看到了如何处理 Unicode 以及我们可以在 Python3.x 中找到的新特性。

在本章中,我们看到了组是正则表达式的关键部分,re模块的许多操作都是用于组的。这就是为什么我们在下一章中深入讨论群体。