正则贪婪匹配与非贪婪匹配
默认量词是贪婪的,会尽可能多吃字符;加 ? 变成非贪婪,尽可能少吃。在提取标签、括号内容时非常关键。
#type / concept
#status / evergreen
#resource / javascript
#resource / ecmascript
[!info] related notes
- 所属 MOC: ecmascript-moc
- 前置概念: ecmascript-regular-expressions
- 并列概念: regex-lookahead-lookbehind, regex-lastindex-pitfall
- 语法速查: regex
正则贪婪匹配与非贪婪匹配
一句话定义
量词默认是贪婪的(greedy):尽可能多吃字符。在量词后加 ? 变成非贪婪的(lazy):尽可能少吃。
贪婪(默认)
量词 * + ? {n,m} 默认都是贪婪的。
const str = "a123b456b";
console.log(str.match(/a.*b/)[0]);
// "a123b456b" ← 吃到最后一个 b
.* 会从 a 一直往后吃,直到再也找不到 b 才回退一步匹配最后一个 b。
非贪婪
在量词后加 ?:
const str = "a123b456b";
console.log(str.match(/a.*?b/)[0]);
// "a123b" ← 吃到第一个 b 就停
常见非贪婪写法:
| 贪婪 | 非贪婪 |
|---|---|
* | *? |
+ | +? |
? | ?? |
{n,m} | {n,m}? |
最典型的使用场景
提取 HTML/XML 标签
const str = "<div>one</div><div>two</div>";
// 贪婪:吃太多
str.match(/<div>.*<\/div>/g);
// ["<div>one</div><div>two</div>"]
// 非贪婪:正确
str.match(/<div>.*?<\/div>/g);
// ["<div>one</div>", "<div>two</div>"]
提取括号内容
const str = "(hello) world (foo)";
// 贪婪
str.match(/\(.*\)/g);
// ["(hello) world (foo)"]
// 非贪婪
str.match(/\(.*?\)/g);
// ["(hello)", "(foo)"]
提取引号内容
const str = 'name="Alice" age="30"';
str.match(/".*?"/g);
// ['"Alice"', '"30"']
原理
正则引擎的回溯机制:
- 贪婪:先尽量多吃,发现后面匹配不上了,再往回退一个字符重试
- 非贪婪:先尽量少吃,发现后面匹配不上了,再多吃一个字符重试
两者都能达到最终匹配,但路径不同,性能也可能不同。
注意事项
非贪婪不一定更快
非贪婪意味着更多的”试探”。在某些场景下,贪婪反而更快——因为引擎一开始就能确定边界。
不能替代断言
非贪婪解决的是”吃多少”的问题,不是”位置条件”的问题。
如果要匹配”后面跟着 px 的数字”,应该用先行断言,而不是非贪婪:
// 非贪婪做不到这件事
// 正确做法:用断言
"12px".match(/\d+(?=px)/)[0]; // "12"
速查
// 贪婪:尽可能多吃
/a.*b/ // 最长匹配
/a.+b/ // 至少一个,最长
// 非贪婪:尽可能少吃
/a.*?b/ // 最短匹配
/a.+?b/ // 至少一个,最短