正则表达式学习参考
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*)”
因为零宽度的子表达式是非互斥的,最后匹配的都是同一个位置,所以先后顺序是不影响最后的匹配结果的,可以任意组合,只是习惯上把逆序环视写在左侧,顺序环视写在右侧。