正则表达式——分组与捕获

前言

之前使用正则表达式的时候大多数状况下只是用用匹配表达式,为什么说是匹配表达式呢,因为其实替换文本也可以使用表达式,我称之为替换表达式。

因为通常使用正则表达式的大多会是以下情况,比如把一段文本中的A字符串更改为B字符串:

  1. 待匹配文本:ABCDABCD
    匹配表达式:B替换文本:E
    替换结果:AECDAECD

其实替换文本这里也可以用表达式的形式,这样的方式会更加方便:

  1. 待匹配文本:ABCDABCD
    匹配表达式:(B)替换表达式:2\1替换结果:A2BCDA2BCD

这里面涉及到的是正则表达式的分组与捕获的知识,接下来我就详细的介绍一下。

分组

分组的引入

对于要重复单个字符,非常简单,直接在字符后卖弄加上限定符即可,例如 a+ 表示匹配1个或一个以上的a,a?表示匹配0个或1个a。这些限定符如下所示:

X ?

X ,一次或一次也没有

X *

X ,零次或多次

X +

X ,一次或多次

X { n }

X ,恰好 n

X { n ,}

X ,至少 n

X { n , m }

X ,至少 n 次,但是不超过 m

但是我们如果要对多个字符进行重复怎么办呢?此时我们就要用到分组,我们可以使用小括号()来指定要重复的子表达式,然后对这个子表达式进行重复,例如:(abc)? 表示0个或1个abc 这里一 个括号的表达式就表示一个分组 。分组可以分为两种形式,捕获组和非捕获组。

捕获

捕获组

捕获组可以通过从左到右计算其开括号来编号 。例如,在表达式(A)(B(C)) 中,存在四个这样的组:

0

(A)(B(C))

1

(A)

2

(B(C))

3

(C)

0始终代表整个表达式

之所以这样命名捕获组是因为在匹配中,保存了与这些组匹配的输入序列的每个子序列。捕获的子序列稍后可以通过 Back 引用(反向引用) 在表达式中使用,也可以在匹配操作完成后从匹配器检索。

Back 引用 是说在后面的表达式中我们可以使用组的编号来引用前面的表达式所捕获到的文本序列。注意:反向引用,引用的是前面捕获组中的文本而不是正则,也就是说反向引用处匹配的文本应和前面捕获组中的文本相同,这一点很重要。

【例】(["']).*\1

其中使用了分组,\1就是对引号这个分组的引用,它匹配包含在两个引号或者两个单引号中的所有字符串,如,”abc” 或 ” ’ ” 或 ’ ” ’ ,但是请注意,它并不会对” a’或者 ‘a”匹配。原因上面已经说明,Back引用只是引用文本而不是表达式。

非捕获组

以 (?) 开头的组是纯的非捕获 组,它不捕获文本 ,也不针对组合计进行计数。就是说,如果小括号中以?号开头,那么这个分组就不会捕获文本,当然也不会有组的编号,因此也不存在Back 引用。

我们通过捕获组就能够得到我们想要匹配的内容了,那为什么还要有非捕获组呢?原因是捕获组捕获的内容是被存储在内存中,可供以后使用,比如反向引用就是引用的内存中存储的捕获组中捕获的内容。而非捕获组则不会捕获文本,也不会将它匹配到的内容单独分组来放到内存中。所以,使用非捕获组较使用捕获组更节省内存。在实际情况中我们要酌情选用。

(?:Pattern)

它的作用就是匹配Pattern字符,好处就是不捕获文本,不将匹配到的字符存储到内存中,从而节省内存。

【例】匹配indestry或者indestries

我们可以使用indestr(y|ies)或者indestr(?:y|ies)

【例】(?:a|A)123(?:b)可以匹配a123b或者A123b

零宽度断言

(?= X )

X* ,通过零宽度的正 lookahead

(?! X )

  • X* ,通过零宽度的负 lookahead

(?<= X )

  • X* ,通过零宽度的正 lookbehind

(?X )

  • X* ,通过零宽度的负 lookbehind

这四个非捕获组用于匹配表达式X,但是不包含表达式的文本。

(?=X )

零宽度正先行断言。仅当子表达式 X 在 此位置的右侧匹配时才继续匹配。也就是说要使此零宽度断言起到我们想要的效果的话,就必须把这个非捕获组放在整个表达式的右侧。例如,/w+(?=/d) 与后跟数字的单词匹配,而不与该数字匹配。此构造不会回溯。

(?!X)

零宽度负先行断言。仅当子表达式 X 不在 此位置的右侧匹配时才继续匹配。例如,例如,/w+(?!/d) 与后不跟数字的单词匹配,而不与该数字匹配 。

(?<=X)

零宽度正后发断言。仅当子表达式 X 在 此位置的左侧匹配时才继续匹配。例如,(?<=19)99 与跟在 19 后面的 99 的实例匹配。此构造不会回溯。

(?

零宽度负后发断言。仅当子表达式 X 不在此位置的左侧匹配时才继续匹配。例如,(?

上面都是理论性的介绍,这里就使用一些例子来说明一下问题:

【例1】正则表达式(?<\!4)56(?=9)

含义:匹配后面的文本56前面不能是4,后面必须是9组成。因此,可以匹配如下文本 5569 ,与4569不匹配。

【例2】提取字符串 da12bka3434bdca4343bdca234bm中包含在字符a和b之间的数字,但是这个a之前的字符不能是c;b后面的字符必须是d才能提取。

显然,这里就只有3434这个数字满足要求。那么我们怎么提取呢?

首先,我们写出含有捕获组的正则表达式:[^c]a\d*bd

然后我们再将其变为非捕获组的正则表达式:(?<=[^c]a)\d*(?=bd)

模式修正符

以(?)开头的非捕获组除了零宽度断言之外,还有模式修正符。

正则表达式中常用的模式修正符有i、g、m、s、x、e等。它们之间可以组合搭配使用。

(?imnsx-imnsx: )应用或禁用子表达式中指定的选项。例如,(?i-s: )将打开不区分大小写并禁用单行模式。关闭不区分大小写的开关可以使用(?-i)。有关更多信息,请参阅正则表达式选项。

【例1】(?i)ab

表示对(?i)后的所有字符都开启不区分大小写的开关。故它可以匹配ab、aB、Ab、AB

【例2】(?i:a)b

它表示只对a开启不区分大小写的开关。故它可以匹配ab和Ab。不能匹配aB和AB。

(?>Pattern)

等同于侵占模式。

配成功不进行回溯,这个比较复杂,与侵占量词“+”可以通用,比如:\d++可以写为(?>\d+)

【例】将一些多位的小数截短到三位小数:\d+\.\d\d[1-9]?\d+

在这种条件下 6.625 能进行匹配,这样做没有必要,因为它本身就是三位小数。最后一个“5”本来是给 [1-9] 匹配的,但是后面还有一个 \d+ 所以,[1-9] 由于是“?”可以不匹配所以只能放弃当前的匹配,将这个“5”送给 \d+ 去匹配,如果改为:

\d+\.\d\d[1-9]?+\d+

的侵占形式,在“5”匹配到 [1-9] 时,由于是侵占式的,所以不会进行回溯,后面的 \d+ 就匹配不到任东西了,所以导致 6.625 匹配失败。

这种情况,在替换时就有效了,比如把数字截短到小数点后三位,如果正好是三位小数的,就可以不用替换了,可以提高效率,侵占量词基本上就是用来提高匹配效率的。

\d+\.\d\d[1-9]?+\d+ 改为 \d+\.\d\d(?>[1-9]?)\d+ 这样是一样的。