正则表达式用于描述一类文本模式。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()只尝试从字符串开头匹配MatchNone
re.fullmatch()要求整个字符串符合模式MatchNone
re.search()查找任意位置的第一个匹配MatchNone
re.findall()以字符串或元组返回全部匹配list
re.finditer()迭代全部 Match 对象迭代器
re.split()按匹配的分隔符切分list
re.sub()替换匹配文本str
re.subn()替换文本并报告替换次数(str, int)

校验完整输入时使用 fullmatch,定位一个片段时使用 search,需要每个结果的位置或捕获字段时使用 finditer。下面的例子展示 matchsearch 的差别:

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 / \DUnicode 十进制数字 / 非十进制数字
\w / \WUnicode 字母、数字或下划线 / 对应补集
\s / \SUnicode 空白字符 / 非空白字符

大写简写字符类是对应小写形式的补集。Python 默认采用 Unicode 语义,范围比 ASCII 更宽:\d 可以匹配 0-9 以外的 Unicode 十进制数字,\w 可以匹配多种文字的字母。协议或文件格式明确要求 ASCII 时,可以启用 re.ASCII,也可以使用 [0-9] 等明确范围。

3.2 连接、分支与量词

相邻的模式元素匹配相邻文本。r"\d\w" 消耗一个数字和紧随其后的一个单词字符。分支运算符 | 在多个候选模式中选择,而且优先级很低:r"ab|c" 表示 abcr"a(?:b|c)" 才表示 abac

量词重复前一个原子或分组:

量词重复次数
?零次或一次
*零次或多次
+一次或多次
{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)+" 匹配 ababab 以及更长的成对重复;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 一类相邻重复词。反向引用比较捕获的文本;量词只重复分组模式,每次重复可以匹配不同文本。

捕获组还会改变 findallsplit 的结果。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 数据表会随版本变化。