正则表达式用于描述一类文本模式。Python 的 re 模块可以按照模式定位文本、提取结构化字段、分割输入,或者替换匹配片段。一次正则操作包含三个不同对象:待处理的文本、用 Python 字符串表示的模式,以及解释模式的 re 引擎。
区分三个对象可以避免初学者最常见的错误。Python 首先解析字符串字面量,正则引擎随后把解析结果当作模式。反斜杠可能同时在两个解析阶段承担特殊作用。
1. Python 字符串与正则模式
1.1 Python 转义序列
普通 Python 字符串使用反斜杠引入转义序列。常见写法包括:\n 表示换行符,\r 表示回车符,\t 表示制表符,\\ 表示字面反斜杠,\x41 表示十六进制字节值,\u4e2d 表示 Unicode 码点。Python 把解析结果作为一个字符,因此 len("\n") 等于 1,尽管源代码里能看到两个符号。
只有引号会结束当前字符串时才需要转义。下面两个变量都只包含一个双引号字符:
quote_a = '"'
quote_b = "\""
assert quote_a == quote_b
普通字符串不能以未转义的反斜杠结尾,因为反斜杠会转义右侧引号。基于相同的词法规则,原始字符串也不能以奇数个反斜杠结尾。
1.2 原始字符串减少一层转义
原始字符串前缀让 Python 保留反斜杠,不再解释大多数转义序列。例如,r"\n" 的长度是 2,而 "\n" 的长度是 1。正则语法大量使用反斜杠,因此正则模式通常应该写成原始字符串:
ordinary_pattern = "\\d+"
raw_pattern = r"\d+"
assert ordinary_pattern == raw_pattern
两个变量都包含模式 \d+,但原始字符串更容易阅读。原始字符串不会关闭正则解析器;re 编译 r"\d" 时,模式仍然表示“一个十进制数字”。
匹配字面反斜杠可以展示两层解析过程。正则引擎需要 \\ 表示字面反斜杠,因此推荐的 Python 模式是 r"\\";不使用原始字符串时,同一个模式必须写成 "\\\\"。
2. 使用 re 模块完成匹配
2.1 选择正确的匹配函数
使用 import re 导入标准库模块。常用函数的搜索范围和返回结果不同:
| 函数 | 行为 | 常见返回值 |
|---|---|---|
re.match() | 只尝试从字符串开头匹配 | Match 或 None |
re.fullmatch() | 要求整个字符串符合模式 | Match 或 None |
re.search() | 查找任意位置的第一个匹配 | Match 或 None |
re.findall() | 以字符串或元组返回全部匹配 | list |
re.finditer() | 迭代全部 Match 对象 | 迭代器 |
re.split() | 按匹配的分隔符切分 | list |
re.sub() | 替换匹配文本 | str |
re.subn() | 替换文本并报告替换次数 | (str, int) |
校验完整输入时使用 fullmatch,定位一个片段时使用 search,需要每个结果的位置或捕获字段时使用 finditer。下面的例子展示 match 与 search 的差别:
import re
text = "prefix abc suffix"
assert re.match(r"abc", text) is None
found = re.search(r"abc", text)
assert found is not None
assert found.span() == (7, 10)
匹配成功后,函数返回 re.Match 对象。start() 和 end() 返回左闭右开的边界,span() 以元组返回两个边界,group() 返回匹配文本。group(0) 始终表示完整匹配,带编号或名称的 group 用于读取捕获的子模式。
match = re.search(r"(?P<name>[a-z]+)=(\d+)", "size=42")
assert match is not None
assert match.group() == "size=42"
assert match.group(1) == "size"
assert match.group(2) == "42"
assert match.group("name") == "size"
assert match.groupdict() == {"name": "size"}
调用 Match 方法之前必须处理 None。如果文本没有匹配,未经检查的 re.search(...).group() 会抛出 AttributeError。
2.2 编译模式与设置标志
re.compile() 创建可重复使用的模式对象。同一个模式需要反复运行时,编译对象能够明确表达用途,调用对象方法时也不必重复传入模式:
assignment = re.compile(r"(?P<name>[a-z_]+)=(?P<value>\d+)", re.IGNORECASE)
for text in ["size=42", "COUNT=7"]:
match = assignment.fullmatch(text)
if match:
print(match.groupdict())
Python 会缓存一定数量的近期模式,因此显式编译的主要价值是复用和可读性,不应把编译当作所有场景的性能技巧。常见标志包括:re.IGNORECASE 忽略大小写,re.DOTALL 允许点号匹配换行符,re.MULTILINE 让 ^ 和 $ 在每一行的边界生效,re.VERBOSE 允许复杂模式包含排版空白和注释。
3. 核心模式语法
3.1 字面字符、字符类与简写字符类
大多数字符匹配自身。模式 r"abc" 查找准确的 abc 序列;除非启用 re.IGNORECASE,匹配会区分大小写。点号默认匹配一个非换行字符,启用 re.DOTALL 后也能匹配换行符。
方括号定义字符类,每次准确消耗一个字符。r"[1234]" 匹配四个数字中的一个,r"[0-9]" 匹配一个 ASCII 数字,r"[^0-9]" 匹配范围之外的一个字符。脱字符只有紧跟 [ 时才会对字符类取反。连字符在合适的两个端点之间定义范围;需要匹配字面连字符时,可以把连字符放在字符类开头或结尾,也可以转义连字符。
大部分元字符进入字符类后不再承担特殊作用。例如,r"[.]" 只匹配字面点号。右方括号和反斜杠使用转义写法更清楚:r"[\]\[]" 匹配任意一侧方括号,r"[\\]" 匹配一个反斜杠。
正则引擎提供以下简写字符类:
| 模式 | Python str 模式中的含义 |
|---|---|
\d / \D | Unicode 十进制数字 / 非十进制数字 |
\w / \W | Unicode 字母、数字或下划线 / 对应补集 |
\s / \S | Unicode 空白字符 / 非空白字符 |
大写简写字符类是对应小写形式的补集。Python 默认采用 Unicode 语义,范围比 ASCII 更宽:\d 可以匹配 0-9 以外的 Unicode 十进制数字,\w 可以匹配多种文字的字母。协议或文件格式明确要求 ASCII 时,可以启用 re.ASCII,也可以使用 [0-9] 等明确范围。
3.2 连接、分支与量词
相邻的模式元素匹配相邻文本。r"\d\w" 消耗一个数字和紧随其后的一个单词字符。分支运算符 | 在多个候选模式中选择,而且优先级很低:r"ab|c" 表示 ab 或 c,r"a(?:b|c)" 才表示 ab 或 ac。
量词重复前一个原子或分组:
| 量词 | 重复次数 |
|---|---|
? | 零次或一次 |
* | 零次或多次 |
+ | 一次或多次 |
{m} | 准确 m 次 |
{m,n} | 从 m 次到 n 次 |
{m,} | 至少 m 次 |
{,n} | 至多 n 次 |
量词默认采用贪婪模式。re.search(r"m{2,3}", "mmm") 消耗三个字符,re.search(r"m{2,3}?", "mmm") 只消耗两个字符。在 *、+、? 或花括号量词后添加 ?,可以选择仍然满足完整模式的最短结果。
分组决定量词重复的对象。r"(?:ab)+" 匹配 ab、abab 以及更长的成对重复;r"ab+" 则匹配一个 a 和一个或多个 b。
4. 分组与反向引用
4.1 捕获组与非捕获组
圆括号用于控制优先级,默认也会创建捕获组。分组编号按照左括号从左到右出现的顺序计算:
match = re.search(r"((\d)\w(\d))", "a1b3x")
assert match is not None
assert match.group(0) == "1b3"
assert match.group(1) == "1b3"
assert match.group(2) == "1"
assert match.group(3) == "3"
只需要控制优先级或重复范围时,应使用 (?:...)。非捕获组没有编号,也不会改变周围捕获组的编号。例如,模式 r"([a-c])(?:[d-f](<[g-h]>))" 包含三对圆括号,但只有两个捕获组。
4.2 命名组与反向引用
命名组使用 (?P<name>...)。相较于位置编号,名称更容易表达字段含义,也更便于维护提取代码:
line_pattern = re.compile(
r"^(?P<level>INFO|WARN|ERROR)\s+"
r"(?P<date>\d{4}-\d{2}-\d{2})\s+"
r"(?P<message>.+)$"
)
match = line_pattern.fullmatch("ERROR 2026-09-10 disk full")
assert match is not None
assert match.groupdict()["level"] == "ERROR"
反向引用要求后续文本与已经捕获的文本完全一致。编号反向引用使用 \1、\2 等形式,命名反向引用使用 (?P=name)。模式 r"\b(?P<word>\w+)\s+(?P=word)\b" 可以找到 the the 一类相邻重复词。反向引用比较捕获的文本;量词只重复分组模式,每次重复可以匹配不同文本。
捕获组还会改变 findall 和 split 的结果。findall 模式只有一个捕获组时,列表包含该组内容而不是完整匹配;包含多个捕获组时,列表元素是元组。如果调用者需要完整匹配,而模式内部又必须分组,应使用 (?:...)。
5. 位置、边界与前后断言
5.1 锚点、边界与前后断言
锚点只判断位置,不消耗字符。^ 匹配字符串开头,$ 匹配字符串结尾或末尾换行符之前的位置。启用 re.MULTILINE 后,两个锚点也会在每一行的边界生效。\A 和 \Z 不受多行模式影响,始终针对完整字符串。严格校验整个输入时,fullmatch 通常比手动添加首尾锚点更清楚。
\b 判断单词字符与非单词字符之间的边界,\B 判断相反情况。单词边界采用正则引擎对 \w 的定义;在默认 Unicode 模式下,ASCII 以外的字母也属于单词字符。
前后断言要求周围文本满足条件,但不会把条件文本放入匹配结果:
| 形式 | 条件 |
|---|---|
(?=...) | 肯定前向断言:后续文本必须匹配 |
(?!...) | 否定前向断言:后续文本不能匹配 |
(?<=...) | 肯定后向断言:前面的文本必须匹配 |
(?<!...) | 否定后向断言:前面的文本不能匹配 |
模式 r"(?<!\d)abc(?=\d)" 只在 abc 前面不是数字、后面紧跟数字时匹配。返回结果仍然只包含 abc。
Python 内置 re 引擎要求后向断言内部具有固定宽度。(?<=ab) 和 (?<=\d{3}) 合法,可变长度形式 (?<=a+) 会被拒绝。如果规则能够通过消耗前置文本并捕获目标字段来表达,普通分组往往比堆叠多个断言更容易理解。
5.2 优先级与复杂模式的可读性
实用的优先级顺序是:转义序列和字符类最高,随后是量词,再后是相邻连接,分支运算符最低。圆括号可以明确表达预期分组。长模式可以配合 re.VERBOSE,通过缩进和注释组织结构,而不把排版空格当作待匹配空格:
date_pattern = re.compile(
r"""
(?P<year>\d{4}) -
(?P<month>0[1-9]|1[0-2]) -
(?P<day>0[1-9]|[12]\d|3[01])
""",
re.VERBOSE,
)
该模式可以校验日期的数字结构和大致范围,但无法判断 2 月 30 日无效。正则表达式适合处理词法结构,业务规则仍可能需要普通 Python 代码或 datetime 等专用解析器。
6. 查找、分割与替换文本
6.1 收集匹配与分割文本
只需要匹配值时,findall 最为直接。模式没有捕获组时返回完整匹配;模式包含捕获组时返回捕获字符串或元组:
text = "aAxxxxbB"
assert re.findall(r"[a-z][A-Z]", text) == ["aA", "bB"]
assert re.findall(r"([a-z])([A-Z])", text) == [("a", "A"), ("b", "B")]
finditer 惰性返回 Match 对象,适合大型输入,也适合需要结果位置的任务:
for match in re.finditer(r"\d+", "ports: 80, 443"):
print(match.group(), match.span())
re.split(pattern, text, maxsplit=0) 默认按照全部匹配切分,maxsplit 可以限制次数。连续分隔符以及字符串端点上的分隔符可能产生空字符串。捕获分隔符会把分隔符插入结果,适合需要保留标点的任务。
assert re.split(r"[,;]\s*", "alpha, beta;gamma") == [
"alpha",
"beta",
"gamma",
]
assert re.split(r"([,;])\s*", "alpha, beta;gamma") == [
"alpha",
",",
"beta",
";",
"gamma",
]
固定字面分隔符已经足够时,应使用 str.split()。需要识别多种分隔符、可变空白或上下文规则时,正则表达式才有明显优势。
6.2 使用字符串或函数替换
re.sub(pattern, replacement, text, count=0) 默认替换全部匹配。替换字符串可以用 \g<name> 或 \g<1> 引用捕获组。可调用替换对象会接收 Match,并为每个匹配计算新值:
text = "Ada:5 Linus:8"
normalized = re.sub(
r"(?P<name>[A-Za-z]+):(?P<score>\d+)",
r"\g<name>=\g<score>",
text,
)
incremented = re.sub(
r"\d+",
lambda match: str(int(match.group()) + 1),
text,
)
assert normalized == "Ada=5 Linus=8"
assert incremented == "Ada:6 Linus:9"
re.subn() 执行相同的替换,并返回 (new_text, replacement_count)。迁移和清理脚本可以检查替换次数;数量异常往往说明输入格式已经变化。
7. 设计和测试可靠的模式
先准备具有代表性的输入,再明确任务属于定位、提取、分割、替换还是校验。模式应该只表达输入的实际结构;重复使用时编译模式,并同时测试应该匹配和不应该匹配的样例。边界样例应覆盖空文本、Unicode 字符、换行符、连续分隔符和异常长输入。
语法正确的模式仍可能产生过量回溯。面对不可信输入时,(a+)+ 一类嵌套且含义重叠的量词具有风险。明确的分隔符、有限重复和更简单的解析逻辑通常更安全。正则表达式适合 token 和扁平记录;HTML、JSON 与编程语言等具有嵌套语法的格式已经有专用解析器。
权威资料包括 Python 官方的 re 模块文档和正则表达式 HOWTO。如果程序依赖具体版本的行为,应在生产环境使用的 Python 版本上执行测试,因为可用语法和 Unicode 数据表会随版本变化。