正则(regex)
量词(Quantifiers)
在正则表达式中,* 和 + 都是量词(Quantifiers),它们决定了前面的字符或分组可以重复出现的次数。
它们的核心区别在于:“是否允许出现 0 次”。
| 符号 | 名称 | 匹配次数 | 是否必选 | 含义 |
|---|---|---|---|---|
* | 星号 (Asterisk) | 0 次或多次 | 可选 | 前面的内容可有可无,有的话可以有很多个。 |
+ | 加号 (Plus) | 1 次或多次 | 必选 | 前面的内容至少要出现一次,也可以有很多个。 |
如果想限制具体的次数,可以使用大括号:
{n}:正好出现n次。{n,}:至少出现n次。{n,m}:出现n到m次。
此外,?跟在前面的字符后代表可有可无。
等价符号
| . | 匹配除换行符(\n、\r)之外的任何单个字符。要匹配包括 ‘\n’ 在内的任何字符,请使用像"(.|\n)"的模式。 |
|---|---|
| \d | 匹配一个数字字符。等价于 [0-9]。 |
| \D | 匹配一个非数字字符。等价于 [ ^0-9]。 |
| \w | 匹配一个字母、数字、下划线。等价于[A-Za-z0-9_]。 |
| \W | 匹配非(字母、数字、下划线)。等价于 [ ^A-Za-z0-9_]。 |
| \s | 匹配任何空白字符,包括空格、制表符、换页符等等。等价于 [ \f\n\r\t\v]。 |
| \S | 匹配任何非空白字符。等价于 [ ^ \f\n\r\t\v]。 |
| \n | 匹配一个换行符。等价于 \x0a 和 \cJ。 |
| \r | 匹配一个回车符。等价于 \x0d 和 \cM。 |
| \t | 匹配一个制表符。等价于 \x09 和 \cI。 |
贪婪匹配与懒惰匹配
简单来说,**贪婪匹配(Greedy)和懒惰匹配(Lazy/Non-greedy)**的区别在于:面对多个可能的匹配结果时,是“能拿多少拿多少”还是“够了就行”。在正则表达式中,这两种模式直接影响你抓取数据的范围。
1. 贪婪匹配 (Greedy)
这是正则表达式的默认模式。
- 行为: 匹配器会尽可能多地吞掉字符,直到不符合条件为止。
- 常用符号:
*(0或多个)、+(1或多个)、{n,}。 - 例子: 假设我们要匹配
<div>内容1</div><div>内容2</div>中的 HTML 标签。- 正则:
<div>.*</div> - 匹配结果:
<div>内容1</div><div>内容2</div> - 解析: 它没有在第一个
</div>停止,而是直接匹配到了文档的最后一个</div>。
- 正则:
2. 懒惰匹配 (Lazy / Non-greedy)
- 行为: 匹配器在满足条件的前提下,尽可能少地匹配字符。一旦找到匹配项,立即停止。
- 实现方式: 在贪婪量词后面加一个问号
?。 - 常用符号:
*?、+?、??、{n,}?。 - 例子: 同样的 HTML 字符串。
- 正则:
<div>.*?</div> - 匹配结果:
<div>内容1</div>(以及接下来的<div>内容2</div>) - 解析: 碰到第一个符合条件的
</div>它就“收手”了。
- 正则:
核心区别对比
| 特性 | 贪婪匹配 (Greedy) | 懒惰匹配 (Lazy) |
|---|---|---|
| 性格 | 贪心,全都要 | 知足,够了就好 |
| 匹配长度 | 匹配最长的字符串 | 匹配最短的字符串 |
| 控制符 | *, +, {n,} | *?, +?, {n,}? |
| 回溯机制 | 从后往前找(先吞下全部再吐出来) | 从前往后找(吃一点看一点) |
举个直观的例子
字符串:"apple" "banana" "cherry"
- 贪婪模式
".*":匹配结果是整个字符串"apple" "banana" "cherry"。它认为最开头的"和最末尾的"才是这一对。 - 懒惰模式
".*?":匹配结果是三个独立的项:"apple"、"banana"和"cherry"。
特殊字符
**^:**匹配搜索字符串开始的位置。 如果标志中包括 m(多行搜索)字符,^ 还将匹配 \n 或 \r 后面的位置。
**$:**匹配搜索字符串结尾的位置。 如果标志中包括 m(多行搜索)字符,^ 还将匹配 \n 或 \r 前面的位置。
正则表达式 ^.{2,100}$ 匹配的是 任意长度在 2 到 100 个字符之间的字符串。
3. .*?http 与 .*http 的区别
这是理解贪婪和非贪婪匹配的直观例子。假设字符串是:
访问 http://first.example,然后继续访问 http://second.example
非贪婪:.*?http
.*? 会尽可能少地匹配字符。它从当前位置开始一点点扩展,直到后面的 http 第一次匹配成功:
正则:.*?http
结果:访问 http
↑
第一个 http
匹配过程可以简化为:
.*? 先匹配 0 个字符
↓ 后面的 http 匹配失败
.*? 再多匹配一个字符
↓ 继续尝试 http
...
↓ 到达第一个 http
http 匹配成功,立即停止
因此,.*?http 通常用于“从当前位置开始,截取到第一个 http 为止”的场景。需要注意,它匹配到的整体结果包含前面的字符和 http,并不只是 http 本身;如果只想提取关键字,应直接使用 http。
贪婪:.*http
.* 是贪婪量词,会先尽可能多地吞掉字符。它会先尝试匹配到字符串末尾,然后为了让后面的 http 成功,从末尾开始回溯,最终停在最后一个可以匹配的 http:
正则:.*http
结果:访问 http://first.example,然后继续访问 http
↑
最后一个 http
匹配过程可以简化为:
.* 先尽可能匹配到末尾
↓ 后面的 http 匹配失败
.* 向前回溯字符
↓ 继续尝试 http
...
↓ 回溯到最后一个 http
http 匹配成功,停止回溯
所以,在同一个字符串和同一个起始位置上,通常可以这样记忆:
| 正则 | 量词行为 | http 命中的位置 |
|---|---|---|
.*?http | 非贪婪,尽快满足后续条件 | 第一个 http |
.*http | 贪婪,先尽量扩展再回溯 | 最后一个 http |
这里的“第一个”和“最后一个”是指在同一次匹配、同一个起始位置和同一个文本范围内的结果。正则引擎是否跨越换行,还取决于点号的行为以及是否启用了 DOTALL、s 等模式。
与捕获组结合
如果希望只提取目标标记,可以把目标放在捕获组中:
正则:.*?(http)
捕获组:第一个 http
如果要匹配第一个 URL,可以使用更明确的字符范围,而不是让点号任意跨越内容:
正则:https?://[^\s]+
结果:http://... 或 https://...
.*? 适合快速表达“到第一个目标标记为止”,但在 HTML、日志或嵌套结构中,最好根据实际分隔符使用更精确的字符类;正则表达式并不能完整解析任意嵌套结构。
表达式
括号表达式
绝大多数在外部具有特殊意义的元字符,一旦进入方括号,都会自动失去其特殊魔力,变成一个普通的字面量字符。但也有一些字符需要转义:
\(反斜杠):它在任何地方几乎都需要转义,所以你会看到正则里写的是\\。^(脱字符):只有当它放在[的第一位时,它才代表“取反”。如果它在中间(如[a^b]),它就只代表普通字符。-(连字符):在[]内部表示范围(如a-z)。如果想匹配减号本身,得把它放在开头或结尾。](闭括号):如果你想在集合里包含闭括号,通常需要转义\],否则正则引擎会认为集合在这里结束了。