正则表达式学习参考

正则表达式学习参考

1       概述
正则表达式(Regular Expression)是一种匹配模式,描述的是一串文本的特征。

正如自然语言中“高大”、“坚固”等词语抽象出来描述事物特征一样,正则表达式就是字符的高度抽象,用来描述字符串的特征。

正则表达式(以下简称正则,Regex)通常不独立存在,各种编程语言和工具作为宿主语言提供对正则的支持,并根据自身语言的特点,进行一定的剪裁或扩展。

正则入门很容易,有限的语法规则很容易掌握,但是目前正则的普及率并不高,主要是因为正则的流派众多,各种宿主语言提供的文档都过多的关注于自身的一些细节,而这些细节通常是初学者并不需要关注的。

当然,如果想要深入的了解正则表达式,这些细节又是必须被关注的,这是后话,让我们先从正则的基础开始,进入正则表达式的世界。

2       正则表达式基础
2.1     基本概念
2.1.1  字符串组成


对于字符串“a5”,是由两个字符“a”、“5”以及三个位置组成的,这一点对于正则表达式的匹配原理理解很重要。

2.1.2  占有字符和零宽度
正则表达式匹配过程中,如果子表达式匹配到的是字符内容,而非位置,并被保存到最终的匹配结果中,那么就认为这个子表达式是占有字符的;如果子表达式匹配的仅仅是位置,或者匹配的内容并不保存到最终的匹配结果中,那么就认为这个子表达式是零宽度的。

占有字符还是零宽度,是针对匹配的内容是否保存到最终的匹配结果中而言的。

占有字符是互斥的,零宽度是非互斥的。也就是一个字符,同一时间只能由一个子表达式匹配,而一个位置,却可以同时由多个零宽度的子表达式匹配。

2.1.3  正则表达式构成
正则表达式由两种字符构成。一种是在正则表达式中具体特殊意义的“元字符”,另一种是普通的“文本字符”。

元字符可以是一个字符,如“^”,也可以是一个字符序列,如“/w”。

2.2     元字符(Meta Character)
2.2.1  […] 字符组(Character Classes)
字符组可以匹配[ ]中包含的任意一个字符。虽然可以是任意一个,但只能是一个。

字符组支持由连字符“-”来表示一个范围。当“-”前后构成范围时,要求前面字符的码位小于后面字符的码位。

[^…] 排除型字符组。排除型字符组表示任意一个未列出的字符,同样只能是一个。排除型字符组同样支持由连字符“-”来表示一个范围。

表达式
 说明
 
[abc]
 表示“a”或“b”或“c”
 
[0-9]
 表示0~9中任意一个数字,等价于[0123456789]
 
[/u4e00-/u9fa5]
 表示任意一个汉字
 
[^a1<]
 表示除“a”、“1”、“<”外的其它任意一个字符
 
[^a-z]
 表示除小写字母外的任意一个字符
 


举例:

“[0-9][0-9]”在匹配“Windows 2003”时,匹配成功,匹配的结果为“20”。

“[^inW]”在匹配“Windows 2003”时,匹配成功,匹配的结果为“d”。

2.2.2  常见字符范围缩写
对于一些常用的字符范围,如数字等,由于非常常用,即使使用[0-9]这样的字符组仍显得麻烦,所以定义了一些元字符,来表示常见的字符范围。

表达式
 说明
 
/d
 任意一个数字,相当于[0-9],即0~9 中的任意一个
 
/w
 任意一个字母或数字或下划线,相当于[a-zA-Z0-9_]
 
/s
 任意空白字符,相当于[ /r/n/f/t/v]
 
/D
 任意一个非数字字符,/d取反,相当于[^0-9]
 
/W
 /w取反,相当于[^a-zA-Z0-9_]
 
/S
 任意非空白字符,/s取反,相当于[^ /r/n/f/t/v]
 


举例:

“/w/s/d”在匹配“Windows 2003”时,匹配成功,匹配的结果为“s 2”。

2.2.3  . 小数点
小数点可以匹配除“/n”以外的任意一个字符。如果要匹配包括“/n”在内的所有字符,一般用[/s/S],或者是用“.”加(?s)匹配模式来实现。

表达式
 说明
 
.
 匹配除了换行符 /n 以外的任意一个字符
 


2.2.4  其它元字符
表达式
 说明
 
^
 匹配字符串开始的位置,不匹配任何字符
 
