Python 正则表达式完全指南
相关文档:内置函数完全参考(/python-nei-zhi-han-shu-wan-quan-can-kao/) | 数据类型(/python-nei-zhi-shu-ju-lei-xing-wan-quan-can-kao/) | 装饰器与函数高级(/python-zhuang-shi-qi-yu-han-shu-gao-ji-yong-fa/) 1. 正则表达式基础语法(#%E4%B8%80%E3%80%81%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%E5%9F%BA%E7%A1%80%E8%AF%AD
官方文档:https://docs.python.org/3/library/re.html
适用版本:Python 3.12(2026-05-07 核实)
相关文档:内置函数完全参考 | 数据类型 | 装饰器与函数高级
目录
一、正则表达式基础语法
1.1 普通字符
普通字符匹配其自身。字母、数字、下划线等非特殊字符直接匹配对应字符。
1.2 元字符总览
以下字符在正则中具有特殊含义,匹配字面量时需用反斜杠转义:
. ^ $ * + ? { } [ ] \ | ( )
1.3 字符类
| 语法 | 说明 | 示例 |
|---|---|---|
. |
匹配除换行符 \n 以外的任意字符(re.DOTALL 时包括换行) |
a.c 匹配 abc、a1c |
[abc] |
字符集,匹配括号内任意一个字符 | [abc] 匹配 a、b 或 c |
[^abc] |
取反字符集,匹配括号内字符以外的任意字符 | [^abc] 不匹配 a、b、c |
[a-z] |
字符范围,匹配 a 到 z 任意字符 |
[0-9] 匹配任意数字 |
[a-zA-Z0-9] |
多范围组合 | 匹配字母和数字 |
\d |
等价于 [0-9],匹配数字(Unicode 模式下匹配所有 Unicode 数字) |
\d+ 匹配一串数字 |
\D |
等价于 [^\d],匹配非数字 |
|
\w |
等价于 [a-zA-Z0-9_],匹配单词字符 |
|
\W |
等价于 [^\w],匹配非单词字符 |
|
\s |
匹配空白字符:空格、制表符、换行符等 | |
\S |
匹配非空白字符 | |
\b |
单词边界(零宽断言) | \bword\b 精确匹配单词 |
\B |
非单词边界 |
1.4 量词
| 语法 | 说明 | 等价写法 |
|---|---|---|
* |
匹配前一个元素 0 次或多次 | {0,} |
+ |
匹配前一个元素 1 次或多次 | {1,} |
? |
匹配前一个元素 0 次或 1 次 | {0,1} |
{n} |
精确匹配 n 次 | |
{n,} |
匹配至少 n 次 | |
{,m} |
匹配最多 m 次 | |
{n,m} |
匹配 n 到 m 次 |
所有量词默认贪婪,在量词后加 ? 变为非贪婪(懒惰)模式:
| 语法 | 说明 |
|---|---|
*? |
非贪婪,尽可能少匹配 |
+? |
非贪婪 |
?? |
非贪婪 |
{n,m}? |
非贪婪 |
1.5 锚点(位置断言)
| 语法 | 说明 |
|---|---|
^ |
匹配字符串开头(re.MULTILINE 时匹配每行开头) |
$ |
匹配字符串结尾(re.MULTILINE 时匹配每行结尾,允许结尾有 \n) |
\A |
仅匹配整个字符串的开头,不受 re.MULTILINE 影响 |
\Z |
仅匹配整个字符串的结尾,不受 re.MULTILINE 影响 |
\b |
单词边界,\w 与 \W 之间的位置(包括字符串开头/结尾) |
\B |
非单词边界 |
1.6 分组与反向引用
| 语法 | 说明 |
|---|---|
(pattern) |
捕获分组,捕获匹配内容,可用 \1、\2 反向引用 |
(?:pattern) |
非捕获分组,仅分组不捕获 |
(?P<name>pattern) |
命名捕获分组 |
(?P=name) |
命名反向引用,引用已命名的分组 |
\1、\2... |
数字反向引用,引用第 n 个捕获分组 |
(?#comment) |
注释,不参与匹配 |
1.7 零宽断言
| 语法 | 说明 |
|---|---|
(?=pattern) |
正向先行断言:当前位置右侧能匹配 pattern |
(?!pattern) |
负向先行断言:当前位置右侧不能匹配 pattern |
(?<=pattern) |
正向后行断言:当前位置左侧能匹配 pattern |
(?<!pattern) |
负向后行断言:当前位置左侧不能匹配 pattern |
1.8 条件分组
| 语法 | 说明 |
|---|---|
(?(id)yes|no) |
若第 id 个分组已匹配,使用 yes 模式,否则使用 no 模式 |
(?(name)yes|no) |
命名分组版本 |
1.9 特殊转义序列
| 语法 | 说明 |
|---|---|
\n |
换行符 |
\t |
制表符 |
\r |
回车符 |
\f |
换页符 |
\v |
垂直制表符 |
\xhh |
十六进制字符 |
\uhhhh |
Unicode 字符(Unicode 模式) |
\N{name} |
Unicode 字符名称 |
二、修饰符(Flags)
修饰符可以通过两种方式设置:
- 函数参数:
re.compile(pattern, re.IGNORECASE) - 内联语法:
(?i)放在模式内部(局部生效)
| 修饰符常量 | 简写 | 内联语法 | 说明 |
|---|---|---|---|
re.IGNORECASE |
re.I |
(?i) |
忽略大小写匹配 |
re.MULTILINE |
re.M |
(?m) |
^ 和 $ 匹配每行开头和结尾 |
re.DOTALL |
re.S |
(?s) |
. 匹配包括 \n 在内的所有字符 |
re.VERBOSE |
re.X |
(?x) |
允许写带空格和注释的可读性更高的正则 |
re.ASCII |
re.A |
(?a) |
使 \w、\d、\s 只匹配 ASCII 字符 |
re.UNICODE |
re.U |
(?u) |
使 \w、\d、\s 匹配 Unicode 字符(Python 3 默认) |
re.LOCALE |
re.L |
(?L) |
使 \w、\b、\s 根据当前 locale 设置(不推荐,已过时) |
re.NOFLAG |
— | — | 无标志,值为 0 |
多个修饰符可以用 | 组合:re.I | re.M | re.S
修饰符详细说明
re.IGNORECASE(re.I)
import re
# 大小写不敏感匹配
pattern = re.compile(r'hello', re.I)
print(pattern.findall('Hello HELLO hello')) # ['Hello', 'HELLO', 'hello']
# 内联写法,只在部分模式中生效
result = re.findall(r'(?i)hello', 'Hello HELLO hello')
print(result) # ['Hello', 'HELLO', 'hello']
re.MULTILINE(re.M)
import re
text = "first line\nsecond line\nthird line"
# 不加 MULTILINE,^ 只匹配字符串开头
print(re.findall(r'^\w+', text)) # ['first']
# 加 MULTILINE,^ 匹配每行开头
print(re.findall(r'^\w+', text, re.M)) # ['first', 'second', 'third']
# $ 同理
print(re.findall(r'\w+$', text, re.M)) # ['line', 'line', 'line']
re.DOTALL(re.S)
import re
html = "<div>\n content\n</div>"
# 不加 DOTALL,. 不匹配换行
print(re.findall(r'<div>.*</div>', html)) # []
# 加 DOTALL,. 匹配所有字符包括换行
print(re.findall(r'<div>.*</div>', html, re.S)) # ['<div>\n content\n</div>']
re.VERBOSE(re.X)
import re
# 可读性更高的正则,空格和 # 注释被忽略
email_pattern = re.compile(r"""
[\w.+-]+ # 用户名部分(字母、数字、.、+、-)
@ # @ 符号
[\w-]+ # 域名主体
(?:\.[\w-]+)* # 子域名(可选,可多个)
\. # 点号
[a-zA-Z]{2,} # 顶级域名
""", re.VERBOSE)
print(email_pattern.findall('[email protected] [email protected]'))
# ['[email protected]', '[email protected]']
re.ASCII(re.A)
import re
text = "hello 你好 123 ١٢٣" # ١٢٣ 是阿拉伯数字
# 默认 Unicode 模式,\d 匹配所有 Unicode 数字
print(re.findall(r'\d+', text)) # ['123', '١٢٣']
# ASCII 模式,\d 只匹配 0-9
print(re.findall(r'\d+', text, re.A)) # ['123']
# \w 同理
print(re.findall(r'\w+', text)) # ['hello', '你好', '123', '١٢٣']
print(re.findall(r'\w+', text, re.A)) # ['hello', '123']
三、re 模块所有函数
3.1 re.compile()
将正则表达式字符串编译为 Pattern 对象,供反复使用。
函数签名:
re.compile(pattern, flags=0)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pattern |
str 或 bytes |
必填 | 正则表达式字符串 |
flags |
int |
0 |
修饰符,多个用 | 组合 |
返回值: re.Pattern 对象
import re
# 基本编译
pattern = re.compile(r'\d+')
print(pattern.findall('abc123def456')) # ['123', '456']
# 带修饰符
pattern = re.compile(r'hello', re.I | re.M)
# 编译后的 pattern 可重复使用,性能更好
email_re = re.compile(r'[\w.+-]+@[\w-]+\.[\w.]+')
urls = ['[email protected]', 'bad-email', '[email protected]']
for url in urls:
if email_re.match(url):
print(f"{url} 是有效邮箱")
3.2 re.match()
从字符串的起始位置尝试匹配,若起始位置不匹配则返回 None。
函数签名:
re.match(pattern, string, flags=0)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pattern |
str |
必填 | 正则表达式字符串 |
string |
str 或 bytes |
必填 | 待匹配的字符串 |
flags |
int |
0 |
修饰符 |
返回值: 匹配成功返回 Match 对象,失败返回 None
import re
# match 只从起始位置匹配
m = re.match(r'\d+', '123abc')
print(m.group()) # '123'
m = re.match(r'\d+', 'abc123')
print(m) # None,因为起始位置不是数字
# 注意:match 不需要匹配整个字符串
m = re.match(r'\d+', '123abc456')
print(m.group()) # '123',只匹配到第一段数字
# 若要匹配整个字符串,使用 re.fullmatch 或在模式末尾加 $
m = re.match(r'\d+$', '123abc')
print(m) # None
3.3 re.fullmatch()
要求正则表达式匹配整个字符串。
函数签名:
re.fullmatch(pattern, string, flags=0)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pattern |
str |
必填 | 正则表达式字符串 |
string |
str 或 bytes |
必填 | 待匹配的字符串 |
flags |
int |
0 |
修饰符 |
返回值: 整个字符串匹配返回 Match 对象,否则返回 None
import re
# fullmatch 要求整个字符串匹配
m = re.fullmatch(r'\d+', '12345')
print(m.group()) # '12345'
m = re.fullmatch(r'\d+', '123abc')
print(m) # None,因为 'abc' 部分不匹配
# 常用于输入验证
def is_valid_phone(phone):
return re.fullmatch(r'1[3-9]\d{9}', phone) is not None
print(is_valid_phone('13812345678')) # True
print(is_valid_phone('138123456789')) # False(太长)
print(is_valid_phone('23812345678')) # False(不以1开头)
3.4 re.search()
扫描整个字符串,找到第一个匹配的位置。
函数签名:
re.search(pattern, string, flags=0)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pattern |
str |
必填 | 正则表达式字符串 |
string |
str 或 bytes |
必填 | 待匹配的字符串 |
flags |
int |
0 |
修饰符 |
返回值: 找到返回 Match 对象,未找到返回 None
import re
# search 扫描整个字符串
m = re.search(r'\d+', 'abc123def456')
print(m.group()) # '123',返回第一个匹配
m = re.search(r'\d+', 'no digits here')
print(m) # None
# match vs search 对比
text = 'abc123'
print(re.match(r'\d+', text)) # None(起始位置不是数字)
print(re.search(r'\d+', text)) # <re.Match object>(找到 '123')
# 使用 search 查找首个匹配
m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', 'Today is 2026-03-05')
if m:
print(m.group('year')) # '2026'
print(m.group('month')) # '03'
print(m.group('day')) # '05'
3.5 re.findall()
找到字符串中所有非重叠匹配,返回列表。
函数签名:
re.findall(pattern, string, flags=0)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pattern |
str |
必填 | 正则表达式字符串 |
string |
str 或 bytes |
必填 | 待匹配的字符串 |
flags |
int |
0 |
修饰符 |
返回值:
- 无捕获分组:返回匹配字符串列表
- 一个捕获分组:返回捕获内容字符串列表
- 多个捕获分组:返回元组列表,每个元组对应一次匹配的各分组内容
import re
# 无分组:返回完整匹配列表
result = re.findall(r'\d+', 'abc123def456ghi789')
print(result) # ['123', '456', '789']
# 一个分组:返回分组内容列表
result = re.findall(r'(\d+)', 'abc123def456')
print(result) # ['123', '456']
# 多个分组:返回元组列表
result = re.findall(r'(\w+)=(\w+)', 'a=1 b=2 c=3')
print(result) # [('a', '1'), ('b', '2'), ('c', '3')]
# 非捕获分组 (?:) 不影响返回结构
result = re.findall(r'(?:\d{3})-(\d{4})', '010-1234 021-5678')
print(result) # ['1234', '5678']
# 空匹配
result = re.findall(r'\d*', 'a1b2')
print(result) # ['', '1', '', '2', ''](\d* 可匹配空字符串)
3.6 re.finditer()
找到所有非重叠匹配,返回迭代器(每个元素为 Match 对象)。
函数签名:
re.finditer(pattern, string, flags=0)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pattern |
str |
必填 | 正则表达式字符串 |
string |
str 或 bytes |
必填 | 待匹配的字符串 |
flags |
int |
0 |
修饰符 |
返回值: 迭代器,每次产出一个 Match 对象
import re
# finditer 返回迭代器,内存效率更高
text = 'abc123def456ghi789'
for m in re.finditer(r'\d+', text):
print(f"匹配: {m.group()}, 位置: {m.start()}-{m.end()}")
# 匹配: 123, 位置: 3-6
# 匹配: 456, 位置: 9-12
# 匹配: 789, 位置: 15-18
# 比 findall 更灵活,可获取位置信息
pattern = re.compile(r'(\w+)@(\w+)\.(\w+)')
for m in pattern.finditer('[email protected] [email protected]'):
print(f"完整: {m.group()}, 用户: {m.group(1)}, 域名: {m.group(2)}")
3.7 re.sub()
将字符串中匹配的部分替换为指定内容。
函数签名:
re.sub(pattern, repl, string, count=0, flags=0)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pattern |
str |
必填 | 正则表达式字符串 |
repl |
str 或 callable |
必填 | 替换内容。字符串时可用 \1、\g<1>、\g<name> 引用分组;可调用对象接收 Match 对象,返回替换字符串 |
string |
str 或 bytes |
必填 | 原始字符串 |
count |
int |
0 |
最多替换次数,0 表示替换全部 |
flags |
int |
0 |
修饰符 |
返回值: 替换后的字符串
import re
# 基本替换
result = re.sub(r'\d+', 'NUM', 'abc123def456')
print(result) # 'abcNUMdefNUM'
# count 限制替换次数
result = re.sub(r'\d+', 'NUM', 'abc123def456ghi789', count=2)
print(result) # 'abcNUMdefNUMghi789'
# 使用分组引用(\1 或 \g<1>)
result = re.sub(r'(\w+)\s(\w+)', r'\2 \1', 'hello world')
print(result) # 'world hello'
# 使用命名分组引用(\g<name>)
result = re.sub(
r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})',
r'\g<day>/\g<month>/\g<year>',
'2026-03-05'
)
print(result) # '05/03/2026'
# repl 为可调用对象
def uppercase_match(m):
return m.group().upper()
result = re.sub(r'[a-z]+', uppercase_match, 'abc123def456')
print(result) # 'ABC123DEF456'
# 更复杂的替换逻辑
def add_thousands_sep(m):
num = int(m.group())
return f"{num:,}"
result = re.sub(r'\d+', add_thousands_sep, 'price: 1234567 count: 89012')
print(result) # 'price: 1,234,567 count: 89,012'
3.8 re.subn()
与 re.sub() 相同,但同时返回替换次数。
函数签名:
re.subn(pattern, repl, string, count=0, flags=0)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pattern |
str |
必填 | 正则表达式字符串 |
repl |
str 或 callable |
必填 | 替换内容(同 re.sub 的 repl) |
string |
str 或 bytes |
必填 | 原始字符串 |
count |
int |
0 |
最多替换次数 |
flags |
int |
0 |
修饰符 |
返回值: (new_string, number_of_subs_made) 元组
import re
result, count = re.subn(r'\d+', 'NUM', 'abc123def456ghi789')
print(result) # 'abcNUMdefNUMghiNUM'
print(count) # 3
# 判断是否发生了替换
new_text, n = re.subn(r'foo', 'bar', 'hello world')
if n == 0:
print("没有发生替换")
3.9 re.split()
用匹配的子字符串分割字符串。
函数签名:
re.split(pattern, string, maxsplit=0, flags=0)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pattern |
str |
必填 | 分隔符的正则表达式 |
string |
str 或 bytes |
必填 | 待分割的字符串 |
maxsplit |
int |
0 |
最大分割次数,0 表示不限制 |
flags |
int |
0 |
修饰符 |
返回值: 分割后的字符串列表
import re
# 基本分割
result = re.split(r'\s+', 'one two\tthree\nfour')
print(result) # ['one', 'two', 'three', 'four']
# maxsplit 限制分割次数
result = re.split(r'\s+', 'one two three four', maxsplit=2)
print(result) # ['one', 'two', 'three four']
# 分隔符含捕获分组时,分隔符也会出现在结果中
result = re.split(r'(\s+)', 'one two three')
print(result) # ['one', ' ', 'two', ' ', 'three']
# 使用非捕获分组则不保留分隔符
result = re.split(r'(?:\s+)', 'one two three')
print(result) # ['one', 'two', 'three']
# 按多种分隔符分割
result = re.split(r'[,;|]+', 'a,b;;c|d')
print(result) # ['a', 'b', 'c', 'd']
# 字符串开头或结尾匹配时,结果中会有空字符串
result = re.split(r',', ',a,b,')
print(result) # ['', 'a', 'b', '']
3.10 re.escape()
对字符串中的所有非字母数字字符进行转义,使其可作为字面量正则模式使用。
函数签名:
re.escape(pattern)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pattern |
str 或 bytes |
必填 | 需要转义的字符串 |
返回值: 转义后的字符串
import re
# 转义特殊字符
user_input = 'price: $1.99 (special offer)'
escaped = re.escape(user_input)
print(escaped) # 'price\:\ \$1\.99\ \(special\ offer\)'
# 安全地将用户输入用于正则
def search_literal(text, query):
"""安全地搜索字面量字符串"""
pattern = re.escape(query)
return re.findall(pattern, text, re.I)
text = 'The price is $1.99, not $2.99'
print(search_literal(text, '$1.99')) # ['$1.99']
# 构建动态正则
keywords = ['c++', 'c#', '.net', 'node.js']
pattern = '|'.join(re.escape(kw) for kw in keywords)
print(re.findall(pattern, 'I like c++ and c# but not .net', re.I))
# ['c++', 'c#', '.net']
3.11 re.purge()
清除正则表达式的内部缓存。
函数签名:
re.purge()
参数表: 无参数
说明: Python 会缓存最近编译的正则表达式(默认缓存 512 个)。re.purge() 用于清除该缓存,释放内存。通常不需要手动调用。
import re
# 使用大量不同的临时正则后,手动清除缓存
for i in range(1000):
re.findall(f'pattern_{i}', 'test string')
re.purge() # 清除缓存,释放内存
四、Pattern 编译对象
re.compile() 返回的 Pattern 对象拥有与 re 模块顶层函数相同的方法,但不需要再传入 pattern 参数,且性能更好。
Pattern 对象的属性
| 属性 | 类型 | 说明 |
|---|---|---|
pattern.pattern |
str |
编译时使用的正则表达式字符串 |
pattern.flags |
int |
编译时使用的修饰符(包含默认值) |
pattern.groups |
int |
模式中捕获分组的数量 |
pattern.groupindex |
dict |
命名分组名称到分组编号的映射字典 |
Pattern 对象的方法
所有方法与顶层函数签名相同,但省略第一个 pattern 参数。
| 方法 | 说明 |
|---|---|
pattern.match(string[, pos[, endpos]]) |
从 pos 开始匹配(默认 0) |
pattern.fullmatch(string[, pos[, endpos]]) |
全串匹配 |
pattern.search(string[, pos[, endpos]]) |
搜索第一个匹配 |
pattern.findall(string[, pos[, endpos]]) |
找所有匹配 |
pattern.finditer(string[, pos[, endpos]]) |
找所有匹配(迭代器) |
pattern.sub(repl, string, count=0) |
替换 |
pattern.subn(repl, string, count=0) |
替换并返回次数 |
pattern.split(string, maxsplit=0) |
分割 |
pos 和 endpos 参数说明:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
pos |
int |
0 |
从字符串的哪个索引位置开始匹配,相当于对字符串切片但不影响 ^ |
endpos |
int |
len(string) |
匹配到字符串的哪个索引位置结束 |
import re
pattern = re.compile(r'\d+')
# 查看 Pattern 属性
print(pattern.pattern) # '\d+'
print(pattern.flags) # 32(默认 re.UNICODE)
print(pattern.groups) # 0
# 带命名分组的 Pattern
p = re.compile(r'(?P<year>\d{4})-(?P<month>\d{2})')
print(p.groups) # 2
print(p.groupindex) # {'year': 1, 'month': 2}
# 使用 pos 和 endpos
text = 'abc123def456'
print(pattern.findall(text, 3, 9)) # ['123'](只在索引 3-9 范围内查找)
print(pattern.search(text, 6)) # 在 'def456' 中找,找到 '456'
# pos 不影响 ^ 的行为
p = re.compile(r'^\d+')
text = 'abc123'
print(p.match(text, 3)) # None(pos=3 处是 '1',但 ^ 仍锚定字符串开头)
五、Match 对象
匹配成功后,re.match()、re.search()、re.finditer() 返回 Match 对象。
Match 对象的属性
| 属性 | 类型 | 说明 |
|---|---|---|
m.re |
Pattern |
产生此匹配的 Pattern 对象 |
m.string |
str |
传入的原始字符串 |
m.pos |
int |
搜索开始位置 |
m.endpos |
int |
搜索结束位置 |
m.lastindex |
int 或 None |
最后一个参与匹配的捕获分组编号 |
m.lastgroup |
str 或 None |
最后一个参与匹配的命名分组名称 |
5.1 m.group()
函数签名:
m.group([group1, ...])
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
group1, ... |
int 或 str |
0 |
分组编号或命名分组名称。0 或缺省表示整个匹配;多个参数时返回元组 |
返回值: 单个参数时返回字符串(分组未参与匹配时返回 None);多个参数时返回元组
import re
m = re.match(r'(\d{4})-(\d{2})-(\d{2})', '2026-03-05')
print(m.group()) # '2026-03-05'(整个匹配)
print(m.group(0)) # '2026-03-05'(同上)
print(m.group(1)) # '2026'(第一个分组)
print(m.group(2)) # '03'
print(m.group(3)) # '05'
print(m.group(1, 3)) # ('2026', '05')(多个参数返回元组)
# 命名分组
m = re.match(r'(?P<year>\d{4})-(?P<month>\d{2})', '2026-03')
print(m.group('year')) # '2026'
print(m.group('month')) # '03'
# 未参与匹配的分组返回 None
m = re.match(r'(\d+)|([a-z]+)', '123')
print(m.group(1)) # '123'
print(m.group(2)) # None(第二个分组未参与匹配)
5.2 m.groups()
函数签名:
m.groups(default=None)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
default |
any |
None |
未参与匹配的分组的默认值 |
返回值: 包含所有捕获分组内容的元组
import re
m = re.match(r'(\d{4})-(\d{2})-(\d{2})', '2026-03-05')
print(m.groups()) # ('2026', '03', '05')
# default 参数处理未匹配分组
m = re.match(r'(\d+)\.?(\d*)', '123')
print(m.groups()) # ('123', '')
print(m.groups('N/A')) # ('123', '')(空字符串不是 None,不替换)
m = re.match(r'(\d+)|([a-z]+)', '123')
print(m.groups()) # ('123', None)
print(m.groups('N/A')) # ('123', 'N/A')
5.3 m.groupdict()
函数签名:
m.groupdict(default=None)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
default |
any |
None |
未参与匹配的命名分组的默认值 |
返回值: 包含所有命名分组的字典,键为分组名
import re
m = re.match(
r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})',
'2026-03-05'
)
print(m.groupdict())
# {'year': '2026', 'month': '03', 'day': '05'}
# 含未匹配命名分组时使用 default
m = re.match(r'(?P<num>\d+)|(?P<word>[a-z]+)', '123')
print(m.groupdict()) # {'num': '123', 'word': None}
print(m.groupdict('N/A')) # {'num': '123', 'word': 'N/A'}
5.4 m.start() / m.end() / m.span()
函数签名:
m.start([group])
m.end([group])
m.span([group])
参数表(三者相同):
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
group |
int 或 str |
0 |
分组编号或命名分组名称,0 表示整个匹配 |
返回值:
start():匹配开始的索引end():匹配结束的索引(不含)span():(start, end)元组
import re
text = 'hello 2026-03-05 world'
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', text)
print(m.start()) # 6(整个匹配开始位置)
print(m.end()) # 16(整个匹配结束位置)
print(m.span()) # (6, 16)
print(m.start(1)) # 6(第一个分组开始位置)
print(m.end(1)) # 10
print(m.span(1)) # (6, 10)
# 验证:原始字符串切片
print(text[m.start():m.end()]) # '2026-03-05'
print(text[m.start(2):m.end(2)]) # '03'
# 未参与匹配的分组返回 -1
m = re.match(r'(\d+)|([a-z]+)', '123')
print(m.start(2)) # -1
print(m.end(2)) # -1
print(m.span(2)) # (-1, -1)
5.5 m.expand()
函数签名:
m.expand(template)
参数表:
| 参数名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
template |
str |
必填 | 模板字符串,可使用 \1、\g<1>、\g<name> 引用分组 |
返回值: 替换分组引用后的字符串
import re
m = re.match(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-03-05')
print(m.expand(r'\g<day>/\g<month>/\g<year>')) # '05/03/2026'
print(m.expand(r'\1年\2月\3日')) # '2026年03月05日'
六、贪婪与非贪婪
6.1 贪婪模式(默认)
量词默认是贪婪的,尽可能多地匹配字符。
import re
html = '<b>bold</b> and <i>italic</i>'
# 贪婪:匹配从第一个 < 到最后一个 >
print(re.findall(r'<.+>', html))
# ['<b>bold</b> and <i>italic</i>'](全部匹配)
# 解释:.+ 尽可能多匹配,直到整个字符串都能满足模式
6.2 非贪婪模式
在量词后加 ?,尽可能少地匹配字符。
import re
html = '<b>bold</b> and <i>italic</i>'
# 非贪婪:每次找最短的匹配
print(re.findall(r'<.+?>', html))
# ['<b>', '</b>', '<i>', '</i>']
# 数量对比
print(re.findall(r'".*"', '"a" and "b"')) # ['"a" and "b"'](贪婪)
print(re.findall(r'".*?"', '"a" and "b"')) # ['"a"', '"b"'](非贪婪)
6.3 回溯机制
理解贪婪模式的工作原理有助于优化正则:
import re
# 贪婪匹配的工作过程:
# 模式:a.*b,字符串:aXXbXXb
# 1. .* 先吃掉整个字符串 aXXbXXb
# 2. 尝试匹配末尾 b,失败
# 3. 回吐一个字符,尝试匹配...直到成功
# 最终匹配到 aXXbXXb(贪婪,找最长匹配)
text = 'aXXbXXb'
print(re.search(r'a.*b', text).group()) # 'aXXbXXb'
print(re.search(r'a.*?b', text).group()) # 'aXXb'(非贪婪,找最短)
# 固化量词(possessive quantifier):Python 3.11+ 支持原子组 (?>...)
# 可避免不必要的回溯(见"原子组"章节)
七、零宽断言
零宽断言(Zero-Width Assertion)匹配的是位置而非字符,不消耗字符。
7.1 正向先行断言 (?=pattern)
当前位置右侧能匹配 pattern 时成立。
import re
# 匹配后面跟着 "元" 的数字
text = '苹果5元,香蕉3元,橙子12元'
prices = re.findall(r'\d+(?=元)', text)
print(prices) # ['5', '3', '12']
# 密码验证:必须包含数字(先行断言同时满足多个条件)
def is_strong_password(pwd):
has_digit = bool(re.search(r'(?=.*\d)', pwd))
has_upper = bool(re.search(r'(?=.*[A-Z])', pwd))
has_lower = bool(re.search(r'(?=.*[a-z])', pwd))
has_special = bool(re.search(r'(?=.*[!@#$%^&*])', pwd))
return all([has_digit, has_upper, has_lower, has_special, len(pwd) >= 8])
# 或者用单个正则
password_pattern = re.compile(
r'^(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*]).{8,}$'
)
print(bool(password_pattern.match('MyPass@1'))) # True
print(bool(password_pattern.match('mypassword'))) # False
7.2 负向先行断言 (?!pattern)
当前位置右侧不能匹配 pattern 时成立。
import re
# 匹配不以 .min.js 结尾的 .js 文件
files = ['main.js', 'main.min.js', 'app.js', 'vendor.min.js']
for f in files:
if re.search(r'\.js(?!\.min)', f): # 注意这里逻辑需调整
pass
# 正确写法:文件名不包含 .min 的 .js 文件
for f in files:
if re.match(r'(?!.*\.min).*\.js$', f):
print(f) # main.js, app.js
# 匹配不跟在 "un" 后面的 "happy"
text = 'I am happy and unhappy'
result = re.findall(r'(?<!un)happy', text)
print(result) # ['happy'](只匹配独立的 happy)
7.3 正向后行断言 (?<=pattern)
当前位置左侧能匹配 pattern 时成立。pattern 必须是固定宽度(不能用 *、+)。
import re
# 匹配 $ 后面的数字
text = 'Price: $19.99, Cost: $5.00'
prices = re.findall(r'(?<=\$)\d+\.\d{2}', text)
print(prices) # ['19.99', '5.00']
# 提取引号中的内容(正向后行断言)
text = 'name="Alice" role="admin"'
values = re.findall(r'(?<==")[^"]+', text)
print(values) # ['Alice', 'admin']
# 提取 http/https 后的域名
urls = 'Visit http://example.com or https://test.org'
domains = re.findall(r'(?<=https?://)[a-zA-Z0-9.-]+', urls)
print(domains) # ['example.com', 'test.org']
7.4 负向后行断言 (?<!pattern)
当前位置左侧不能匹配 pattern 时成立。
import re
# 匹配不在小数点后的数字
text = '3 items cost $12.50 each'
nums = re.findall(r'(?<!\.\d*)(?<!\$)\d+', text) # 这类需求更适合专门逻辑
# 更实用的例子:匹配不在 HTML 标签内的文字
# 提取非转义的引号内容
text = r'say "hello" and \"world\"'
result = re.findall(r'(?<!\\)"([^"]*?)(?<!\\)"', text)
print(result) # ['hello']
# Python 3.11+ 后行断言支持可变宽度
# 之前版本后行断言必须固定宽度
7.5 断言组合使用
import re
# 同时使用多个断言
# 匹配前有 $ 且后有 USD 的数字
text = '$100 USD and €200 EUR and $300 USD'
result = re.findall(r'(?<=\$)\d+(?= USD)', text)
print(result) # ['100', '300']
# 提取函数调用中的参数(简化版)
code = 'func(arg1) other_func(arg2) call(arg3)'
args = re.findall(r'(?<=\()[^)]+(?=\))', code)
print(args) # ['arg1', 'arg2', 'arg3']
八、分组高级用法
8.1 命名分组 (?P<name>pattern)
import re
# 基本命名分组
m = re.match(
r'(?P<last>\w+),\s*(?P<first>\w+)',
'Smith, John'
)
print(m.group('last')) # 'Smith'
print(m.group('first')) # 'John'
print(m.groupdict()) # {'last': 'Smith', 'first': 'John'}
# 命名分组反向引用(在模式内部)
# 匹配 HTML 开闭标签一致的结构
html_tag = re.compile(r'<(?P<tag>[a-z]+)>.*?</(?P=tag)>', re.S)
print(html_tag.findall('<div>content</div>')) # ['div'],注意只返回分组内容
# 改用 search 获取完整匹配
m = html_tag.search('<div>hello world</div>')
print(m.group()) # '<div>hello world</div>'
print(m.group('tag')) # 'div'
# 在 re.sub 中引用命名分组
result = re.sub(
r'(?P<first>\w+)\s+(?P<last>\w+)',
r'\g<last>, \g<first>',
'John Smith'
)
print(result) # 'Smith, John'
8.2 非捕获分组 (?:pattern)
import re
# 捕获分组 vs 非捕获分组
text = 'color: red, colour: blue'
# 使用捕获分组
result = re.findall(r'colou?r:\s*(\w+)', text)
print(result) # ['red', 'blue']
# 如果需要对 colou?r 分组但不捕获
result = re.findall(r'(?:colou?r):\s*(\w+)', text)
print(result) # ['red', 'blue'](只捕获颜色值)
# 非捕获分组常用于量词应用于多字符
text = 'abcabcabc'
print(re.findall(r'(?:abc)+', text)) # ['abcabcabc']
print(re.findall(r'(abc)+', text)) # ['abc'](返回最后一次捕获的分组值)
# 性能:非捕获分组略快于捕获分组
8.3 条件分组 (?(id)yes|no)
根据某个分组是否参与了匹配,选择不同的匹配模式。
import re
# 基本语法:(?(id)yes_pattern|no_pattern)
# 如果第 id 个分组已匹配,则使用 yes_pattern,否则使用 no_pattern
# 匹配带括号或不带括号的数字
# 有左括号时,要求有右括号
pattern = re.compile(r'(\()?\d+(?(1)\)|)')
tests = ['(123)', '456', '(789']
for t in tests:
m = pattern.fullmatch(t)
print(f"{t}: {'匹配' if m else '不匹配'}")
# (123): 匹配
# 456: 匹配
# (789: 不匹配(有左括号但没有右括号)
# 匹配可选的 http/https 前缀
pattern = re.compile(r'(https?://)?www\.(?(1)[\w.]+|[\w.]+\.com)')
print(bool(pattern.fullmatch('http://www.example.org'))) # True
print(bool(pattern.fullmatch('www.example.com'))) # True
8.4 原子组 (?>pattern)(Python 3.11+)
原子组匹配后不允许回溯,可以提升性能并避免灾难性回溯。
import re
import sys
# Python 3.11+
if sys.version_info >= (3, 11):
# 不使用原子组(可能发生灾难性回溯)
# pattern = re.compile(r'(a+)+b')
# 使用原子组(禁止回溯)
pattern = re.compile(r'(?>a+)+b')
print(bool(pattern.search('aaab'))) # True
print(bool(pattern.search('aaac'))) # False(快速失败,无回溯)
# Python 3.10 及以前的替代方案:使用非贪婪或重写模式
# 避免嵌套量词如 (a+)+ 的写法
8.5 数字反向引用
import re
# 匹配重复的单词
pattern = re.compile(r'\b(\w+)\s+\1\b', re.I)
text = 'the the quick brown fox over the the lazy dog'
result = pattern.findall(text)
print(result) # ['the', 'the']
# 匹配 HTML 配对标签
pattern = re.compile(r'<([a-z]+)>(.*?)</\1>', re.S)
html = '<div>hello</div><span>world</span>'
for m in pattern.finditer(html):
print(f"标签: {m.group(1)}, 内容: {m.group(2)}")
# 标签: div, 内容: hello
# 标签: span, 内容: world
# 匹配引号(单引号或双引号配对)
pattern = re.compile(r'([\'"])(.*?)\1')
text = '"hello" and \'world\' and "mixed\''
for m in pattern.finditer(text):
print(f"引号: {m.group(1)}, 内容: {m.group(2)}")
# 引号: ", 内容: hello
# 引号: ', 内容: world
九、实战案例
案例 1:邮箱提取
import re
EMAIL_PATTERN = re.compile(
r"""
(?P<local>
[\w.!#$%&'*+/=?^_`{|}~-]+ # 本地部分:允许的字符
)
@
(?P<domain>
(?:[a-zA-Z0-9] # 域名:以字母或数字开头
(?:[a-zA-Z0-9-]{0,61} # 中间部分
[a-zA-Z0-9])? # 以字母或数字结尾
\.)+ # 点号分隔
[a-zA-Z]{2,} # 顶级域名至少2字符
)
""",
re.VERBOSE
)
def extract_emails(text):
return EMAIL_PATTERN.findall(text)
# 注意:findall 返回分组内容,需要调整
EMAIL_SIMPLE = re.compile(r'[\w.!#$%&\'*+/=?^_`{|}~-]+@[\w.-]+\.[a-zA-Z]{2,}')
def extract_emails(text):
return EMAIL_SIMPLE.findall(text)
test_text = """
联系邮箱:[email protected] 或 [email protected]
无效邮箱:@invalid.com,user@,plain-text
另一个:[email protected]
"""
emails = extract_emails(test_text)
for email in emails:
print(email)
# [email protected]
# [email protected]
# [email protected]
案例 2:手机号匹配
import re
# 中国大陆手机号:1 开头,第二位 3-9,共 11 位
PHONE_PATTERN = re.compile(r'(?<!\d)1[3-9]\d{9}(?!\d)')
def extract_phones(text):
return PHONE_PATTERN.findall(text)
def is_valid_phone(phone):
return bool(PHONE_PATTERN.fullmatch(phone))
test_text = "联系电话:13812345678,备用:19912345678,固话:010-12345678"
print(extract_phones(test_text)) # ['13812345678', '19912345678']
# 带格式的手机号(支持 +86、空格、横线分隔)
PHONE_FORMAT = re.compile(
r'(?:\+86\s?)?' # 可选国家代码
r'(?:0\d{2,3}[-\s]?)?' # 可选区号
r'1[3-9]\d{9}' # 手机号主体
)
test_phones = ['13812345678', '+8613812345678', '138 1234 5678', '138-1234-5678']
for phone in test_phones:
cleaned = re.sub(r'[\s\-+]', '', phone)
cleaned = re.sub(r'^86', '', cleaned)
print(f"{phone} -> {cleaned}, valid: {is_valid_phone(cleaned)}")
案例 3:IP 地址验证
import re
# IPv4 地址验证
# 每段 0-255
def _build_ipv4():
# 250-255
p1 = r'25[0-5]'
# 200-249
p2 = r'2[0-4]\d'
# 100-199
p3 = r'1\d{2}'
# 10-99
p4 = r'[1-9]\d'
# 0-9
p5 = r'\d'
octet = f'(?:{p1}|{p2}|{p3}|{p4}|{p5})'
return re.compile(rf'^{octet}\.{octet}\.{octet}\.{octet}$')
IPV4_PATTERN = _build_ipv4()
def is_valid_ipv4(ip):
return bool(IPV4_PATTERN.match(ip))
test_ips = ['192.168.1.1', '255.255.255.255', '0.0.0.0',
'256.1.1.1', '1.2.3', '192.168.01.1']
for ip in test_ips:
print(f"{ip}: {is_valid_ipv4(ip)}")
# 192.168.1.1: True
# 255.255.255.255: True
# 0.0.0.0: True
# 256.1.1.1: False
# 1.2.3: False
# 192.168.01.1: True(注意:01 被接受,如需拒绝需额外处理)
# 从文本中提取 IP(不验证合法性)
IP_EXTRACT = re.compile(r'\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b')
text = "服务器 192.168.1.100 和网关 10.0.0.1 以及无效的 999.999.999.999"
print(IP_EXTRACT.findall(text)) # ['192.168.1.100', '10.0.0.1', '999.999.999.999']
案例 4:URL 提取
import re
URL_PATTERN = re.compile(
r'https?://' # 协议
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+' # 域名
r'(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?))' # TLD
r'(?::\d+)?' # 可选端口
r'(?:/?|[/?]\S+)', # 路径
re.IGNORECASE
)
test_text = """
访问 https://www.example.com/path?q=1&p=2 或
http://api.test.org:8080/v1/users 获取更多信息。
无效的 ftp://not-http.com 不会被匹配。
"""
urls = URL_PATTERN.findall(test_text)
for url in urls:
print(url)
# https://www.example.com/path?q=1&p=2
# http://api.test.org:8080/v1/users
# 解析 URL 组成部分
URL_PARTS = re.compile(
r'(?P<scheme>https?)://'
r'(?P<host>[^/:?#]+)'
r'(?::(?P<port>\d+))?'
r'(?P<path>/[^?#]*)?'
r'(?:\?(?P<query>[^#]*))?'
r'(?:#(?P<fragment>.*))?'
)
m = URL_PARTS.match('https://api.example.com:8080/v1/users?role=admin#section1')
if m:
print(m.groupdict())
# {'scheme': 'https', 'host': 'api.example.com', 'port': '8080',
# 'path': '/v1/users', 'query': 'role=admin', 'fragment': 'section1'}
案例 5:HTML 标签提取
import re
# 提取 HTML 标签及其属性(警告:复杂 HTML 请使用 BeautifulSoup)
HTML_TAG = re.compile(r'<([a-zA-Z][a-zA-Z0-9]*)\b([^>]*)>', re.S)
def parse_attributes(attr_string):
"""解析 HTML 属性字符串"""
attr_pattern = re.compile(r'(\w[\w-]*)(?:\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|(\S+)))?')
attrs = {}
for m in attr_pattern.finditer(attr_string):
name = m.group(1)
value = m.group(2) or m.group(3) or m.group(4) or True
attrs[name] = value
return attrs
html = '<div class="container" id="main"><a href="http://example.com" target="_blank">link</a></div>'
for m in HTML_TAG.finditer(html):
tag = m.group(1)
attrs = parse_attributes(m.group(2))
print(f"标签: {tag}, 属性: {attrs}")
# 标签: div, 属性: {'class': 'container', 'id': 'main'}
# 标签: a, 属性: {'href': 'http://example.com', 'target': '_blank'}
# 提取特定属性
hrefs = re.findall(r'<a\s[^>]*href=["\']([^"\']+)["\']', html, re.I)
print(hrefs) # ['http://example.com']
案例 6:中文提取
import re
# 中文 Unicode 范围:\u4e00-\u9fff(基本汉字)
# 扩展:\u3400-\u4dbf(扩展A)、\u20000-\u2a6df(扩展B)等
CHINESE_PATTERN = re.compile(r'[\u4e00-\u9fff\u3400-\u4dbf]+')
def extract_chinese(text):
return CHINESE_PATTERN.findall(text)
text = "Hello 你好 World 中文提取 Test 测试123"
print(extract_chinese(text)) # ['你好', '中文提取', '测试']
# 包含中文标点符号
CHINESE_WITH_PUNCT = re.compile(
r'[\u4e00-\u9fff\u3400-\u4dbf' # 汉字
r'\u3000-\u303f' # CJK 标点
r'\uff00-\uffef]+' # 全角字符
)
# 统计中文字符数
def count_chinese_chars(text):
return sum(len(m) for m in CHINESE_PATTERN.findall(text))
text = "这是一段中文,contains English and 数字123"
print(count_chinese_chars(text)) # 10('这是一段中文' + '数字')
# 移除非中文字符(只保留中文)
def keep_chinese_only(text):
return ''.join(CHINESE_PATTERN.findall(text))
print(keep_chinese_only("Hello世界!abc中文123")) # '世界中文'
案例 7:日期格式匹配
import re
from datetime import datetime
# 匹配多种日期格式
DATE_PATTERNS = {
'ISO': re.compile(r'\b(\d{4})-(\d{2})-(\d{2})\b'),
'CN': re.compile(r'\b(\d{4})年(\d{1,2})月(\d{1,2})日\b'),
'US': re.compile(r'\b(\d{1,2})/(\d{1,2})/(\d{2,4})\b'),
'EU': re.compile(r'\b(\d{1,2})\.(\d{1,2})\.(\d{2,4})\b'),
}
def extract_dates(text):
results = []
for fmt, pattern in DATE_PATTERNS.items():
for m in pattern.finditer(text):
results.append({'format': fmt, 'raw': m.group(), 'groups': m.groups()})
return results
text = "日期:2026-03-05,或者2026年3月5日,也可以是03/05/2026"
for date in extract_dates(text):
print(f"格式: {date['format']}, 原始: {date['raw']}")
# 验证并解析日期
def parse_date(text):
pattern = re.compile(
r'(?P<year>\d{4})[年/-](?P<month>\d{1,2})[月/-](?P<day>\d{1,2})日?'
)
m = pattern.search(text)
if m:
try:
return datetime(int(m.group('year')), int(m.group('month')), int(m.group('day')))
except ValueError:
return None
return None
print(parse_date("今天是2026-03-05")) # datetime(2026, 3, 5, 0, 0)
print(parse_date("2026年13月5日")) # None(月份无效)
案例 8:密码强度验证
import re
def check_password_strength(password):
"""
检查密码强度
强密码条件:
- 长度至少 8 位
- 包含大写字母
- 包含小写字母
- 包含数字
- 包含特殊字符
"""
checks = {
'length': len(password) >= 8,
'uppercase': bool(re.search(r'[A-Z]', password)),
'lowercase': bool(re.search(r'[a-z]', password)),
'digit': bool(re.search(r'\d', password)),
'special': bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', password)),
}
score = sum(checks.values())
strength_map = {5: '强', 4: '中等', 3: '弱', 2: '很弱', 1: '极弱', 0: '不合格'}
strength = strength_map.get(score, '不合格')
return {'score': score, 'strength': strength, 'checks': checks}
# 单一强密码正则(使用前行断言)
STRONG_PASSWORD = re.compile(
r'^'
r'(?=.*[a-z])' # 至少一个小写字母
r'(?=.*[A-Z])' # 至少一个大写字母
r'(?=.*\d)' # 至少一个数字
r'(?=.*[!@#$%^&*])' # 至少一个特殊字符
r'.{8,}$' # 至少 8 位
)
passwords = ['MyPass@1', 'weakpass', 'NoSpecial1', 'MyPassword@123']
for pwd in passwords:
result = check_password_strength(pwd)
valid = bool(STRONG_PASSWORD.match(pwd))
print(f"{pwd}: {result['strength']} (强密码: {valid})")
案例 9:JSON 字段提取
import re
# 注意:复杂 JSON 请使用 json 模块。以下适用于日志中嵌入的 JSON 片段。
def extract_json_string_field(json_str, field_name):
"""提取 JSON 字符串字段值"""
pattern = re.compile(
rf'"{re.escape(field_name)}"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"'
)
m = pattern.search(json_str)
return m.group(1) if m else None
def extract_json_number_field(json_str, field_name):
"""提取 JSON 数字字段值"""
pattern = re.compile(
rf'"{re.escape(field_name)}"\s*:\s*(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)'
)
m = pattern.search(json_str)
return m.group(1) if m else None
json_str = '{"name": "Alice", "age": 30, "email": "[email protected]", "score": 98.5}'
print(extract_json_string_field(json_str, 'name')) # 'Alice'
print(extract_json_string_field(json_str, 'email')) # '[email protected]'
print(extract_json_number_field(json_str, 'age')) # '30'
print(extract_json_number_field(json_str, 'score')) # '98.5'
# 提取所有键值对(仅字符串值)
ALL_STRING_FIELDS = re.compile(r'"(\w+)"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"')
fields = dict(ALL_STRING_FIELDS.findall(json_str))
print(fields) # {'name': 'Alice', 'email': '[email protected]'}
案例 10:去除 HTML 标签
import re
def strip_html_tags(html):
"""移除所有 HTML 标签,保留文本内容"""
# 移除 script 和 style 标签及其内容
clean = re.sub(r'<(script|style)[^>]*>.*?</\1>', '', html, flags=re.S | re.I)
# 移除所有 HTML 标签
clean = re.sub(r'<[^>]+>', '', clean)
# 替换 HTML 实体
entities = {
'&': '&', '<': '<', '>': '>',
'"': '"', ''': "'", ' ': ' ',
}
for entity, char in entities.items():
clean = clean.replace(entity, char)
# 去除多余空白
clean = re.sub(r'\s+', ' ', clean).strip()
return clean
html = """
<html>
<head><title>测试页面</title>
<style>body { color: red; }</style>
<script>alert('hello');</script>
</head>
<body>
<h1>标题</h1>
<p>这是<b>一段</b>文字,包含<a href="#">链接</a>。</p>
<p>HTML实体:& < > 空格</p>
</body>
</html>
"""
print(strip_html_tags(html))
# '测试页面 标题 这是一段文字,包含链接。 HTML实体:& < > 空格'
案例 11:爬虫中的常见提取场景
import re
# 提取页面标题
def extract_title(html):
m = re.search(r'<title[^>]*>(.*?)</title>', html, re.S | re.I)
return m.group(1).strip() if m else None
# 提取 meta 描述
def extract_meta_description(html):
m = re.search(
r'<meta\s[^>]*name=["\']description["\'][^>]*content=["\']([^"\']+)["\']',
html, re.I
)
if not m:
m = re.search(
r'<meta\s[^>]*content=["\']([^"\']+)["\'][^>]*name=["\']description["\']',
html, re.I
)
return m.group(1).strip() if m else None
# 提取所有图片 src
def extract_images(html):
return re.findall(r'<img\s[^>]*src=["\']([^"\']+)["\']', html, re.I)
# 提取所有链接
def extract_links(html):
return re.findall(r'<a\s[^>]*href=["\']([^"\']+)["\']', html, re.I)
# 提取 JSON-LD 结构化数据
def extract_jsonld(html):
pattern = re.compile(
r'<script\s[^>]*type=["\']application/ld\+json["\'][^>]*>(.*?)</script>',
re.S | re.I
)
return pattern.findall(html)
# 示例
html = """
<html>
<head>
<title>商品详情 - 示例网站</title>
<meta name="description" content="这是商品描述信息">
</head>
<body>
<img src="/images/product.jpg" alt="商品图片">
<img src="/images/thumb.png">
<a href="/category/1">分类一</a>
<a href="https://external.com">外部链接</a>
</body>
</html>
"""
print(extract_title(html)) # '商品详情 - 示例网站'
print(extract_meta_description(html)) # '这是商品描述信息'
print(extract_images(html)) # ['/images/product.jpg', '/images/thumb.png']
print(extract_links(html)) # ['/category/1', 'https://external.com']
案例 12:日志解析
import re
from datetime import datetime
# Nginx 访问日志格式:
# 127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326
NGINX_LOG = re.compile(
r'(?P<ip>\d{1,3}(?:\.\d{1,3}){3})\s+' # IP 地址
r'(?P<ident>\S+)\s+' # 标识(通常为 -)
r'(?P<user>\S+)\s+' # 用户名
r'\[(?P<time>[^\]]+)\]\s+' # 时间
r'"(?P<method>\w+)\s+' # HTTP 方法
r'(?P<path>[^\s"]+)\s+' # 请求路径
r'(?P<protocol>[^"]+)"\s+' # 协议
r'(?P<status>\d{3})\s+' # 状态码
r'(?P<size>\d+|-)' # 响应大小
)
LOG_TIME_FORMAT = '%d/%b/%Y:%H:%M:%S %z'
def parse_nginx_log(line):
m = NGINX_LOG.match(line)
if not m:
return None
data = m.groupdict()
try:
data['time'] = datetime.strptime(data['time'], LOG_TIME_FORMAT)
except ValueError:
pass
data['status'] = int(data['status'])
data['size'] = int(data['size']) if data['size'] != '-' else 0
return data
# Python 应用日志格式
APP_LOG = re.compile(
r'(?P<level>DEBUG|INFO|WARNING|ERROR|CRITICAL)\s+'
r'(?P<timestamp>\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+'
r'(?P<module>[\w.]+):(?P<lineno>\d+)\s+-\s+'
r'(?P<message>.*)'
)
log_line = 'ERROR 2026-03-05 14:30:00.123 myapp.views:45 - Database connection failed'
m = APP_LOG.match(log_line)
if m:
print(m.groupdict())
案例 13:CSV 字段解析
import re
# 注意:复杂 CSV 请使用 csv 模块。以下用于理解正则处理带引号字段。
CSV_FIELD = re.compile(
r'(?:^|,)' # 行首或逗号
r'(?:'
r'"((?:[^"]*(?:""[^"]*)*)*)"' # 双引号包裹的字段(允许 "" 转义)
r'|'
r'([^,]*)' # 普通字段
r')'
)
def parse_csv_line(line):
fields = []
for m in CSV_FIELD.finditer(line):
if m.group(1) is not None:
# 双引号字段:还原 "" 为 "
fields.append(m.group(1).replace('""', '"'))
else:
fields.append(m.group(2))
return fields
test_lines = [
'Alice,30,[email protected]',
'"Smith, John",25,"New York"',
'"He said ""hello""",admin,active',
]
for line in test_lines:
print(parse_csv_line(line))
# ['Alice', '30', '[email protected]']
# ['Smith, John', '25', 'New York']
# ['He said "hello"', 'admin', 'active']
案例 14:重复单词检测
import re
def find_repeated_words(text):
"""检测文本中连续重复的单词"""
pattern = re.compile(r'\b(?P<word>\w+)\s+(?P=word)\b', re.I)
results = []
for m in pattern.finditer(text):
results.append({
'word': m.group('word'),
'position': m.span(),
'context': text[max(0, m.start()-10):m.end()+10]
})
return results
text = "The the quick brown fox fox jumped over the lazy lazy dog."
repeated = find_repeated_words(text)
for item in repeated:
print(f"重复词: '{item['word']}' 在位置 {item['position']}")
# 重复词: 'The' 在位置 (0, 7)
# 重复词: 'fox' 在位置 (20, 27)
# 重复词: 'lazy' 在位置 (40, 49)
def remove_repeated_words(text):
"""移除连续重复的单词"""
return re.sub(r'\b(\w+)(\s+\1)+\b', r'\1', text, flags=re.I)
print(remove_repeated_words("The the quick fox fox"))
# 'The quick fox'
案例 15:代码注释提取
import re
def extract_python_comments(code):
"""提取 Python 代码中的注释"""
# 单行注释
single_line = re.findall(r'#\s*(.*?)$', code, re.M)
# 多行字符串(docstring)
docstrings = re.findall(r'"""(.*?)"""|\'\'\'(.*?)\'\'\'', code, re.S)
doc_list = [d[0] or d[1] for d in docstrings]
return {'single_line': single_line, 'docstrings': doc_list}
def extract_js_comments(code):
"""提取 JavaScript 代码中的注释"""
results = []
# 多行注释 /* ... */
for m in re.finditer(r'/\*(.*?)\*/', code, re.S):
results.append({'type': 'block', 'content': m.group(1).strip()})
# 单行注释 //
for m in re.finditer(r'//\s*(.*?)$', code, re.M):
results.append({'type': 'line', 'content': m.group(1).strip()})
return results
python_code = '''
def hello(name):
"""
打招呼函数
参数:name - 姓名
"""
# 打印问候语
print(f"Hello, {name}") # 这是行内注释
'''
comments = extract_python_comments(python_code)
print("单行注释:", comments['single_line'])
print("文档字符串:", comments['docstrings'])
十、性能优化
10.1 预编译正则
import re
import timeit
# 不预编译:每次调用都重新编译(有内部缓存,但缓存满后仍需重编译)
def search_without_compile(texts):
return [re.search(r'\d{4}-\d{2}-\d{2}', t) for t in texts]
# 预编译:直接使用编译好的对象
DATE_RE = re.compile(r'\d{4}-\d{2}-\d{2}')
def search_with_compile(texts):
return [DATE_RE.search(t) for t in texts]
texts = ['date: 2026-03-05'] * 10000
t1 = timeit.timeit(lambda: search_without_compile(texts), number=10)
t2 = timeit.timeit(lambda: search_with_compile(texts), number=10)
print(f"不预编译: {t1:.3f}s")
print(f"预编译: {t2:.3f}s")
# 预编译通常快 10-40%
10.2 避免灾难性回溯
灾难性回溯(Catastrophic Backtracking)是正则性能陷阱,可使匹配时间呈指数增长。
import re
import time
# 危险模式示例:嵌套量词
# (a+)+ 对于不匹配的输入会指数级回溯
DANGEROUS = re.compile(r'(a+)+b')
# 对于这类输入,回溯次数是指数级的
test = 'a' * 20 # 不含 b,必然失败
start = time.time()
result = DANGEROUS.search(test) # 可能非常慢!
elapsed = time.time() - start
print(f"耗时: {elapsed:.3f}s, 结果: {result}")
# 优化方案:
# 1. 重写模式,消除嵌套量词
SAFE = re.compile(r'a+b') # 直接写,无嵌套
# 2. 使用原子组(Python 3.11+)
# SAFE = re.compile(r'(?>a+)+b')
# 3. 使用固定宽度模式代替变长模式
# 不好:r'(.+?)@(.+?)\.(.+)'
# 好: r'([^@]+)@([^.]+)\.(\w+)'
# 更多危险模式
PATTERNS_TO_AVOID = [
r'(a*)*', # 嵌套量词
r'(a|aa)+', # 交替且有重叠的子模式
r'(\w+\s?\w+)*', # 嵌套量词且有公共字符
]
10.3 使用字符类代替交替
import re
# 慢:使用交替
SLOW = re.compile(r'a|e|i|o|u')
# 快:使用字符类
FAST = re.compile(r'[aeiou]')
# 慢:对固定字符集使用 .
SLOW2 = re.compile(r'.+@')
# 快:用否定字符类明确范围
FAST2 = re.compile(r'[^@]+@')
10.4 锚点和边界的合理使用
import re
# 使用锚点减少匹配范围
# 不好:扫描整个字符串
BAD = re.compile(r'\d{4}-\d{2}-\d{2}')
# 好:如果知道日期在行首,加锚点
GOOD = re.compile(r'^\d{4}-\d{2}-\d{2}', re.M)
# 单词边界可以避免不必要的匹配
# 不好:可能匹配 'python3' 中的 'python'
BAD2 = re.compile(r'python', re.I)
# 好:精确匹配单词
GOOD2 = re.compile(r'\bpython\b', re.I)
10.5 选择合适的函数
import re
text = 'hello world'
# 只需判断是否匹配:使用 search/match 而非 findall
# 不好
if re.findall(r'hello', text):
pass
# 好
if re.search(r'hello', text):
pass
# 大文本查找所有匹配:使用 finditer(迭代器)而非 findall(列表)
# findall 把所有结果存入内存
# finditer 按需生成
large_text = 'data ' * 100000
# 好:内存效率高
for m in re.finditer(r'data', large_text):
process = m.start() # 逐个处理,不需要存储全部结果
10.6 使用 re.VERBOSE 提升可维护性
import re
# 复杂模式使用 VERBOSE 模式提高可读性和可维护性
# 可读性差
EMAIL_BAD = re.compile(r'[\w.!#$%&\'*+/=?^_`{|}~-]+@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}')
# 可读性好
EMAIL_GOOD = re.compile(r"""
[\w.!#$%&'*+/=?^_`{|}~-]+ # 本地部分
@ # @ 符号
(?: # 域名部分(非捕获分组)
[a-zA-Z0-9] # 域名段开头
(?:[a-zA-Z0-9-]{0,61} # 域名段中间
[a-zA-Z0-9])? # 域名段结尾(可选)
\. # 点号
)+
[a-zA-Z]{2,} # 顶级域名
""", re.VERBOSE)
十一、最佳实践
11.1 始终使用原始字符串
import re
# 不好:\b 可能被 Python 字符串转义解释
# pattern = re.compile('\bword\b') # \b 是退格符 chr(8)
# 好:使用原始字符串
pattern = re.compile(r'\bword\b') # \b 是单词边界
# 不好:\d 在普通字符串中虽然有效(\d 不是标准转义序列),但不清晰
# pattern = re.compile('\d+')
# 好:明确使用原始字符串
pattern = re.compile(r'\d+')
11.2 编译一次,多次使用
import re
# 模块级别预编译,避免在循环中重复编译
EMAIL_RE = re.compile(r'[\w.+-]+@[\w-]+\.[\w.]+')
PHONE_RE = re.compile(r'1[3-9]\d{9}')
DATE_RE = re.compile(r'\d{4}-\d{2}-\d{2}')
def process_records(records):
for record in records:
email = EMAIL_RE.search(record.get('contact', ''))
phone = PHONE_RE.search(record.get('contact', ''))
date = DATE_RE.search(record.get('date', ''))
# ...处理结果
11.3 处理 None 返回值
import re
# 不好:不检查 None,可能 AttributeError
def bad_extract(text):
return re.search(r'\d+', text).group() # 若无匹配则崩溃
# 好:检查匹配结果
def good_extract(text):
m = re.search(r'\d+', text)
return m.group() if m else None
# 或使用三元表达式
def extract_or_default(text, pattern, default=''):
m = re.search(pattern, text)
return m.group() if m else default
11.4 使用非捕获分组减少捕获
import re
# 只需要分组用于量词应用,不需要捕获时,使用 (?:)
# 不好:findall 返回分组内容,不是完整匹配
result = re.findall(r'(https?://)[\w./]+', 'http://example.com https://test.org')
print(result) # ['http://', 'https://'](只返回分组内容!)
# 好:使用非捕获分组
result = re.findall(r'(?:https?://)[\w./]+', 'http://example.com https://test.org')
print(result) # ['http://example.com', 'https://test.org']
11.5 测试边界条件
import re
EMAIL_RE = re.compile(r'[\w.+-]+@[\w-]+\.[\w.]+')
test_cases = [
# (输入, 是否应该匹配)
('[email protected]', True),
('[email protected]', True),
('[email protected]', True),
('@example.com', False), # 无本地部分
('user@', False), # 无域名
('[email protected]', False), # 域名以点开头
('user@example', False), # 无顶级域名
('plainaddress', False), # 无 @
('user [email protected]', False), # 本地部分有空格
]
for email, expected in test_cases:
m = EMAIL_RE.fullmatch(email)
matched = m is not None
status = 'PASS' if matched == expected else 'FAIL'
print(f"{status}: '{email}' -> {'匹配' if matched else '不匹配'} (期望: {'匹配' if expected else '不匹配'})")
十二、常见陷阱与注意事项
12.1 re.match 与 re.search 的区别
import re
text = 'abc123'
# match 只从起始位置匹配
print(re.match(r'\d+', text)) # None('a' 不是数字)
print(re.search(r'\d+', text)) # 匹配 '123'
# 容易出错:以为 match 匹配整个字符串
# match 不是全串匹配!
print(re.match(r'\d+', '123abc')) # 匹配 '123',不是 None
# 全串匹配用 fullmatch
print(re.fullmatch(r'\d+', '123abc')) # None
print(re.fullmatch(r'\d+', '123')) # 匹配
12.2 findall 分组陷阱
import re
text = 'id=123 name=Alice'
# 陷阱:有分组时 findall 只返回分组内容
result = re.findall(r'(\w+)=(\w+)', text)
print(result) # [('id', '123'), ('name', 'Alice')](元组列表)
# 想要完整匹配时,使用非捕获分组或 finditer
result = re.findall(r'(?:\w+)=(?:\w+)', text)
print(result) # ['id=123', 'name=Alice']
# 或者使用 finditer
for m in re.finditer(r'(\w+)=(\w+)', text):
print(m.group()) # 完整匹配
print(m.group(1, 2)) # 分组内容
12.3 贪婪匹配导致超出预期
import re
html = '<b>one</b> and <b>two</b>'
# 陷阱:贪婪匹配吃掉了中间内容
result = re.findall(r'<b>.*</b>', html)
print(result) # ['<b>one</b> and <b>two</b>'](贪婪,匹配过多)
# 解决:使用非贪婪
result = re.findall(r'<b>.*?</b>', html)
print(result) # ['<b>one</b>', '<b>two</b>']
# 或使用否定字符类(更高效)
result = re.findall(r'<b>[^<]*</b>', html)
print(result) # ['<b>one</b>', '<b>two</b>']
12.4 re.MULTILINE 不影响 \A 和 \Z
import re
text = "line1\nline2\nline3"
# ^ 在 MULTILINE 模式下匹配每行开头
print(re.findall(r'^\w+', text, re.M)) # ['line1', 'line2', 'line3']
# \A 始终只匹配字符串开头
print(re.findall(r'\A\w+', text, re.M)) # ['line1']
# $ 在 MULTILINE 模式下匹配每行结尾(注意:结尾换行符前)
print(re.findall(r'\w+$', text, re.M)) # ['line1', 'line2', 'line3']
# \Z 只匹配字符串结尾
print(re.findall(r'\w+\Z', text, re.M)) # ['line3']
12.5 特殊字符在字符类中的行为
import re
# 字符类内部,大多数元字符失去特殊含义
# . 在 [] 中是字面量点号
print(re.findall(r'[.]', 'a.b')) # ['.'](匹配字面量点)
print(re.findall(r'.', 'a.b')) # ['a', '.', 'b'](. 匹配任意字符)
# ] 需要放在首位或转义
print(re.findall(r'[]a]', 'a]b')) # ['a', ']']
print(re.findall(r'[a\]]', 'a]b')) # ['a', ']']
# - 在字符类中需要放首位、末位或转义
print(re.findall(r'[a-z]', 'abc')) # ['a', 'b', 'c'](范围)
print(re.findall(r'[-az]', 'a-z')) # ['a', '-', 'z'](- 在首位是字面量)
# ^ 只在首位有特殊含义(取反)
print(re.findall(r'[a^]', 'a^b')) # ['a', '^'](^ 不在首位是字面量)
print(re.findall(r'[^a]', 'a^b')) # ['^', 'b'](取反)
12.6 后行断言的固定宽度限制
import re
# Python 3.10 及以前,后行断言必须是固定宽度
# 这样会报错:
try:
re.compile(r'(?<=a+)b') # 可变宽度,报错
except re.error as e:
print(f"错误: {e}") # look-behind requires fixed width pattern
# 固定宽度的后行断言是合法的
print(re.findall(r'(?<=abc)d', 'abcd')) # ['d']
print(re.findall(r'(?<=ab|cd)e', 'abe')) # Python 3.6+ 支持交替(等宽)
# Python 3.11+ 支持可变宽度后行断言
import sys
if sys.version_info >= (3, 11):
print(re.findall(r'(?<=a+)b', 'aaab')) # ['b']
12.7 字节串与字符串的区别
import re
# 字符串模式
text = "hello 123"
print(re.findall(r'\d+', text)) # ['123']
# 字节串模式(pattern 和 string 必须同类型)
data = b"hello 123"
print(re.findall(rb'\d+', data)) # [b'123']
# 混用会报错
try:
re.findall(r'\d+', b'hello 123')
except TypeError as e:
print(f"错误: {e}") # cannot use a string pattern on a bytes-like object
12.8 re.sub 替换字符串中的反斜杠
import re
# repl 字符串中,\ 有特殊含义(\1、\g<1> 等)
# 如果替换内容本身含有反斜杠,需要双重转义或使用 lambda
# 陷阱:直接用含反斜杠的字符串替换
text = 'path: /usr/local/bin'
# 想把 / 替换为 \
result = re.sub(r'/', r'\\', text) # 注意:r'\\' 是两个反斜杠在原始字符串中
print(result) # 'path: \usr\local\bin'(正确)
# 更安全:使用 lambda 避免歧义
result = re.sub(r'/', lambda m: '\\', text)
print(result) # 'path: \usr\local\bin'
# 替换字符串中 \1 等的问题
text = 'John Smith'
# 想在两个词之间加逗号,同时保留原文
result = re.sub(r'(\w+)\s+(\w+)', r'\2, \1', text)
print(result) # 'Smith, John'(正确使用分组引用)
12.9 Unicode 与 ASCII 模式差异
import re
# 默认 Unicode 模式
text = "hello 你好 123 Ⅱ"
# \w 在 Unicode 模式下匹配所有 Unicode 字母数字
print(re.findall(r'\w+', text)) # ['hello', '你好', '123', 'Ⅱ']
# \w 在 ASCII 模式下只匹配 ASCII 字母数字下划线
print(re.findall(r'\w+', text, re.A)) # ['hello', '123']
# 处理多语言文本时要注意这个差异
def is_word(s, ascii_only=False):
flags = re.A if ascii_only else 0
return bool(re.fullmatch(r'\w+', s, flags))
print(is_word('hello')) # True
print(is_word('你好')) # True(Unicode 模式)
print(is_word('你好', True)) # False(ASCII 模式)
附录:速查表
常用正则模式速查
| 用途 | 正则表达式 |
|---|---|
| 整数 | [+-]?\d+ |
| 浮点数 | [+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)? |
| 中国手机号 | 1[3-9]\d{9} |
| 电子邮箱(简化) | [\w.+-]+@[\w-]+\.[\w.]+ |
| IPv4 地址 | \b(?:\d{1,3}\.){3}\d{1,3}\b |
| HTTP/HTTPS URL | https?://[^\s<>"]+ |
| HTML 标签 | <[^>]+> |
| 中文字符 | [\u4e00-\u9fff]+ |
| 日期(ISO) | \d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01]) |
| 时间(24H) | (?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d)? |
| 十六进制颜色 | #(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})\b |
| 空行 | ^\s*$ |
| 行首/尾空白 | ^\s+|\s+$ |
| 重复单词 | \b(\w+)\s+\1\b |
| 身份证(中国) | \d{17}[\dXx] |
| 邮政编码(中国) | [1-9]\d{5} |
re 模块函数速查
| 函数 | 返回值 | 说明 |
|---|---|---|
re.compile(pat) |
Pattern |
编译正则 |
re.match(pat, s) |
Match|None |
从起始匹配 |
re.fullmatch(pat, s) |
Match|None |
全串匹配 |
re.search(pat, s) |
Match|None |
搜索第一个 |
re.findall(pat, s) |
list |
所有匹配列表 |
re.finditer(pat, s) |
iterator |
所有匹配迭代器 |
re.sub(pat, repl, s) |
str |
替换 |
re.subn(pat, repl, s) |
(str, int) |
替换并计数 |
re.split(pat, s) |
list |
分割 |
re.escape(s) |
str |
转义特殊字符 |
re.purge() |
None |
清除缓存 |
最佳实践
编译复用:频繁使用的正则表达式应提前 compile:re.compile() 将正则预编译为 Pattern 对象,避免每次调用都重新解析。内部有 512 个 pattern 的 LRU 缓存,但显式 compile 语义更清晰且不受缓存淘汰影响。
# 正确:提前 compile,用于循环或频繁调用
EMAIL_RE = re.compile(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$')
for email in email_list:
if EMAIL_RE.match(email):
...
# 低效:每次都解析正则
for email in email_list:
if re.match(r'^[a-zA-Z0-9_.+-]+@...', email):
...
用具名捕获组(?P<name>)提高可读性:位置编号 \1、\2 在正则修改后极易出错,具名捕获组让代码自描述。
import re
pattern = re.compile(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})')
m = pattern.search("发布日期:2026-05-07")
print(m.group('year'), m.group('month'), m.group('day'))
# 2026 05 07
非贪婪匹配处理嵌套内容:默认贪婪匹配 .* 会尽可能多匹配,提取 HTML 标签内容时匹配到最后一个闭合标签。用 .*? 改为非贪婪。
html = "<b>hello</b> and <b>world</b>"
# 贪婪:匹配从第一个 <b> 到最后一个 </b> 的所有内容
re.findall(r'<b>.*</b>', html) # ['<b>hello</b> and <b>world</b>']
# 非贪婪:匹配每个 <b>...</b>
re.findall(r'<b>.*?</b>', html) # ['<b>hello</b>', '<b>world</b>']
用 re.VERBOSE 写可读的多行正则:复杂正则应分行书写并加注释,re.VERBOSE(或 re.X)忽略空白和 # 后的注释。
EMAIL_RE = re.compile(r"""
^ # 字符串起始
[a-zA-Z0-9_.+-]+ # 用户名
@ # @符号
[a-zA-Z0-9-]+ # 域名
\. # 点
[a-zA-Z0-9-.]+ # 顶级域名
$ # 字符串结尾
""", re.VERBOSE)
常见陷阱
陷阱:re.match 只匹配字符串开头
现象: re.match(r'\d+', 'abc123') 返回 None,以为正则写错了。
原因: re.match() 只从字符串开头开始匹配,等价于在正则前加了 ^。re.search() 才是在整个字符串中搜索。
解决: 搜索任意位置用 re.search();全串匹配用 re.fullmatch();只需开头匹配用 re.match()。
re.match(r'\d+', 'abc123') # None(不从开头)
re.search(r'\d+', 'abc123') # Match('123')
re.fullmatch(r'\d+', '123') # Match('123')
re.fullmatch(r'\d+', 'abc123') # None(不是全串数字)
陷阱:贪婪匹配导致提取范围过大
现象: 提取 HTML 属性值时,".*" 从第一个引号匹配到了最后一个引号。
原因: 默认贪婪,".*" 会尽可能多匹配。
解决: 改为非贪婪 ".*?",或用字符类排除引号 "[^"]+" (更高效)。
text = 'class="header" id="main"'
re.findall(r'".*"', text) # ['"header" id="main"'] 贪婪
re.findall(r'".*?"', text) # ['"header"', '"main"'] 非贪婪
re.findall(r'"[^"]+"', text) # ['"header"', '"main"'] 排除法(最快)
陷阱:忘记转义特殊字符
现象: 正则 re.search(r'1.2', '1X2') 意外匹配成功。
原因: . 在正则中是通配符,匹配任意一个字符(除换行)。字面点号需要转义为 \.。
解决: 匹配用户输入的字面字符串时,用 re.escape() 转义。
# 错误:匹配了任意字符
re.search(r'1.2', '1X2') # Match
# 正确:匹配字面点号
re.search(r'1\.2', '1X2') # None
re.search(r'1\.2', '1.2') # Match
# 动态字符串转义
user_input = "1.2"
re.search(re.escape(user_input), text)