Skip to content

Latest commit

 

History

History
390 lines (263 loc) · 17.8 KB

File metadata and controls

390 lines (263 loc) · 17.8 KB

四、环顾四周

到目前为止,我们已经了解了在丢弃字符时匹配字符的不同机制。无法再次比较已匹配的角色,匹配任何即将出现的角色的唯一方法是丢弃该角色。

例外情况是我们研究过的一些元字符,即所谓的零宽度****断言。这些字符表示位置,而不是实际内容。例如,插入符号(^)表示一行的开头,或表示一行的结尾的美元符号($)。它们只是确保输入中的位置是正确的,而没有实际使用或匹配任何字符。

一种更强大的零宽度断言是环视,这是一种机制,通过该机制可以将某个先前的(向后看或其他(向前看值)匹配到当前位置。他们在不消耗字符的情况下有效地进行断言;他们只是返回一个积极或消极的比赛结果。

环顾机制可能是正则表达式中最未知的,同时也是最强大的技术。这种机制允许我们创建功能强大的正则表达式,而这些正则表达式是无法以其他方式编写的,要么是因为它所代表的复杂性,要么只是因为正则表达式的技术限制,而无需四处查看。

在本章中,我们将学习如何使用 Python 正则表达式利用环视机制。我们将了解如何应用它们,它们在幕后如何工作,以及 Python 正则表达式模块将对我们施加的一些限制。

向前看和向后看可分为另外两种类型:正面和负面:

  • 正向前瞻:该机制表示为一个表达式,在前面有一个问号和一个等于的符号?=,位于括号内。例如,如果传递的正则表达式do与即将到来的输入匹配,(?=regex)将匹配。
  • 否定前瞻:该机制被指定为一个表达式,在前面有一个问号和一个感叹号标记?!,位于括号块内。例如,如果传递的正则表达式与即将到来的输入不匹配,(?!regex)将匹配。
  • 后面的积极观察:该机制表示为一个表达式,在前面有一个问号、一个小于号和一个等号?<=,位于括号块内。例如,如果传递的正则表达式do与之前的输入匹配,(?<=regex)将匹配。
  • 反向注视:此机制表示为一个表达式,在前面有一个问号、一个小于的符号和一个感叹号?<!,位于括号块内。例如,如果传递的正则表达式与之前的输入不匹配,(?<!regex)将匹配。

让我们开始期待下一节。

向前看

我们将要研究的第一种环顾机制是前瞻机制。它尝试在前面匹配作为参数传递的子表达式。两个环视操作的零宽度特性使它们变得复杂且难以理解。

正如我们从上一节了解到的,它被表示为一个表达式,前面有一个问号和一个等号,?=,位于括号块内:(?=regex)

让我们通过比较两个类似正则表达式的结果来解决这个问题。我们记得在第一章引入正则表达式时,我们将表达式/fox/与短语The quick brown fox jumps over the lazy dog匹配。我们也将表达式/(?=fox)/应用于相同的输入:

>>>pattern = re.compile(r'fox')
>>>result = pattern.search("The quick brown fox jumps over the lazy dog")
>>>print result.start(), result.end()
16 19

我们刚刚搜索了输入字符串中的文本fox,正如我们所料,我们在索引1619之间找到了它。让我们看一下前瞻机制的以下示例:

>>>pattern = re.compile(r'(?=fox)')
>>>result = pattern.search("The quick brown fox jumps over the lazy dog")
>>>print result.start(), result.end()
16 16

这次我们使用了/(?=fox)/表达式。结果只是索引16的一个位置(同一索引的起点和终点)。这是因为环视不会使用字符,因此,它可以用于筛选表达式应匹配的位置。但是,它不会定义结果的内容。我们可以在下图中直观地比较这两个表达式:

Look ahead

正常和前瞻性比赛的比较

让我们再次使用此功能,尝试使用以下正则表达式/\w+(?=,)/和文本They were three: Felix, Victor, and Carlos匹配后跟逗号字符(,)的任何单词:

>>>pattern = re.compile(r'\w+(?=,)')
>>>pattern.findall("They were three: Felix, Victor, and Carlos.")
['Felix', 'Victor']

我们创建了一个正则表达式,该表达式接受字母数字字符后跟逗号字符的任何重复,而逗号字符不会用作结果的一部分。因此,只有FelixVictor是结果的一部分,因为Carlos在名称后没有逗号。

与本章之前我们使用的正则表达式相比,这有多大的不同?让我们通过将/\w+,/应用于同一文本来比较结果:

>>>pattern = re.compile(r'\w+,')
>>>pattern.findall("They were three: Felix, Victor, and Carlos.")
['Felix,', 'Victor,']

对于前面的正则表达式,我们要求正则表达式引擎接受字母数字字符后跟逗号字符的任何重复。因此,将返回字母数字字符和逗号字符,如清单所示。

值得注意的是,前瞻机制是另一个子表达式,它可以利用正则表达式的所有功能(与我们稍后将发现的查找机制不同)。因此,我们可以使用到目前为止所学的所有结构:

>>>pattern = re.compile(r'\w+(?=,|\.)')
>>>pattern.findall("They were three: Felix, Victor, and Carlos.")
['Felix', 'Victor', 'Carlos']

在前面的示例中,我们使用了交替(即使我们可以使用其他更简单的技术作为字符集)来接受字母数字字符的任何重复,后跟逗号或点字符,而这些字符不会用作结果的一部分。

负面展望

负前瞻机制呈现出与前瞻相同的性质,但有一个显著的区别:只有在子表达式不匹配的情况下,结果才有效。

它表示为一个表达式,前面有一个问号和一个感叹号,?!,位于括号内:(?!regex)

当我们想要表达不应该发生的事情时,这很有用。例如,要查找任何不是John Smith的名称John,我们可以执行以下操作:

>>>pattern = re.compile(r'John(?!\sSmith)')                                    >>> result = pattern.finditer("I would rather go out with John McLane than with John Smith or John Bon Jovi")
>>>for i in result:
...print i.start(), i.end()
...
27 31
63 67

在前面的示例中,我们通过使用这五个字符来查找John,然后查找后跟单词Smith的空白字符。如果匹配,匹配将只包含John的起始和结束位置。在这种情况下,John McLane的位置为27-31,而John Bon Jovi的位置为63-67

现在,我们能够利用更基本的环顾形式:积极和消极的展望。让我们学习如何在替换和分组中充分利用它。

环顾四周,换人

环视操作的零宽度特性在替换中特别有用。多亏了它们,我们能够执行转换,否则读写起来会非常复杂。

前瞻和替换的一个典型示例是将仅由数字字符(如 1234567890)组成的数字转换为逗号分隔的数字,即 1234567890。

为了编写这个正则表达式,我们需要遵循一个策略。我们要做的是将数字分成三块,然后用同一组加上一个逗号字符替换。

我们可以很容易地从一个近乎幼稚的方法开始,使用以下突出显示的正则表达式:

>>>pattern = re.compile(r'\d{1,3}')
>>>pattern.findall("The number is: 12345567890")
['123', '455', '678', '90']

我们这次尝试失败了。我们有效地将三个数字分组,但它们应该从右到左。我们需要一种不同的方法。让我们试着找到一个、两个或三个数字,后面必须跟任意数量的三位数块,直到我们找到一些不是数字的东西。

这将对我们的人数产生以下影响。当试图查找一位、两位或三位数字时,正则表达式将只取一位,这将是数字1。然后,它将尝试捕捉正好由三个数字组成的块,例如 234567890,直到找到一个非数字。这是输入的结尾。

如果我们用正则表达式表达我们刚才用普通英语解释的内容,我们将得到以下结果:

/\d{1,3}(?=(\d{3})+(?!\d))/
|

要素

|

描述

| | --- | --- | |

\d

| 此匹配一个十进制字符 | |

{1,3}

| 这表示匹配重复一到三次 | |

(?=

| 这表示该字符后跟(但不使用)此表达式 | |

(

| 这表示一个组 | |

\d

| 这表示存在一组十进制字符 | |

\s

| 这表示匹配重复三次 | |

)

|   | |

+

| 这表示十进制字符应出现一次或多次 | |

(?!

| 这表示匹配后面没有(但没有使用)以下表达式定义的内容 | |

\d

| 这表示一个十进制字符 | |

))

|   |

让我们在 Python 控制台中使用这个新的正则表达式再试一次:

>>>pattern = re.compile(r'\d{1,3}(?=(\d{3})+(?!\d))')
>>>results = pattern.finditer('1234567890')
>>>for result in results:
...    print result.start(), result.end()
...
0 1
1 4
4 7

这一次,我们可以看到我们使用了正确的方法,因为我们刚刚确定了正确的块:1234567890

现在,我们只需要使用一个替换来替换我们找到的每个匹配,以获得相同的匹配结果加上一个逗号字符。我们已经知道如何使用替换,正如我们在第 2 章正则表达式与 Python中所学到的,所以让我们将其付诸实践:

>>>pattern = re.compile(r'\d{1,3}(?=(\d{3})+(?!\d))')
>>>pattern.sub(r'\g<0>,', "1234567890")
'1,234,567,890'

瞧!我们刚刚将一个未格式化的数字转换成一个带有 1000 个分隔符的美丽数字。

我们刚刚学习了两种技术,可以预见未来。我们还研究了它们在替换中的用法。现在,让我们回头看看我们留下了什么回头看

回头看

我们可以放心地将“向后看”定义为“向前看”的相反操作。它尝试在作为参数传递的子表达式后面进行匹配。它也具有零宽度特性,因此,它不会成为结果的一部分。

它表示为一个表达式,前面有一个问号、一个小于号和一个等号?<=,位于括号块内:(?<=regex)

例如,我们可以在一个类似于我们在“消极展望”中使用的示例中使用它来查找名为John McLane的人的姓氏。为了实现这一点,我们可以像下面这样写一个回顾:

>>>pattern = re.compile(r'(?<=John\s)McLane')
>>>result = pattern.finditer("I would rather go out with John McLane than with John Smith or John Bon Jovi")
>>>for i in result:
...    print i.start(), i.end()
...
32 38

在前面的回顾中,我们要求正则表达式引擎只匹配前面有John和空格的位置,然后使用McLane

然而,在 Python 的re模块中,如何实现“向前看”和“向后看”之间存在着根本的区别。由于许多根深蒂固的技术原因,后视机构只能匹配固定宽度的图案。如果需要后视中的可变宽度模式,则在处显示正则表达式模块 https://pypi.python.org/pypi/regex 可以使用代替标准 Pythonre模块。

固定宽度模式不包含可变长度匹配符,如我们在第 1 章引入正则表达式中研究的量词。也不允许使用其他可变长度构造,如反向引用。允许替换,但前提是替换长度相同。同样,上述 regex 模块中不存在这些限制。

让我们看看如果我们在 back 引用中使用具有不同长度备选方案的备选方案会发生什么:

>>>pattern = re.compile(r'(?<=(John|Jonathan)\s)McLane')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/re.py", line 190, in compile
return _compile(pattern, flags)
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/re.py", line 242, in _compile
raise error, v # invalid expression
sre_constants.error: look-behind requires fixed-width pattern

我们有一个例外,因为“向后看”需要固定宽度的图案。如果我们尝试使用量词或其他变长结构,我们将得到类似的结果。

既然我们已经学会了在不使用角色的情况下前后匹配的不同技术以及可能存在的不同限制,那么我们可以尝试编写另一个示例,其中包含了我们为解决现实问题而研究的一些机制。

假设我们想要提取 tweet 中存在的任何 Twitter 用户名,以便创建一个自动情绪检测系统。要编写正则表达式以提取它们,我们应该首先确定 Twitter 用户名的表示方式。如果我们浏览推特的网站https://support.twitter.com/articles/101299-why-can-t-i-register-certain-usernames 我们可能会发现以下描述:

“如上所述,用户名只能包含字母数字字符(字母 A-Z,数字 0-9),下划线除外。请检查以确保所需用户名不包含任何符号、破折号或空格。”

对于我们的开发测试,我们将使用此 Packt 发布推文:

Look behind

第一件事我们应该能够构建一个字符集,其中包含可能在 Twitter 用户名中使用的所有字符。这可以是任何字母数字字符,后跟下划线字符,正如我们在上一篇 Twitter 支持文章中所发现的那样。因此,我们可以构造一个类似于以下内容的字符集:

[\w_]

这将表示我们要从用户名中提取的所有部分。然后,我们需要预先设置一个单词边界和用于定位用户名的 at 符号(@):

/\B@[\w_]+/

之所以使用边界这个词,是因为我们不想被电子邮件之类的东西弄糊涂。我们只查找在行首或单词边界之后的文本,然后是@符号,然后是一些字母数字或下划线字符。举例如下:

  • @vromer0是有效的用户名
  • iam@vromer0不是有效的用户名,因为它应该以@符号开头
  • @vromero.org不是有效的用户名,因为它包含无效字符

如果我们使用目前的正则表达式,我们将得到以下结果:

>>>pattern = re.compile(r'\B@[\w_]+') 
>>>pattern.findall("Know your Big Data = 5 for $50 on eBooks and 40% off all eBooks until Friday #bigdata #hadoop @HadoopNews packtpub.com/bigdataoffers")
['@HadoopNews']

我们确实希望只匹配用户名,而不包括前面的@符号。在这一点上,向后看机制变得很有用。我们可以在 look behind 子表达式中包含单词 boundary 和@符号,这样它们就不会成为匹配结果的一部分:

>>>pattern = re.compile(r'(?<=\B@)[\w_]+')
>>>pattern.findall("Know your Big Data = 5 for $50 on eBooks and 40% off all eBooks until Friday #bigdata #hadoop @HadoopNews packtpub.com/bigdataoffers")
['HadoopNews']

现在我们已经完成了我们的目标。

消极回头看

负的后向查找机制与主后向查找机制具有非常相同的性质,但只有当传递的子表达式不匹配时,我们才会得到有效的结果。

它表示为一个表达式,前面有一个问号、一个小于号和一个感叹号?<!,位于括号块内:(?<!regex)

值得一提的是,消极落后不仅具有落后机制的大部分特征,而且也具有局限性。反向后视机制只能匹配固定宽度的图案。这与我们在上一节中研究的原因和含义相同。

我们可以通过尝试将任何未命名为John的姓Doe的人与如下正则表达式匹配来实现这一点:/(?<!John\s)Doe/。如果我们在 Python 的控制台中使用它,我们将获得以下结果:

>>>pattern = re.compile(r'(?<!John\s)Doe')
>>>results = pattern.finditer("John Doe, Calvin Doe, Hobbes Doe")
>>>for result in results:
...   print result.start(), result.end()
...
17 20
29 32

环顾四周,分组

环视结构的另一个有益用途是在群体内部。通常,当使用组时,必须在组内匹配并返回非常特定的结果。由于我们不想用不需要的信息污染团队,在其他潜在的选择中,我们可以利用环顾四周作为一个有利的解决方案。

假设我们需要得到一个逗号分隔的值,值的第一部分是名称,而第二部分是值。格式与此类似:

INFO 2013-09-17 12:13:44,487 authentication failed

正如我们在第 3 章分组中了解到的,我们可以很容易地编写一个表达式来获得这两个值,如下所示:

/\w+\s[\d-]+\s[\d:,]+\s(.*\sfailed)/

但是,我们只希望在失败不是身份验证失败时进行匹配。我们可以通过增加一个负面的回顾来实现这一点。它将如下所示:

/\w+\s[\d-]+\s[\d:,]+\s(.*(?<!authentication\s)failed)/

一旦我们将其放入 Python 控制台,我们将获得以下输出:

>>>pattern = re.compile(r'\w+\s[\d-]+\s[\d:,]+\s(.*(?<!authentication\s)failed)')
>>>pattern.findall("INFO 2013-09-17 12:13:44,487 authentication failed")
[]
>>>pattern.findall("INFO 2013-09-17 12:13:44,487 something else failed")
['something else failed']

总结

在本章中,我们通过断言学习了零的概念,以及如何在不干扰结果内容的情况下找到文本中的确切内容。

我们还学习了如何利用四种环顾机制:积极展望、消极展望、积极展望和消极展望。

我们还特别感兴趣地回顾了使用变量断言的两种类型的查找的局限性。

在此基础上,我们总结了有关正则表达式的基本和高级技术。现在,我们准备在下一章中重点介绍性能调优。