正则表达式(Regular Expression,简称 RegEx)用一段模式描述文本的结构,常用于查找、校验、提取、拆分和替换。读一个表达式时,可以依次问:从哪里开始、匹配什么、重复几次、满足什么条件、在哪里结束?

例如,[A-Z]{2}-[0-9]{4} 描述“两个大写英文字母、一个连字符、四个数字”,可以从 订单 AB-2026 已创建 中提取出 AB-2026。如果要判断整个输入是否符合规则,还需要使用完整匹配接口或合适的边界。

本文从原笔记中的“边界”开始,整理常用语法和容易混淆的细节。代码以 Python 3.11+ 的标准库 re 为主,JavaScript 示例单独标注。 Python 3.14 才加入的语法会注明版本;PCRE2 扩展只作对照。正则表达式有不同“方言”,不要把某个引擎的所有语法直接搬到另一个引擎。

1. 边界:匹配的是位置

边界通常不消耗字符。把字符串想象成一排字符,边界检查的是字符之前、之间和之后的位置。

符号 含义 示例与注意事项
^ 字符串开头;多行模式下也匹配行首 ^foo 默认只检查整串开头;Python 加 re.M 后也检查每个 \n 之后
$ 字符串结尾;具体的末尾换行规则因引擎而异 Python 中 bar$ 也可匹配末尾 \n 前的位置;加 re.M 后也检查行尾
\A 整个字符串开头,不受多行模式影响 Python:\Ahttps?:// 只从整串最开头匹配
\Z Python 中为严格的字符串结尾 Python:foo\Z 不接受 foo\n;PCRE2 的 \Z 还可位于末尾换行之前
\z 严格的字符串结尾 Python 3.14+、PCRE2 支持;旧版 Python 请使用 \Zfullmatch()
\b 单词字符与非单词字符的交界;也包括相应的字符串首尾位置 \bcat\b 能找出 a cat! 中的 cat,不能找出 scatter 中的 cat
\B 非单词边界 \Bcat\B 可以找出 scattered 内部的 cat
\G 当前匹配尝试的起始位置;由调用方设置起始偏移 PCRE2 中常用于连续扫描;Python 标准库 re 不支持

Python 的 $\Z 不完全等价。因此,校验整个字符串时,优先使用 re.fullmatch(),不要只凭“加上 ^$ 就一定严格匹配”的印象。\b 的行为又取决于引擎如何定义 \wPython 语法参考

import re

assert re.search(r"foo$", "foo\n") is not None
assert re.search(r"foo\Z", "foo\n") is None
assert re.fullmatch(r"foo", "foo\n") is None
assert re.findall(r"^foo", "foo\nfoo", re.M) == ["foo", "foo"]
assert re.search(r"\Afoo", "bar\nfoo", re.M) is None
assert re.findall(r"\bcat\b", "cat scatter cat_ cat!") == ["cat", "cat"]
assert re.search(r"\Bcat\B", "scattered").group() == "cat"

cat_ 后面没有单词边界,是因为下划线属于 \w。中文分词也不能简单依靠 \b:Python 通常把中文字符视作单词字符,而 JavaScript 的 \w 主要围绕 ASCII 字母、数字与下划线工作。

版本细节:Python 3.14 的 \B 可以匹配空字符串,较早版本不可以。不要把这个边界情况用作跨版本的业务逻辑。

2. 字符与字符类:一次匹配什么

