介绍
正则表达式(regular expression,或称 regex)是文本处理的强大工具。它是一系列定义了搜索模式的字符序列,允许你以极大的灵活性来查找、匹配和管理文本。在 Python 中,正则表达式的功能由 re 模块处理。
在这个 Lab 中,你将学习使用 Python 正则表达式的基础知识。我们将从匹配模式的基础开始,逐步过渡到更高级的主题,例如分组、使用量词(quantifiers)进行重复、定义字符类(character classes),以及利用强大的方法和标志(flags)来处理复杂的匹配场景。到最后,你将能够熟练地使用正则表达式来执行复杂的字符串操作。
正则表达式匹配简介
在这一步中,你将学习在 Python 中使用正则表达式的基本工作流程。这个过程通常涉及四个主要步骤:
- 导入
re模块。 - 使用
re.compile()创建一个 Regex(正则表达式)对象。推荐使用原始字符串(raw string,即r"...")以防止反斜杠被解释为转义字符。 - 使用 Regex 对象的
search()方法在字符串中查找一个模式(pattern)。如果找到该模式,它会返回一个Match(匹配)对象。 - 使用
Match对象的group()方法获取实际匹配到的文本。
首先,我们来看一些你将要使用的常见 Regex 符号。
| 符号 | 匹配内容 |
|---|---|
? |
前一个分组零次或一次。 |
* |
前一个分组零次或多次。 |
+ |
前一个分组一次或多次。 |
{n} |
前一个分组恰好 n 次。 |
^spam |
字符串必须以 spam 开头。 |
spam$ |
字符串必须以 spam 结尾。 |
. |
任何字符,但不包括换行符。 |
\d, \w, 和 \s |
分别代表一个数字(digit)、单词(word)或空格(space)字符。 |
\D, \W, 和 \S |
分别代表除数字、单词或空格以外的任何字符。 |
[abc] |
方括号中的任意一个字符(例如 a、b 或 c)。 |
[^abc] |
除方括号中的字符以外的任意字符。 |
现在,让我们将这些付诸实践。我们将编写一个脚本来查找字符串中的电话号码。美国电话号码的模式是 XXX-XXX-XXXX,其中 X 是一个数字。在 Regex 中,\d 代表一个数字。
注意:如果你不熟悉使用 WebIDE(类似 VS Code),请先在 这个实验 学习。
从左侧的文件浏览器中打开 main.py 文件,并添加以下代码。
## Import re module for regular expression operations
import re
## re.compile(): create regex pattern object (use raw string r'' to avoid escaping)
## The pattern \d\d\d-\d\d\d-\d\d\d\d matches three digits, a hyphen, three digits, a hyphen, and four digits.
phone_num_regex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d')
## The search() method scans the string, looking for the first location where this regex produces a match.
mo = phone_num_regex.search('My number is 415-555-4242.')
## The group() method returns the string of the matched text.
print(f'Phone number found: {mo.group()}')
要运行该脚本,请打开终端并执行以下命令:
python3 main.py
你应该会在控制台中看到匹配到的电话号码被打印出来。
Phone number found: 415-555-4242
这个基本工作流程是 Python 中所有 Regex 操作的基础。
分组与交替(或或)
在这个步骤中,你将学习如何使用分组(groups)来隔离匹配文本中的特定部分,以及如何使用交替(alternation)来匹配多个可能模式中的一个。
使用圆括号 () 进行分组
圆括号 () 用于在正则表达式中创建分组。当你想要提取匹配内容的部分信息时,例如从电话号码中提取区号,分组就非常有用。你可以使用 group(n) 来访问单个分组,其中 group(1) 是第一个分组,group(2) 是第二个,依此类推。group(0) 或 group() 返回整个匹配的字符串。
让我们修改 main.py 来将区号与电话号码的其余部分分开。
import re
## 圆括号创建分组:group(1) 对应第一个分组,group(2) 对应第二个分组。
## 第一个分组 (\d\d\d) 捕获区号。
## 第二个分组 (\d\d\d-\d\d\d\d) 捕获主体号码。
phone_num_regex = re.compile(r'(\d\d\d)-(\d\d\d-\d\d\d\d)')
mo = phone_num_regex.search('My number is 415-555-4242.')
## 要一次性检索所有分组,请使用 groups() 方法,它返回一个元组(tuple)。
area_code, main_number = mo.groups()
print(f"Area Code: {area_code}")
print(f"Main Number: {main_number}")
print(f"Full Match: {mo.group(0)}")
运行脚本:
python3 main.py
输出将显示号码被分隔开的部分:
Area Code: 415
Main Number: 555-4242
Full Match: 415-555-4242
使用竖线 | 进行交替匹配
竖线字符 | 充当“或”("OR")运算符。它允许你匹配多个表达式中的任意一个。例如,Batman|Tina Fey 将匹配 "Batman" 或 "Tina Fey"。
你也可以在分组内使用竖线,以便在正则表达式中匹配多个可能模式中的一个。让我们尝试匹配不同的 "Bat-" 相关术语。使用以下代码更新你的 main.py:
import re
## 竖线 | 匹配分组内多个模式中的一个。
## 在这里,它匹配 'man', 'mobile', 'copter', 或 'bat'。
bat_regex = re.compile(r'Bat(man|mobile|copter|bat)')
mo = bat_regex.search('Batmobile lost a wheel')
print(f"Full match: {mo.group()}")
print(f"Group 1: {mo.group(1)}")
再次运行脚本:
python3 main.py
你将看到正则表达式匹配了 "Batmobile",并且第一个分组捕获了 "mobile"。
Full match: Batmobile
Group 1: mobile
量词重复
在这个步骤中,我们将探讨量词,它们是特殊的字符,允许你指定前面分组应该重复多少次。
使用 ? 进行可选匹配
问号 ? 使前面的分组成为可选的,意味着它将匹配零次或一次。这对于可能出现也可能不出现的模式非常理想。例如,我们可以同时匹配 "Batman" 和 "Batwoman"。
用以下代码更新 main.py:
import re
## (wo)? 分组是可选的。如果存在,它会匹配 'wo';如果不存在,则匹配空。
bat_regex = re.compile(r'Bat(wo)?man')
mo1 = bat_regex.search('The Adventures of Batman')
print(mo1.group())
mo2 = bat_regex.search('The Adventures of Batwoman')
print(mo2.group())
运行脚本:
python3 main.py
输出显示两个字符串都成功匹配:
Batman
Batwoman
使用 * 进行零次或多次匹配
星号 *(或称星号)匹配前面的分组零次或多次。当一个模式可以重复任意次数(包括不重复)时,这非常有用。
import re
## (wo)* 分组可以出现零次或多次。
bat_regex = re.compile(r'Bat(wo)*man')
mo1 = bat_regex.search('The Adventures of Batman') ## 零个 'wo'
print(mo1.group())
mo2 = bat_regex.search('The Adventures of Batwowowowoman') ## 四个 'wo'
print(mo2.group())
使用 + 进行一次或多次匹配
加号 + 匹配前面的分组一次或多次。与星号不同,该分组必须至少出现一次。
import re
## (wo)+ 分组必须至少出现一次。
bat_regex = re.compile(r'Bat(wo)+man')
mo1 = bat_regex.search('The Adventures of Batwoman') ## 一个 'wo'
print(mo1.group())
mo2 = bat_regex.search('The Adventures of Batman') ## 零个 'wo'
print(mo2 is None) ## 由于没有找到匹配,这将打印 True。
使用 {} 进行特定次数的重复
花括号 {} 允许你指定确切的重复次数或一个范围。
{3}匹配恰好 3 次。{3,5}匹配 3 到 5 次之间。{3,}匹配 3 次或更多次。{,5}匹配最多 5 次。
import re
## (Ha){3} 匹配 'Ha' 重复正好 3 次。
ha_regex = re.compile(r'(Ha){3}')
mo1 = ha_regex.search('HaHaHa')
print(mo1.group())
贪婪匹配与非贪婪匹配
默认情况下,Python 的正则表达式是“贪婪的”(greedy),意味着它会匹配最长的可能字符串。要使其成为“非贪婪的”(non-greedy,匹配最短的可能字符串),请在量词后添加一个问号 ?。
让我们看看区别。清空 main.py 并添加以下内容:
import re
## 贪婪匹配:{3,5} 匹配最长的可能字符串(5 个 'Ha')。
greedy_ha_regex = re.compile(r'(Ha){3,5}')
mo1 = greedy_ha_regex.search('HaHaHaHaHa')
print(f"Greedy match: {mo1.group()}")
## 非贪婪匹配:{3,5}? 匹配最短的可能字符串(3 个 'Ha')。
non_greedy_ha_regex = re.compile(r'(Ha){3,5}?')
mo2 = non_greedy_ha_regex.search('HaHaHaHaHa')
print(f"Non-greedy match: {mo2.group()}")
运行脚本:
python3 main.py
输出清晰地显示了区别:
Greedy match: HaHaHaHaHa
Non-greedy match: HaHaHa
字符类、锚点和通配符
在这个步骤中,你将学习更高级的技术,包括查找所有匹配项、创建自定义字符集、将匹配项锚定到字符串的开头或结尾,以及使用通配符。
findall() 方法
虽然 search() 只返回第一个匹配项,但 findall() 方法会返回字符串中所有不重叠匹配项的列表。如果正则表达式包含分组,findall() 会返回一个元组列表,其中每个元组包含每个分组的匹配字符串。
用以下示例更新 main.py:
import re
## 如果正则表达式没有分组,findall() 返回一个字符串列表。
phone_num_regex = re.compile(r'\d\d\d-\d\d\d-\d\d\d\d')
matches = phone_num_regex.findall('Cell: 415-555-9999 Work: 212-555-0000')
print(matches)
运行脚本查看输出:
python3 main.py
['415-555-9999', '212-555-0000']
创建自定义字符集 []
方括号 [] 允许你定义一个自定义字符集,它将匹配其中的任意单个字符。例如,[aeiou] 匹配任何小写元音字母。你也可以指定一个范围,如 [a-z] 匹配任何小写字母,或 [0-9] 匹配任何数字。
如果脱字符 ^ 作为方括号内的第一个字符,它会创建一个否定字符集,匹配类中不包含的任何字符。
让我们在一个字符串中查找所有元音字母。更新 main.py:
import re
## [aeiouAEIOU] 匹配任何元音字母,无论大小写。
vowel_regex = re.compile(r'[aeiouAEIOU]')
vowels = vowel_regex.findall('Robocop eats baby food. BABY FOOD.')
print(vowels)
锚点:脱字符 ^ 和美元符号 $
锚点用于断言字符串中的位置。
- 脱字符
^匹配字符串的开头。 - 美元符号
$匹配字符串的结尾。
当它们一起使用(^...$)时,它们强制要求整个字符串必须匹配该模式。
import re
## ^Hello 匹配以 'Hello' 开头的字符串。
begins_with_hello = re.compile(r'^Hello')
match1 = begins_with_hello.search('Hello world!')
print(match1 is not None) ## True
## \d+$ 匹配以一个或多个数字结尾的字符串。
ends_with_number = re.compile(r'\d+$')
match2 = ends_with_number.search('Your number is 42')
print(match2.group()) ## '42'
## ^\d+$ 匹配仅由数字组成的字符串。
whole_string_is_num = re.compile(r'^\d+$')
match3 = whole_string_is_num.search('1234567890')
print(match3 is not None) ## True
通配符字符 .
点号 .(或通配符)字符匹配除换行符外的任何单个字符。它是“任何字符”的有用简写。
让我们查找所有以 "at" 结尾的三个字母的单词。更新 main.py:
import re
## . 字符匹配任何字符(除了换行符)。
## 因此,.at 匹配 'cat', 'hat', 'sat' 等。
at_regex = re.compile(r'.at')
matches = at_regex.findall('The cat in the hat sat on the flat mat.')
print(matches)
运行脚本:
python3 main.py
输出将是所有匹配单词的列表:
['cat', 'hat', 'sat', 'lat', 'mat']
高级方法和标志 (Flags)
在最后这个步骤中,我们将介绍一些强大的方法和标志(flags),它们可以修改正则表达式的行为,使其更加通用。
使用点星 .* 匹配所有内容
组合 .* 是一个常见的模式,它匹配任意字符(.)重复零次或多次(*)。它用于匹配任意字符序列。默认情况下,它是贪婪的,会匹配尽可能多的文本。
import re
## 模式 (.*) 捕获 'First Name: ' 和 ' Last Name: ' 之间的所有内容。
name_regex = re.compile(r'First Name: (.*) Last Name: (.*)')
mo = name_regex.search('First Name: Al Last Name: Sweigart')
print(f"First Name: {mo.group(1)}")
print(f"Last Name: {mo.group(2)}")
要使其非贪婪,请使用 .*?。这将匹配最短的可能字符串。
使用 re.DOTALL 匹配换行符
默认情况下,点号 . 字符不匹配换行符(\n)。要更改此行为,你可以将 re.DOTALL 作为第二个参数传递给 re.compile()。此标志允许 . 匹配所有字符,包括换行符。
import re
## 使用 re.DOTALL,'.*' 将会跨越多行进行匹配。
newline_regex = re.compile('.*', re.DOTALL)
mo = newline_regex.search('Serve the public trust.\nProtect the innocent.\nUphold the law.')
print(mo.group())
使用 re.IGNORECASE 进行不区分大小写的匹配
为了让你的正则表达式忽略大小写(例如,匹配 "cat"、"Cat" 和 "CAT"),请将 re.IGNORECASE 或其简写 re.I 作为第二个参数传递给 re.compile()。
import re
## re.I 使模式 'robocop' 能够匹配 'Robocop', 'ROBOCOP' 等。
robocop = re.compile(r'robocop', re.I)
match = robocop.search('Robocop is part man, part machine, all cop.')
print(match.group())
使用 sub() 方法替换字符串
sub() 方法查找模式的所有出现并将其替换为指定的字符串。它接受两个参数:替换字符串和要搜索的字符串。
让我们将特工名称替换为 "CENSORED"。更新 main.py:
import re
## 模式 'Agent \w+' 匹配 'Agent ' 后面跟着一个或多个单词字符。
names_regex = re.compile(r'Agent \w+')
result = names_regex.sub('CENSORED', 'Agent Alice gave the secret documents to Agent Bob.')
print(result)
运行脚本:
python3 main.py
输出将显示替换后的字符串:
CENSORED gave the secret documents to CENSORED.
使用 re.VERBOSE 管理复杂的正则表达式
当正则表达式模式变得复杂时,它们可能难以阅读。通过向 re.compile() 传递 re.VERBOSE,你可以将正则表达式写在多行上,并添加注释来解释不同的部分。字符串中的空白字符和注释(#)将被忽略。
这个复杂的正则表达式:
phone_regex = re.compile(r'((\d{3}|\(\d{3}\))?(\s|-|\.)?\d{3}(\s|-|\.)\d{4}(\s*(ext|x|ext.)\s*\d{2,5})?)')
使用 re.VERBOSE 变为以下易读版本:
import re
phone_regex = re.compile(r'''(
(\d{3} | \(\d{3}\))? ## 地区代码(可选)
(\s | - | \.)? ## 分隔符(可选)
\d{3} ## 前 3 位数字
(\s | - | \.) ## 分隔符
\d{4} ## 后 4 位数字
(\s*(ext|x|ext.)\s*\d{2,5})? ## 分机号(可选)
)''', re.VERBOSE)
mo = phone_regex.search("My phone number is 415-555-4242 ext. 55")
if mo:
print(f"Found: {mo.group()}")
这使得你的代码更易于维护。
总结
在这个实验中,你已经使用 Python 的 re 模块掌握了正则表达式的基础知识。你学习了从使用 re.compile() 编译模式到使用 search() 查找匹配项并用 group() 检索文本的完整工作流程。
你探索了关键概念,包括:
- 使用
()进行分组,以捕获匹配项的具体部分。 - 使用
|进行交替(或),以匹配多个模式中的一个。 - 如
?、*、+和{}等量词,用于控制重复次数。 - 贪婪匹配与非贪婪匹配,用于控制匹配的长度。
- 高级工具,如
findall()用于获取所有匹配项,字符集[]用于自定义集合,以及锚点^和$用于将匹配项固定在字符串边界上。 - 强大的标志(flags),如
re.DOTALL用于多行匹配,re.IGNORECASE用于不区分大小写,以及re.VERBOSE用于编写清晰、带注释的正则表达式。 - 使用
sub()方法进行字符串替换。
掌握了这些技能后,你现在可以处理各种文本处理任务,从简单的数据验证到复杂的数据提取和操作。



