Perl学习笔记10——正则表达式(下)

捕获组与反向引用

在正则表达式中,圆括号的一大作用是进行模式分组,而其还有另一个非常重要的作用,即定义捕获组。捕获组是由捕获圆括号构建的模式分组,其中的内容可以被正则表达式捕获并进行反向引用。反向引用的意思即引用某捕获组中已经捕获到的某些内容,其简单写法为反斜线加捕获组编号。例:

/(..)\1/    #使用圆括号构造一个捕获组,并反向引用

上例中,圆括号构建了一个捕获组,首先由通配符进行匹配,如果匹配到了ab,那么反向引用的字符就又是ab,即这个模式匹配abab。类似的,如果通配符匹配到了bb,那么模式就匹配bbbb。即反向引用的作用就是再次重复其对应捕获组中匹配到的全部内容。反向引用无需紧跟其对应捕获组,而是可以用在任何地方。

捕获组的编号较为简单,只需要从左向右根据左括号的次序来编号即可。下例为一个较复杂的捕获组:

/((a)(b)\3\2)\1/    #通过数左括号来确定捕获组编号

上例中,通过数左括号可知,最外围的是1号捕获组,a和b所在的两个捕获组分别是2、3号捕获组。所以这样的反向引用可以匹配abbaabba类型的字符串。

\g{N}写法与相对反向引用

在上文的反向引用写法中,由于捕获组编号没有任何的界限标识,所以很容易出现歧义。例:

/(.)\11/    #引起歧义的捕获组

很明显,这里反向引用的是\1,但由于Perl的最长且合理逻辑,这里的代码会被认为是反向引用\11,这就是由于捕获组编号没有任何界限标识所导致的歧义。从Perl 5.10开始,可以使用一种新的写法来进行反向引用,即\g{N}写法,花括号内书写的是捕获组编号,从而使捕获组编号具有了界限标识,消除了可能产生的歧义。例:

/(.)\g{1}1/    #消除了花括号内的捕获组编号1与后面的字符1之间的歧义

使用\g{N}写法后,还可以使用一种新的捕获组编号方式来进行反向引用,即相对反向引用。相对反向引用均使用负数作为捕获组编号,其编号方法不再是以从左向右数左括号的方式进行的绝对编号,而是从相对反向引用位点开始向前数捕获组左括号,捕获组与相对反向引用位点之间的距离就是该捕获组的编号。例:

/(.)(.)\g{-1}\g{-2}/    #第一个相对反向引用对应的捕获组就是其左边的组,第二个对应的是从其位点向左数第二个组,即从左数第一个组,可匹配abba类型字符串

这种根据引用位点进行的相对编号较之于绝对编号,其最大的优点就在于不太容易因捕获组的数量和位置变动而导致所有的捕获组编号全部改变。所以在实际的应用中,应根据实际需求选择合适的编号及反向引用方式。如下例:

/(.)(.)\2\1/=> /(.)(.)(.)\3\2/               #使用绝对编号的情形,当增加一个捕获组时,1、2号捕获组编号全部改变

/(.)(.)\g{-1}\g{-2}/=> /(.)(.)(.)\g{-1}\g{-2}/    #使用相对编号的情形,由于增加捕获组并没有改变引用位点与捕获组的相对距离,所以编号不改变

捕获变量

使用捕获组捕获到的内容不仅可以用于反向引用,其还会被保存到捕获变量中,从而可以对这些捕获变量进行更高级的操作。捕获变量都是标量变量,在默认情况下,其以数字进行命名,数字就是该捕获组的绝对编号,这种数字变量名称是Perl中的一类特殊的变量名。例:

print$1 if/(.)/;    #如果匹配成功,就输出捕获变量$1的内容,也就是模式中绝对编号为1的捕获组捕获到的内容

捕获变量可以对一个字符串中的数据进行精确定位、筛选、提取、修改等,是正则表达式的核心内容之一。例:

s/(.*)/\U$1/;    #匹配任意字符,并使用s///与\U将捕获到的内容全部转为大写,这样的操作类似于uc函数

捕获变量的存续期

