分组
正则表达式中有一种使用括号()的功能,叫分组。如果用量词限定出现次数的元素不是字符或者字符组,而是几个字符甚至表达式,就应该用括号将它们“分为一组”。比如,希望字符串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] 。括号的转义与它们都不同,与括号有关的所有三个元字符 (,),| 都必须转义。因为括号非常重要,所以无论是开括号还是闭括号,只要出现,正则表达式就会尝试寻找整个括号,如果只转义了开括号而没有转义闭括号,一般会报告“括号不匹配”的错误。另一方面,多选结构中的 |也必须转义(多选结构可以不用括号只出现|),所以,也不要忘记对 |的转义。