微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

正则指引之括号

分组

正则表达式中有一种使用括号()功能,叫分组。如果用量词限定出现次数的元素不是字符或者字符组,而是几个字符甚至表达式,就应该用括号将它们“分为一组”。比如,希望字符串ab重复出现一次一以,就应该写作(ab)+,此时(ab)成为一个整体,由量词+来限定;如果不用括号而直接写ab+,受+限定的就只有b。示例:

//用括号改变量词的作用
Stringtext="abab";
Patternp=Pattern.compile("(ab){1,}");
Matcherm=p.matcher(text);
System.out.println(m.matches());

多选结构

多选结构的形式是(...|...),在括号内以竖线 | 分隔开多个子表达式,这些子表达式也叫多选分支(option);在一个多选结构内,多选分支的数目没有限制。在匹配时,整个多选结构被视为单个元素只要其中某个子表达式能够匹配,整个多选结构的匹配就成功;如果所有子表达式都不能匹配,则整个多选结构匹配失败。示例:

//用多选结构匹配身份证号码
Stringtext="110101198001017016";
Patternp=Pattern.compile("([1-9]\\d{14}|[1-9]\\d{14}\\d{2}[0-9x])");
Matcherm=p.matcher(text);
System.out.println(m.matches());

关于多选结构,还要补充三点

第一,多选结构的一般表示法是 (option1 |option2)(其中option1和option2是两个作为多选分支的正则表达式),多选结构中一般会同时使用括号()和竖线|;但是如果没有括号(),只出现竖线 |,仍然是多选结构。因为竖线|的优先级很低,在不使用()时,要特别注意。如 ^ab|cd其实是(^ab|cd),而不是^(ab|cd)。我们推荐使用括号()。

第二多选分支并不等于字符组。多选分支看起来类似字符组,如[abc]能匹配的字符串和(a|b|c)一样。从理论上说,可以完全用多选结构来替换字符组,但这种做法不推荐。

第三,多选分支的排列是有讲究的。比如这个表达式(jeff|jeffrey),用它匹配jeffrey,结果到底是jeff还是jeffrey呢?这个问题并没有标准的答案。在java中,多选结构都会优先选择最左侧的分支。如例:

//多选结构的匹配顺序
Stringtext="jeffrey";
Patternp=Pattern.compile("(jeff|jeffrey)");
Matcherm=p.matcher(text);
if(m.find()){
System.out.println(m.group());//jeff
}

引用分组

括号不仅仅能把有联系的元素归扰起来并分组,还有其他的作用——使用括号之后,正则表达式会保存每个分组真正匹配的文本,等到匹配完成后,通过group(num)之类的方法“引用”分组在匹配时捕获的内容。其中,num表示对应括号的编号,括号分组的编号规则是从左向右计数,从1开始。因为“捕获”了文本,所以这种功能叫做捕获分组(capturing group)。对应的,这种括号叫做捕获型括号。

举个例子,我们经常遇到诸如 2010-12-22,2011-01-03 这类表示日期的字符串,希望从中提取出年,月,日之类的信息,就可以借助捕获分组来实现,如例:

//引用捕获分组
Stringtext="2015-01-22";
Patternp=Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcherm=p.matcher(text);
while(m.find()){
System.out.println(m.group());
System.out.println(m.group(1));
System.out.println(m.group(2));
System.out.println(m.group(3));
}

前面说过,num的编号从1开始。不过,也有编号为0的分组,它是认存在的,对应整个表达式匹配的文本。在许多语言中,如果调用group()方法,不给出参数num,认就等于调用group(0),比如Java。

有些正则表达式里可能包含嵌套的括号,比如:((\d{4})-(\d{2})-(\d{2})),除了能单独提取出年,月,日之外,再给整个表达式加上一重括号,就出现了嵌套括号,这时候括号的编号是怎样的呢?答案很简单:无论括号如何嵌套,分组的编号都是根据开括号出现顺序来计数的;开括号是从左向右数起第多少个开括号,整个括号分组的编号就是多少。只要记往:分组编号只取决于开括号出现的顺序。

