Jan Goyvaerts正则表达式(二)

转载地址:http://blog.csdn.net/DeRoshia/archive/2006/12/29/1467241.aspx

前言:
       
本文是前一片文章《Jan Goyvaerts正则表达式(一)》的续篇,在本文中讲述了正则表达式中的组与向后引用,先前向后查看,条件测试,单词边界,选择符等表达式及例子,并分析了正则引擎在执行匹配时的内部机理。
      
本文是Jan GoyvaertsRegexBuddy写的教程的译文,版权归原作者所有,欢迎转载。但是为了尊重原作者和译者的劳动,请注明出处!谢谢!

 
9.       单词边界
元字符 <</b>> 也是一种对位置进行匹配的“锚”。这种匹配是 0 长度匹配。
4 种位置被认为是“单词边界”:
1)         在字符串的第一个字符前的位置 ( 如果字符串的第一个字符是一个“单词字符” )
2)         在字符串的最后一个字符后的位置 ( 如果字符串的最后一个字符是一个“单词字符” )
3)         在一个“单词字符”和“非单词字符”之间,其中“非单词字符”紧跟在“单词字符”之后
4)         在一个“非单词字符”和“单词字符”之间,其中“单词字符”紧跟在“非单词字符”后面
  “单词字符”是可以用“ /w ”匹配的字符,“非单词字符”是可以用“ /W ”匹配的字符。在大多数的正则表达式实现中,“单词字符”通常包括 <<[a-zA-Z0-9_]>>
例如: <</b4/b>> 能够匹配单个的 4 而不是一个更大数的一部分。这个正则表达式不会匹配“ 44 ”中的 4
换种说法,几乎可以说 <</b>> 匹配一个“字母数字序列”的开始和结束的位置。
 
“单词边界”的取反集为 <</B>> ,他要匹配的位置是两个“单词字符”之间或者两个“非单词字符”之间的位置。
 
·         深入正则表达式引擎内部
让我们看看把正则表达式 <</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 ”形成单词边界,所以匹配成功 。引擎“急着”返回成功匹配的结果。
 
10.   选择符
正则表达式中“ | ”表示选择。你可以用选择符匹配多个可能的正则表达式中的一个。
如果你想搜索文字“ cat ”或“ dog ”,你可以用 <<cat|dog>> 。如果你想有更多的选择,你只要扩展列表 <<cat|dog|mouse|fish>>
选择符在正则表达式中具有最低的优先级 ,也就是说,它告诉引擎要么匹配选择符左边的所有表达式,要么匹配右边的所有表达式。你也可以用圆括号来限制选择符的作用范围。如 <</b(cat|dog)/b>> ,这样告诉正则引擎把 (cat|dog) 当成一个正则表达式单位来处理。
·         注意正则引擎的“急于表功”性
正则引擎是急切的,当它找到一个有效的匹配时,它会停止搜索。因此在一定条件下,选择符两边的表达式的顺序对结果会有影响。假设你想用正则表达式搜索一个编程语言的函数列表: Get GetValue Set SetValue 。一个明显的解决方案是 <<Get|GetValue|Set|SetValue>> 。让我们看看当搜索 SetValue 时的结果。
因为 <<Get>> <<GetValue>> 都失败了,而 <<Set>> 匹配成功。因为正则导向的引擎都是“急切”的,所以它会返回第一个成功的匹配,就是“ 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>>
 
