自我感觉量词是正则表达式里最不容易理解的地方,所以特别为它做了个总结。
为了容易理解,会简单地结合正则表达式引擎的工作方式来讲。正则表达式引擎分为文本导向型(Text-directed Engines)和正则表达式导向型(Regex-directed Engines)两种。因为基本上采用的是正则表达式导向型的引擎,所以下文关于引擎工作方式的部分都是基于正则表达式导向型引擎的。
一、没有量词时正则表达式引擎的工作方式
在没有量词之前,正则表达式的一个符号块只能匹配文本中的一个符号,如[abc]匹配字符a或b或c。此时,正则表达式的匹配流程非常的简单。
正则表达式引擎按从左到右的顺序读取正则表达式中的字符块和文本中的字符,并检查字符块和字符是否匹配。根据匹配的结果和匹配符号的位置,后续的操作分为四种。
匹配成功,且匹配的是正则表达式的第一个符号块。说明文本中以该字符开始的一段字符串可能会是我们需要的字符串,所以引擎接着向右读取正则表达式中的字符块和文本中的字符进行匹配。为了说明的方便,我们把这个字符记为A。
匹配成功,且匹配的是正则表达式的最后一个符号块。说明文本中从A开始到目前读取位置的这一段字符是我们需要的字符串。于是,引擎将这段文本输出,然后接着寻找下一个匹配的字符串,它继续向右读取文本中的字符,但是从头开始读取正则表达式中的字符块,将它们进行匹配。
匹配成功,且匹配的是正则表达式中间的符号块。说明文本中从A开始到目前为止的这一段字符还是匹配的,如果之后的字符也匹配的话就找到所需的字符串。所以引擎接着向右读取正则表达式中的字符块和文本中的字符进行匹配。
匹配失败,无论匹配的是正则表达式中的哪个符号块。说明在从文本中从A开始的各种字符串中,并不存在我们所需的字符串。因此,引擎重新从A的后一个位置开始读取字符,并从头开始读取正则表达式中的字符块,进行匹配。
引擎不断重复这样的操作,直到读取到文本的终结符。
比如,我们用正则表达式<[ou]l>去匹配文本<ol>This is ol</ol>。引擎先读取正则表达式的第一个字符块<和文本的第一个字符<。因为它们成功匹配,所以引擎继续读取正则表达式的第二个字符块[ou]和文本的第二个字符o,也成功匹配了,就继续……,直到匹配到>,正则表达式和字符串<ol>完全匹配了,于是找到了第一个我们所需字符串。之后,引擎继续读取文本中的字符T和正则表达式中的第一个字符块<,匹配失败,引擎读取文本中的下一个字符h,还是失败,直到读取到第15个字符<,匹配成功。然后引擎读取正则表达式中的[ou]字符块和文本中的/字符块,匹配失败,引擎重新从文本的第15个<之后开始读取字符,从正则表达式的开头读取字符块……直到引擎读到了终结符,查找结束,找到了一个字符串,开始于文本的第1位,结束于文本的第4位。
注意:在这种实现方式下,正则表达式会优先匹配文本最左边的字符串,而且匹配过的内容不会重复出现。如用正则表达式a.{4,4}去匹配My dear grandparent,得到的匹配结果是ar gr和andpa。其中没有arent,因为arent中的首字母a已经在andpa中被匹配了。这消除了匹配顺序导致的歧义和是否能重复匹配导致的歧义。
二、量词带来的不确定性
但是,引入了量词之后,事情就变得复杂了起来。量词可以让被修饰的字符重复若干次,如a*表示任意个a组成的字符串。量词在正则表达式中起着很大的作用,但使用中总是出现意想不到的结果。
问题的起因是,被修饰字符的重复次数往往是不确定的。文本中以同一个字符开头,可能会有好多种长度不同的字符串形式与正则表达式匹配,这就引起了歧义。如用<.*>去匹配<p>first</p> yeah,以第一个<打头,可以有两种长度不同的匹配,<p>和<p>first</p>,到底采用哪一种匹配呢?总不能靠运气吧。于是,在量词的基础上又给他分了三类。
量词有三类,贪婪型量词(Greedy quantifiers),勉强型量词(Reluctant quantifiers)和占有型量词(Possessive quantifiers)。
三、贪婪型量词和勉强型量词
由于引擎按从左往右的顺序读取,它并不能提前预知后面的字符串是什么,也就不知道到底让被修饰字符重复多少次能获得一个匹配的字符串。虽然你一眼看去能看出来,上一个例子中的.*让.重复1次匹配文本中的p和重复10次匹配文本中的p>first</p就能获得一个匹配的字符串。但是计算机并不能,他只能一个一个可能性地试过去,他可能从重复次数为0开始尝试,0次不行就1次,1次不行就两次……,也有可能从最大的重复次数开始试,最大的不行就最大减一次……。这样直到获得第一个匹配,那么匹配成功,引擎就把它当作了匹配结果,或者最大到最小的所有的可能都试过了都不行,那么当前这段字符串和正则表达式的匹配就失败了。
有些同学可能对最大重复次数有些迷茫。它就是不考虑后面的符号块能不能匹配的情况下,被修饰字符块能连续匹配多少个文本中的字符。以上一个例子来说,当引擎将正则表达式的<和文本的第一个字符<匹配起来后,读取到正则表达式的.*后,不考虑它之后的>,.*可以匹配文本中的p>first</p> yeah,这就是在尝试匹配以文本中第一个字符<开头的字符串时,它的最大重复次数。
从最大重复次数开始逐步减小重复次数的量词是贪婪型量词,而从最小重复次数开始逐步增大重复次数的量词是勉强型量词。
量词默认是贪婪的,贪婪型量词会使被修饰字符重复尽可能多的次数。用以上例子来说明引擎处理贪婪型量词的方式,首先引擎读取了正则表达式的首字符块<和文本的首字符<,并且匹配成功。之后引擎会读取正则表达式的.*,.可以匹配所有的字符,而*使.重复出现,而*又是贪婪的,所以引擎会不停地重复用.去匹配文本中的字符,直到读到文本的终结符,.和终结符匹配失败,这个时候.的重复次数达到最大了,而引擎读取了之后的>,发现已经匹配不了了,这个时候引擎知道重复最大次数是不行的,于是让.少重复了一次,.*少匹配了文本中最后的h,引擎拿着这个h和正则表达式的>去匹配,匹配还是不成功,所以这个重复此时还是不行,再减一……直到重复次数为10为止,.*吐出来的>和正则表达式的>成功匹配,而此时正则表达式中的字符块被全部匹配了,结果就产生了。
在贪婪型量词的后面加一个?就成了勉强型量词,勉强型量词会使被修饰字符重复尽可能少的次数。对以上例子来说,正则表达式为<.*?>,匹配方式如下。首先引擎读取了正则表达式的首字符块<和文本的首字符<,并且匹配成功。之后引擎读取正则表达式的.*?,*?是勉强的,引擎首先会让.重复最小的次数,由于*代表任意次,所以最小次数为0。之后,引擎会读取.*?之后的>,尝试着让它去匹配文本中的下一个字符p,匹配失败,这个时候引擎知道重复最小次数是匹配不了了,于是让.多重复了一次,让.*?匹配了p,之后再用>去匹配文本的下一个字符>,匹配成功,此时此时正则表达式中的字符块被全部匹配了,结果就产生了。
四、占有型量词
在贪婪型量词的后面加一个+就成了占有型量词,占有型量词让被修饰字符重复最大次数。乍一看和贪婪型量词没啥区别啊,其实少了三个字,尽可能。还是用上面的例子来说,此时正则表达式为.*+,引擎只会尝试一种可能,前面的过程和贪婪型相同。当引擎发现.的重复次数达到最大了,引擎读取了之后的>,发现匹配不了,这时引擎知道重复最大次数是不行的,然后整个匹配就失败了,引擎不会去尝试其他的重复次数。
占有型量词可以提高匹配的效率,因为它不会遍历所有可能性去匹配一段字符串,它只匹配重复次数最大的那种可能,行就行,不行就不行。如果我们需要的结果只有在最大重复次数时才会出现,那其余的尝试都是不必要的
比如,我们要得到文本<p>one</p> two中每对尖括号包裹的内容,那我们可能会用<.*?>,但我们也可以用表达式<[^>]*+>。后者效率更高,因为它不用尝试其他的可能性。