Python 正则表达式

PythonBeginner
立即练习

介绍

正则表达式(regular expression,或称 regex)是文本处理的强大工具。它是一系列定义了搜索模式的字符序列,允许你以极大的灵活性来查找、匹配和管理文本。在 Python 中,正则表达式的功能由 re 模块处理。

在这个 Lab 中,你将学习使用 Python 正则表达式的基础知识。我们将从匹配模式的基础开始,逐步过渡到更高级的主题,例如分组、使用量词(quantifiers)进行重复、定义字符类(character classes),以及利用强大的方法和标志(flags)来处理复杂的匹配场景。到最后,你将能够熟练地使用正则表达式来执行复杂的字符串操作。

正则表达式匹配简介

在这一步中,你将学习在 Python 中使用正则表达式的基本工作流程。这个过程通常涉及四个主要步骤:

  1. 导入 re 模块。
  2. 使用 re.compile() 创建一个 Regex(正则表达式)对象。推荐使用原始字符串(raw string,即 r"...")以防止反斜杠被解释为转义字符。
  3. 使用 Regex 对象的 search() 方法在字符串中查找一个模式(pattern)。如果找到该模式,它会返回一个 Match(匹配)对象。
  4. 使用 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() 方法进行字符串替换。

掌握了这些技能后,你现在可以处理各种文本处理任务,从简单的数据验证到复杂的数据提取和操作。