反向引用:英文的不少单词中都有重叠出现的字母,比如shoot或beep,如果希望检查某个单词是否包含重叠出现的字母,该怎么办呢?这个问题有点复杂。“重叠出现”的字母,取决于第一个匹配结果,而不能预先设定。也就是说必须“知道”之前匹配的确切内容:如果前面匹配的是e,后面就只能匹配e;如果前面的匹配是o,后面就只能匹配o。

前面我们看到了引用分组,能引用某个分组内的子表达式匹配的文本,但引用都是在匹配完成后进行的能不能在正则表达式中引用呢?答案是可以的,这种功能被称作反向引用(back-reference),它允许在正则表达式内部引用之前的捕获分组匹配的文本(也就是左侧),其形式也是 \num,其中 num 表示所引用分组的编号,编号规则与之前介绍的相同

根据反向引用,查找连续重叠字母的表达式就是 ([a-z])\1,其中[a-z]匹配第一个字母,再用括号将匹配分组,然后用 \1 来反向引用,示例如下:

//反向引用
Stringtext="fooot";
Patternp=Pattern.compile("[a-z](([a-z])\\2\\2)[a-z]");
Matcherm=p.matcher(text);
while(m.find()){
System.out.println(m.group());//fooot
System.out.println(m.group(1));//ooo
}

关于反向引用,还有一点需要强调:反向引用重复的是对应捕获分组匹配的文本,而不是之前的表达式;也就是说,反向引用的是由之前表达式决定的具体文本,而不是符合某种规则的未知文本。这一点,新手常犯错误

各种引用的记法:根据前面的介绍,对分组的引用可能出现在三种场合:在匹配完成后,用group(num)之类的方法提取数据; 在进行正则表达式替换时,用 $num 引用;在正则表达式内部,用 \num 引用。不过,这只是Java语言中的规定,事情并不总是如此。下表中总结了各种常用语言中的两类记法

语言
表达式中的反向引用
替换中的反向引用
.NET
\num
$num
Java
\num
$num
JavaScript
$num
$num
PHP
\num
\num或$num
Python
\num
\num
Ruby
\num
\num

无论是 \num 还是 $num,都有可能遇到二义性的问题:如果出现了 \10 (或者$10),它到底表示第10个捕获分组 \10,还是第1个捕获分组 \1 之后跟着一个字符0?比如Java对\num中的num是这样规定的如果是一位数,则引用对应的捕获分组;如果是两位数且存在对应捕获分组时,引用对应的捕获分组,如果不存在对应的捕获分组,则引用一位数编号的捕获分组。也就是说,如果确实存在编号为10的捕获分组,则\10引用此捕获分组匹配的文本;否则,\10表示“第1个捕获分组匹配的文本”和“字符0”。替换中的反向引用示例:

//替换中的反向引用
Stringtext="abcdef45678ghijkl";
Patternp=Pattern.compile("([a-z]{2})([a-z]{2})([a-z]{2})");
Matcherm=p.matcher(text);
StringBuffersb=newStringBuffer();
while(m.find()){
/*
*将当前匹配子串(如:abcdef)替换为指定字符串($2,第二个捕获分组),
*并且将替换后的子串(如:cd)以及其之前到上次匹配子串之后的字符串(5678)
*添加一个StringBuffer对象里
*/
m.appendReplacement(sb,"$2");//反向引用第2个捕获分组
}
//将最后一次匹配工作后剩余的字符串添加一个StringBuffer对象里
m.appendTail(sb);
System.out.println(sb.toString());//替换后的结果:cd45678ij

命名分组: 捕获分组通常用数字编号来标识,但这样有几个问题:数字编号不够直观,虽然规则是“从左向右按照开括号出现的顺序计数”,但括号多了难免混淆;引用时也不够方便。为了解决这类问题,一些语言和工具提供了命名分组,可以将它看做另一种捕获分组,但是标识是容易记忆和辨别的名字,而不是数字编号。

