正则表达式
正则用一小段模式描述一组字符串。编辑器搜索、grep、测试框架的用例筛选、日志解析都靠它。
语法骨架
| 写法 | 含义 |
|---|---|
abc | 字面量 abc |
. | 任意单个字符 |
[abc] | 括号内任一字符 |
[^abc] | 不在括号内的字符 |
[a-f] | 范围内任一字符 |
| `a | b` |
\d | 数字,等价 [0-9] |
\w | 单词字符 |
\s | 空白字符 |
\b | 单词边界,宽度为零 |
X? | 零个或一个 |
X* | 零个或多个 |
X+ | 一个或多个 |
X{N} | 恰好 N 个 |
(...) | 分组并捕获 |
^ $ | 行首、行尾 |
\. | 转义,匹配字面量点号 |
., *, +, ?, (, ), [, ], {, }, |, ^, $, \ 都有特殊含义,要匹配本身就得加反斜杠。\d{4}-\d{2}-\d{2} 只保证四位数字、横线、两位、横线、两位,2026-01-99 照样通过,格式校验和取值校验是两件事。
写模式时还有两个易错点:
- Python 里统一加
r前缀。普通字符串中的\d属于无效转义,Python 3.12 起会给出SyntaxWarning;\b更糟,会被解释成退格符 ^和$的语义取决于引擎与选项。Python 的re默认按整串,加re.MULTILINE才逐行;grep与多数编辑器默认逐行
分组与替换
import re
m = re.match(r"\d{4}-(\d{2})-\d{2}", "2026-01-14")
print(m.group(1)) # 01
print(re.sub(r"(\w+)@(\w+)", r"\2:\1", "a@b")) # b:a替换串里的反向引用,Python、sed、Vim 写 \1,VS Code 的替换框写 $1。抄别人的正则时先确认方言,再确认分组编号是从 0 还是 1 开始。
re.match 从字符串开头匹配,re.search 扫描整个字符串,re.fullmatch 要求整串都匹配,校验逻辑该用后者。命名分组写 (?P<year>\d{4}),取值用 m.group("year"),比数字下标耐改。
前后查找不消耗字符,可以用来做「后面跟着」的条件:
| 写法 | 含义 |
|---|---|
(?=x) | 后面是 x,不含 x 本身 |
(?!x) | 后面不是 x |
(?<=x) | 前面是 x |
(?<!x) | 前面不是 x |
re.compile 预编译模式,循环里反复匹配时省掉解析开销;模式只用一个正则字符串、匹配上千行日志时差别明显。
贪婪与懒惰
.* 是贪婪的,会尽量多吃,然后回溯。匹配 JSON 里的名字:
re.search(r'"name": "(.*)"', '{"name": "Alyssa", "college": "MIT"}').group(1)结果是 Alyssa", "college": "MIT——它吃到了这一行最后一个引号。
加个问号变成懒惰:".*?",匹配到第一个可行位置就停。更稳的写法是把转义考虑进去:
r'"name": "((?:[^"\\]|\\.)*)"'[^"\\] 吃掉普通字符,\\. 吃掉转义序列,整个分组不会越过未转义的引号。
. 默认不匹配换行。要跨行用 re.DOTALL;要让 ^ 和 $ 按行生效用 re.MULTILINE。两个标志可以按位或:re.I | re.M。
常用片段
| 目标 | 模式 | 备注 |
|---|---|---|
| 日期 | \d{4}-\d{2}-\d{2} | 不校验月份与日 |
| 邮箱粗筛 | .+@.+ | 只排除明显不是的 |
| IPv4 粗筛 | \d{1,3}(\.\d{1,3}){3} | 不校验 0 到 255 |
| 重命名导入 | import .* as .* | 配合 ag、rg 搜代码 |
| 测试筛选 | go test -run 'TestUser' | 按测试名子串匹配 |
| 4xx 与 5xx | `" (4\d | 5\d{2}) ` |
这些模式用于粗筛,不用于校验。真要做校验,先把输入解析成结构,再检查取值范围。
在编辑器里替换
# 只替换行首的列表标记,不动日期里的横线
sed -E 's/^([[:space:]]*)- /\1* /' notes.md" Vim 基本正则,分组要转义
:%s/^\(\s*\)- /\1* /g| 环境 | 引用分组 | 说明 |
|---|---|---|
| VS Code | $1 | 替换框里也支持 \n |
| Vim | \1 | 整个匹配是 & |
| sed | \1 | 基本正则要写 \( \) |
| PCRE | \1 | -P 或 pcre2grep |
直接全局替换 - 会改坏日期、复合词和 --flag。正确做法是把匹配锚定到行首,再要求后面跟一个空格:^(\s*)- 。
拼接用户输入进模式时用 re.escape,否则输入里的 . 或 ( 会被当成语法,轻则匹配错误,重则抛异常。
方言与边界
| 场景 | 用法 |
|---|---|
| POSIX 工具 | grep -E,扩展正则 |
| GNU grep | grep -P,PCRE 方言 |
| Python | re 模块 |
| 测试筛选 | `go test -run 'TestFoo |
| 代码搜索 | ag、rg 默认支持正则 |
| 调试 | regex101 可选方言逐步解释 |
不同引擎在命名分组、后顾、懒惰量词上的细节不一致,复杂模式先到 regex101 选好 flavor 再试。POSIX 字符类写作 [[:digit:]],在 grep -E 里比 \d 可靠。
正则的表达力有上限,几类东西写不出来或写不好:
{a^n b^n}这种「a 的个数等于 b 的个数」- HTML、JSON 这类嵌套结构
- 嵌套量词引发的灾难性回溯,比如
(a+)+$遇到一长串a就崩了
硬写会让模式又长又脆;回溯那类还会在 CI 里跑大文件时直接挂住,用时先想想能不能换成解析器。
解析 JSON 就该用 JSON 解析器:
python -c 'import json,sys; print(json.load(sys.stdin)["name"])'一行代码,处理转义、嵌套、Unicode 全部正确。正则留给日志行、配置项、批量重命名这类扁平文本。