分组捕获和分组引用

在基础正则中,使用括号可以对匹配的内容进行分组,这种行为称为分组捕获。捕获后可以通过\1这种反向引用方式去引用(访问)保存在分组中的匹配结果。

例如:

"abc11ddabc11" =~ /([a-z]*)([0-9]*)dd\1\2/;

在Perl中,还可以使用\gN的方式来反向引用分组。例如,和上面等价的几种写法:

"abc11ddabc11" =~ /([a-z]*)([0-9]*)dd\g1\g2/;
"abc11ddabc11" =~ /([a-z]*)([0-9]*)dd\g{1}\g{2}/;
"abc11ddabc11" =~ /([a-z]*)([0-9]*)dd\g{-2}\g{-1}/;

Perl还会把分组的内容放进Perl自带的特殊变量$1,$2,...,$N中,它们和\1,\2,...\N在匹配成功时的结果上没有区别,但是:

  • 反向引用\N可在正则表达式中使用,且只在正则匹配中有效,正则匹配结束后就消亡了
  • 分组变量$N不能在正则表达式中使用,它是Perl变量,在本次正则匹配结束后、下次正则匹配前可用

所以,可以使用$N的方式来输出分组匹配的结果:

"abc11ddabc11" =~ /([a-z]*)([0-9]*)dd\1\2/;
say "first group: $1";
say "second group: $2";

有几点需要注意:

  • 分组可能未捕获任何内容(比如那些允许匹配0次的量词),但整个匹配是成功的。这时$N引用分组时,得到的结果将是空串
  • 每次正则匹配都重置$N,因此$N在下一次正则匹配之前一直有效

例如:

# 分组没有捕获内容,但匹配是成功的,$1是空串
"abcde" =~ /([0-9]*)de/;
say "empty group: $1";

# 分组捕获到数值4,$1保存了4,
# 但下次正则匹配后,将重置$1
"abc4de" =~ /([0-9]*)de/;
say "group: $1";    # 4
"abcde" =~ /([0-9]*)de/;
say "empty group: $1";  # 空串

Perl更强大的分组捕获

Perl正则除了支持普通分组(也就是直接用括号的分组),还支持:

  • 命名分组(?<NAME>...):捕获后放进一个名为NAME的分组,正则表达式中可使用该名称来反向引用该分组内容,如\g{NAME}
  • 匿名分组(?:...):仅分组,不捕获,所以后面无法再引用这个捕获
  • 固化分组(?>...):一匹配成功就永不交还内容

匿名分组

匿名捕获是指仅分组,不捕获。因为不捕获,所以无法使用反向引用,也不会将分组结果赋值给$1这种特殊变量。

例如:

my $str = "xiaofang or longshuai";
if ($str =~ /(\w+) or (\w+)/){
  say "name1: $1, name2: $2";
}

但如果中间的or也可以换成and,为了同时满足and和or两种需求,使用模式/(\w+) (and|or) (\w+)/去匹配,但这时引用的序号就得由$2变为$3

my $str = "xiaofang or longshuai";
if ($str =~ /(\w+) (or|and) (\w+)/){
  say "name1: $1, name2: $3";
}

可使用匿名分组解决这样的问题:

my $str = "xiaofang or longshuai";
if ($str =~ /(\w+) (?:or|and) (\w+)/){
  say "name1: $1, name2: $2";
}

另外,Perl有一个n修饰符,它使得普通分组变成匿名分组,但对命名分组无效。且因为它是修饰符,它会使修饰范围内的所有普通分组都变成匿名分组。

my $str = "xiaofang or longshuai";
# 等价于:
# if ($str =~ /(?:\w+) (?:or|and) (?:\w+)/n){
if ($str =~ /(\w+) (or|and) (\w+)/n){
  say "name1: $1, name2: $2";  # 错,$1 $2不可用
}

命名分组

命名分组是指将捕获到的内容放进分组,这个分组是有名称的分组,后面可以使用分组名去引用已捕获进这个分组的内容。除此之外,和普通分组并无区别。

当要进行命名分组时,使用(?<NAME>)的方式替代普通分组的括号()即可。例如,要匹配abc并将其分组,以前普通分组的方式是(abc),如果将其放进命名为name1的分组中:(?<name1>abc)

当使用命名捕获的时候,要在正则内部引用这个命名捕获,除了可以使用序号类的绝对引用(如\1\g1\g{1}),还可以使用以下任意一种按名称的引用方式:\g{NAME} \k{NAME} \k<NAME> \k'NAME'

如果要在正则外部引用这个命名捕获,除了可以使用序号类的绝对应用(如$1),还可以使用$+{NAME}的方式。可使用后一种引用方式的原因是,Perl将命名捕获的内容放进了一个名为%+的特殊hash中。

例如:

my $str = "ma xiaofang or ma longshuai";
if ($str =~ /
            (?<firstname>\w+)\s  # firstname -> ma
            (?<name1>\w+)\s      # name1 -> xiaofang
            (?:or|and)\s         # group only, no capture
            \g1\s                # \g1 -> ma
            (?<name2>\w+)        # name2 -> longshuai
            /x){
    say "$1";
    say "$2";
    say "$3 ";
    # 或者指定名称来引用
    say "$+{firstname}\n$+{name1}\n$+{name2}";
}

固化分组

固化分组的用法是(?>PATTERN),它会将PATTERN匹配的内容吞掉且绝不交还,相当于是将其匹配的内容进行固化。它和占有优先匹配模式在行为上是类似的:占有之后不回溯。

另外,固化分组不是一种分组,因此无法去引用它吞掉的内容。

例如"hello world"可以被hel.* world成功匹配,但不能被hel(?>.*) world匹配。因为固化分组时,.*匹配后面所有内容并将其吞掉,这些内容一经匹配绝不交回。

但如果正则表达式是hel(?>.* world)将能匹配成功,即将原来分组外面的内容放进了分组内部。固化分组的内部是允许回溯的。

再例如:

my $str="ma longshuai gao xiaofang";
if($str =~ /ma (?>long.*)/){     # 成功
  say "matched";
}

if($str =~ /ma (?>long.*)gao/){   # 失败
  say "matched";
}

if($str =~ /ma (?>long.*gao)/){   # 成功
  say "matche";
}

if($str =~ /ma (?>long.*g)ao/){   # 成功
  say "matched";
}