目录
- Python 正则表达式
- Finding Patterns of Text Without Regular Expressions
- Finding Patterns of Text with Regular Expressions
- Creating Regex Objects
- Matching Regex Objects
- Review of Regular Expression Matching
- More Pattern Matching with Regular Expressions
- Grouping with Parentheses
- Matching Multiple Groups with the Pipe
- Optional Matching with the Question Mark
- Matching Zero or More with the Star
- Matching One or More with the Plus
- Matching Specific Repetitions with Braces
- Greedy and Non-greedy Matching
- The findall() Method
- Character Classes
- Making Your Own Character Classes
- The Caret and Dollar Sign Characters
- The Wildcard Character
- Matching Everything with Dot-Star
- Matching Newlines with the Dot Character
- Review of Regex Symbols
- Case-Insensitive Matching
- Substituting Strings with the sub() Method
- Managing Complex Regexes
- Combining re.IGNORECASE, re.DOTALL, and re.VERBOSE
- Project: Phone Number and Email Address Extractor
- Step 1: Create a Regex for Phone Numbers
- Step 2: Create a Regex for Email Addresses
- Step 3: Find All Matches in the Clipboard Text
- Step 4: Join the Matches into a String for the Clipboard
Python 正则表达式
您可能熟悉通过按 CTRL-F 并输入您要查找的单词来搜索文本。正则表达式更进一步:它们允许您指定要搜索的文本模式。您可能不知道企业的确切电话号码,但如果您居住在美国或加拿大,您知道它将是三位数字,后跟连字符,然后是四位数字(以及可选的三位数区号,位于开始)。作为人类,您看到电话号码时就是这样知道的:415-555-1234 是电话号码,但 4,155,551,234 不是。
我们每天还识别各种其他文本模式:电子邮件地址中间有@符号,美国社会安全号码有九个数字和两个连字符,网站URL通常有句号和正斜杠,新闻标题使用标题大小写,社交媒体主题标签以 # 开头且不包含空格等。
正则表达式很有用,但很少有非程序员了解它们,尽管大多数现代文本编辑器和文字处理程序(例如 Microsoft Word 或 OpenOffice)都具有可以基于正则表达式进行搜索的查找和查找和替换功能。正则表达式不仅对于软件用户而且对于程序员来说都可以节省大量时间。事实上,科技作家科里·多克托罗 (Cory Doctorow) 认为,我们甚至应该在编程之前就学习正则表达式:
知道[正则表达式]意味着用 3 步解决问题和用 3,000 步解决问题之间的区别。当你是一个书呆子时,你会忘记,你通过几次按键就能解决的问题可能需要其他人花费数天的乏味且容易出错的工作才能完成。
在本章中,您将首先编写一个程序来查找文本模式而不使用正则表达式,然后了解如何使用正则表达式来使代码不那么臃肿。我将向您展示与正则表达式的基本匹配,然后继续介绍一些更强大的功能,例如字符串替换和创建您自己的字符类。最后,在本章末尾,您将编写一个程序,可以自动从文本块中提取电话号码和电子邮件地址。
Finding Patterns of Text Without Regular Expressions
假设您想在字符串中查找美国电话号码。如果您是美国人,您就知道这种模式:三个数字、一个连字符、三个数字、一个连字符和四个数字。示例如下:415-555-4242。
让我们使用名为 isPhoneNumber() 的函数来检查字符串是否与此模式匹配,返回 True 或 False。打开一个新的文件编辑器选项卡并输入以下代码;然后将文件另存为 isPhoneNumber.py:
def isPhoneNumber(text):➊ if len(text) != 12:return Falsefor i in range(0, 3):➋ if not text[i].isdecimal():return False➌ if text[3] != '-':return Falsefor i in range(4, 7):➍ if not text[i].isdecimal():return False➎ if text[7] != '-':return Falsefor i in range(8, 12):➏ if not text[i].isdecimal():return False➐ return True
print('Is 415-555-4242 a phone number?')
print(isPhoneNumber('415-555-4242'))
print('Is Moshi moshi a phone number?')
print(isPhoneNumber('Moshi moshi'))
当这个程序运行时,输出如下所示:
isPhoneNumber()
函数的代码会执行多次检查以查看文本中的字符串是否为有效的电话号码。如果其中任何检查失败,该函数将返回 False。首先,代码检查字符串是否正好是 12 个字符 ➊。然后它检查区号(即文本中的前三个字符)是否仅由数字字符 ➋ 组成。该函数的其余部分检查字符串是否遵循电话号码的模式:该号码必须在区号 ➌ 之后包含第一个连字符,再添加三个数字字符 ➍,然后再添加一个连字符 ➎,最后再添加四个数字 ➏。如果程序执行成功通过了所有检查,则返回 True ➐。
使用参数“415-555-4242”调用 isPhoneNumber()
将返回 True
。使用 ‘Moshi moshi’ 调用 isPhoneNumber()
将返回 False
;第一个测试失败,因为“Moshi moshi”长度不足 12 个字符。
如果您想在较大的字符串中查找电话号码,则必须添加更多代码来查找电话号码模式。将 isPhoneNumber.py
中的最后四个 print()
函数调用替换为以下内容:
message = 'Call me at 415-555-1011 tomorrow. 415-555-9999 is my office.'
for i in range(len(message)):➊ chunk = message[i:i+12]➋ if isPhoneNumber(chunk):print('Phone number found: ' + chunk)
print('Done')
当这个程序运行时,输出将如下所示:
在 for
循环的每次迭代中,消息中的 12 个字符的新块被分配给变量块 ➊。例如,在第一次迭代中,i 为 0,块被分配为 message[0:12]
(即字符串“Call me at 4”)。在下一次迭代中,i 为 1,块被分配为 message[1:13]
(字符串“all me at 41”)。换句话说,在 for
循环的每次迭代中, chunk
都采用以下值:
您将块传递给 isPhoneNumber()
以查看它是否与电话号码模式 ➋ 匹配,如果是,则打印该块。
继续循环message
,最终chunk中的12个字符将是一个电话号码。该循环遍历整个字符串,测试每个 12 个字符的片段并打印它发现的满足 isPhoneNumber()
的任何块。一旦我们浏览完消息,我们就会打印 Done
。
虽然在此示例中消息中的字符串很短,但它可能有数百万个字符长,并且程序仍将在不到一秒的时间内运行。使用正则表达式查找电话号码的类似程序也将在不到一秒的时间内运行,但正则表达式可以更快地编写这些程序。
Finding Patterns of Text with Regular Expressions
以前的电话号码查找程序可以工作,但它使用大量代码来完成一些有限的事情: isPhoneNumber()
函数有 17 行,但只能查找一种模式的电话号码。电话号码格式如 415.555.4242
或 (415) 555-4242
怎么样?如果电话号码有分机号(例如 415-555-4242 x99
)怎么办? isPhoneNumber()
函数将无法验证它们。您可以为这些附加模式添加更多代码,但有一种更简单的方法。
正则表达式,简称正则表达式,是对文本模式的描述。例如,正则表达式中的 \d 代表数字字符,即从 0 到 9 的任何单个数字。正则表达式 \d\d\d-\d\d\d-\d\d\d\d
Pytho
n 使用 isPhoneNumber(
) 函数来匹配与之前的 isPhoneNumber()
函数相同的文本模式:由三个数字、一个连字符、另外三个数字、另一个连字符和四个数字组成的字符串。任何其他字符串都不会与 \d\d\d-\d\d\d-\d\d\d\d
正则表达式匹配。
但正则表达式可以更加复杂。例如,在模式后添加大括号 ({3}) 中的 3 就像在说“匹配该模式三次”。因此,稍短的正则表达式 \d{3}-\d{3}-\d{4}
也匹配正确的电话号码格式。
Creating Regex Objects
Python 中的所有正则表达式函数都位于 re 模块中。在交互式 shell 中输入以下内容以导入此模块:
Note
本章中的大多数示例都需要 re 模块,因此请记住在您编写的任何脚本的开头或每次重新启动 Mu 时导入它。否则,您将收到 NameError: name 're' is not Defined
错误消息。
将表示正则表达式的字符串值传递给 re.compile()
将返回一个 Regex
模式对象(或简称为 Regex
对象)。
要创建与电话号码模式匹配的 Regex
对象,请在交互式 shell 中输入以下内容。 (请记住, \d
表示“数字字符”,而 \d\d\d-\d\d\d-\d\d\d\d
是电话号码模式的正则表达式。)
>>>phoneNumRegex = re.compile(r’\d\d\d-\d\d\d-\d\d\d\d’)
现在,phoneNumRegex 变量包含一个 Regex 对象。
Matching Regex Objects
Regex
对象的 search()
方法搜索它传递的字符串以查找与正则表达式的任何匹配项。如果在字符串中找不到正则表达式模式,则 search()
方法将返回 None
。如果找到模式,search()
方法将返回一个 Match
对象,该对象有一个 group()
方法,该方法将从搜索字符串中返回实际匹配的文本。 (我将很快解释组。)例如,在交互式 shell
中输入以下内容:
mo
变量名称只是用于 Match
对象的通用名称。这个示例乍一看可能很复杂,但它比之前的 isPhoneNumber.py
程序要短得多,并且执行相同的操作。
在这里,我们将所需的模式传递给 re.compile()
并将生成的 Regex
对象存储在 phoneNumRegex
中。然后我们在 phoneNumRegex
上调用search()
并将我们想要在搜索过程中匹配的字符串传递给 search()
。搜索结果存储在变量 mo
中。在此示例中,我们知道将在字符串中找到我们的模式,因此我们知道将返回一个 Match
对象。知道 mo
包含 Match
对象而不是空值 None
,我们可以在 mo
上调用 group()
以返回匹配项。在 print()
函数调用中写入 mo.group()
会显示整个匹配项,415-555-4242
。
Review of Regular Expression Matching
虽然在 Python 中使用正则表达式有几个步骤,但每个步骤都相当简单。
- 使用 import re 导入正则表达式模块。
- 使用 re.compile() 函数创建 Regex 对象。 (记住使用原始字符串。)
- 将要搜索的字符串传递到 Regex 对象的 search() 方法中。这将返回一个 Match 对象。
- 调用 Match 对象的 group() 方法返回实际匹配文本的字符串。
Note
虽然我鼓励您将示例代码输入交互式 shell,但您还应该使用基于 Web 的正则表达式测试器,它可以准确地向您展示正则表达式如何与您输入的一段文本匹配。我推荐 https://pythex.org/ 上的测试器。
More Pattern Matching with Regular Expressions
既然您已经了解了使用 Python 创建和查找正则表达式对象的基本步骤,您就可以尝试它们的一些更强大的模式匹配功能了。
Grouping with Parentheses
假设您要将区号与电话号码的其余部分分开。添加括号将在正则表达式中创建组:(\d\d\d)-(\d\d\d-\d\d\d\d)
。然后,您可以使用 group()
匹配对象方法从一组中获取匹配文本。
正则表达式字符串中的第一组括号将是组 1。第二组括号将是组 2。通过将整数 1 或 2 传递给 group()
匹配对象方法,您可以抓取匹配文本的不同部分。向 group()
方法传递 0 或不传递任何内容将返回整个匹配的文本。在交互式 shell
中输入以下内容:
如果您想一次检索所有组,请使用 groups() 方法 - 请注意名称的复数形式。
由于 mo.groups()
返回多个值的元组,因此您可以使用多重赋值技巧将每个值分配给单独的变量,如前面的 areaCode, mainNumber = mo.groups()
行中所示。
括号在正则表达式中具有特殊含义,但是如果需要匹配文本中的括号该怎么办?例如,您尝试匹配的电话号码可能在括号中设置了区号。在这种情况下,您需要使用反斜杠转义 ( 和 ) 字符。在交互式 shell
中输入以下内容:
传递给 re.compile()
的原始字符串中的 \(
和 \)
转义字符将与实际的括号字符匹配。在正则表达式中,以下字符具有特殊含义:
. ^ $ * + ? { } [ ] \ | ( )
. ^ $ * + ? { } [ ] \ | ( )
如果您想将这些字符检测为文本模式的一部分,则需要使用反斜杠对它们进行转义:
确保仔细检查您没有将转义括号 ( 和 ) 误认为是正则表达式中的括号 ( 和 )。如果您收到有关“缺少 )”或“不平衡括号”的错误消息,则您可能忘记包含组的未转义右括号,如下例所示:
该错误消息告诉您,r'(\(Parentheses\)'
字符串的索引 0 处有一个左括号,缺少相应的右括号。
Matching Multiple Groups with the Pipe
|
的 |
字符称为管道。您可以在任何想要匹配多个表达式之一的地方使用它。例如,正则表达式 r'Batman|Tina Fey'
将匹配“Batman”
或“Tina Fey”
。
当 Batman
和 Tina Fey
同时出现在搜索字符串中时,第一次出现的匹配文本将作为 Match
对象返回。在交互式 shell
中输入以下内容:
您还可以使用管道来匹配多种模式之一作为正则表达式的一部分。例如,假设您想要匹配任何字符串“Batman”、“Batmobile”、“Batcopter”和“Batbat”。由于所有这些字符串都以 Bat 开头,因此如果您只能指定该前缀一次,那就太好了。这可以通过括号来完成。在交互式 shell 中输入以下内容:
方法调用 mo.group()
返回完整匹配的文本“Batmobile”
,而 mo.group(1)
仅返回第一个括号组“mobile”
内匹配文本的部分。通过使用管道字符和分组括号,您可以指定您希望正则表达式匹配的几种替代模式。
如果需要匹配实际的管道字符,请使用反斜杠对其进行转义,例如 \|
。
Optional Matching with the Question Mark
有时,您只想选择性地匹配某个模式。也就是说,无论该文本是否存在,正则表达式都应该找到匹配项。 ?
字符将其前面的组标记为模式的可选部分。例如,在交互式 shell 中输入以下内容:
(wo)?
正则表达式的一部分表示模式 wo
是可选组。正则表达式将匹配其中包含零个实例或一个 wo
实例的文本。这就是正则表达式同时匹配“Batwoman”
和“Batman”
的原因。
使用前面的电话号码示例,您可以使正则表达式查找包含或不包含区号的电话号码。在交互式 shell 中输入以下内容:
你能想到的吗?
意思是“匹配此问号之前的组中的零个或一个。”
如果需要匹配实际的问号字符,请使用\?
进行转义。
Matching Zero or More with the Star
*(称为星号或星号)表示“匹配零个或多个”——星号之前的组可以在文本中出现任意多次。它可以完全不存在或一遍又一遍地重复。让我们再看一下蝙蝠侠的例子。
对于“Batman”
,正则表达式的(wo)*
部分与字符串中 wo
的零个实例匹配;对于“Batwoman”
,(wo)*
匹配 wo
的一个实例;对于“Batwowowowoman”
,(wo)*
匹配wo
的四个实例。
如果需要匹配实际的星号字符,请在正则表达式中的星号前面加上反斜杠 \*
。
Matching One or More with the Plus
*
表示“匹配零个或多个”,+
(或加号)表示“匹配一个或多个”。与星号不同,星号不要求其组出现在匹配的字符串中,加号前面的组必须至少出现一次。它不是可选的。在交互式shell
中输入以下内容,并将其与上一节中的星号正则表达式进行比较:
正则表达式 Bat(wo)+man
不会匹配字符串“The Adventures of Batman”
,因为加号至少需要一个 wo
。
如果需要匹配实际的加号字符,请在加号前面加上反斜杠以将其转义:\+
。]
Matching Specific Repetitions with Braces
如果您想要重复某个组特定次数,请在正则表达式中的该组后面加上大括号中的数字。例如,正则表达式 (Ha){3}
将匹配字符串“HaHaHa”
,但不会匹配“HaHa”
,因为后者只有两次(Ha)
组重复。
您可以通过在大括号之间写入最小值、逗号和最大值来指定范围,而不是一个数字。例如,正则表达式 (Ha){3,5}
将匹配“HaHaHa”
、“HaHaHaHa”
和“HaHaHaHaHa”
。
您还可以省略大括号中的第一个或第二个数字,以使最小值或最大值不受限制。例如,(Ha){3,}
将匹配 (Ha)
组的三个或多个实例,而 (Ha){,5}
将匹配零到五个实例。大括号可以帮助缩短正则表达式。这两个正则表达式匹配相同的模式:
这两个正则表达式也匹配相同的模式:
在交互式 shell 中输入以下内容:
此处,(Ha){3}
匹配“HaHaHa”
,但不匹配“Ha”
。由于它与“Ha”
不匹配,search()
返回 None
。
Greedy and Non-greedy Matching
由于 (Ha){3,5}
可以匹配字符串 'HaHaHaHaHa'
中的三个、四个或五个Ha
实例,因此您可能想知道为什么上一个大括号示例中Match
对象对 group()
的调用返回'HaHaHaHaHa'
而不是更短的可能性。毕竟,“HaHaHa”
和“HaHaHaHa”
也是正则表达式(Ha){3,5}
的有效匹配。
Python 的正则表达式默认是贪婪的,这意味着在不明确的情况下它们将匹配尽可能长的字符串。大括号的非贪婪(也称为惰性)版本与可能的最短字符串匹配,右大括号后跟一个问号。
在交互式shell
中输入以下内容,并注意搜索同一字符串的大括号的贪婪和非贪婪形式之间的区别:
请注意,问号在正则表达式中可以有两种含义:声明非贪婪匹配或标记可选组。这些含义完全无关。
The findall() Method
除了search()
方法之外,Regex
对象还有 findall()
方法。search()
将返回搜索字符串中第一个匹配文本的 Match
对象,而findall()
方法将返回搜索字符串中每个匹配的字符串。要查看 search()
如何仅在匹配文本的第一个实例上返回 Match
对象,请在交互式 shell
中输入以下内容:
另一方面,只要正则表达式中没有组,findall()
就不会返回 Match
对象,而是返回字符串列表。列表中的每个字符串都是与正则表达式匹配的搜索文本的一部分。在交互式 shell
中输入以下内容:
如果正则表达式中有组,则 findall()
将返回元组列表。每个元组代表一个找到的匹配项,其项是正则表达式中每个组的匹配字符串。要查看 findall()
的实际效果,请在交互式shell
中输入以下内容(请注意,正在编译的正则表达式现在包含括号中的组):
要总结 findall() 方法返回的内容,请记住以下几点:
- 当在没有组的正则表达式上调用时,例如
\d\d\d-\d\d\d-\d\d\d\d
,方法findall()
返回字符串匹配列表,例如[' 415-555-9999','212-555-0000']
。 - 当调用具有组的正则表达式时,例如
(\d\d\d)-(\d\d\d)-(\d\d\d\d)
,方法findall()
返回元组列表字符串(每组一个字符串),例如[('415', '555', '9999'), ('212', '555', '0000')]
。
Character Classes
在前面的电话号码正则表达式示例中,您了解到 \d
可以代表任何数字。也就是说,\d
是正则表达式(0|1|2|3|4|5|6|7|8|9)
的简写。这样的简写字符类有很多,如表 7-1
所示。
Shorthand character class | Represents |
---|---|
\d | 0 到 9 之间的任何数字。 |
\D | 任何非 0 到 9 数字的字符。 |
\w | 任何字母、数字或下划线字符。 (将此视为匹配“单词”字符。) |
\W | 任何非字母、数字或下划线字符的字符。 |
\s | 任何空格、制表符或换行符。 (将此视为匹配“空格”字符。) |
\S | 除空格、制表符或换行符之外的任何字符。 |
字符类非常适合缩短正则表达式。字符类[0-5]
将只匹配数字0到5;这比输入 (0|1|2|3|4|5)
要短得多。请注意,虽然\d
匹配数字,\w
匹配数字、字母和下划线,但不存在仅匹配字母的速记字符类。 (尽管您可以使用[a-zA-Z]
字符类,如下所述。)
例如,在交互式 shell
中输入以下内容:
正则表达式 \d+\s\w+
将匹配包含一个或多个数字 (\d+)
、后跟一个空格字符 (\s)
、后跟一个或多个字母/数字/下划线字符 (\w+)
的文本。 findall()
方法返回列表中正则表达式模式的所有匹配字符串。
Making Your Own Character Classes
有时您想要匹配一组字符,但简写字符类(\d、\w、\s
等)太宽泛。您可以使用方括号定义自己的字符类。例如,字符类[aeiouAEIOU]
将匹配任何元音,包括小写和大写。在交互式 shell
中输入以下内容:
您还可以使用连字符包含字母或数字范围。例如,字符类[a-zA-Z0-9]
将匹配所有小写字母、大写字母和数字。
请注意,方括号内的正常正则表达式符号不会被如此解释。这意味着您不需要使用前面的反斜杠转义.、*、? 或 ()
字符。例如,字符类 [0-5.]
将匹配数字 0 到 5 和句点。不需要写成[0-5\.]
。
通过在字符类的左括号后面放置插入符号 (^)
,可以创建负字符类。负字符类将匹配所有不属于该字符类的字符。例如,在交互式shell
中输入以下内容:
现在,我们不是匹配每个元音,而是匹配每个非元音的字符。
The Caret and Dollar Sign Characters
您还可以在正则表达式的开头使用插入符号 (^) 来指示匹配必须出现在搜索文本的开头。同样,您可以在正则表达式末尾放置一个美元符号 ($)
,以指示字符串必须以此正则表达式模式结尾。您可以一起使用 ^
和 $
来指示整个字符串必须与正则表达式匹配 - 也就是说,仅在字符串的某些子集上进行匹配是不够的。
例如,r'^Hello'
正则表达式字符串匹配以'Hello'
开头的字符串。在交互式shell
中输入以下内容:
r'\d$'
正则表达式字符串匹配以 0 到 9 之间的数字字符结尾的字符串。在交互式 shell
中输入以下内容:
r'^\d+$'
正则表达式字符串匹配以一个或多个数字字符开头和结尾的字符串。在交互式shell
中输入以下内容:
前面的交互式 shell
示例中的最后两个search()
调用演示了如果使用^
和 $
,整个字符串必须如何与正则表达式匹配。
我总是混淆这两个符号的含义,因此我使用助记符“Carrots costdollar”
来提醒自己插入符号在前,美元符号在后。
The Wildcard Character
.
正则表达式中的(或点)字符称为通配符,将匹配除换行符之外的任何字符。例如,在交互式shell
中输入以下内容:
请记住,点字符仅匹配一个字符,这就是为什么上一示例中的文本flat
匹配仅匹配lat
的原因。要匹配实际的点,请使用反斜杠转义点:\..
Matching Everything with Dot-Star
有时您会想要匹配所有内容。例如,假设您想要匹配字符串“First Name:”,后跟任何和所有文本,然后是“Last Name:”,最后再跟任何内容。您可以使用点星号 (.*)
来代表“任何内容”。请记住,点字符表示“除换行符之外的任何单个字符”,星号字符表示“零个或多个前面的字符”。
点星使用贪婪模式:它将始终尝试匹配尽可能多的文本。要以非贪婪方式匹配任何和所有文本,请使用点、星号和问号 (.*?)
。与大括号一样,问号告诉Python
以非贪婪的方式进行匹配。
在交互式shell
中输入以下内容,查看贪婪版本和非贪婪版本之间的区别:
这两个正则表达式大致翻译为“匹配左尖括号,后跟任何内容,然后是右尖括号。”但是字符串 '<Toserve man> fordinner.>'
的右尖括号有两个可能的匹配项。在正则表达式的非贪婪版本中,Python 匹配最短的可能字符串:“<Toserve man>”
。在贪婪版本中,Python 匹配尽可能长的字符串:“<Toserve man> fordinner.>”
。
Matching Newlines with the Dot Character
点星将匹配除换行符之外的所有内容。通过将 re.DOTALL
作为第二个参数传递给 re.compile()
,可以使点字符匹配所有字符,包括换行符。
正则表达式noNewlineRegex
没有将re.DOTALL
传递给创建它的 re.compile()
调用,它只会匹配第一个换行符之前的所有内容,而 newlineRegex
确实将 re.DOTALL
传递给 re.compile( )
,匹配一切。这就是 newlineRegex.search()
调用匹配完整字符串(包括换行符)的原因。
Review of Regex Symbols
Case-Insensitive Matching
通常,正则表达式将文本与您指定的确切大小写进行匹配。例如,以下正则表达式匹配完全不同的字符串:
但有时您只关心匹配字母,而不关心它们是大写还是小写。要使正则表达式不区分大小写,可以将 re.IGNORECASE
或 re.I
作为第二个参数传递给 re.compile()
。在交互式shell
中输入以下内容:
Substituting Strings with the sub() Method
正则表达式不仅可以查找文本模式,还可以用新文本代替这些模式。 Regex 对象的 sub() 方法传递两个参数。第一个参数是一个字符串,用于替换任何匹配项。第二个是正则表达式的字符串。 sub() 方法返回应用了替换的字符串。
例如,在交互式 shell 中输入以下内容:
有时您可能需要使用匹配的文本本身作为替换的一部分。在 sub() 的第一个参数中,您可以键入 \1、\2、\3
等,表示“在替换中输入组 1、2、3
等的文本”。
例如,假设您想通过仅显示秘密特工姓名的第一个字母来审查他们的姓名。为此,您可以使用正则表达式代理 (\w)\w*
并将r'\1****'
作为第一个参数传递给 sub()
。该字符串中的\1
将被第 1 组(即正则表达式的(\w)
组)匹配的任何文本替换。
Managing Complex Regexes
如果您需要匹配的文本模式很简单,则正则表达式就可以了。但匹配复杂的文本模式可能需要长而复杂的正则表达式。您可以通过告诉 re.compile()
函数忽略正则表达式字符串内的空格和注释来缓解这种情况。这种“详细模式”可以通过将变量re.VERBOSE
作为第二个参数传递给re.compile()
来启用。
现在,而不是像这样难以阅读的正则表达式:
您可以使用如下注释将正则表达式分散在多行中:
请注意前面的示例如何使用三引号语法 (‘’') 创建多行字符串,以便您可以将正则表达式定义分散到多行中,从而使其更加清晰。
正则表达式字符串内的注释规则与常规 Python 代码相同:# 符号及其后面到行尾的所有内容都将被忽略。此外,正则表达式的多行字符串内的额外空格不被视为要匹配的文本模式的一部分。这使您可以组织正则表达式,使其更易于阅读。
Combining re.IGNORECASE, re.DOTALL, and re.VERBOSE
What if you want to use re.VERBOSE to write comments in your regular expression but also want to use re.IGNORECASE to ignore capitalization? Unfortunately, the re.compile() function takes only a single value as its second argument. You can get around this limitation by combining the re.IGNORECASE, re.DOTALL, and re.VERBOSE variables using the pipe character (|), which in this context is known as the bitwise or operator.
So if you want a regular expression that’s case-insensitive and includes newlines to match the dot character, you would form your re.compile() call like this:
这种语法有点过时,源自 Python 的早期版本。位运算符的详细信息超出了本书的范围,但请查看 https://nostarch.com/automatestuff2/ 上的资源以获取更多信息。您还可以为第二个参数传递其他选项;它们并不常见,但您也可以在资源中阅读有关它们的更多信息。
Project: Phone Number and Email Address Extractor
假设您有一项无聊的任务:在长网页或文档中查找每个电话号码和电子邮件地址。如果您手动滚动页面,您可能会搜索很长时间。但是,如果您有一个程序可以在剪贴板中的文本中搜索电话号码和电子邮件地址,则只需按 CTRL-A 选择所有文本,按 CTRL-C 将其复制到剪贴板,然后运行您的程序。它可以用它找到的电话号码和电子邮件地址替换剪贴板上的文本。
每当您处理一个新项目时,您都会很想直接开始编写代码。但通常情况下,最好退后一步,考虑更大的前景。我建议首先为您的程序需要做什么制定一个高级计划。现在先不要考虑实际的代码——您可以稍后再考虑。现在,坚持大纲。
例如,您的电话和电子邮件地址提取器需要执行以下操作:
- 将文本从剪贴板中取出。
- 查找文本中的所有电话号码和电子邮件地址。
- 将它们粘贴到剪贴板上。
现在您可以开始思考这在代码中如何工作。该代码需要执行以下操作:
使用pyperclip
模块复制和粘贴字符串。
- 创建两个正则表达式,一个用于匹配电话号码,另一个用于匹配电子邮件地址。
- 查找两个正则表达式的所有匹配项,而不仅仅是第一个匹配项。
- 将匹配的字符串整齐地格式化为单个字符串以进行粘贴。
- 如果在文本中未找到匹配项,则显示某种消息。
该列表就像该项目的路线图。在编写代码时,您可以分别关注其中的每个步骤。每个步骤都相当易于管理,并且用您已经知道如何在 Python 中执行的操作来表达。
Step 1: Create a Regex for Phone Numbers
首先,您必须创建正则表达式来搜索电话号码。创建一个新文件,输入以下内容,并将其另存为 phoneAndEmail.py
:
TODO 注释只是程序的骨架。当您编写实际代码时,它们将被替换。
电话号码以可选的区号开头,因此区号组后跟一个问号。由于区号可以仅为三位数字(即 \d{3})或括号内的三位数字(即 (\d{3})),因此您应该使用管道连接这些部分。您可以将正则表达式注释 # Area code 添加到多行字符串的这一部分,以帮助您记住什么 (\d{3}|(\d{3}))?应该匹配。
电话号码分隔符可以是空格 (\s)、连字符 (-) 或句点 (.),因此这些部分也应该用管道连接。正则表达式的接下来的几个部分很简单:三个数字,后跟另一个分隔符,然后是四个数字。最后一部分是可选的扩展名,由任意数量的空格组成,后跟 ext、x 或 ext.,后跟两到五位数字。
Note
很容易与包含带括号 ( ) 和转义括号 ( ) 的组的正则表达式混淆。如果您收到“缺少 )、未终止的子模式”错误消息,请记住仔细检查您使用的是否正确。
Step 2: Create a Regex for Email Addresses
您还需要一个可以匹配电子邮件地址的正则表达式。使您的程序如下所示:
电子邮件地址 ➊ 的用户名部分是一个或多个字符,可以是以下任意一种:小写和大写字母、数字、点、下划线、百分号、加号或连字符。您可以将所有这些放入一个字符类中:[a-zA-Z0-9._%±]。
域名和用户名由 @ 符号 ➋ 分隔。域名 ➌ 的字符类稍微宽松一些,仅包含字母、数字、句点和连字符:[a-zA-Z0-9.-]。最后是“dot-com”部分(技术上称为顶级域),它实际上可以是任何点。这是两个到四个字符之间。
电子邮件地址的格式有很多奇怪的规则。此正则表达式不会匹配所有可能的有效电子邮件地址,但它会匹配您遇到的几乎所有典型电子邮件地址。
Step 3: Find All Matches in the Clipboard Text
现在您已经指定了电话号码和电子邮件地址的正则表达式,您可以让 Python
的 re
模块完成查找剪贴板上所有匹配项的艰苦工作。 pyperclip.paste()
函数将获取剪贴板上文本的字符串值,findall()
正则表达式方法将返回元组列表。
使您的程序如下所示:
每个匹配项都有一个元组,每个元组包含正则表达式中每个组的字符串。请记住,组 0 匹配整个正则表达式,因此元组索引 0 处的组是您感兴趣的组。
正如您在 ➊ 中看到的,您将把匹配项存储在名为 matches 的列表变量中。它以一个空列表和几个 for 循环开始。对于电子邮件地址,您附加每个匹配项的第 0 组 ➌。对于匹配的电话号码,您不想只附加组 0。虽然程序检测多种格式的电话号码,但您希望附加的电话号码采用单一标准格式。 phoneNum 变量包含一个由匹配文本 ➋ 的第 1、3、5 和 8 组构建的字符串。 (这些组是区号、前三位数字、后四位数字和分机号。)
Step 4: Join the Matches into a String for the Clipboard
现在您已将电子邮件地址和电话号码作为匹配项中的字符串列表,您希望将它们放在剪贴板上。 pyperclip.copy()
函数仅接受单个字符串值,而不是字符串列表,因此您可以在匹配时调用join()
方法。
为了更容易地看到程序正在运行,让我们将找到的任何匹配项打印到终端。如果没有找到电话号码或电子邮件地址,程序应该告诉用户这一点。
使您的程序如下所示: