跳到主要内容

正则表达式

提示

正则表达式(Regular Expression,简称 Regex 或 Regexp)是用于匹配字符串中字符组合的模式。它就像是文本处理的“瑞士军刀”。在很多高级编程语言中,正则表达式都被作为一种极其重要且强大的文本处理工具。虽然它看似晦涩难懂,但只要掌握了它的核心规律,你就能轻松应对各种复杂的文本搜索、提取和替换任务。

Python 通过内置的 re 模块提供了对正则表达式的完美支持。在本章中,我们将由浅入深,先学习正则表达式的匹配语法,再掌握 Python 中 re 模块的实战用法。

为什么需要正则表达式?

在学习正则表达式之前,我们先来看看它能解决什么问题。

假设我们有一段文本,需要从中找出所有的电话号码。电话号码的格式可能是 010-12345678021-87654321,即“3位区号-8位电话”。

如果只用我们之前学过的字符串方法,代码可能会非常冗长且脆弱:

text = "请拨打电话:010-12345678 或者 021-87654321。"

# 如果仅用普通的字符串方法,我们需要非常繁琐地切片、判断长度、
# 判断“-”的位置、判断各个部分是否全是数字等,且极易出错。

而如果使用正则表达式,我们只需要写一行简洁的模式:r"\d{3}-\d{8}",然后调用 re.findall() 即可!

正则表达式的基础语法

正则表达式由普通字符(如字母 az)以及特殊字符(称为“元字符”,Metacharacters)组成。元字符赋予了正则表达式超强的模式描述能力。

下面是正则表达式中最常用的一些语法规则:

1. 字符匹配(Character Classes)

元字符描述示例与解释
.匹配除换行符之外的任意单个字符p.t 可匹配 pat, pet, p-t, p9t 等。
[abc]字符集:匹配方括号中的任意一个字符。p[ae]t 仅匹配 patpet
[^abc]排除字符集:匹配不在方括号中的任意一个字符。p[^ae]t 匹配 pot,但不匹配 patpet
[a-z]字符范围:匹配指定范围内的任意单个字符。[0-9] 匹配任意数字,[a-zA-Z] 匹配任意英文字母。

2. 预定义字符集(Shorthands)

为了方便书写,正则表达式为常用的字符集提供了快捷方式(转义字符):

快捷键对应字符集描述
\d[0-9]匹配任意数字(d 代表 decimal)。
\D[^0-9]匹配任意非数字
\w[a-zA-Z0-9_]匹配任意字母、数字或下划线(w 代表 word)。在 Python 3 中默认也包括中文字符。
\W[^a-zA-Z0-9_]匹配任意非字母、数字或下划线的字符。
\s[ \t\n\r\f\v]匹配任意空白字符(包括空格、制表符、换行符等,s 代表 space)。
\S[^ \t\n\r\f\v]匹配任意非空白字符

3. 位置锚定(Anchors)

锚定符不匹配具体的字符,而是匹配字符之间的位置字符串的边界

元字符描述示例
^匹配字符串的开头。如果开启了多行模式,也匹配每行的开头。^Hello 匹配以 "Hello" 开头的字符串。
$匹配字符串的结尾。如果开启了多行模式,也匹配每行的结尾。world$ 匹配以 "world" 结尾 of 字符串。
\b匹配一个单词边界(即单词字符与非单词字符之间交界的位置)。\bcat\b 只匹配独立的单词 "cat",不匹配 "category" 或 "copycat"。

4. 量词(Quantifiers)

量词用于指定前一个字符或字符集需要重复出现的次数

元字符描述示例与解释
*重复 0 次或多次(等价于 {0,})。ab* 匹配 a, ab, abb, abbb 等。
+重复 1 次或多次(等价于 {1,})。ab+ 匹配 ab, abb,但不匹配 a
?重复 0 次或 1 次(即该字符是可选的,等价于 {0,1})。colors? 匹配 colorcolors
{n}正好重复 n 次\d{6} 匹配连续的 6 位数字(如邮政编码)。
{n,}重复 至少 n 次\d{3,} 匹配 3 位及以上的连续数字。
{n,m}重复 n 到 m 次\d{3,5} 匹配 3 位、4 位或 5 位连续数字。

[!WARNING] 贪婪与非贪婪(Greedy vs. Non-greedy)

默认情况下,所有的量词都是贪婪的,即它们会尽可能多地匹配字符。 比如用模式 a.*b 去匹配字符串 "a1b2b3b" 时,它会匹配整个 "a1b2b3b"(直到最后一个 b)。 如果我们想让它非贪婪(懒惰),即尽可能少地匹配,只需在量词后面加上一个问号 ?。 比如用模式 a.*?b 去匹配同一个字符串,它只会匹配到第一个 b,即 "a1b"

5. 分组与分支选择

  • 分组 (...):使用圆括号可以将多个字符组合成一个单元,作为一个整体应用量词。此外,括号还会“捕获”匹配到的子字符串,方便后续提取。例如 (ab)+ 可以匹配 ab, abab, ababab 等。
  • 分支选择 |:相当于逻辑“或”(OR),用于匹配多个可选模式之一。例如 cat|dog 可以匹配 catdog。通常与括号结合使用,如 I like (apples|bananas).

Python 的 re 模块实战

掌握了正则表达式的基本语法后,我们来看看如何在 Python 代码中使用它们。

原始字符串(Raw Strings)的救赎

在 Python 中写正则表达式时,强烈建议使用原始字符串(以 r 开头的字符串),例如 r"\d{3}-\d{8}"

为什么呢?因为在普通字符串中,反斜杠 \ 具有转义含义(例如 \n 代表换行,\t 代表制表符)。如果不用原始字符串,为了表示正则里的 \d,你必须写成 "\\d";为了匹配一个字面上的反斜杠 \,你甚至得写成 "\\\\"(四斜杠地狱!)。

而使用 r"...",Python 会原封不动地将反斜杠传递给正则引擎,让代码清爽得多:

# 推荐写法
pattern = r"\d+"

1. 查找第一个匹配:re.search()re.match()

这两个函数都用于在字符串中寻找匹配,但有核心区别:

  • re.match():**必须从字符串的开头(索引 0)**开始匹配,如果开头不匹配,直接返回 None
  • re.search():在整个字符串中扫描,寻找第一个匹配项。

它们在匹配成功时都会返回一个 Match 对象,未找到时返回 None

import re

text = "Python is 100% awesome, 99% useful!"

# match 必须从开头匹配
match_result = re.match(r"\d+", text)
print(f"re.match 结果: {match_result}") # 输出: None,因为开头是 "Python" 而非数字

# search 在整个字符串中寻找第一个匹配
search_result = re.search(r"\d+", text)
print(f"re.search 结果: {search_result}") # 返回 Match 对象

if search_result:
# 使用 Match 对象的 group() 获取匹配到的文本
print(f"匹配到的内容: {search_result.group()}") # 输出: 100
# 获取匹配文本在原字符串中的起止索引
print(f"匹配位置: {search_result.span()}") # 输出: (10, 13)

2. 找出所有匹配项:re.findall()re.finditer()

  • re.findall():找出字符串中所有满足模式的匹配项,并以**列表(list)**形式返回匹配到的文本。
  • re.finditer():返回一个迭代器(iterator),每次迭代产生一个 Match 对象。在处理超长文本时,使用 re.finditer 可以节省大量内存。
import re

text = "Python is 100% awesome, 99% useful, and 0% boring!"

# findall 返回纯文本列表
numbers = re.findall(r"\d+", text)
print(f"findall 结果: {numbers}") # 输出: ['100', '99', '0']

# finditer 返回 Match 对象的迭代器,能获取每个匹配的具体位置
for m in re.finditer(r"\d+", text):
print(f"找到数字: {m.group()},位置: {m.span()}")

3. 强大的文本替换:re.sub()

re.sub(pattern, repl, string) 用于将字符串中匹配到模式的部分替换为指定的文本 repl

import re

text = "我的手机号是 138-1234-5678,他的手机号是 139-8765-4321。"

# 示例 1:将手机号中的连字符替换为空格
new_text = re.sub(r"-", " ", text)
print(new_text) # 输出: 我的手机号是 138 1234 5678,他的手机号是 139 8765 4321。

# 示例 2:手机号脱敏(隐藏中间四位)
# 我们用圆括号将手机号分成三组:前三位、中间四位、后四位
# \1, \2, \3 分别代表第一、二、三组捕获的内容。我们在替换时将第二组替换为 ****
secure_text = re.sub(r"(\d{3})-(\d{4})-(\d{4})", r"\1-****-\3", text)
print(secure_text) # 输出: 我的手机号是 138-****-5678,他的手机号是 139-****-4321。

[!TIP] re.sub 的替换项 repl 不仅可以是一个字符串,还可以是一个函数! 该函数会接收一个 Match 对象作为参数,并需要返回一个用于替换的字符串。这在需要动态计算替换内容的场景极其强大:

# 动态替换:将文本中所有的数字都翻倍
text = "小明有 5 个苹果和 12 个香蕉。"
doubled_text = re.sub(r"\d+", lambda m: str(int(m.group()) * 2), text)
print(doubled_text) # 输出: 小明有 10 个苹果和 24 个香蕉。

4. 字符串分割:re.split()

Python 自带的 str.split() 只能按照固定的单一字符或字符串分割,而 re.split() 可以按照复杂的正则表达式模式来分割字符串。

import re

text = "苹果,香蕉;橘子 西瓜\t哈密瓜"

# 按照逗号、分号、或任意个空格(包括制表符)分割
fruits = re.split(r"[,;]|\s+", text)
print(fruits) # 输出: ['苹果', '香蕉', '橘子', '西瓜', '哈密瓜']

编译正则表达式:re.compile()

在实际开发中,如果同一个正则表达式需要在一个循环或程序中被使用成千上万次,频繁解析正则模式会消耗宝贵的 CPU 时间。

此时,我们可以使用 re.compile() 将正则表达式模式提前编译为一个特殊的 Pattern 对象。编译后的对象可以直接调用 search, findall, sub 等方法,从而极达到优化运行效率的目的。

import re

# 编译正则表达式
phone_pattern = re.compile(r"\b1[3-9]\d{9}\b")

# 直接在 Pattern 对象上调用方法,不需要再次传入模式参数
text1 = "联系方式:13800138000"
text2 = "客服热线:18911112222"

print(phone_pattern.search(text1).group()) # 输出: 13800138000
print(phone_pattern.search(text2).group()) # 输出: 18911112222

分组捕获与匹配对象的深入探究

当正则表达式中包含圆括号 (...) 时,它们在完成匹配的同时,也会捕获对应的子文本。我们可以通过 Match 对象的特有方法轻松提取它们:

  • group(0)group():返回整个匹配到的文本。
  • group(n):返回第 n 个括号分组捕获的文本(1-indexed)。
  • groups():以元组形式返回所有括号分组捕获的内容。
import re

email = "user.name@example.com"
# 分组 1: 用户名 (\w+\.?\w+)
# 分组 2: 域名 (\w+\.\w+)
pattern = r"^([\w\.]+)@([\w\.]+)$"

match = re.match(pattern, email)
if match:
print(f"完整邮箱: {match.group(0)}") # 输出: user.name@example.com
print(f"用户名: {match.group(1)}") # 输出: user.name
print(f"域名: {match.group(2)}") # 输出: example.com
print(f"全部分组: {match.groups()}") # 输出: ('user.name', 'example.com')

[!NOTE] 非捕获分组 (?:...)

有时候,我们使用圆括号仅仅是为了把某些字符归为一组应用量词(例如 (?:abc)+ 表示 abc 重复出现),而并不需要捕获它、提取它或在 groups() 中返回它。 此时,只要在左括号后面加上 ?:,就可以定义一个非捕获分组。这在优化性能和清晰处理分组索引时非常有用。


正则表达式的匹配标志(Flags)

Python 的 re 模块允许我们传入可选的“标志参数”来调整匹配的行为。多个标志可以通过按位或运算符 | 组合使用:

标志名称简写描述
re.IGNORECASEre.I忽略大小写进行匹配。
re.MULTILINEre.M多行模式。使得 ^ 匹配字符串开头和每一行的开头,$ 匹配字符串结尾和每一行的结尾。
re.DOTALLre.S点任意匹配模式。使得 . 元字符也可以匹配包括换行符 \n 在内的任意字符。
import re

# 1. 忽略大小写
text = "Python python PYTHON"
print(re.findall(r"python", text, re.IGNORECASE)) # 输出: ['Python', 'python', 'PYTHON']

# 2. 点任意匹配模式 (DOTALL)
html = "<div>第一行\n第二行</div>"
# 默认情况下,. 无法跨越换行符,匹配失败
print(re.search(r"<div>.*</div>", html)) # 输出: None
# 加上 re.S,. 就可以匹配换行符了
print(re.search(r"<div>.*</div>", html, re.S).group())
# 输出: <div>第一行\n第二行</div>

练习题

通过亲手编写和测试正则表达式,你的理解会更加深刻。试着完成以下练习:

  1. 基本验证:编写一个正则表达式,判断输入的字符串是否是合法的中国邮政编码(6位数字且首位不为 0)。
  2. 提取信息:给定一段包含 HTML 标签的文本,编写程序提取出其中所有的链接地址(即 href="..." 内部的 URL)。 例如从 <a href="https://google.com">Google</a> 中提取出 https://google.com
  3. 文本脱敏:编写程序,使用 re.sub 将文本中所有出现的身份证号(18位数字,或者前17位数字+最后一位是X/x)的中间8位(代表出生年月日的部分)替换为 ********
  4. 日志解析:假设有一行 Web 服务器日志: 192.168.1.100 - - [28/May/2026:12:34:56 +0800] "GET /index.html HTTP/1.1" 200 4523 请编写正则表达式和代码,从中分别提取出:IP 地址、请求时间、请求方法(如 GET/POST)、请求路径(如 /index.html)、HTTP 状态码(如 200)。
  5. 词频统计:编写程序,使用 re.split 分割一篇英文文章中的全部单词(排除逗号、句号、感叹号、双引号及空格),并统计出现次数最高的前 3 个单词。