11.   组与向后引用
把正则表达式的一部分放在圆括号内,你可以将它们形成组。然后你可以对整个组使用一些正则操作,例如重复操作符。
要注意的是,只有圆括号“ () ”才能用于形成组。“ [] ”用于定义字符集。“ {} ”用于定义重复操作。
当用“ () ”定义了一个正则表达式组后,正则引擎则会把被匹配的组按照顺序编号, 存入缓存。 当对被匹配的组进行向后引用的时候,可以用“ / 数字”的方式进行引用。 <</1>> 引用第一个匹配的后向引用组, <</2>> 引用第二个组,以此类推, <</n>> 引用第 n 个组。而 <</0>> 则引用整个被匹配的正则表达式本身。我们看一个例子。
假设你想匹配一个 HTML 标签的开始标签和结束标签,以及标签中间的文本。比如 <B>This is a test</B> ,我们要匹配 <B> </B> 以及中间的文字。我们可以用如下正则表达式:“ <([A-Z][A-Z0-9]*)[^>]*>.*?<//1>
首先,“ < ”将会匹配“ <B> ”的第一个字符“ < ”。然后 [A-Z] 匹配 B [A-Z0-9]* 将会匹配 0 到多次字母数字,后面紧接着 0 到多个非“ > ”的字符。最后正则表达式的“ > ”将会匹配“ <B> ”的“ > ”。接下来正则引擎将对结束标签之前的字符进行惰性匹配,直到遇到一个“ </ ”符号。然后正则表达式中的“ /1 ”表示对前面匹配的组“ ([A-Z][A-Z0-9]*) ”进行引用,在本例中,被引用的是标签名“ B ”。所以需要被匹配的结尾标签为“ </B>
你可以对相同的后向引用组进行多次引用, <<([a-c])x/1x/1>> 将匹配“ axaxa ”、“ bxbxb ”以及“ cxcxc 。如果用数字形式引用的组没有有效的匹配,则引用到的内容简单的为空。
一个后向引用不能用于它自身。 <<([abc]/1)>> 是错误的。因此你不能将 <</0>> 用于一个正则表达式匹配本身,它只能用于替换操作中。
后向引用不能用于字符集内部。 <<(a)[/1b]>> 中的 <</1>> 并不表示后向引用。在字符集内部, <</1>> 可以被解释为八进制形式的转码。
向后引用会降低引擎的速度,因为它需要存储匹配的组。如果你不需要向后引用,你可以告诉引擎对某个组不存储。例如: <<Get(?:Value)>> 。其中“ ( ”后面紧跟的“ ?: ”会告诉引擎对于组 (Value) ,不存储匹配的值以供后向引用。
·         重复操作与后向引用
当对组使用重复操作符时,缓存里后向引用内容会被不断刷新,只保留最后匹配的内容。例如: <<([abc]+)=/1>> 将匹配“ cab=cab ,但是 <<([abc])+=/1>> 却不会。因为 ([abc]) 第一次匹配“ c ”时,“ /1 ”代表“ c ”;然后 ([abc]) 会继续匹配“ a ”和“ b ”。最后“ /1 ”代表“ b ”,所以它会匹配“ cab=b
应用:检查重复单词 -- 当编辑文字时,很容易就会输入重复单词,例如“ the the ”。使用 <</b(/w+)/s+/1/b>> 可以检测到这些重复单词 。要删除第二个单词,只要简单的利用替换功能替换掉“ /1 ”就可以了。
 
 
·         组的命名和引用
PHP Python 中,可以用 <<(?P<name>group)>> 来对组进行命名。在本例中,词法 ?P<name> 就是对组 (group) 进行了命名。其中 name 是你对组的起的名字。你可以用 (?P=name) 进行引用。
.NET 的命名组
.NET framework 也支持命名组。不幸的是,微软的程序员们决定发明他们自己的语法,而不是沿用 Perl Python 的规则。目前为止,还没有任何其他的正则表达式实现支持微软发明的语法。
下面是 .NET 中的例子:
(?<first>group)(?’second’group)
正如你所看到的, .NET 提供两种词法来创建命名组:一是用尖括号“ <> ”,或者用单引号“ ’’ ”。尖括号在字符串中使用更方便,单引号在 ASP 代码中更有用,因为 ASP 代码中“ <> ”被用作 HTML 标签。
要引用一个命名组,使用 /k<name> /k’name’.
当进行搜索替换时,你可以用“ ${name} ”来引用一个命名组。
 
12.   正则表达式的匹配模式
本教程所讨论的正则表达式引擎都支持三种匹配模式:
<</i>> 使正则表达式对大小写不敏感
<</s>> 开启“单行模式”,即点号“ . ”匹配新行符
<</m>> 开启“多行模式”,即“ ^ ”和“ $ ”匹配新行符的前面和后面的位置。
 
·         在正则表达式内部打开或关闭模式
如果你在正则表达式内部插入修饰符 (?ism) ,则该修饰符只对其右边的正则表达式起作用。 (?-i) 是关闭大小写不敏感。你可以很快的进行测试。 <<(?i)te(?-i)st>> 应该匹配 TEst ,但是不能匹配 teST TEST.
 
13.   原子组与防止回溯
在一些特殊情况下,因为回溯会使得引擎的效率极其低下
让我们看一个例子:要匹配这样的字串,字串中的每个字段间用逗号做分隔符,第 12 个字段由 P 开头。
我们容易想到这样的正则表达式 <<^(.*?,){11}P>> 。这个正则表达式在正常情况下工作的很好。但是在极端情况下,如果第 12 个字段不是由 P 开头,则会发生灾难性的回溯。如要搜索的字串为“ 1,2,3,4,5,6,7,8,9,10,11,12,13”。首先,正则表达式一直成功匹配直到第12 个字符。这时,前面的正则表达式消耗的字串为“ 1,2,3,4,5,6,7,8,9,10,11, ”,到了下一个字符, <<P>> 并不匹配“ 12 ”。所以引擎进行回溯,这时正则表达式消耗的字串为“ 1,2,3,4,5,6,7,8,9,10,11”。继续下一次匹配过程,下一个正则符号为点号<<.>> ,可以匹配下一个逗号“ , ”。然而 << >> 并不匹配字符 12 ”中的“ 1 。匹配失败,继续回溯。大家可以想象,这样的回溯组合是个非常大的数量。因此可能会造成引擎崩溃。
用于阻止这样巨大的回溯有几种方案:
一种简单的方案是尽可能的使匹配精确。用取反字符集代替点号。例如我们用如下正则表达式 <<^([^,/r/n]*,){11}P>> ,这样可以使失败回溯的次数下降到 11
另一种方案是使用原子组。
原子组的目的是使正则引擎失败的更快一点。因此可以有效的阻止海量回溯 。原子组的语法是 <<(?> 正则表达式 )>> 。位于 (?>) 之间的所有正则表达式都会被认为是一个单一的正则符号。一旦匹配失败,引擎将会回溯到原子组前面的正则表达式部分。前面的例子用原子组可以表达成 <<^(?>(.*?,){11})P>> 。一旦第十二个字段匹配失败,引擎回溯到原子组前面的 <<^>>
 
14.   向前查看与向后查看
Perl 5 引入了两个强大的正则语法:“向前查看”和“向后查看”。他们也被称作“零长度断言”。他们和锚定一样都是零长度的(所谓零长度即指该正则表达式不消耗被匹配的字符串)。不同之处在于“前后查看”会实际匹配字符,只是他们会抛弃匹配只返回匹配结果:匹配或不匹配。这就是为什么他们被称作“断言”。他们并不实际消耗字符串中的字符,而只是断言一个匹配是否可能
几乎本文讨论的所有正则表达式的实现都支持“向前向后查看”。唯一的一个例外是 Javascript 只支持向前查看
·         肯定和否定式的向前查看
如我们前面提过的一个例子:要查找一个 q ,后面没有紧跟一个 u 。也就是说,要么 q 后面没有字符,要么后面的字符不是 u 采用否定式向前查看后的一个解决方案为 <<q(?!u)>> 。否定式向前查看的语法是 <<(?! 查看的内容 )>>
肯定式向前查看和否定式向前查看很类似: <<(?= 查看的内容 )>>
如果在“查看的内容”部分有组,也会产生一个向后引用。但是向前查看本身并不会产生向后引用,也不会被计入向后引用的编号中。这是因为向前查看本身是会被抛弃掉的,只保留匹配与否的判断结果。如果你想保留匹配的结果作为向后引用,你可以用 <<(?=(regex))>> 来产生一个向后引用
·         肯定和否定式的先后查看
向后查看和向前查看有相同的效果,只是方向相反
否定式向后查看的语法是: <<(?<! 查看内容 )>>
肯定式向后查看的语法是: <<(?<= 查看内容 )>>
我们可以看到,和向前查看相比,多了一个表示方向的左尖括号。
例: <<(?<!a)b>> 将会匹配一个没有“ a ”作前导字符的“ b
值得注意的是:向前查看从当前字符串位置开始对“查看”正则表达式进行匹配;向后查看则从当前字符串位置开始先后回溯一个字符,然后再开始对“查看”正则表达式进行匹配。
 
·         深入正则表达式引擎内部
让我们看一个简单例子。
把正则表达式 <<q(?!u)>> 应用到字符串“ Iraq ”。正则表达式的第一个符号是 <<q>> 。正如我们知道的,引擎在匹配 <<q>> 以前会扫过整个字符串。当第四个字符“ q ”被匹配后,“ q ”后面是空字符 (void) 。而下一个正则符号是向前查看。引擎注意到已经进入了一个向前查看正则表达式部分。下一个正则符号是 <<u>> ,和空字符不匹配,从而导致向前查看里的正则表达式匹配失败。因为是一个否定式的向前查看,意味着整个向前查看结果是成功的。于是匹配结果“ q ”被返回了。
我们在把相同的正则表达式应用到“ quit ”。 <<q>> 匹配了“ q ”。下一个正则符号是向前查看部分的 <<u>> ,它匹配了字符串中的第二个字符“ i ”。引擎继续走到下个字符“ i ”。然而引擎这时注意到向前查看部分已经处理完了,并且向前查看已经成功。于是引擎抛弃被匹配的字符串部分,这将导致引擎回退到字符“ u ”。
因为向前查看是否定式的,意味着查看部分的成功匹配导致了整个向前查看的失败,因此引擎不得不进行回溯。最后因为再没有其他的“ q ”和 <<q>> 匹配,所以整个匹配失败了。
为了确保你能清楚地理解向前查看的实现,让我们把 <<q(?=u)i>> 应用到“ quit ”。 <<q>> 首先匹配“ q ”。然后向前查看成功匹配“ u ”,匹配的部分被抛弃,只返回可以匹配的判断结果。引擎从字符“ i ”回退到“ u ”。由于向前查看成功了,引擎继续处理下一个正则符号 <<i>> 。结果发现 <<i>> 和“ u ”不匹配。因此匹配失败了。由于后面没有其他的“ q ”,整个正则表达式的匹配失败了。
 
·         更进一步理解正则表达式引擎内部机制
让我们把 <<(?<=a)b>> 应用到“ thingamabob ”。引擎开始处理向后查看部分的正则符号和字符串中的第一个字符。在这个例子中,向后查看告诉正则表达式引擎回退一个字符,然后查看是否有一个“ a ”被匹配。因为在“ t ”前面没有字符,所以引擎不能回退。因此向后查看失败了。引擎继续走到下一个字符“ h ”。再一次,引擎暂时回退一个字符并检查是否有个“ a ”被匹配。结果发现了一个“ t ”。向后查看又失败了。
向后查看继续失败,直到正则表达式到达了字符串中的“ m ”,于是肯定式的向后查看被匹配了。因为它是零长度的,字符串的当前位置仍然是“ m ”。下一个正则符号是 <<b>> ,和“ m ”匹配失败。下一个字符是字符串中的第二个“ a ”。引擎向后暂时回退一个字符,并且发现 <<a>> 不匹配“ m ”。
在下一个字符是字符串中的第一个“ b ”。引擎暂时性的向后退一个字符发现向后查看被满足了,同时 <<b>> 匹配了“ b ”。因此整个正则表达式被匹配了。作为结果,正则表达式返回字符串中的第一个“ b ”。
·         向前向后查看的应用
我们来看这样一个例子:查找一个具有 6 位字符的,含有“ cat ”的单词。
首先,我们可以不用向前向后查看来解决问题,例如:
<< cat/w{3}|/wcat/w{2}|/w{2}cat/w|/w{3}cat>>
足够简单吧!但是当需求变成查找一个具有 6-12 位字符,含有“ cat ”,“ dog ”或“ mouse ”的单词时,这种方法就变得有些笨拙了。
我们来看看使用向前查看的方案。在这个例子中,我们有两个基本需求要满足:一是我们需要一个 6 位的字符,二是单词含有“ cat ”。
满足第一个需求的正则表达式为 <</b/w{6}/b>> 。满足第二个需求的正则表达式为 <</b/w*cat/w*/b>>
把两者结合起来,我们可以得到如下的正则表达式:
    <<(?=/b/w{6}/b)/b/w*cat/w*/b>>
具体的匹配过程留给读者。但是要注意的一点是,向前查看是不消耗字符的,因此当判断单词满足具有 6 个字符的条件后,引擎会从开始判断前的位置继续对后面的正则表达式进行匹配。
最后作些优化,可以得到下面的正则表达式:
<</b(?=/w{6}/b)/w{0,3}cat/w*>>
 
15.   正则表达式中的条件测试
条件测试的语法为 <<(?ifthen|else)>> 。“ if ”部分可以是向前向后查看表达式。如果用向前查看,则语法变为: <<(?(?=regex)then|else)>> ,其中 else 部分是可选的。
如果 if 部分为 true ,则正则引擎会试图匹配 then 部分,否则引擎会试图匹配 else 部分。
需要记住的是,向前先后查看并不实际消耗任何字符 ,因此后面的 then else 部分的匹配时从 if 测试前的部分开始进行尝试。
 
16.   为正则表达式添加注释
在正则表达式中添加注释的语法是: <<(?#comment)>>
例:为用于匹配有效日期的正则表达式添加注释:
 (?#year)(19|20)/d/d[- /.](?#month)(0[1-9]|1[012])[- /.](?#day)(0[1-9]|[12][0-9]|3[01])
 
,(?=([^']*'[^']*')*(?![^']*'))

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值