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. 正则表达式基础语法
  2. 修饰符(Flags)
  3. re 模块所有函数
  4. Pattern 编译对象
  5. Match 对象
  6. 贪婪与非贪婪
  7. 零宽断言
  8. 分组高级用法
  9. 实战案例
  10. 性能优化
  11. 最佳实践
  12. 常见陷阱与注意事项

一、正则表达式基础语法

1.1 普通字符

普通字符匹配其自身。字母、数字、下划线等非特殊字符直接匹配对应字符。

1.2 元字符总览

以下字符在正则中具有特殊含义,匹配字面量时需用反斜杠转义:

. ^ $ * + ? { } [ ] \ | ( )

1.3 字符类

语法 说明 示例
. 匹配除换行符 \n 以外的任意字符(re.DOTALL 时包括换行) a.c 匹配 abca1c
[abc] 字符集,匹配括号内任意一个字符 [abc] 匹配 abc
[^abc] 取反字符集,匹配括号内字符以外的任意字符 [^abc] 不匹配 abc
[a-z] 字符范围,匹配 az 任意字符 [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)

修饰符可以通过两种方式设置:

  1. 函数参数:re.compile(pattern, re.IGNORECASE)
  2. 内联语法:(?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 strbytes 必填 正则表达式字符串
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 strbytes 必填 待匹配的字符串
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 strbytes 必填 待匹配的字符串
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 strbytes 必填 待匹配的字符串
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 strbytes 必填 待匹配的字符串
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 strbytes 必填 待匹配的字符串
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 strcallable 必填 替换内容。字符串时可用 \1\g<1>\g<name> 引用分组;可调用对象接收 Match 对象,返回替换字符串
string strbytes 必填 原始字符串
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 strcallable 必填 替换内容(同 re.sub 的 repl)
string strbytes 必填 原始字符串
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 strbytes 必填 待分割的字符串
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 strbytes 必填 需要转义的字符串

返回值: 转义后的字符串

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 intNone 最后一个参与匹配的捕获分组编号
m.lastgroup strNone 最后一个参与匹配的命名分组名称

5.1 m.group()

函数签名:

m.group([group1, ...])

参数表:

参数名 类型 默认值 说明
group1, ... intstr 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 intstr 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 = {
        '&amp;': '&', '&lt;': '<', '&gt;': '>',
        '&quot;': '"', '&apos;': "'", '&nbsp;': ' ',
    }
    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实体:&amp; &lt; &gt; &nbsp;空格</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 清除缓存

最佳实践

编译复用:频繁使用的正则表达式应提前 compilere.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)

参见

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog