正则表达式速查表

正则表达式(Regex)语法速查:元字符、字符类、锚点、量词、分组与前后行断言,附中文说明,调试正则时快速查阅。

原理解析

正则表达式是描述「字符串模式」的迷你语言:普通字符逐字匹配,元字符赋予特殊含义——. 匹配任意单字符,* 重复前项 0 次以上,+ 1 次以上,? 0 或 1 次,^/$ 锚定首尾,\d \w \s 匹配数字/词/空白。

贪婪量词(* +)会尽可能多匹配,加 ? 变懒惰(*?);括号用于分组与捕获,反向引用 \1 复用已捕获内容。危险点是「灾难性回溯」——某些模式在坏输入下复杂度指数爆炸,须避免在用户输入上套用嵌套量词。

什么时候该查这张表

这张表在你要查「正则里这个符号是什么意思」时有用。它按字符类、锚点、分组、量词、断言分类,把 PCRE/JavaScript 风格的写法列清楚。忘记某个符号时查这里比翻文档快。

不该查的情况:你在设计复杂匹配或调性能。正则表达式的可读性、回溯控制、灾难性回溯的规避属于工程判断,本表只回答符号语义,不回答「这个正则该怎么写」。

判断标准:先看引擎与方言。同一个 d 在 JavaScript、Python、PCRE 下有细微差别,A 与 ^ 在多行模式下的行为不同。跨语言复用正则前,必须确认目标引擎的语法支持。

特别注意贪婪与惰性:* 和 + 默认贪婪(尽可能多匹配),? 后缀使其惰性(尽可能少匹配)。这一个差异经常决定匹配结果对不对,尤其是配合锚点时——(.*) 匹配到最后一个分隔符,(.*?) 匹配到第一个。

边界与常见陷阱

  • 贪婪量词加锚点导致匹配到行尾而非预期位置

    日志解析里典型症状:用 「^(\S+)\s+(\S+)」 提取前两列,结果第二组吃掉了整行剩余内容。原因是 「\S+」 贪婪。修法是加惰性或明确排除:改 「^(\S+?)\s+」 或 「^(\S+)\s+(\S+)\s*$」。另一个常见变体是 「.*」 在多行文本里跨行匹配,需要用 「[^\n]*」 或开启单行模式修饰。

  • 捕获组编号会随嵌套改变,替换时容易错位

    替换串里的 「$1」、「\1」 引用的是捕获组序号,一旦在模式里插入新的括号,所有后续编号都会顺移。「(a)(b)」 改成 「(a)(x)(b)」 后,原来 「$2」 指向 b,现在指向 x。命名捕获组(「(?<name>...)」 配 「\k<name>」 或 「$<name>」)在多数现代引擎都支持,可读性远好于数字,且不受插入影响。

  • 灾难性回溯:嵌套量词会让匹配耗时指数增长

    「(a+)+$」 匹配一长串 a 后跟一个非 a 的输入时,会产生指数级回溯,几万字符就可能卡死页面。同类写法还有 「(\S+)*」、「(.*)*」。在 JavaScript 里这会冻结主线程,在 Node 服务端则阻塞事件循环。规避方法:避免嵌套量词、把量词改为惰性或具体次数、或用「先整体匹配再判断」的写法替代回溯。

  • 零宽断言的匹配位置容易被误读

    「(?=...)」 是正向预查、「(?!...)」 是负向预查,它们不消耗字符,所以相邻的两个预查可以同时匹配同一个位置。常见困惑:「\b\w+@\w+.\w+\b」 中两个 「\b」 看似多余,实际是必要的边界断言。另一个坑是预查内的分组会消耗编号,「(?=(a))b」 的组 1 是参与的,虽不消耗字符但仍占编号。

与相邻内容的分工

要查某个符号的完整语法(含命名组、递归、Unicode 属性等),以目标语言的官方文档为准;本表覆盖常用子集。

要检查正则是否匹配目标字符串,用站内正则测试工具实测;本表只给语义,不给验证。

要处理正则与替换的多行编辑场景,涉及语言特定标志(如 Python 的 re.M、JS 的 m/s/u/y),需查对应语言文档。

若目的是写日志解析器或数据清洗流程,模式设计往往还需考虑流式处理与部分匹配,不在本表范围。

实战片段

要点:① 默认量词贪婪,(.*) 会吃到最后一个分隔符,提取列要用惰性或 [^\s]+ 这类排除类;② 需要多组捕获时优先用命名捕获组 (?<name>…),插入新组不会让后续编号错位;③ 预查/边界断言不消耗字符,但预查内的分组仍占编号;④ 禁止嵌套量词((a+)+$、(.*)*),它会指数级回溯并冻结主线程,长串务必用线性写法。