$
 匹配字符串结束的位置,不匹配任何字符
 
/b
 匹配单词边界,不匹配任何字符
 


举例:

“^a”在匹配“cba”时,匹配失败,因为表达式要求开始位置后面是字符“a”,而“cba”显然是不满足的。

“/d$”在匹配“123”时,匹配成功,匹配结果为“3”,这个表达式要求匹配结尾处的数字,如果结尾处不是数字,如“123abc”,则是匹配失败的。

2.2.5  转义字符
一些不可见字符,或是在正则中具有特殊意义的元字符,如想匹配字符本身,需要用“/”对其进行转义。

 

表达式
 说明
 
/r,/n
 回车和换行
 
//
 匹配“/”本身
 
/^,/$,/.
 分别匹配“^”、“$”和“.”
 


以下字符在匹配其本身时,通常需要进行转义。在实际应用中,根据具体情况,需要转义的字符可能不止如下所列字符

 .  $  ^  {  [  (  |  )  *  +  ?  /

2.2.6  量词(Quantifier)
量词表示一个子表达式可以匹配的次数。量词可以用来修饰一个字符、字符组,或是用()括起来的子表达式。一些常用的量词被定义成独立的元字符。

表达式
 说明
 举例
 
{m}
 表达式匹配m次
 “/d{3}”相当于“/d/d/d ”

“(abc){2}”相当于“abcabc”
 
{m,n}
 表达式匹配最少m次,最多n次
 “/d{2,3}”可以匹配“12”或“321”等2到3位的数字
 
{m,}
 表达式至少匹配m次
 “[a-z]{8,}”表示至少8位以上的字母
 
?
 表达式匹配0次或1次,相当于{0,1}
 “ab?”可以匹配“a”或“ab”
 
*
 表达式匹配0次或任意多次,相当于{0,}
 “<[^>]*>”中“[^>]*”表示0个或任意多个不是“>”的字符
 
+
 表达式匹配1次或意多次,至少1次,相当于{1,}
 “/d/s+/d”表示两个数字中间,至少有一个以上的空白字符
 


注意:在不是动态生成的正则表达式中,不要出现“{1}”这样的量词,如“/w{1}”在结果上等价于“/w”,但是会降低匹配效率和可读性,属于画蛇添足的做法。

2.2.7  分支结构(Alternation)
当一个字符串的某一子串具有多种可能时,采用分支结构来匹配,“|”表示多个子表达式之间“或”的关系,“|”是以()限定范围的,如果在“|”的左右两侧没有()来限定范围,那么它的作用范围即为“|”左右两侧整体。

表达式
 说明
 
|
 多个子表达式之间取“或”的关系
 


举例:

“^aa|b$”在匹配“cccb”时,是可以匹配成功的,匹配的结果是“b”,因为这个表达式表示匹配“^aa”或“b$”,而“b$”在匹配“cccb ”时是可以匹配成功的。

“^(aa|b)$”在区配“cccb”时,是匹配失败的,因为这个表达式表示在“开始”和“结束”位置之间只能是“aa”或“b”,而“cccb”显然是不满足的。

3       正则表达式进阶
3.1     捕获组(Capture Group)
捕获组就是把正则表达式中子表达式匹配的内容,保存到内存中以数字编号或手动命名的组里,以供后面引用。

表达式
 说明
 
(Expression)
 普通捕获组,将子表达式Expression匹配的内容保存到以数字编号的组里
 
(?<name> Expression)
 命名捕获组,将子表达式Expression匹配的内容保存到以name命名的组里
 


普通捕获组(在不产生歧义的情况下,简称捕获组)是以数字进行编号的,编号规则是以“(”从左到右出现的顺序,从1开始进行编号。通常情况下,编号为0的组表示整个表达式匹配的内容。

命名捕获组可以通过捕获组名,而不是序号对捕获内容进行引用,提供了更便捷的引用方式,不用关注捕获组的序号,也不用担心表达式部分变更会导致引用错误的捕获组。

3.2     非捕获组
一些表达式中,不得不使用( ),但又不需要保存( )中子表达式匹配的内容,这时可以用非捕获组来抵消使用( )带来的副作用。

表达式
 说明
 
(?:Expression)
 进行子表达式Expression的匹配,并将匹配内容保存到最终的整个表达式的区配结果中,但Expression匹配的内容不单独保存到一个组内
 


3.3     反向引用
捕获组匹配的内容,可以在正则表达式的外部程序中进行引用,也可以在表达式中进行引用,表达式中引用的方式就是反向引用。

反向引用通常用来查找重复的子串,或是限定某一子串成对出现。

表达式
 说明
 
/1,/2
 对序号为1和2的捕获组的反向引用
 
/k<name>
 对命名为name的捕获组的反向引用
 


举例:

“(a|b)/1”在匹配“abaa”时,匹配成功,匹配到的结果是“aa”。“(a|b)”在尝试匹配时,虽然既可以匹配“a”,也可以匹配“b”,但是在进行反向引用时,对应()中匹配的内容已经是固定的了。

3.4     环视(Look Around)
环视只进行子表达式的匹配,匹配内容不计入最终的匹配结果,是零宽度的。

环视按照方向划分有顺序和逆序两种,按照是否匹配有肯定和否定两种,组合起来就有四种环视。环视相当于对所在位置加了一个附加条件。

 

表达式
 说明
 
(?<=Expression)
 逆序肯定环视,表示所在位置左侧能够匹配Expression
 
(?<!Expression)
 逆序否定环视,表示所在位置左侧不能匹配Expression
 
(?=Expression)
 顺序肯定环视,表示所在位置右侧能够匹配Expression
 
(?!Expression)
 顺序否定环视,表示所在位置右侧不能匹配Expression
 


举例:

“(?<=Windows )/d+”在匹配“Windows 2003”时,匹配成功,匹配结果为“2003”。我们知道“/d+”表示匹配一个以上的数字,而“(?<=Windows )”相当于一个附加条件,表示所在位置左侧必须为“Windows ”,它所匹配的内容并不计入匹配结果。同样的正则在匹配“Office 2003”时,匹配失败,因为这里任意一串数字子串的左侧都不是“Windows ”。

“(?!1)/d+”在匹配“123”时,匹配成功,匹配的结果为“23”。“/d+”匹配一个以上数字,但是附加条件“(?!1)”要求所在位置右侧不能是“1”,所以匹配成功的位置是“2”前面的位置。

3.5     忽略优先和匹配优先
或者叫做正则表达式匹配的贪婪与非贪婪模式。

标准量词修饰的子表达式,在可匹配可不匹配的情况下,总会先尝试进行匹配,称这种方式为匹配优先,或者贪婪模式。此前介绍的一些量词,“{m}”、“{m,n}”、“{m,}”、“?”、“*”和“+”都是匹配优先的。

一些NFA正则引擎支持忽略优先量词,也就是在标准量词后加一个“?”,此时,在可匹配可不匹配的情况下,总会先忽略匹配,只有在由忽略优先量词修饰的子表达式,必须进行匹配才能使整个表达式匹配成功时,才会进行匹配,称这种方式为忽略优先,或者非贪婪模式。忽略优先量词包括“{m}?”、“{m,n}?”、“{m,}?”、“??”、“*?”和“+?”。

举例:

源字符串:<div>aaa</div><div>bbb</div>

正则表达式1:<div>.*</div>      匹配结果:<div>aaa</div><div>bbb</div>

正则表达式2:<div>.*?</div>     匹配结果:<div>aaa</div>


[ ] 字符组(Character Classes)
 
    []能够匹配所包含的一系列字符中的任意一个。需要注意的是,[]虽然能匹配其中的任意一个字符,但匹配的结果只能是一个字符,不是多个。
    例如[abc]表示字符“a”或“b”或“c”。
 
    []支持用连字符“-”连接两个字符,来表示一个字符范围。需要注意的是,“-”前后的两个字符是有顺序的,即使用相同的编码时,后面的字符码位应大于或等于前面字符的码位。
    例如[a-z]表示任意一个小写字母。而在程序中使用[z-a]则会报“[x-y] 范围的顺序颠倒”这样的异常。
 
    大部分在正则中有特殊意义、在匹配其本身时需转义的字符,在[]内是不需要转义的。必须转义的只有“/”、“[”和“]”,而“^”出现在[]开始位置,“-”前后构成范围区间时,需要转义,出现在其它位置不需要转义,例如[/^.$^{/[(|)*+?-//]
    在.NET中,不构成歧义的情况下,“[”和“]”可以不用转义,程序也可以得到预期结果,但是这种写法可读性较差,而且出现错误时不易排查,因此不推荐这种写法
    举例:Regex reg = new Regex("[^]]+");  //不推荐
 
    .NET的字符组中支持集合减法,语法[set1-[set2]],例如[a-z-[aeiou]]表示除元音外的小写字母。但是除非你很了解这种语法的支持范围,以及确实有必要这样做,否则不要轻易使用这种可读性较差的语法。事实上完全可以用多个范围区间[b-df-hj-np-tv-z],在区间较多,导致可读性差、容易出错的情况下,可以使用正向预搜索来实现以上需求,(?![aeiou])[a-z],这种语法规则,支持范围要大得多,可读性也要好一些。
 
 
常见错误用法:
    因为[]本身表示的就是字符之间“或”的关系,因此在[]中使用“|”来表示“或”的关系是错误的。
举例:[a|b|c]表示的是“a”或“b”或“c”或“|”中的任意一个字符。
 
举例
       源字符串:ab|ac
       正则表达式:[a|b|c]+
       匹配结果:ab|ac
 
 
[^ ] 排除型字符组
 
    [^ ]表示匹配任意一个未列举的字符,同样的,匹配的结果也只能是一个字符。
    例如[^abc]表示除字符“a”、“b”、“c”外的任意一个字符。
    [^ ]也支持字符分组,例如[^0-9]表示除数字外的任意一个字符。
 
    新手最容易犯的错误就是,用[^abc]或[^(abc)]这样的表达式来匹配不包含“abc”子字符串的字符串

    
举例
       源字符串:string yourStr = "<aaa>bbb<abc>ccc<ddd>";
       规则描述:取出yourStr中格式为<...>,但<>中不是abc的内容
       预期结果:<aaa>、<ddd>
       错误写法:<[^abc]*>
       正确写法:<(?!abc>)[^>]*>
      

    还有一点并不常见,/b在字符组外表示单词边界,但是在字符组内[/b]表示退格符


  捕获组(capture group) 收藏
捕获组就是把正则表达式中子表达式匹配的内容,保存到内存中以数字编号或手动命名的组里,方便后面引用
 
捕获组有两种形式
一种是普通的捕获组,不产生歧义的情况下,后面简称捕获组,语法规则:(expression);
另一种是命名捕获组,语法规则:(?<name>expression)或者(?'name'expression),这两种写法是等价的。
 
1、编号规则
如果没有显式为捕获组命名,即没有使用命名捕获组,那么需要按数字顺序来访问所有捕获组
在只有普通捕获组的情况下,捕获组的编号是按照“(”出现的顺序,从左到右编号的
 
(/d{4})-(/d{2}-(/d/d))
1        1 2          3    3 2
 
上面的正则表达式可以用来匹配格式为yyyy-MM-dd的日期,为了在下表中得以区分,采用了/d{2}和/d/d两种写法
还有一个默认编号为0的组,表示的是正则表达式的整体
用以上正则表达式匹配字符串:2008-12-31
匹配结果为:
编号 命名 捕获组 匹配内容
0   (/d{4})-(/d{2}-(/d/d)) 2008-12-31
1   (/d{4}) 2008
2   (/d{2}-(/d/d)) 12-31
3   (/d/d) 31

 
如果对组进行了显式命名,即命名捕获组,那么捕获的内容可以通过组名称来引用
但是如果正则表达式中既使用了普通捕获组,也使用了命名捕获组,那么捕获组的编号就要特别注意,编号的规则是先对普通捕获组进行编号,再对命名捕获组进行编号
 
(/d{4})-(?<date>/d{2}-(/d/d))
1        1 3                       2     23
 
用以上正则表达式匹配字符串:2008-12-31
匹配结果为:
编号 命名 捕获组 匹配内容
0   (/d{4})-(/d{2}-(/d/d)) 2008-12-31
1   (/d{4}) 2008
2   (/d/d) 31
3 date (?<date>/d{2}-(/d/d)) 12-31

 
 
2、捕获组的引用
对捕获组的引用一般有以下几种
a) 正则表达式中,对前面捕获组捕获的内容进行引用,称为反向引用
b) 正则表达式中,(?(表达式)true|false)的条件表达式
c) 在程序中,对捕获组捕获内容的引用
 
反向引用
对于普通捕获组的引用,语法规则为:/k<num>,通常简写为/num,其中num是十进制的数字,即捕获组的编号
对于命名捕获组的引用,语法规则为:/k<name>或者/k'name'


  正则基础之——小数点 收藏
 
小数点可以匹配除了换行符“/n”以外的任意一个字符,如要匹配小数点本身,用“/”进行转义“/.”。

 

一些细节

 

对于使用传统NFA引擎的大多数语言和工具,如Java、.NET来说,“.”的匹配范围是匹配除了换行符“/n”以外的任意一个字符。

但是对于javascript来说有些特殊,由于各浏览器的解析引擎不同,“.”的匹配范围也有所不同,对于Trident内核的浏览器,如IE来说,“.”同样是匹配除了换行符“/n”以外的任意一个字符,但是对于其它内核的浏览器,如Firefox、Opera、Chrome来说,“.”是匹配除了回车符“/r”和换行符“/n”以外的任意一个字符。


 


关于此细节的一些猜测
 


view plaincopy to clipboardprint?
<script type="text/javascript">  
    document.write(/./.test("/r") + "<br />");  
    document.write(/./.test("/n") + "<br />");  
</script>  
//IE下输出  
true 
false 
//Firefox、Opera、Chrome下输出  
false 
false 
<script type="text/javascript">
    document.write(/./.test("/r") + "<br />");
    document.write(/./.test("/n") + "<br />");
</script>
//IE下输出
true
false
//Firefox、Opera、Chrome下输出
false
false

大概测了一下,Trident、Presto和Gecko应该都是采用的传统NFA引擎,而webkit至少是支持传统NFA引擎的,但是又与传统NFA引擎表现的不太一样,估计不是做了高级优化的传统NFA引擎,就是DFA/NFA混合引擎。 
由于Windows下支持“/r”和“/n”,而UNIX下只支持“/n”,所以我猜想可能是由于其它浏览器引擎并不来自于Windows,所以没有提供对“/r”的支持,从而导致在正则中“.”也不匹配“/r”吧。没做深入研究,只是一些猜测罢了。
 
常见应用误区
注意

在匹配多行时,不要试图用“[./n]”来匹配任意字符,这种写法表示的只是小数点和换行符两个字符中的一个,可以使用“(.|/n)”,但一般不这样用,这样写可读性差,效率也低,一般用“[/s/S]”,或者是用“.”加(?s)匹配模式来达到这一效果。

 
举例
需求描述:匹配<td>标签中的内容
源字符串:<td>This is a test line.
              Another line. </td>
匹配结果:<td>This is a test line.
              Another line. </td>
正则表达式一:<td>[/s/S]*</td>
正则表达式二:(?s)<td>.*</td>
 
匹配效率测试
以下为测试用字符串,即下面richTextBox1.Text里输入的内容(取自CSDN首页):

测试字符串
<link href="images/favicon.ico" rel="SHORTCUT ICON" />
 
<title>CSDN.NET - 中国领先的IT技术社区,为IT专业技术人员提供最全面的信息传播和服务平台</title>
<script language='JavaScript' type='text/javascript' src='http://www.csdn.net/ggmm/csdn_ggmm.js'></script>
<script type="text/javascript" src="http://counter.csdn.net/a/js/AreaCounter.js%22%3E%3C/script>
<script type="text/javascript">

测试代码:   

view plaincopy to clipboardprint?
string yourStr = richTextBox1.Text;  
StringBuilder src = new StringBuilder(4096);  
for (int i = 0; i < 10000; i++)  
{  
    src.Append(yourStr);  
}  
string strData = src.ToString();  
List<Regex> reg = new List<Regex>();  
reg.Add(new Regex(@"[/s/S]"));  
reg.Add(new Regex(@"[/w/W]"));  
reg.Add(new Regex(@"[/d/D]"));  
reg.Add(new Regex(@"(.|/n)"));  
reg.Add(new Regex(@"(?s)."));  
string test = string.Empty;  
Stopwatch stopW = new Stopwatch();  
foreach (Regex re in reg)  
{  
    stopW.Reset();  
    stopW.Start();  
    test = strData;  
    test = re.Replace(test, "");  
    stopW.Stop();  
    richTextBox2.Text += "正则表达式:" + re.ToString().PadRight(10) + "执行时间:" + stopW.ElapsedMilliseconds.ToString() + " ms";  
    richTextBox2.Text += "/n---------------------------------------/n";  

string yourStr = richTextBox1.Text;
StringBuilder src = new StringBuilder(4096);
for (int i = 0; i < 10000; i++)
{
    src.Append(yourStr);
}
string strData = src.ToString();
List<Regex> reg = new List<Regex>();
reg.Add(new Regex(@"[/s/S]"));
reg.Add(new Regex(@"[/w/W]"));
reg.Add(new Regex(@"[/d/D]"));
reg.Add(new Regex(@"(.|/n)"));
reg.Add(new Regex(@"(?s)."));
string test = string.Empty;
Stopwatch stopW = new Stopwatch();
foreach (Regex re in reg)
{
    stopW.Reset();
    stopW.Start();
    test = strData;
    test = re.Replace(test, "");
    stopW.Stop();
    richTextBox2.Text += "正则表达式:" + re.ToString().PadRight(10) + "执行时间:" + stopW.ElapsedMilliseconds.ToString() + " ms";
    richTextBox2.Text += "/n---------------------------------------/n";
}
 
 
测试结果:

测试分两组进行,程序执行前内存占用为921M

一组是未使用量词,每次仅替换一个字符,执行时间如下,占用内存938M

  测试结果(不带量词)
正则表达式:[/s/S]     执行时间:2651 ms
---------------------------------------
正则表达式:[/w/W]    执行时间:2515 ms
---------------------------------------
正则表达式:[/d/D]     执行时间:2187 ms
---------------------------------------
正则表达式:(.|/n)      执行时间:2470 ms
---------------------------------------
正则表达式:(?s).       执行时间:1969 ms
---------------------------------------
 
另一组使用了量词,一次替换所有字符,执行时间如下,占用内存1128M
  测试结果(带量词)
正则表达式:[/s/S]+     执行时间:249 ms
---------------------------------------
正则表达式:[/w/W]+   执行时间:348 ms
---------------------------------------
正则表达式:[/d/D]+    执行时间:198 ms
---------------------------------------
正则表达式:(.|/n)+     执行时间:879 ms
---------------------------------------
正则表达式:(?s).+      执行时间:113 ms
---------------------------------------
  
测试结果分析:

匹配效率最高的是采用了Singleline这种匹配模式的“.”

其次是“[/d/D]”,而“(.|/n)”的匹配效率最低

“[/s/S]”的匹配效率居中,只是习惯上用得多些

 

注:由于各语言支持的引擎不同,即使使用同一种引擎,对正则做的优化也有所不同,所以以上性能测试结论可能仅适用于.NET。

环视(Lookaround)

1       环视基础
环视只进行子表达式的匹配,不占有字符,匹配到的内容不保存到最终的匹配结果,是零宽度的。环视匹配的最终结果就是一个位置。

环视的作用相当于对所在位置加了一个附加条件,只有满足这个条件,环视子表达式才能匹配成功。

环视按照方向划分有顺序和逆序两种,按照是否匹配有肯定和否定两种,组合起来就有四种环视。顺序环视相当于在当前位置右侧附加一个条件,而逆序环视相当于在当前位置左侧附加一个条件。

表达式
 说明
 
(?<=Expression)
 逆序肯定环视,表示所在位置左侧能够匹配Expression
 
(?<!Expression)
 逆序否定环视,表示所在位置左侧不能匹配Expression
 
(?=Expression)
 顺序肯定环视,表示所在位置右侧能够匹配Expression
 
(?!Expression)
 顺序否定环视,表示所在位置右侧不能匹配Expression
 

 对于环视的叫法,有的文档里叫预搜索,有的叫什么什么断言的,这里使用了更多人容易接受的《精通正则表达式》中“环视”的叫法,其实叫什么无所谓,只要知道是什么作用就是了,就这么几个语法规则, 还是很容易记的

2       环视匹配原理
 环视是正则中的一个难点,对于环视的理解,可以从应用和原理两个角度理解,如果想理解得更清晰、深入一些,还是从原理的角度理解好一些,正则匹配基本原理参考 NFA引擎匹配原理。

上面提到环视相当于对“所在位置”附加了一个条件,环视的难点在于找到这个“位置”,这一点解决了,环视也就没什么秘密可言了。

顺序环视匹配过程
对于顺序肯定环视(?=Expression)来说,当子表达式Expression匹配成功时,(?=Expression)匹配成功,并报告(?=Expression)匹配当前位置成功。

对于顺序否定环视(?!Expression)来说,当子表达式Expression匹配成功时,(?!Expression)匹配失败;当子表达式Expression匹配失败时,(?!Expression)匹配成功,并报告(?!Expression)匹配当前位置成功;

顺序肯定环视的例子已在NFA引擎匹配原理中讲解过了,这里再讲解一下顺序否定环视。

 

 

源字符串:aa<p>one</p>bb<div>two</div>cc

正则表达式:<(?!/?p/b)[^>]+>

这个正则的意义就是匹配除<p…>或</p>之外的其余标签。

匹配过程:

 

首先由字符“<”取得控制权,从位置0开始匹配,由于“<”匹配“a”失败,在位置0处整个表达式匹配失败,第一次迭代匹配失败,正则引擎向前传动,由位置1处开始尝试第二次迭代匹配。

重复以上过程,直到位置2,“<”匹配“<”成功,控制权交给“(?!/?p/b)”;“(?!/?p/b)”子表达式取得控制权后,进行内部子表达式的匹配。首先由“/?”取得控制权,尝试匹配“p”失败,进行回溯,不匹配,控制权交给“p”;由“p”来尝试匹配“p”,匹配成功,控制权交给“/b”;由“/b”来尝试匹配位置4,匹配成功。此时子表达式匹配完成,“/?p/b”匹配成功,那么环视表达式“(?!/?p/b)”就匹配失败。在位置2处整个表达式匹配失败,新一轮迭代匹配失败,正则引擎向前传动,由位置3处开始尝试下一轮迭代匹配。

在位置8处也会遇到一轮“/?p/b”匹配“/p”成功,而导致环视表达式“(?!/?p/b)”匹配失败,从而导致整个表达式匹配失败的过程。

重复以上过程,直到位置14,“<”匹配“<”成功,控制权交给“(?!/?p/b)”;“/?”尝试匹配“d”失败,进行回溯,不匹配,控制权交给“p”;由“p”来尝试匹配“d”,匹配失败,已经没有备选状态可供回溯,匹配失败。此时子表达式匹配完成,“/?p/b”匹配失败,那么环视表达式“(?!/?p/b)”就匹配成功。匹配的结果是位置15,然后控制权交给“[^>]+”;由“[^>]+”从位置15进行尝试匹配,可以成功匹配到“div”,控制权交给“>”;由“>”来匹配“>”。

此时正则表达式匹配完成,报告匹配成功。匹配结果为“<div>”,开始位置为14,结束位置为19。其中“<”匹配“<”,“(?!/?p/b)”匹配位置15,“[^>]+”匹配字符串“div”,“>”匹配“>”。

逆序环视基础
对于逆序肯定环视(?<=Expression)来说,当子表达式Expression匹配成功时,(?<=Expression)匹配成功,并报告(?<=Expression)匹配当前位置成功。

对于逆序否定环视(?<!Expression)来说,当子表达式Expression匹配成功时,(?<!Expression)匹配失败;当子表达式Expression匹配失败时,(?<!Expression)匹配成功,并报告(?<!Expression)匹配当前位置成功;

顺序环视相当于在当前位置右侧附加一个条件,所以它的匹配尝试是从当前位置开始的,然后向右尝试匹配,直到某一位置使得匹配成功或失败为止。而逆序环视的特殊处在于,它相当于在当前位置左侧附加一个条件,所以它不是在当前位置开始尝试匹配的,而是从当前位置左侧某一位置开始,匹配到当前位置为止,报告匹配成功或失败。

顺序环视尝试匹配的起点是确定的,就是当前位置,而匹配的终点是不确定的。逆序环视匹配的起点是不确定的,是当前位置左侧某一位置,而匹配的终点是确定的,就是当前位置。

所以顺序环视相对是简单的,而逆序环视相对是复杂的。这也就是为什么大多数语言和工具都提供了对顺序环视的支持,而只有少数语言提供了对逆序环视支持的原因。

JavaScript中只支持顺序环视,不支持逆序环视。

Java中虽然顺序环视和逆序环视都支持,但是逆序环视只支持长度确定的表达式,逆序环视中量词只支持“?”,不支持其它长度不定的量词。长度确定时,引擎可以向左查找固定长度的位置作为起点开始尝试匹配,而如果长度不确定时,就要从位置0开始尝试匹配,处理的复杂度是显而易见的。

目前只有.NET中支持不确定长度的逆序环视。

逆序环视匹配过程


源字符串:<div>a test</div>

正则表达式:(?<=<div>)[^<]+(?=</div>)

这个正则的意义就是匹配<div>和</div>标签之间的内容,而不包括<div>和</div>标签本身。

匹配过程:

首先由“(?<=<div>)”取得控制权,从位置0开始匹配,由于位置0是起始位置,左侧没有任何内容,所以“<div>”必然匹配失败,从而环视表达式“(?<=<div>)”匹配失败,导致整个表达式在位置0处匹配失败。第一轮迭代匹配失败,正则引擎向前传动,由位置1处开始尝试第二次迭代匹配。

直到传动到位置5,“(?<=<div>)”取得控制权,向左查找5个位置,由位置0开始匹配,由“<div>”匹配“<div>”成功,从而“(?<=<div>)”匹配成功,匹配的结果为位置5,控制权交给“[^<]+”;“[^<]+”从位置5开始尝试匹配,匹配“a test”成功,控制权交给“(?=</div>)”;由“</div>”匹配“</div>”成功,从而“(?=</div>)”匹配成功,匹配结果为位置11。

此时正则表达式匹配完成,报告匹配成功。匹配结果为“a test”,开始位置为5,结束位置为11。其中“(?<=<div>)”匹配位置5,“[^<]+”匹配“a test”,“(?=</div>)”匹配位置11。

逆序否定环视的匹配过程与上述过程类似,区别只是当Expression匹配失败时,逆序否定表达式(?<!Expression)才匹配成功。

到此环视的匹配原理已基本讲解完,环视也就没有什么秘密可言了,所需要的,也只是多加练习而已。

3       环视应用
今天写累了,暂时就给出一个环视的综合应用实例吧,至于环视的应用场景和技巧,后面再整理。

需求:数字格式化成用“,”的货币格式。

正则表达式:(?<=/d)(?<!/./d*)(?=(?:/d{3})+(?:/./d+|$))

测试代码:

double[] data = new double[] { 0, 12, 123, 1234, 12345, 123456, 1234567, 123456789, 1234567890, 12.345, 123.456, 1234.56, 12345.6789, 123456.789, 1234567.89, 12345678.9 };

foreach (double d in data)

{

    richTextBox2.Text += "源字符串:" + d.ToString().PadRight(15) + "格式化:" + Regex.Replace(d.ToString(), @"(?<=/d)(?<!/./d*)(?=(?:/d{3})+(?:/./d+|$))", ",") + "/n";

}

输出结果:

源字符串:0              格式化:0

源字符串:12             格式化:12

源字符串:123            格式化:123

源字符串:1234           格式化:1,234

源字符串:12345          格式化:12,345

源字符串:123456         格式化:123,456

源字符串:1234567        格式化:1,234,567

源字符串:123456789      格式化:123,456,789

源字符串:1234567890     格式化:1,234,567,890

源字符串:12.345         格式化:12.345

源字符串:123.456        格式化:123.456

源字符串:1234.56        格式化:1,234.56

源字符串:12345.6789     格式化:12,345.6789

源字符串:123456.789     格式化:123,456.789

源字符串:1234567.89     格式化:1,234,567.89

源字符串:12345678.9     格式化:12,345,678.9

实现分析:

首先根据需求可以确定是把一些特定的位置替换为“,”,接下来就是分析并找到这些位置的规律,并抽象出来以正则表达式来表示。

1、   这个位置的左侧必须为数字

2、   这个位置右侧到出现“.”或结尾为止,必须是数字,且数字的个数必须为3的倍数

3、   这个位置左侧相隔任意个数字不能出现“.”

由以上三条,就可以完全确定这些位置,只要实现以上三条,组合一下正则表达式就可以了。

根据分析,最终匹配的结果是一个位置,所以所有子表达式都要求是零宽度。

1、   是对当前所在位置左侧附加的条件,所以要用到逆序环视,因为要求必须出现,所以是肯定的,符合这一条件的子表达式即为“(?<=/d)”

2、   是对当前所在位置右侧附加的条件,所以要用到顺序环视,也是要求出现,所以是肯定的,是数字,且个数为3的倍数,即“(?=(?:/d{3})*)”,到出现“.”或结尾为止,即“(?=(?:/d{3})*(?:/.|$))”

3、   是对当前所在位置左侧附加的条件,所以要用到逆序环视,因为要求不能出现,所以是否定的,即“(?<!/./d*)”

因为零宽度的子表达式是非互斥的,最后匹配的都是同一个位置,所以先后顺序是不影响最后的匹配结果的,可以任意组合,只是习惯上把逆序环视写在左侧,顺序环视写在右侧。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

!chen

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值