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