写法 含义 示例
abc 按顺序匹配字面字符 匹配连续的 abc
. 默认匹配除特定换行字符以外的一个字符 Python 的 a.b 可匹配 a-b,默认不匹配 a\nb
[abc] 从字符集合中取一个字符 匹配 abc,不是整个 abc
[^abc] 一个不在集合中的字符 可匹配 x,也可能匹配换行
[a-z] 范围内的一个字符 一个小写 ASCII 英文字母
[A-Za-z0-9_] 多个范围和字符的并集 明确限定 ASCII 标识符字符
\d / \D 数字 / 非数字 Python 的 Unicode 字符串模式下,\d 不只包含 0–9
\w / \W 单词字符 / 非单词字符 Python 默认包括 Unicode 字母、数字、下划线
\s / \S 空白 / 非空白 \s 包括换行;默认也可包含 Unicode 空白
\t\n\r 制表符、换行、回车 Windows 常见行结束序列是 \r\n
\.\+\? 匹配有特殊含义的符号本身 3\.14 匹配 3.14
\\ 匹配一个反斜杠 Python 模式可写成 r"\\"

写业务规则时,最好先明确字符范围。如果订单号只允许半角数字,用 [0-9]\d 更准确;如果只想合并横向的普通空格和制表符,用 [ \t]+,不要用可能吞掉换行的 \s+

import re

assert re.fullmatch(r"\d+", "123") is not None
assert re.fullmatch(r"[0-9]+", "123") is None
assert re.fullmatch(r"\d+", "123", re.ASCII) is None
assert re.sub(r"[ \t]+", " ", "a  b\tc\nd") == "a b c\nd"
assert re.sub(r"\s+", " ", "a  b\tc\nd") == "a b c d"

方括号里的特殊规则

  • ^ 只有在字符类的开头才表示取反;在外面通常表示开头边界。
  • - 常用于范围;想表达连字符,可以放在末尾,例如 [A-Z-],或按引擎规则转义。
  • . 在普通字符类中就是句点:[.]\. 都可以匹配句点。
  • \b 在 Python 字符类 [...] 内表示退格字符,不再是单词边界。
  • [A-z] 不是“所有英文字母”:ASCII 中 Za 之间还有其他符号,应写 [A-Za-z]
  • [^\n] 仍可能匹配 \r。处理 CRLF 文本时,可明确写成 [^\r\n]

3. 量词:重复几次

量词只作用于前面的一个字符、字符类或分组。

写法 重复次数 示例
* 0 次或更多 ab* 可匹配 aababb
+ 1 次或更多 [0-9]+ 至少一个数字
? 0 次或 1 次 https? 匹配 httphttps
{n} 恰好 n 次 [0-9]{4} 匹配四个数字
{n,} 至少 n 次 [a-z]{2,} 至少两个小写字母
{n,m} n 到 m 次,含端点 [A-Za-z]{2,8} 匹配 2 至 8 个字母

ab{2} 匹配 abb(?:ab){2} 匹配 abab。分组改变了量词所作用的单位。

量词允许零次时,模式可能匹配空字符串。比如 re.fullmatch(r"[0-9]*", "") 会成功,要求至少一个数字应使用 +。循环扫描还要注意零长度匹配,优先使用库提供的迭代接口。

贪婪、懒惰与占有

模式 策略 遇到后续条件失败时
*+{n,m} 贪婪:先尝试较多字符 必要时回退,交还部分字符
*?+?{n,m}? 懒惰:先尝试较少字符 必要时扩展匹配
*+++{n,m}+ 占有:匹配后不交还字符 不回退这个量词已消耗的部分

Python 3.11+ 支持占有量词和原子组 (?>...)。这是控制回溯的工具,也会改变能否匹配;不能机械地把所有 + 替换成 ++Python 量词说明

import re

text = '"red" and "blue"'
assert re.findall(r'".*"', text) == ['"red" and "blue"']
assert re.findall(r'".*?"', text) == ['"red"', '"blue"']
assert re.findall(r'"[^"\r\n]*"', text) == ['"red"', '"blue"']
assert re.fullmatch(r"a+a", "aaaa") is not None
assert re.fullmatch(r"a++a", "aaaa") is None
assert re.fullmatch(r"(?>a+)a", "aaaa") is None

第三个模式明确禁止内容中出现双引号或换行,适用于没有转义引号的简单输入;它不是通用字符串解析器。懒惰匹配也不等于“自动选择语义正确的结束位置”。

