量词与贪婪模式:掌握匹配的“量”与“度”
正则里有个很实在的概念——量词。说白了,就是告诉引擎“前面的东西要出现几次”。
我刚开始学的时候,觉得这东西太简单了。不就是星号、加号、问号吗?直到在一次日志解析项目里,被贪婪匹配坑得体无完肤……嗯,从那以后我才真正重视起来。
量词分两种性格:贪婪和懒惰。默认都是贪婪的,这容易出问题。咱们得搞明白它们是怎么工作的。
基础量词一览
先列个表,把常用的量词看清楚。你想想看,这些符号你肯定见过,但用对了吗?
| 量词 | 含义 | 等价形式 |
|---|---|---|
* |
零次或多次 | {0,} |
+ |
一次或多次 | {1,} |
? |
零次或一次 | {0,1} |
{n} |
恰好 n 次 | 固定次数 |
{n,} |
至少 n 次 | 下限固定 |
{n,m} |
n 到 m 次 | 区间范围 |
我个人习惯:能用 {n,m} 的地方,尽量不用 * 或 +。显式写清楚范围,代码可读性高很多,也少出bug。
贪婪匹配:多拿多占
默认情况下,所有量词都是贪婪的。它会尽可能多地匹配字符。
看个例子:
文本:<div>内容1</div><div>内容2</div>
正则:<.*>
匹配结果:<div>内容1</div><div>内容2</div>
为什么匹配了整行?因为 .* 的贪婪本性,它会一直吞到最后一个 > 才停下。
我遇到过这种情况:写爬虫提取HTML标签时,用了 <.*>,结果把整个页面内容都匹配进去了。排查半天才发现是贪婪搞的鬼。
我曾经吃过的大亏:用 .* 匹配多行文本,没想到它不匹配换行符。后来改成 [\s\S]* 才解决问题。记住,. 默认不匹配 \n。
懒惰匹配:够用就行
在量词后面加个 ?,它就变懒惰了。能少匹配就少匹配。
还是刚才的例子:
文本:<div>内容1</div><div>内容2</div>
正则:<.*?>
匹配结果:<div>
看到了吗?.*? 在遇到第一个 > 就停了。这才是我们想要的效果。
为什么会这样?懒惰匹配每次扩展一个字符,发现满足条件就立刻返回。贪婪则相反,先吞下全部,再一个个吐出来检查。
核心记忆点:
- 贪婪:能拿100个,绝不拿99个
- 懒惰:给1个就要,绝不多拿
各种懒惰量词对照
懒惰量词就是在原有量词后面加 ?。我来列个表,一目了然:
| 贪婪版本 | 懒惰版本 | 含义 |
|---|---|---|
* |
*? |
尽可能少地匹配零次或多次 |
+ |
+? |
尽可能少地匹配一次或多次 |
? |
?? |
尽可能少地匹配零次或一次 |
{n,} |
{n,}? |
尽可能少地匹配至少 n 次 |
{n,m} |
{n,m}? |
尽可能少地匹配 n 到 m 次 |
你想想看,?? 这个写法挺有意思的。两个问号叠一起,第一个是量词,第二个是懒惰开关。记住这个规律就忘不了。
实战对比:文本提取的坑
来看一个常见的场景:从键值对文本中提取值。
文本:name=张三&age=25&city=北京
贪婪提取:name=(.*)&
匹配结果:name=张三&age=25&city=
用贪婪匹配,结果把从第一个 = 到最后一个 & 之间的内容全吞了。显然不对。
改成懒惰试试:
正则:name=(.*?)&
匹配结果:name=张三
正确!.*? 在遇到第一个 & 就停下了。
我建议:在提取结构化文本(如URL参数、JSON片段、XML标签)时,优先考虑懒惰匹配。不确定的时候,先用懒惰试,大概率是对的。
性能小贴士:别滥用懒惰
有一点要注意:懒惰匹配不一定比贪婪快。引擎需要频繁回溯,反复尝试。
场景不同,选择不同:
- 提取短内容:懒惰更直观,不容易出错
- 匹配长文本结尾:贪婪反而更高效,因为它一次到位
- 多层嵌套结构:都不好用,考虑用递归匹配或解析器
知识体系:量词与模式选择
下面这张图,帮你梳理清楚量词的核心逻辑:
避坑指南:量词使用三原则
总结几条实战经验,都是我踩过的坑:
- 明确边界:写量词前,确认你要匹配的内容边界在哪里。前后锚点(
^、$、\b)能帮你精确定位。 - 能用具体就不用模糊:如果知道要匹配3个数字,写
\d{3}比\d+好一万倍。 - 测试懒惰:不确定用贪婪还是懒惰?先用懒惰版测试。大部分内容提取场景,懒惰更符合直觉。
最后分享一个经验:我每次写完正则,都会用最短和最长两种文本测试。短的看它会不会漏,长的看它会不会吞。这样能快速发现贪婪/懒惰的问题。