值得注意的是,命名分组不是目前通行的功能,不同语言的记法也不同,下表总结了目前常见的用法

分组记法
表达式中的引用记法
替换时的引用记法
.NET
(?<name>...)
\k<name>
${name}
PHP
(?P<name>...)
(?P=name)
不支持
Python
(?P<name>...)
(?P=name)
\g<name>
Ruby
(?<name>...)
\k<name>
\k<name>

Java5和Java6都不支持命名分组,Java7开始支持命名分组,其记法与.NET相同。我用JDK8开发的示例:

//命名分组示例
Stringtext="abcdef45678ghijkl";
Patternp=Pattern.compile("(?<M1>[a-z]{2})(?<M2>[a-z]{2})(?<M3>[a-z]{2})");
Matcherm=p.matcher(text);
StringBuffersb=newStringBuffer();
while(m.find()){
/*
*将当前匹配子串(如:abcdef)替换为指定字符串(${M1},捕获分组M1),
*并且将替换后的子串(如:cd)以及其之前到上次匹配子串之后的字符串(5678)
*添加一个StringBuffer对象里
*/
m.appendReplacement(sb,"${M1}");//反向引用捕获分组M1
}
//将最后一次匹配工作后剩余的字符串添加一个StringBuffer对象里
m.appendTail(sb);
System.out.println(sb.toString());//替换后的结果:ab45678gh

非捕获分组

目前为止,总共介绍了括号的三种用途分组将相关的元素归拢到一起,构成单个元素多选结构规定可能出现的多个子表达式引用分组将子表达式匹配的文本存储起来,供之后引用。这三种用途并不是彼此独立的,而是互相重叠的:单纯的分组可以视为“只包含一个多选分支的多选结构”;整个多选结构也会被视为单个元素,可以由单个量词限定。最重要的是,无论是否需要引用分组,只要出现了括号,正则表达式在匹配时就会把括号内的子表达式存储起来,提供引用。如果并不需要引用,保存这些信息无疑会影响正则表达式的性能;如果表达式比较复杂,要处理的文本又很多,更可能严重影响性能

为了解决这种问题,正则表达式提供了非捕获分组(non-capturing group),非捕获分组类似普通的捕获分组,只是在开括号后紧跟一个问号冒号?:,这样的括号叫做非捕获型括号,它只能限定量词的作用范围,不捕获任何文本。在引用分组时,分组的编号同样会按开括号出现的顺序从左到右递增,只是必须以捕获分组为准,非捕获分组会略过,如下示例:

//非捕获分组的使用
Stringtext="abcdef45678ghijkl";
//这里使用了非捕获分组
Patternp=Pattern.compile("(?:[a-z]{2})([a-z]{2})([a-z]{2})");
Matcherm=p.matcher(text);
while(m.find()){
System.out.println(m.group());
System.out.println(m.group(1));//ab与gh都未捕获
}

非捕获分组不需要保存匹配的文本,整个表达式的效率也因此提高,但是看起来不如捕获分组美观,所以很多人不习惯这种记法。不过,如果只需要使用括号的分组或者多选结构的功能,而没有用到引用分组,则应当尽量使用非捕获型括号。

补充:转义

之前讲到,如果元字符是单个出现的,直接添加反斜线字符转义即可转义,所以 *,+,? 的转义形式分别是 \*,\+,\? 。如果元字符是成对出现的,则有可能只对第一个字符转义,比如 {6} 和 [a-z]的转义分别是 \{6} 和 \[a-z] 。括号的转义与它们都不同,与括号有关的所有三个元字符 (,),| 都必须转义。因为括号非常重要,所以无论是开括号还是闭括号,只要出现,正则表达式就会尝试寻找整个括号,如果只转义了开括号而没有转义闭括号,一般会报告“括号不匹配”的错误。另一方面,多选结构中的 |也必须转义(多选结构可以不用括号只出现|),所以,也不要忘记对 |的转义。

原文地址:https://www.jb51.cc/regex/360828.html

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