4. 分组、分支与反向引用

写法 用途 示例
(...) 捕获分组,记录匹配到的内容 ([0-9]{4})-([0-9]{2}) 捕获年、月
(?:...) 分组但不捕获 (?:ab)+ab 作为整体重复
A|B 分支,匹配 A 或 B cat|dog 匹配 catdog
(?P<name>...) Python 命名捕获 (?P<year>[0-9]{4})
(?<name>...) JavaScript 命名捕获 (?<year>[0-9]{4})
\1\2 再次匹配对应捕获组中的文本 (ha)\1 匹配 haha
(?P=name) Python 命名反向引用 (?P<q>['"])text(?P=q)
\k<name> JavaScript 命名反向引用 (?<q>['"])text\k<q>

分支的范围由分组和优先级决定:

cat|dog              两种完整选择
gr(a|e)y             gray 或 grey
^(?:cat|dog)$        首尾边界作用于整个选择
^cat|dog$            以 cat 开头,或者以 dog 结尾

在 Python 这类引擎中,分支按从左到右的顺序尝试,并不是自动选最长的一项。re.search(r"a|ab", "ab") 会得到 a;如果后续条件要求更多字符,引擎才可能回退并尝试其他分支。

import re

m = re.fullmatch(r"(?P<year>[0-9]{4})-(?P<month>[0-9]{2})", "2026-09")
assert m is not None
assert m.group(0) == "2026-09"
assert m.group("year") == "2026"
assert m.groups() == ("2026", "09")
assert m.groupdict() == {"year": "2026", "month": "09"}
assert re.search(r"a|ab", "ab").group() == "a"
assert re.fullmatch(r"a|ab", "ab").group() == "ab"
assert re.search(r"\b(\w+)\s+\1\b", "go go home").group() == "go go"

捕获组一般按左括号出现的顺序编号。group(0) 是整个匹配,group(1) 才是第一个捕获组。仅为改变优先级而加括号时,优先使用 (?:...),减少无意的编号变化。

5. 环视断言:检查上下文

环视检查当前位置的前后文本,本身不把检查到的内容计入消耗的匹配长度。

写法 含义 示例
(?=...) 正向先行断言:后面必须符合 [0-9]+(?=元) 提取 25元 中的 25
(?!...) 负向先行断言:后面不能符合 foo(?!bar) 匹配后面不紧跟 barfoo
(?<=...) 正向后行断言:前面必须符合 (?<=¥)[0-9]+ 提取 ¥25 中的 25
(?<!...) 负向后行断言:前面不能符合 (?<![0-9])[A-Z] 匹配前面不是数字的大写字母
import re

assert re.search(r"[0-9]+(?=元)", "价格25元").group() == "25"
assert re.search(r"(?<=¥)[0-9]+", "¥25").group() == "25"
assert re.findall(r"foo(?!bar)", "foobar foo!") == ["foo"]
assert re.findall(r"(?<![0-9])[A-Z]", "A 1B _C") == ["A", "C"]
# 正常查找不返回重叠结果;零宽先行断言可捕获重叠片段。
assert re.findall(r"(?=(ana))", "banana") == ["ana", "ana"]

Python 标准库 re 要求后行断言是固定长度,例如 (?<=ab) 可以,(?<=a+) 不可以。遇到可变前缀,通常可以消费前缀后再捕获目标,例如 a+([0-9]+),然后取第一个捕获组。其他引擎的后行规则可能不同。

负向断言还需要明确检查的位置。[0-9]+(?!元) 不等于“提取所有不带元的金额”:在 123元 中,数字量词可能回退,匹配到 12。模式必须同时约束数字的结束边界,或先提取整个字段再判断后缀。

6. 模式标志:改变匹配规则

需求 Python JavaScript
忽略大小写 re.I / re.IGNORECASE i
多行首尾边界 re.M / re.MULTILINE m
. 包含换行 re.S / re.DOTALL s
模式中忽略部分空白并允许注释 re.X / re.VERBOSE 没有直接对应的通用 x 标志
限制部分简写字符类为 ASCII re.A / re.ASCII 用显式字符类表达规则
返回多个匹配 findall() / finditer() 常用 g,例如 matchAll()
按 Unicode 码点解释模式 str 模式默认使用 Unicode uv 提供扩展 Unicode 集合语法
从指定位置开始黏连匹配 用接口与位置参数控制 y
返回捕获结果的索引范围 匹配对象的 span() d,结果中包含 indices

m 只改变 ^$,不会让句点跨行;跨行句点是 s 的职责。JavaScript 中 uv 不能同时使用。标志和较新的语法需要结合目标运行环境确认支持情况。JavaScript 正则参考

import re

assert re.search(r"a.b", "a\nb") is None
assert re.search(r"a.b", "a\nb", re.M) is None
assert re.search(r"a.b", "a\nb", re.S) is not None
assert re.findall(r"^cat$", "Cat\ncat\ndog", re.I | re.M) == ["Cat", "cat"]

order = re.compile(r"""
    (?P<prefix>[A-Z]{2})   # 两个大写字母
    -                     # 连字符
    (?P<number>[0-9]{4})   # 四位数字
""", re.X)
assert order.fullmatch("AB-2026").group("number") == "2026"

re.X 模式中,字符类外的普通空格通常被忽略,# 开始注释。要匹配一个空格,可以写 [ ];要匹配井号,可以写 [#]

7. 转义:区分正则层和字符串层

同一个模式写在纸上、Python 字符串、JavaScript 字面量、JSON 配置里,反斜杠的数量可能不同。先弄清每一层由谁解析。

import re

# 两种字符串写法产生相同的正则模式。
assert r"\d+" == "\\d+"
assert re.fullmatch(r"a\.b", "a.b") is not None

# 把外部输入当作普通文本搜索,而不是当作正则执行。
keyword = "a+b.txt"
assert re.search(re.escape(keyword), "打开 a+b.txt").group() == keyword
const literal = /\d+/;
const constructed = new RegExp("\\d+");
console.assert(literal.source === constructed.source);
console.assert(/https?:\/\//.test("https://example.com"));

Python 的原始字符串前缀 r 能避免大多数字符串层转义,但不能让一个字符串以奇数个反斜杠结尾。JavaScript 的 /.../ 中,斜杠还兼任分隔符,所以通常需要转义;用 RegExp() 时则要处理字符串层的反斜杠。

re.escape() 用于构造模式中的字面文本,不适合处理替换模板。需要原样插入任意替换文本时,可以用返回该文本的回调函数。

8. Python 常用接口

接口 返回结果 / 行为 适合的任务
re.search() 在任意位置找第一个匹配;失败返回 None 判断是否包含、提取第一项
re.match() 只从字符串开头尝试 匹配前缀,不要求整串匹配
re.fullmatch() 要求整个字符串符合模式 字段校验
re.findall() 返回全部非重叠匹配;捕获组会改变结果结构 简单批量提取
re.finditer() 返回匹配对象迭代器 需要位置、分组等详细信息
re.sub() / re.subn() 替换;后者同时返回替换次数 清洗与重排文本
re.split() 按匹配处拆分;捕获分隔符会进入结果 拆分简单字段
re.compile() 编译成可复用模式对象 固定规则重复使用
import re

text = "x=12, y=34"
assert re.search(r"[0-9]+", text).group() == "12"
assert re.match(r"[0-9]+", text) is None
assert re.fullmatch(r"[0-9]+", "12x") is None
assert re.findall(r"[0-9]+", text) == ["12", "34"]
assert re.findall(r"([a-z])=([0-9]+)", text) == [("x", "12"), ("y", "34")]
assert [(m.group(), m.span()) for m in re.finditer(r"[0-9]+", text)] == [
    ("12", (2, 4)), ("34", (8, 10))
]
assert re.split(r"[,;]\s*", "a, b;c") == ["a", "b", "c"]
assert re.split(r"([,;])\s*", "a, b;c") == ["a", ",", "b", ";", "c"]
assert re.subn(r"[0-9]+", "#", text) == ("x=#, y=#", 2)

span() 的结束索引不包含在匹配区间内,与 Python 切片一致。对可能匹配失败的输入,先判断 m is not None,再访问 m.group();上面的直接调用用于结果已知的演示数据。

9. 替换:模式语法与替换语法不同

目的 Python re.sub() 的替换模板 JavaScript replace() 的替换模板
整个匹配 \g<0> $&
第一个捕获组 \g<1> $1
名为 year 的组 \g<year> $<year>
动态计算结果 传入函数,参数为匹配对象 传入替换函数

Python 推荐用 \g<1> 这种明确写法,避免 \10 被当成第 10 组。JavaScript 的 $1 属于替换字符串规则,不是正则模式中的反向引用写法。JavaScript 替换文档

import re

assert re.sub(r"([0-9]{4})-([0-9]{2})-([0-9]{2})", r"\g<3>/\g<2>/\g<1>",
              "2026-09-21") == "21/09/2026"
assert re.sub(r"[0-9]+", lambda m: str(int(m.group()) * 2),
              "x=12 y=3") == "x=24 y=6"
assert re.sub(r"\b(?P<word>\w+)\s+(?P=word)\b", r"\g<word>",
              "go go home") == "go home"
console.assert("2026-09-21".replace(
  /([0-9]{4})-([0-9]{2})-([0-9]{2})/, "$3/$2/$1"
) === "21/09/2026");
console.assert("a1 b2".replace(/[0-9]/g, "#") === "a# b#");
console.assert("a1 b2".replace(/[0-9]/, "#") === "a# b2");

10. JavaScript:全局搜索与状态

JavaScript 中用 test() 判断、exec() 获取匹配对象、matchAll() 遍历所有匹配。给 matchAll() 传入正则时,需要使用 g 标志。

const text = "x=12 y=34";
const pairs = [...text.matchAll(/(?<key>[a-z])=(?<value>[0-9]+)/g)]
  .map(m => [m.groups.key, m.groups.value]);
console.assert(JSON.stringify(pairs) === '[["x","12"],["y","34"]]');

// g 和 y 会让 test()/exec() 使用并更新 lastIndex。
const stateful = /a/g;
console.assert(stateful.test("a") === true);
console.assert(stateful.test("a") === false);
stateful.lastIndex = 0;
console.assert(stateful.test("a") === true);

// 普通字段校验一般不要使用 g。
console.assert(/^[A-Za-z][A-Za-z0-9_]{2,15}$/.test("Janus_01"));

反复调用带 gtest() 得到不同结果,常常是 lastIndex 的影响,而不是表达式突然失效。需要逐项判断独立输入时,使用不带 g 的正则,或明确重置状态。lastIndex 文档

11. 实战:先写清规则,再写表达式

以下是针对明确约束的示例,不是适用于所有系统的万能校验器。Python 示例默认使用 fullmatch(),防止只匹配到输入中的一个片段。

11.1 用户名、整数和十六进制颜色

import re

# 3–16 个 ASCII 字符;字母开头,其后只允许字母、数字、下划线。
username = re.compile(r"[A-Za-z][A-Za-z0-9_]{2,15}")
assert username.fullmatch("Janus_01") is not None
assert username.fullmatch("1janus") is None
assert username.fullmatch("ab") is None
assert username.fullmatch("Janus_01\n") is None

# 可带正负号;不接受 01、1.0、空串;允许 +0 和 -0。
integer = re.compile(r"[+-]?(?:0|[1-9][0-9]*)")
assert integer.fullmatch("-42") is not None
assert integer.fullmatch("01") is None

# 只处理 3 位和 6 位 RGB 十六进制颜色,不包含 alpha 或其他 CSS 颜色格式。
color = re.compile(r"#(?:[0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})")
assert color.fullmatch("#3aF") is not None
assert color.fullmatch("#12ab34") is not None
assert color.fullmatch("#12345") is None

11.2 日期:格式与真实日期分开判断

[0-9]{4}-[0-9]{2}-[0-9]{2} 只检查位数,连 2026-99-99 都能通过。即使限制月份和日数,也还要判断大小月、闰年与年份范围。

import re
from datetime import date

def valid_date(text):
    if re.fullmatch(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", text) is None:
        return False
    try:
        date.fromisoformat(text)
        return True
    except ValueError:
        return False

assert valid_date("2024-02-29")
assert not valid_date("2026-02-29")
assert not valid_date("2026-9-21")
assert not valid_date("2026-99-99")

11.3 邮箱与电话号码:粗筛不等于验证成功

邮箱粗筛可使用 [^\s@]+@[^\s@]+\.[^\s@]+,配合完整匹配只要求“无空白的本地部分、一个 @、带句点的域名部分”。它既可能接受错误输入,也可能拒绝某些合法地址;不能证明域名存在,更不能证明用户拥有邮箱。生产系统应结合成熟校验库和验证邮件。

电话号码要先确定国家或地区、国际区号、分隔符以及是否允许分机。正则可以检查某个明确的格式,例如“字面 + 后跟 8–15 个 ASCII 数字”:\+[0-9]{8,15}这只是示例产品规则,并不是全球号码有效性的判断标准。真实业务需要号码解析库和必要的验证流程。

11.4 日志提取

假设日志格式固定为 日期 时间 级别 消息,消息可以含空格:

import re

log = re.compile(
    r"(?P<date>[0-9]{4}-[0-9]{2}-[0-9]{2})[ \t]+"
    r"(?P<time>[0-9]{2}:[0-9]{2}:[0-9]{2})[ \t]+"
    r"(?P<level>DEBUG|INFO|WARN|ERROR)[ \t]+"
    r"(?P<message>[^\r\n]*)"
)
m = log.fullmatch("2026-09-21 09:30:05 INFO server started")
assert m is not None
assert m.group("level") == "INFO"
assert m.group("message") == "server started"

这个模式负责提取格式化字段,并没有验证日期与时间的真实有效性。格式不固定、支持嵌套对象或跨行堆栈时,应结合日志格式规范处理。

11.5 文件扩展名、URL、IP 和结构化数据

提取一个文件名末尾的简单扩展名,可以用 \.([A-Za-z0-9]+)\Z。但 .bashrcarchive.tar.gz、目录名中的句点和 URL 查询参数都有不同语义;处理文件路径应优先使用路径库,并明确“扩展名”的业务定义。

同样,URL 用 URL 解析器拆解后检查协议与主机;IP 地址用 IP 地址库验证;JSON、HTML、CSV 使用对应解析器。正则适合在约束明确的文本里定位片段,不适合代替完整的结构化格式解析。尤其不要用“能够提取主机名”的正则代替 URL 访问权限判断。

12. Unicode 与跨引擎差异

特性 Python 标准库 re JavaScript 示例基线 PCRE2
\d str 模式默认支持 Unicode 十进制数字;re.A 可限制 [0-9] 受 UCP 等选项影响
命名组 (?P<name>...) (?<name>...) 支持多种命名写法
严格整串匹配 fullmatch() 常用首尾锚点,注意不要误加 m 可使用 \A...\z
后行断言 固定长度 支持,具体兼容性取决于运行时 长度限制取决于版本与配置
原子组、占有量词 3.11+ 本文不依赖这些扩展 支持
\p{...} Unicode 属性 标准库不支持 u / v 模式支持 支持,结合 Unicode 配置
\R\X\K 标准库不支持 本文不依赖这些扩展 分别用于换行序列、扩展字素簇、重置报告的匹配起点

PCRE2 中的 \K 会让它之前已消耗的文本不出现在最终报告的匹配结果中,不能简单当作通用后行断言。\R 与普通句点的换行配置也不是同一个概念。使用这些扩展前应核对所用引擎的文档。PCRE2 官方说明

JavaScript 的 Unicode 属性匹配示例:

console.assert(/^\p{Script=Han}+$/u.test("正则表达式"));
console.assert(!/^\p{Script=Han}+$/u.test("RegEx中文"));
console.assert(!/^\d+$/u.test("123"));
console.assert(/^\p{Decimal_Number}+$/u.test("123"));

汉字脚本属性不等于“所有中文文本”:标点、拉丁字母、数字和其他符号需要按需求加入规则。不要把 [\u4e00-\u9fa5] 当作完整的汉字集合。

一个用户看到的字符,也不一定只占一个 Unicode 码点。组合音标和 emoji 序列可能包含多个码点,所以 .{1,10} 不能普遍解释为“最多 10 个视觉字符”。如果需求按视觉字符计数,应采用字素簇分段;如果要求不同编码形式等价,还要考虑 Unicode 规范化。

13. 性能、调试与易错点

回溯为什么会变慢

回溯型引擎遇到歧义重复时,可能反复尝试不同拆分方式。典型例子是 ^(a+)+$:输入为很多个 a 再跟一个 ! 时,前面的字符容易匹配,最后失败却可能引发大量回溯。

如果业务规则只是“一个或多个 a”,直接使用 a+ 配合完整匹配即可。不要为了测试性能,把危险模式直接放到生产服务里处理超长字符串。

实用做法包括:

  1. 先限制输入长度,并在实际运行的引擎上测试。
  2. 避免相互重叠的分支与嵌套无界量词,能写明确字符类就写明确字符类。
  3. 用正反两类用例验证;尤其测试“前面都符合、最后一个字符失败”的输入。
  4. 长模式拆成多个条件;简单的包含、前缀、分隔操作优先考虑普通字符串函数。
  5. 对不可信的复杂模式,使用具有适当资源限制的方案;Python 标准库 re 没有通用的匹配超时参数。

“把贪婪改成懒惰”不保证消除性能问题;原子组和占有量词也要经过等价性检查。Python 正则 HOWTO

一次有效的调试

先记下引擎与版本、完整模式、标志和实际输入。然后把期望结果写成可运行的断言,再从最小表达式开始逐步增加条件。

至少覆盖这些输入:正常值、空串、缺失字段、超长字段、末尾换行、CRLF、中文或全角字符,以及只在最后一步失败的字符串。使用在线测试器时,选择与生产环境一致的引擎;测试通过仍应回到真实代码中验证。

速查清单

  • 校验整个值:Python 优先 fullmatch();查找片段:search()
  • ^ 在模式外部表示开头,在 [^...] 中表示字符类取反。
  • . 默认不负责跨行;ms 解决的是不同问题。
  • * 可以匹配零次;+ 才要求至少一次。
  • (...) 会捕获;(?:...) 只分组。
  • \1 是模式中的反向引用;Python 替换模板用 \g<1>,JavaScript 用 $1
  • \d\w\b 不能不看引擎就假定语义相同。
  • 外部输入按字面含义拼入模式时先转义。
  • 格式符合不等于业务有效;日期、号码、URL 等仍有语义校验。
  • 表达式写得短,不一定更清晰或更快;优先让约束可读、结果可测试。

参考资料

本文覆盖日常编程所需的主要知识,不把 PCRE2 的递归、回溯控制动词等扩展当作跨语言通用语法。查阅资料时,以实际部署的引擎和版本为准。