通常情况下,捕获变量的值能够存续到下一次成功匹配为止。也就是说,一次成功的匹配会将所有捕获变量的值重置为新值,而失败的匹配不会改变捕获变量的值,其值仍然是上一次成功匹配时得到的。下例为一个示例程序:

/(a)/;/(b)/;    #假设/(a)/匹配成功,那么当/(b)/又一次匹配成功后,$1的值就会被重置为b,否则其值仍然为a

需要注意的是,当下一次匹配成功时,之前的所有捕获变量都会被重置,而不单单是在新的匹配中用到的捕获变量。例:

/(.)(.)/;

/(.)/;    #如果匹配成功,那么上一次匹配中的$1和$2都会被重置,$1会被重置为新值,而$2会被重置为undef

命名捕获

每一个捕获组都有其自己独立的名称,而事实上,捕获组的绝对编号就是该捕获组的默认名称。而这种默认名称由于使用的是绝对编号,所以在使用时存在种种不便。从Perl 5.10开始,可以对捕获组进行命名,使每一个捕获组拥有其独立的,且不受其他捕获组影响的名称,从而避免了使用绝对编号所带来的不便。

命名捕获使用特殊哈希“%+”来存储捕获组的名称与其捕获到的内容。捕获组名就是%+中的各个哈希键,而其对应的值就是各个捕获组捕获到的内容。将“?<…>”加在捕获组前括号之后即可对捕获组命名,尖括号中书写的就是该捕获组的名称。例:

/(?.*)/    #对捕获组(.*)命名

命名捕获用到了问号在正则表达式中的第三种用法,而前两种分别为表示问号本身和作为量词使用,下文中还会出现问号的另外两种用法。

当提取一个已命名的捕获组捕获到的内容时,根据其哈希键,即该捕获组的名称提取对应的哈希值即可。例:

print”$+{a} $+{b}” if /(?.)(?.)/;    #如果匹配成功,就输出两个捕获组捕获到的内容,即存储在%+中的两个哈希值

对捕获组进行命名后,捕获组的名称就由绝对编号变为了其独立的捕获组名,所以在反向引用的\g{N}写法中,原先使用的绝对编号也要变为相应的捕获组名(仍然使用绝对编号也可以,但是这样无意义)。例:

/(?.)\g{word}/   #对命名的捕获组进行反向引用

命名后的捕获组由于使用了独立的捕获组名,在反向引用时便不再受其他捕获组变动造成的影响,从而使代码更加易于维护。下例仅为一个示例:

/(?.)\g{name}/ =>/((.)(?.))\g{name}/    #在其他捕获组发生变动后,反向引用仍然使用该捕获组独立的名称

需要注意的是,命名捕获并不会改变捕获变量的存续期,其存续期与使用标量变量的情况是一致的。当下一次匹配成功时,用于存储捕获组键值对的特殊哈希%+会被整个重置为新哈希,其中存储了新的键值对。下例可用于验证:

print keys%+ if /(?a.)/;    #如果匹配成功,那么键列表就是(a)

print keys%+ if /(?b.)/;    #如果再一次匹配成功,那么%+会被整个重置为新哈希,其键列表为(b)

非捕获组

到目前为止,在代码中使用的所有圆括号都是捕获圆括号,即这种圆括号的作用不仅是进行模式分组,还会同时构造一个捕获组,用于捕获匹配到的内容。

在正则表达式中,问号的第四种用法就是构造不捕获圆括号,而这种由不捕圆括号构建的模式分组就称为非捕获组。不捕获圆括号相对于普通的圆括号,其功能为进行纯粹的模式分组而不会构建捕获组。非捕获组可以用在许多仅需要模式分组而不对其进行捕获的情况中。将“?:”书写在捕获组前括号之后即可表示一对非捕获圆括号。例:

print$1 if/(?:.)+(.)/;    #第一对圆括号构建的是非捕获组,只有分组功能而不参与捕获,所以后面的圆括号才是1号捕获组,其捕获到的内容存储于$1中

由上例可见,非捕获组可以安全地被添加到现有模式中而不会影响其他捕获组的编号,这对于模式的修改与维护是十分方便的。