// ① 贪婪 vs 惰性:日志解析的经典坑
const line = 'GET  /api/users   200  12ms'
console.log(line.match(/^(S+)s+(S+)/)[2])       // '/api/users'  ✅
console.log(line.match(/^(S+)s+(.*)/)[2])        // '/api/users   200  12ms'  ← 贪婪吃掉整行
console.log(line.match(/^(S+?)s+(S+)/)[2])     // '/api/users'  ✅ 惰性

// ② 行内只取第一段要用排除类,不要用 .*
const safe = /^([^s]+)s+([^s]+)/
console.log(safe.exec(line).slice(1))              // ['GET', '/api/users']

// ③ 命名捕获组:插入新组不会打乱编号
const re = /(?<method>w+) (?<path>S+) (?<status>d{3})/
const m = re.exec(line)
console.log(m.groups)                              // { method: 'GET', path: '/api/users', status: '200' }

// ④ 零宽断言不消耗字符
console.log(/cat/.test('a cat sat'))          // true  单词边界
console.log(/cat(?= )/.test('concatenate'))        // false 预查要求后面是空格

// ⑤ 防灾难性回溯:避免嵌套量词
const BAD = /^(a+)+$/          // ❌ 指数级回溯
const OK  = /^a+$/             // ✅ 线性
// 确认不会卡死(仅测短串,长串请勿在主线程直接跑)
console.log(OK.test('a'.repeat(1000)))

实践注解

  • . 默认不匹配换行,且不同语言默认值不一样多数引擎里 . 不匹配 \n,需要显式开 dotAll(JS 的 s 标志、Python 的 re.S)。但 POSIX 工具(grep、sed)的默认行为又不同,跨语言搬运正则时这是最常见的不一致来源。
  • 灾难性回溯来自嵌套量词(a+)+b 这类模式在长串不匹配时会有指数级回溯,几百字符就能让进程 CPU 打满。改用原子组/占有量词,或把嵌套量词拆成更明确的字符类,能从根本上避免。
  • 贪婪与懒惰只影响匹配长度,不影响能否匹配把 .* 改成 .*? 不会让「本来匹配不上」的变成能匹配(只有当后面还有可匹配内容时才有区别)。误以为加问号能「修好」不匹配的正则,是最常见的排查弯路。

数据来源

  • ECMAScript Language Specification (regular expression semantics)
  • PCRE2 documentation
  • POSIX.1-2017 (BRE/ERE)

上述编号、名称与定义均来自公开规范,本站不主张对规范数据本身的独创性;本页的编排方式、中文释义与实践注解由本站撰写。

符号含义
.任意单个字符(默认不含换行)
\d数字 [0-9]
\D非数字
\w单词字符 [A-Za-z0-9_]
\W非单词字符
\s空白字符(空格/制表/换行)
\S非空白字符
^字符串或行开头
$字符串或行结尾
\b单词边界
\B非单词边界
*前一项 0 次或多次(贪婪)
+前一项 1 次或多次(贪婪)
?前一项 0 或 1 次
{n}恰好 n 次
{n,}至少 n 次
{n,m}n 到 m 次
(...)捕获组
(?:...)非捕获组
[...]字符类(匹配其中任一)
[^...]取反字符类
|或(分支)
\1反向引用第 1 个捕获组
(?=...)正向先行断言
(?!...)负向先行断言
(?<=...)正向后行断言
(?<!...)负向后行断言

常见问题

贪婪量词与非贪婪量词有什么区别?

贪婪(*、+)会尽可能多匹配;在其后加 ?(*?、+?)变为惰性,尽可能少匹配。例如对 "<a>1</a><a>2</a>",<.*> 会贪心匹配到最后一个 </a>,而 <.*?> 只匹配到第一个。

如何让 . 也能匹配换行符?

默认 . 不匹配换行。可启用 dotall 模式(JS 用 s 标志 /[sS]/,或 PHP/Python 的 DOTALL),或用 [sS] 这类写法兼容所有引擎。

字符类和分组有什么不同?

[abc] 只匹配单个字符(a、b 或 c);(abc) 是一个整体序列,匹配字面子串 "abc",并可作为捕获组整体量化(如 (abc)+)。

写正则要注意什么性能问题?

警惕灾难性回溯:避免嵌套量词如 (a+)+ 匹配失败时会指数级回溯。尽量用明确字符类、锚点 ^ $ 缩小范围,必要时用占有量词或原子组。