本文主要介绍正则表达式中的分组、多选结构和引用分组, 补充 Possessive量词
把一个表达式用括号包围起来, 这个元素就是括号里的表达式, 括号内的表达式通常被称为子表达式。括号的这种功能称为分组。
实例: 使用正则匹配价格,小数最多为两位, 例如: ¥189.90 、 $ 49 、 88.00 和 121 等。
货币符号 ¥ 或 $ 都是可能出现也可能不出现,所以正则应该写成 [¥/$]?
, 具体价格之间还可能存在多个空格 所以使用 /s*
, 价格的小数部分可能出现也可能不出现, 使用括号包裹成一个子表达式 (/./d{1,2})?
。综上,价格的正则表达式应该写成 [/$¥]?/s*/d+(/./d{1,2})?
。
使用多选结构可以列出所有的可能结果, 只要其中的一个子表达式匹配上了整个多选结构的匹配就是成功了; 除非多选都匹配失败了, 那么这个多选结构就匹配失败。多选结构的形式是 (...|...)
,中间使用多个 | 分隔开多个子表达式。
实例: 匹配上小时(0-24)、分(00-60)和秒(00-60), 例如 00:01:59。
分析: 小时, 当小时为个位数时, 十位数上的 0 可以存着也可以不存在;当十位数为 1 时, 个位数可以匹配所有的 0-9; 十位数为 2时, 个位数上可以匹配 0-4之间的数, 所以小时的正则可以写作 (0?/d|1/d|2[0-4])
; 同理可以推断出 分或秒的正则表达式为 (0?/d|[1-5]/d|60)
。
注意:
多选结构一般的表达式是 (option1|option2)
, 还有一种写法 option1|option2
。但是第二种形式容易混淆, 一般不采用第二种写法(详细请见本文的优先级部分内容)。
多选分支并不等于字符组。字符组比多选结构看起来要简洁的多, 而且多选结构不支持 - 范围表达式。一般情况下, 能用字符组解决的问题, 就不使用多选分支结构来处理。
多选分支的排序是有讲究的。大部分语言的多选结构都会优先选择最左侧的分支。如果多选分支的排序不合理会存在重复匹配,这样会增大回溯的计算量。所以多选分支的排序是需要好好研究的。
关于转义: 无论分组还是多选结构, 使用普通字符时需要全部转义也就是说 (|)
对应的转义为 /( /| /)
。
上文使用括号后, 正则表达式会保存每个分组真正匹配的文本, 等匹配结束以后, 可以通过 group(num) 之类的方法获取扥组在匹配时捕获的内容, 这种功能叫做捕获分组。num 对应分组的编号, 分组编号从 1 开始, 默认存在 编号 0 匹配整个表达式。
注意: 无论括号嵌套多少层, 分组的编号是根据开括号出现的顺序来计数的, 开括号从左向右暑期第多少个开括号, 整个括号分组的编号就是多少。
引用分组还可以应用于替换。Java 、 JavaScript 替换的引用编号写法是 $num
, 而 Python 的写法确是 /num
, 不同语言请注意区别。
注意: 像 Python 一样使用 /num
形式来进行正则替换的语言, 不用使用 /0, 因为 /0 开头的转义序列通常表示八进制形式的字符。
在正则表达式内部使用引用之前的捕获分组匹配的文本, 其形如 /num 其中 num 为分组的编号, 规则同之前介绍的相同, 这种结构称为反向引用。
注意:
反向引用重复的是对应捕获分组捕获的文本, 而不是之前的表达式。
捕获的二义性: /10
(或 $10
) 是表示第十个捕获分组还是第一个捕获分组和 0 ?Python 中提供 /g<num>
表示法, /10
表示第十个捕获分组, /g<1>0
表示第一个分组和 0 。Java 和 JavaScript 规定 /num
如果是一位数则对应捕获分组; 如果是两位数且存在对应捕获分组则对应捕获分组; 如果为两位数且不存在对应引用分组则为一位数编号的捕获分组。
像 Java 中如果两位数且存在对应捕获分组就无法使用 /10
表示第一个捕获分组和 0 。为了解决这个可以使用命名分组。由于兼容性问题,使用命名分组对原先的编号规则没有影响。
Python的命名分组形式为 (?p<name>regex)
。在表达式中使用反向引用, 必须使用 (?p=name)
的形式; 进行替换是使用 /g<name>
形式。
Java 7 开始支持命名分组, Java 的命名分组形式是 (?<name>...)
, 表达式中反向引用形式为 /k<name>
, 替换时使用 ${name}
。
JavaScript 从 ES2017 开始支持命名分组, JavaScript 的命名分组形式是 (?<name>...)
, 表达式中反向引用形式为 /k<name>
, 替换时使用 $<name>
。
前文介绍的分组和多选结构都可以被引用。引用会严重影响性能。为了减少对性能的影响引入了非捕获组。形式为 (?:...)
。
如果只需要使用括号的分组或者多选结构的功能, 而没有用到引用分组, 则应该使用非捕获组。将捕获组括号修改为非捕获组括号后, 引用分组对应的编号可能也要调整, 这点要注意。
之前在介绍量词时遗漏 Possessive 量词。量词内容详见:正则笔记之量词 。
Possessive 量词 类似懒惰量词。它通过检查整个字符串来只是启动引擎。如果匹配失败不会进行回溯。
量词 | 说明 |
---|---|
{n}+ | 之前的元素必须出现 n 次 |
{m,n}+ | 之前的元素至少出现 m 次, 至多出现 n 次 |
{m,}+ | 之前的元素至少出现 m 次, 出现次数无上限 |
{0,n}+ | 之前的元素可以不出现, 也可以出现, 最多出现 n 次 |
*+ | 等价于 {0,} |
++ | 等价于 {1,} |
?+ | 等价于 {0,1} |
优先级 | 组合 | 说明 |
---|---|---|
1 | (regex) | 真个括号内的子表达式称为单个元素 |
2 | * ? + | 量词限定之前紧邻的元素 |
3 | abc | 普通拼接, 元素相继出现 |
4 | a | bc |
注: 数字越小, 优先级越高
实例:
多选结构的括号可以省略, 但是容易混淆。比如 : ^(ab|cd)$
和 ^ab|cd$
第一眼很大概率会以为这两个正则表达的意思相同, 但是后者实际上等价于 (^ab|cd$)
。所以多选结构中括号一定不能省略。如果不需要捕获文本, 应当把普通括号 (...)
改为 非捕获括号 (?:...)
。