自动捕获变量

Perl中自带有三个自动捕获变量,可以对模式进行自动捕获,其使用时无需捕获圆括号,且其所有性质,如捕获变量的存续期等,均与普通的捕获变量完全一致。这三个自动捕获变量分别为“$&”、“$`”和“$’”。第一个自动捕获变量存储的是整个正则表达式在原字符串中匹配到的区段,第二个和第三个自动捕获变量存储的分别是原字符串中匹配区段前与匹配区段后的所有内容,即正则表达式在原字符串中找到匹配区段之前略过的部分与匹配后剩余的部分。如果将这三个变量的内容连接起来,就一定能得到与进行匹配的原字符串一模一样的一个字符串。例:

$_=’abc’;

print”($`)($&)($’)” if /b/;    #输出(a)(b)(c),分别对应了这三个变量中的内容

绝对首尾锚位

在默认情况下,正则表达式会从给定字符串的开头开始匹配,如果匹配失败,就不断向后顺移一个字符继续匹配,直到匹配成功为止。而锚位可以让正则表达式仅在字符串的某一个固定位置进行匹配,如字符串的开头、末尾或中间的某一个单词。

\A锚位一般书写在模式的开头,表示后面的模式只匹配给定字符串的绝对开头。例:

$_=’abba’;

print if /\Ab/;    #模式只能在$_的开头匹配b而不会向后顺移,所以这里匹配失败

\z锚位的用法与\A锚位类似,其一般书写在模式的最后,表示这里需要锚位到字符串的绝对末尾。为方便起见,还有一个类似于\z的\Z锚位,其可以进行忽略末尾换行符的末尾锚位,即如果字符串末尾有换行符,就会被\Z忽略,从而将锚位定位到换行符之前的那个字符。由于读取的文件内容的末尾常含有换行符,所以这种锚位很方便且常用。例:

$_=”ab\n”;

print if /b\z/;    #绝对末尾锚位,只在$_的末尾匹配b,而由于其末尾是换行符,所以匹配失败

print if /b\Z/;    #忽略换行符的末尾锚位,匹配成功

行首尾锚位

如果一个字符串的内容较长,且其中间有若干换行符将其内容分为多行,那么此时就可以对这个字符串每一行的首尾进行锚位,即行首锚位和行尾锚位。

在Perl 4中,“^”和“$”相当于上文中的\A与\z,并且这两个符号现在也可以发挥同样的功能,但这是不安全也不推荐的写法。在Perl 5中,这两个符号一般用于行首尾锚位,其需要与/m修饰符连用。/m修饰符的作用相当于一个开关,用来打开行首尾锚位功能。此功能打开后,^与$就能对一个字符串中每一行的开头和末尾进行锚位,而不像\A与\z那样只能对一个字符串的绝对开头和末尾进行锚位。进行行末尾锚位时,$会忽略行尾换行符。例:

$_=”ab\ncd\n”;     #包含两行的字符串,第一行为ab\n,第二行为cd\n

print’Z’ if /b\Z/;     #进行忽略换行符的末尾锚位,只能匹配d,匹配失败

print’$’ if /b$/m;    #使用/m修饰符打开行首尾锚位功能,并使用$进行行末锚位。所以会匹配到第一行行末的b

单词锚位

锚位并不局限于整个字符串的首尾,其也可以是字符串中某一个单词的首尾。这里的单词指的是一组由字符集[\w]组成的字符串,单词锚位就是对这些字符串的首尾,即单词边界进行的锚位。

单词由连续的\w字符组成,所以任何位于[\W]字符集中的字符,外加一个字符串的绝对开头和绝对末尾,都可以作为单词边界。每一个单词的两侧都必定各有一个边界,即在一个字符串中,不存在没有开头或没有结尾的一个单词,所以一个字符串中的单词边界一定是偶数个,且至少为两个。

单词锚位分为两种,单词边界锚位“\b”和非单词边界锚位“\B”。\b可以书写在模式字符串中某一个单词的开头、结尾或两边都书写,用来限定匹配单词的首尾。例:

$_=”a-bc-d”;     #包含三个单词的字符串,a、bc和d

print if/\bb\b/;    #同时锚位单词b的两个边界,所以只能匹配单词b,匹配失败

print if/\bb/;      #锚位b作为单词开头,即匹配的单词必须以b开头,能够匹配b、bc等,匹配成功

print if/b\b/;      #锚位b作为单词结尾,即匹配的单词必须以b结尾,能够匹配b、ab等,匹配失败

非单词边界锚位\B的用法和\b一致,但其功能与\b完全相反。\B进行的是不能成为单词边界的锚位,即被锚位的模式字符串不能作为单词边界,而只能作为某一单词的中间部分达成匹配。例:

$_=”a-bc-d”;     #包含三个单词的字符串

print if/\Bb\B/;    #b不能作为两个单词边界,可匹配abc、bbb(第二个b匹配)等,匹配失败

print if/\Bb/;      #b不能作为单词开头,可匹配abc、ab等,但不能匹配bc等,匹配失败

print if/b\B/;      #b不能作为单词结尾,可匹配bc、bb(第一个b匹配)等,但不能匹配ab等,匹配成功

优先级

正则表达式中有许多的元字符,如同操作符一样,这些元字符也存在优先级。正则表达式的优先级表分为五个等级,大致内容如下:

最高优先级的为圆括号,包括具有各种功能的圆括号,如捕获组,非捕获组,命名捕获组等。括号的最高优先级保证了其能够进行分组、捕获等重要操作而不受其他元字符的影响。

第二优先级的为量词,包括三个符号量词与通用量词。量词紧密地与其前面的分组或单个元素相连,从而进行重复。

第三级为锚位和序列。锚位用于固定匹配位置,而序列指的是单个字符与字符之间的连接,如abc这三个字符之间的连接。也就是说,锚位也可以看作是一种特殊的序列,锚位与字符之间的连接(如\babc\b中\b与a、c与\b之间)与序列中字符之间的连接(如\babc\b中ab、bc之间),其紧密程度是同级的。

第四级为择一竖线“|”。这是一个优先级很低的符号,比较重要的意义在于其优先级低于序列,这样就可以使一个单词作为一个整体参与择一匹配,而不会被择一竖线拆成单个字符。如择一匹配:/ab|cd/,由于序列的优先级更高,所以是ab与cd而不是b与c参与了择一匹配,而如果加上括号:/a(b|c)d/,由于括号的优先级高于序列,从而使得b与c参与了择一匹配。

最低优先级的称为原子。原子构成了正则表达式中大多数最基本的内容,也可以理解为除了上述四个优先级中出现的内容,其他部分均属于原子。原子主要包括字符、字符集、通配符以及反向引用等。

split操作符

split操作符是一个使用到了正则表达式的操作符,其可以根据给定模式将一个字符串进行拆分,并返回拆分后形成的列表。

split操作符包含两个参数,第一参数为一个正则表达式,又称为拆分模式,第二参数为需要拆分的字符串。拆分模式会在指定字符串的开头不断向后进行匹配,每一次匹配成功,该处就会成为当前拆分字段的结尾与下一拆分字段的开头,而整个匹配区段会作为拆分位点而被去除,所以在最终拆分的结果中不会出现任何匹配拆分模式的区段。例:

$n=’a;b;c’;

@m=split/;/,$n;    #以分号作为模式拆分$n的内容,所有的分号都将作为拆分位点而被去除。拆分结果为(a,b,c)

如果省略split的第二参数,则其默认对$_中的内容进行操作。而如果省略其全部参数,则就相当于split/\s+/,$_;,这是一种很常见的用法,可以匹配至少一个连续空白符并将其作为$_中内容的拆分位点。例:

@m=split/;/;    #相当于split/;/,$_;

print split;      #相当于split/\s+/,$_;

除了/\s+/拆分模式,split还有一个很常用的拆分模式,即空字符串拆分模式。由于空字符串可以匹配任意两个字符之间的间隙,所以这样的操作会将整个字符串中的每一个字符全部拆分,返回由许多单个字符组成的列表。而这种拆分模式属于split的特殊用法之一,在书写上可以省略拆分模式的定界符。例:

$_=’abcd’;

@n=split”;    #以空字符串拆分$_中的内容。拆分结果为(a,b,c,d)

split操作符在使用时需要注意两点,出现这两点的情况都比较罕见,所以不会构成太大的影响。第一:如果spilt的拆分模式匹配到一个以上连续的拆分位点,那么在返回的列表中就会出现空字符串,空字符串的数量为这些拆分位点的数量减一,且如果这些拆分位点位于字符串开头,那么空字符串的数量就是拆分位点的数量,而如果这些拆分位点位于字符串末尾,那么由其形成的空字符串就会被全部舍弃。下例为一个示例:

$_=’,,,a,,,b,,,’;

$n=split/,/;    #最终返回的列表为(”,”,”,’a’,”,”,’b’)

第二:在拆分模式中需要避免使用捕获圆括号,而应使用不捕获圆括号,否则会出现问题。由于拆分模式往往都十分简单,所以这种情况很罕见。

join函数

上文中的split操作符可以用于拆分字符串并返回列表,而join函数的功能与其相反,可以将一个列表中的所有元素都连接在一起,并返回一个标量。join函数也包含两个参数,第一参数又称为胶水,其可以是任意的一个字符串,胶水会被涂在每两个列表元素之间,从而连接所有的列表元素,第二参数是需要进行连接的列表。例:

print join’-‘,1..5;    #以“-”作为胶水连接参数列表,返回连接后的字符串并输出,输出结果为1-2-3-4-5

由上例可见,胶水只会出现在每两个相邻列表元素之间,所以胶水的数量总是比列表元素的数量少一,这也意味着进行连接的列表元素至少需要两个,否则不会有任何效果。

常用的一种胶水为空字符串,使用其作为胶水就可以无缝连接列表元素,例:

print join”,1..5;    #使用空字符串作为胶水无缝连接列表元素,输出12345

join函数也可以与split连用,如下例:

$_=’a-b-c-d’;

print join”,split/-/;    #以split的返回值作为join的第二参数。首先以“-”作为拆分位点,将$_拆分为列表(a,b,c,d),再使用join对其进行无缝连接,最终输出abcd

列表上下文中的m//与全局匹配

m//一般用于布尔上下文中表示匹配结果,而在列表上下文中,m//也同样能发挥许多强大的功能。在列表上下文中使用m//时,如果匹配成功,那么其返回的是模式中所有捕获变量组成的列表,而如果匹配失败,则会返回空列表。而由于捕获变量在实际运算时会被替换为变量当前值,所以m//返回的内容也可以理解为就是每一个捕获组捕获到的内容形成的列表。例:

($n)=/(.)/;    #在列表上下文中返回该捕获组捕获到的内容并赋值

前文提到,/g修饰符可用于s///进行全局替换,类似的,/g修饰符也可用于m//进行全局匹配,其匹配方式与s///一致,均为进行不重复的匹配。每次成功匹配时,m//就会返回当前所有捕获组捕获到的值,作为最终返回的列表元素的一部分。例:

$_=’abcd’;

@n=/(.)(.)/g;    #进行全局匹配。第一次匹配成功会返回a和b,即模式中两个捕获组分别捕获到的值。第二次匹配成功会返回c和d,所以最终返回的列表为(a,b,c,d)

在列表上下文中使用m//也可以理解为使用反向的split。split的拆分模式指定的是字符串中想要去除的部分,而m//中的捕获组指定的是字符串中想要留下来并返回的部分。例:

$_=’a-b-c-d’;

@n=split/-/;      #拆分模式指定的是去除所有的连字符

@m=/(\w+)/g;    #捕获组指定的是需要留下并返回每一个匹配的单词

在列表上下文中使用m//的另一个重要功能就是可以在模式匹配成功的同时将捕获变量的值存储到外部变量中,从而避免因捕获变量的存续期而导致的问题,也可以使用这种方式达到类似于修改捕获变量名称的效果。这样的操作就相当于在匹配成功的同时将捕获变量拷贝到外部变量中,从而在接下来的任何时候都可以对捕获的内容进行处理。例:

($n,$m)=/(\w+)\W+(\w+)/;   #将捕获的内容拷贝到外部变量中

非贪婪量词

到目前为止,程序中使用的所有的量词都是贪婪量词。贪婪量词在匹配时会匹配尽可能长的字符串,然后再进行回溯动作以实现整体模式匹配。其匹配过程描述如下:

$_=’abcdefg’;

print if/a.*b/;

这里的星号是一个贪婪量词,其会匹配尽可能长的字符串。而由于通配符可以匹配任意非换行符,所以在星号量词的作用下,通配符的匹配区段会从b一直到g。然后由于需要对模式剩下的b进行匹配,正则表达式会进行回溯动作,不断缩短通配符的匹配区段,然后测试是否匹配成功。上例中,直到通配符的匹配区段变为空字符串,才能使整个模式匹配成功。

与贪婪量词的匹配过程相反,非贪婪量词会匹配尽可能短的字符串。在贪婪量词后加一个问号即可将这个量词变为非贪婪量词,对于通用量词也是一样,将问号加在右花括号后即可,这是问号在正则表达式中的第五种用法。非贪婪量词的匹配过程描述如下:

print if/a.*?g/;

这里的“*?”是非贪婪星号量词,其会匹配尽可能短的字符串,也就是从空字符串开始匹配。然后由于需要对g进行匹配,正则表达式会进行回溯,不断加长非贪婪量词作用下的通配符的匹配区段,直到将匹配区段加长到b至f,达成匹配。

从上例可以看出,贪婪量词与非贪婪量词只是在匹配过程上差异较大,但仅从匹配成功与否的角度上看,这二者的结果一定是一致的。

贪婪量词与非贪婪量词由于匹配过程的不同,其导致的差异主要有以下两点:第一为匹配效率。由于贪婪量词会匹配尽可能长的字符串,所以当最终的实际匹配区段很短,而进行匹配的字符串又很长时,正则表达式就会进行大量的回溯动作以缩短量词作用下的匹配区段,同样,非贪婪量词在实际匹配区段很长时亦如此,这会降低正则表达式的效率,所以在实际的应用中应合理选择最合适的量词。

第二点为其最主要的差异所在,即最终匹配区段的长度不同。举例如下:

$_=’fredfredfred’;

s/f.*d/barney/g;     #使用贪婪量词进行匹配的情况。首先在量词的作用下通配符一直匹配到字符串末尾,然后回溯一次即达成匹配,其匹配区段为整个字符串。所以替换结果为barney

s/f.*?d/barney/g;    #使用非贪婪量词进行匹配的情况。通配符的匹配区段由空字符串开始回溯,直到第一个e便达成第一次匹配。所以这种情况下一共会匹配三次,每一次的匹配区段都是fred,替换结果为barneybarneybarney

智能匹配

智能匹配是Perl 5.10.1的新功能,其会根据智能匹配操作符“~~”两边操作数的数据类型自动判断进行的操作。智能匹配操作符的操作数可以为直接量、标量变量、数组变量、哈希、正则表达式等。一般情况下,智能匹配操作符两边的操作数没有左右之分,也就是说,如果将两个操作数调换位置,对操作符是没有任何影响的。智能匹配操作符可以进行的常见操作列举如下:

%a~~%b     #哈希键是否相等

@a~~@b    #数组是否相等

%a~~@b     #是否至少有一个哈希键在数组中

‘a’~~%b      #是否存在a哈希键(此用法不可颠倒顺序)

%a~~/b/     #是否至少有一个哈希键匹配给定模式

@a~~/b/     #是否至少有一个数组元素匹配给定模式

$a~~/b/      #模式匹配

智能匹配的用法示例如下:

use5.10.1;          #启用Perl5.10.1

print’T’if %a~~%b;    #判断%a与%b中的哈希键是否一致

樱雨楼

完于2016.1.24

最后修改于2016.2.3

    原文作者:樱雨楼
    原文地址: https://www.jianshu.com/p/2bc2d7d6d3ed
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