1.不同的正则表达式引擎
正则表达式引擎是一种可以处理正则表达式的软件。通常,引擎是更大的应用程序的一部分。在软件世界,不同的正则表达式并不互相兼容。本文集中讨论Perl 5 类型的引擎,因为这种引擎是应用最广泛的引擎。同时我们也会提到一些和其他引擎的区别。许多近代的引擎都很类似,但不完全一样。例如.NET正则库,JDK正则包。
2.正则表达式引擎的内部工作机制
知道正则表达式引擎是如何工作的有助于你很快理解为何某个正则表达式不像你期望的那样工作。
有两种类型的引擎:文本导向(text-directed)的引擎和正则导向(regex-directed)的引擎。Jeffrey Friedl把他们称作DFA和NFA引擎。本文谈到的是正则导向的引擎。这是因为一些非常有用的特性,如“惰性”量词(lazy quantifiers)和反向引用(backreferences),只能在正则导向的引擎中实现。所以毫不意外这种引擎是目前最流行的引擎。
你可以轻易分辨出所使用的引擎是文本导向还是正则导向。如果反向引用或“惰性”量词被实现,则可以肯定你使用的引擎是正则导向的。你可以作如下测试:将正则表达式<<regex|regex not>>应用到字符串“regex not”。如果匹配的结果是regex,则引擎是正则导向的。如果结果是regex not,则是文本导向的。因为正则导向的引擎是“猴急”的,它会很急切的进行表功,报告它找到的第一个匹配。
正则导向的引擎总是返回最左边的匹配
这是需要你理解的很重要的一点:即使以后有可能发现一个“更好”的匹配,正则导向的引擎也总是返回最左边的匹配。
当把<<cat>>应用到“He captured a catfish for his cat”,引擎先比较<<c>>和“H”,结果失败了。于是引擎再比较<<c>>和“e”,也失败了。直到第四个字符,<<c>>匹配了“c”。<<a>>匹配了第五个字符。到第六个字符<<t>>没能匹配“p”,也失败了。引擎再继续从第五个字符重新检查匹配性。直到第十五个字符开始,<<cat>>匹配上了“catfish”中的“cat”,正则表达式引擎急切的返回第一个匹配的结果,而不会再继续查找是否有其他更好的匹配。
3.元字符
1).文字符号
元字符:有11个:[ ] / ^ $ . | ? * + ( )
如果你想在正则表达式中将这些字符用作文本字符,你需要用反斜杠“/”对其进行换码 (escape)。例如你想匹配“1+1=2”,正确的表达式为<<1/+1=2>>.编程中则是<<1//+1=2>>.
不可显示字符:<</t>>代表Tab(0x09),<</r>>代表回车符(0x0D),<</n>>代表换行符(0x0A)。
要注意的是Windows中文本文件使用“/r/n”来结束一行而Unix使用“/n”。
2).字符集
a).字符集中的元字符只有四个:] / ^ -
“]”代表字符集定义的结束;
“/”代表转义;
“^”代表取反;
“-”代表范围定义。
其他常见的元字符在字符集定义内部都是正常字符,不需要转义。如下匹配:
[]X] =>]或X,[X^] => X或^,[//X] => /或X [+*] => +或*
b).字符集的元字符简写
代码 | 说明 |
. | 匹配除换行符以外的任意字符 |
/w | 匹配字母或数字或下划线或汉字 |
/s | 匹配任意的空白符 |
/d | 匹配数字 |
/b | 匹配单词的开始或结束 |
^ | 匹配字符串一行的开始 |
$ | 匹配字符串一行的结束 |
c).取反字符集的元字符简写
代码 | 说明 |
/W | 匹配除字母或数字或下划线或汉字以外任意字符 |
/S | 匹配除空白符以外任意字符 |
/D | 匹配除数字以外任意字符 |
/B | 匹配除单词的开始或结束的以外任意位置 |
[^X] | 匹配除X以外任意字符 |
[^aeiou] | 匹配除aeiou以外的任意字符 |
元字符<</b>>也是一种对位置进行匹配的“锚”。这种匹配是0长度匹配。
有4种位置被认为是“单词边界”:
1)在字符串的第一个字符前的位置(如果字符串的第一个字符是一个“单词字符”)
2)在字符串的最后一个字符后的位置(如果字符串的最后一个字符是一个“单词字符”)
3)在一个“单词字符”和“非单词字符”之间,其中“非单词字符”紧跟在“单词字符”之后
4)在一个“非单词字符”和“单词字符”之间,其中“单词字符”紧跟在“非单词字符”后面
“单词字符”是可以用“/w”匹配的字符,“非单词字符”是可以用“/W”匹配的字符。
例:
<</b4/b>> =>匹配“45 4 3”中第二个4,而不匹配“4543”中的4
<</B4/B>> =>不匹配“45 4 3”中4,而匹配“4543”中的第二个4
·深入正则表达式引擎内部
让我们看看把正则表达式<</bis/b>>应用到字符串“This island is beautiful”。引擎先处理符号<</b>>。因为/b是0长度 ,所以第一个字符T前面的位置会被考察。因为T是一个“单词字符”,而它前面的字符是一个空字符(void),所以/b匹配了单词边界。接着<<i>>和第一个字符“T”匹配失败。匹配过程继续进行,直到第五个空格符,和第四个字符“s”之间又匹配了<</b>>。然而空格符和<<i>>不匹配。继续向后,到了第六个字符“i”,和第五个空格字符之间匹配了<</b>>,然后<<is>>和第六、第七个字符都匹配了。然而第八个字符和第二个“单词边界”不匹配,所以匹配又失败了。到了第13个字符i,因为和前面一个空格符形成“单词边界”,同时<<is>>和“is”匹配。引擎接着尝试匹配第二个<</b >>。因为第15个空格符和“s”形成单词边界,所以匹配成功。引擎“急着”返回成功匹配的结果。
3).常用的限定符
代码/语法 | 说明 |
* | 重复零次或更多次 |
+ | 重复一次或更多次 |
? | 重复零次或一次 |
{n} | 重复n次 |
{n,} | 重复n次或更多次 |
{n,m} | 重复n到m次 |
a).限制性重复
许多现代的正则表达式实现,都允许你定义对一个字符重复多少次。词法是:{min,max}。min和max都是非负整数。因此{0,}和*一样,{1,}和+ 的作用一样。
你可以用<</b[1-9][0-9]{3}/b>>匹配1000~9999之间的数字(“/b”表示单词边界)。<</b[1-9][0-9]{2,4}/b>>匹配一个在100~99999之间的数字
b).贪婪与懒惰
假设你想用一个正则表达式匹配一个HTML标签。你知道输入将会是一个有效的HTML文件,因此正则表达式不需要排除那些无效的标签。所以如果是在两个尖括号之间的内容,就应该是一个HTML标签。
许多正则表达式的新手会首先想到用正则表达式<< <.+> >>,他们会很惊讶的发现,对于测试字符串,“This is a <EM>first</EM> test”,你可能期望会返回<EM>,然后继续进行匹配的时候,返回</EM>。
但事实是不会。正则表达式将会匹配“<EM>first</EM>”。很显然这不是我们想要的结果。原因在于“+”是贪婪的。也就是说,“+”会导致正则表达式引擎试图尽可能的重复前导字符。只有当这种重复会引起整个正则表达式匹配失败的情况下,引擎会进行回溯。也就是说,它会放弃最后一次的“重复”,然后处理正则表达式余下的部分。(“? *”的重复也是贪婪的。)
·深入正则表达式引擎内部
让我们来看看正则引擎如何匹配前面的例子。第一个记号是“<”,这是一个文字符号。第二个符号是“.”,匹配了字符“E”,然后“+”一直可以匹配其余的字符,直到一行的结束。然后到了换行符,匹配失败(“.”不匹配换行符)。于是引擎开始对下一个正则表达式符号进行匹配。也即试图匹配“>”。到目前为止,“<.+”已经匹配了“<EM>first</EM> test”。引擎会试图将“>”与换行符进行匹配,结果失败了。于是引擎进行回溯。结果是现在“<.+”匹配“<EM>first</EM> tes”。于是引擎将“>”与“t”进行匹配。显然还是会失败。这个过程继续,直到“<.+”匹配“<EM>first</EM”,“>”与“>”匹配。于是引擎找到了一个匹配“<EM>first</EM>”。记住,正则导向的引擎是“急切的”,所以它会急着报告它找到的第一个匹配。而不是继续回溯,即使可能会有更好的匹配,例如“<EM>”。所以我们可以看到,由于“+”的贪婪性,使得正则表达式引擎返回了一个最左边的最长的匹配。
·用懒惰性取代贪婪性
一个用于修正以上问题的可能方案是用“+”的惰性代替贪婪性。你可以在“+”后面紧跟一个问号“?”来达到这一点。“*”,“{}”和“?”表示的重复也可以用这个方案。因此在上面的例子中我们可以使用“<.+?>”。让我们再来看看正则表达式引擎的处理过程。
再一次,正则表达式记号“<”会匹配字符串的第一个“<”。下一个正则记号是“.”。这次是一个懒惰的“+”来重复上一个字符。这告诉正则引擎,尽可能少的重复上一个字符。因此引擎用记号“.” 匹配字符“E”,而不是上面的所提到<EM>first</EM,然后用“>”匹配“M”,结果失败了。引擎会进行回溯,和上一个例子不同,因为是惰性重复,所以引擎是扩展惰性重复而不是减少,于是“<.+”现在被扩展为“<EM”。引擎继续匹配下一个记号“>”。这次得到了一个成功匹配。引擎于是报告“<EM>”是一个成功的匹配。整个过程大致如此。
·惰性扩展的一个替代方案
我们还有一个更好的替代方案。可以用一个贪婪重复与一个取反字符集:“<[^>]+>”。之所以说这是一个更好的方案在于使用惰性重复时,引擎会在找到一个成功匹配前对每一个字符进行回溯。而使用取反字符集则不需要进行回溯。
总之如下:
贪婪性:<.+> => <em>first</em>
懒惰性:<.+?> => <em>或</em>,<[^>]+> => <em>或</em>
最后要记住的是,本文仅仅谈到的是正则导向的引擎。文本导向的引擎是不回溯的。但是同时他们也不支持惰性重复操作。
4).处理选项
常用的处理选项
名称 | 说明 |
IgnoreCase(忽略大小写) | 匹配时不区分大小写。 |
IgnorePatternWhitespace(忽略空白) | 忽略表达式中的非转义空白并启用由#标记的注释。 |
Multiline(多行模式) | 更改^和$的含义,使它们分别在任意一行的行首和行尾匹配,而不仅仅在整个字符串的开头和结尾匹配。(在此模式下,$的精确含意是:匹配/n之前的位置以及字符串结束前的位置。) |
Singleline(单行模式) | 更改.的含义,使它与每一个字符匹配(包括换行符/n)。 |
ExplicitCapture(显式捕获) | 仅捕获已被显式命名的组。 |
一个经常被问到的问题是:是不是只能同时使用多行模式和单行模式中的一种?答案:不是。这两个选项之间没有任何关系,除了它们的名字比较相似(以至于让人感到疑惑)以外。
5).选择符
正则表达式中“|”表示选择。你可以用选择符匹配多个可能的正则表达式中的一个。
选择符在正则表达式中具有最低的优先级,也就是说,它告诉引擎要么匹配选择符左边的所有表达式,要么匹配右边的所有表达式。你也可以用圆括号来限制选择符的作用范围。如<</b(cat|dog)/b>>,这样告诉正则引擎把(cat|dog)当成一个正则表达式单位来处理。
·注意正则引擎的“急于表功”性
正则引擎是急切的,当它找到一个有效的匹配时,它会停止搜索。因此在一定条件下,选择符两边的表达式的顺序对结果会有影响。假设你想用正则表达式搜索一个编程语言的函数列表:Get,GetValue,Set或SetValue。一个明显的解决方案是<<Get|GetValue|Set|SetValue>>。让我们看看当搜索SetValue时的结果。
因为<< GetValue >>和<< SetValue >>都失败了,而<< Get >><<Set>>匹配成功。因为正则导向的引擎都是“急切”的,所以它会返回第一个成功的匹配,就是“Get /Set”,而不去继续搜索是否有其他更好的匹配。和我们期望的相反,正则表达式并没有匹配整个字符串。
有几种可能的解决办法。一是考虑到正则引擎的“急切”性,改变选项的顺序,例如我们使用<<GetValue|Get|SetValue|Set>>,这样我们就可以优先搜索最长的匹配。我们也可以把四个选项结合起来成两个选项:<<Get(Value)?|Set(Value)?>>。因为问号重复符是贪婪的,所以SetValue总会在Set之前被匹配。
一个更好的方案是使用单词边界:<</b(Get|GetValue|Set|SetValue)/b>>或<</b(Get(Value)?|Set(Value)?/b>>。更进一步,既然所有的选择都有相同的结尾,我们可以把正则表达式优化为<</b(Get|Set)(Value)?/b>>。
4. 组与向后引用
把正则表达式的一部分放在圆括号内,你可以将它们形成组。然后你可以对整个组使用一些正则操作,例如重复操作符。
注意:只有圆括号“()”才能用于形成组。“[]”用于定义字符集。“{}”用于定义重复操作。
当用“()”定义了一个正则表达式组后,正则引擎则会把被匹配的组按照顺序编号,存入缓存。当对被匹配的组进行向后引用的时候,可以用“/数字”的方式进行引用。<</1>>引用第一个匹配的向后引用组,<</2>>引用第二个组,以此类推,<</n>>引用第n个组。而<</0>>则引用整个被匹配的正则表达式本身。
例如:<([A-Z][A-Z0-9]*)[^>]*>.*?<//1>匹配<B>This is a test</B>