.net中的正则表达式使用高级技巧
前言
一、本系列文章不讲述基本的正则语法,这些可以在微软的 JS 帮助文档中找到,也可以 Google 一下
二、写系列文章的原因
1 、正则很有用,而且经常要用
2 、正则的一些高级用法有相当一部分人还没有理解和掌握
3 、刚好又在网上看到了一篇文章错误的使用了正则式,使我有了写本文的冲动
4 、本系列文章的大部分知识可同时适用于 .net 语言, JavaScript 等
三、本系列文章特点:尽量使用小例子来说明相对难懂而很多正则书籍都没有说清的正则语法
四、本系列文章内容:替换的高级语法,内联表达式选项,组,反向引用,正声明,负声明,正反声明,负反声明,非回溯匹配,判断式, .net 正则引擎特点等
因为 .net 的基本正则语法和 Perl5 基本相同,所以基本语法你可以去下载一下 M$ 的 JS 帮助文档,上面有详细的说明 /d 表示什么, {,5} 表示什么, /[ 表示什么……,这里我只想提醒大家一点,为了避免和反向引用相冲突,在你用 /nn 表示八进制的 ASCII 码时,请在 / 后加 0 ,就是说,/40在表示ASCII码时,请这样写/040。
替换
Regex 类有一个静态的 Replace 方法,其实例也有一个 Replace 方法,这个方法很强大,因为它可以传入一个 delegate ,这样,你可以自定义每次捕获匹配时,如何处理捕获的内容。
{
string s = " 1 12 3 5 " ;
s = Regex.Replace(s, @" /d+ " , new MatchEvaluator(CorrectString),RegexOptions.Compiled | RegexOptions.IgnoreCase);
Console.WriteLine(s);
Console.ReadLine();
}
private static string CorrectString(Match match)
{
string matchValue = match.Value;
if (matchValue.Length == 1 )
matchValue = " 0 " + matchValue;
return matchValue;
}
以上这段代码说明了如果使用 delegate MatchEvaluator 来处理正则的 Match 结果,该代码返回 "01 12 03 05" 。 Replace 方法除了使用 delegate 来处理捕获的 Match ,还可以用字符串来替换 Match 的结果,而用字符串来替换 Match 结果除了把 Match 结果静态的替换成一个固定的文本外,还可以使用以下语法来更方便的实现你需要的功能:
$number | 把匹配的第 number 组替换成替换表达式,还有这句话怎么写也表达不清楚意思,还是来个例子吧:
public static void
Main() { string s = " 1 12 3 5 " ; s = Regex.Replace(s, @" (/d+)(?#这个是注释) " , " 0$1 " ,RegexOptions.Compiled | RegexOptions.IgnoreCase); Console.WriteLine(s); Console.ReadLine(); } 这段代码返回的是 “ 01 012 03 05 ”
就是说,对组一的每个匹配结果都用 "0$1" 这个表达式来替换, "0$1" 中 "$1" 由组 1 匹配的结果代入 |
${name}
把匹配的组名为 "name" 的组替换成表达式,
上例的 Regex expression 改成 @"(?<name>/d+)(?# 这个是注释 )" 后面的替换式改为 "0${name}" 结果是一样的
$$
做 $ 的转义符,如上例表达式改成 @"(?<name>/d+)(?# 这个是注释 )" 和 "$$${name}" ,则结果为 "$1 $12 $3 $5"
$&
替换整个匹配
$`
替换匹配前的字符
$'
替换匹配后的字符
$+
替换最后匹配的组
$_
替换整个字符串
后面的选项,大家自己写个例子体味一下。
* 注 , 上例中的 (?# 这个是注释 ) 说明了正则的内联注释语法为 (?#)
表达项选项
正则表达式选项 RegexOptions 有如下一下选项,详细说明请参考联机帮助
RegexOptions 枚举值 | 内联标志 | 简单说明 |
ExplicitCapture | n | 只有定义了命名或编号的组才捕获 |
IgnoreCase | i | 不区分大小写 |
IgnorePatternWhitespace | x | 消除模式中的非转义空白并启用由 # 标记的注释。 |
MultiLine | m | 多行模式,其原理是修改了 ^ 和 $ 的含义 |
SingleLine | s | 单行模式,和 MultiLine 相对应 |
这里我提到内联标志,是因为相对于用 RegexOptions 在 new Regex 时定义 Regex 表达式的全局选项来说,内联标志可以更小粒度(以组为单位)的定义匹配选项,从而更方便表达我们的思想
语法是这样的: (?i:expression) 为定义一个选项, (?-i:expression) 为删除一个选项, (?i-s:expression) 则定义 i ,删除 s, 是的,我们可以一次定义很多个选项。这样,通过内联选项,你就可以在一个 Regex 中定义一个组为匹分大小写的,一个组不匹分大小写的,是不是很方便呢 ?
更多精彩内容,请看下回分解,
下一篇: .net中的正则表达式使用高级技巧 (二)
.net中的正则表达式使用高级技巧 (三)
.net中的正则表达式使用高级技巧 (四)
正则表达式中的组是很重要的一个概念,它是我们通向高级正则应用的的桥梁
组的概念
一个正则表达式匹配结果可以分成多个部分,这就是组 (Group) 的目的。能够灵活的使用组后,你会发现 Regex 真是很方便,也很强大。
先举个例子
{
string s = " 2005-2-21 " ;
Regex reg = new Regex( @" (?<y>/d{4})-(?<m>/d{1,2})-(?<d>/d{1,2}) " ,RegexOptions.Compiled);
Match match = reg.Match(s);
int year = int .Parse(match.Groups[ " y " ].Value);
int month = int .Parse(match.Groups[ " m " ].Value);
int day = int .Parse(match.Groups[ " d " ].Value);
DateTime time = new DateTime(year,month,day);
Console.WriteLine(time);
Console.ReadLine();
}
以上的例子通过组来实现分析一个字符串,并把其转化为一个 DateTime 实例,当然,这个功能用 DateTime.Parse 方法就能很方便的实现。
在这个例子中,我把一次 Match 结果用 (?<name>) 的方式分成三个组 "y","m","d" 分别代表年、月、日。
现在我们已经有了组的概念了,再来看如何分组,很简单的,除了上在的办法,我们可以用一对括号就定义出一个组,比如上例可以改成
{
string s = " 2005-2-21 " ;
Regex reg = new Regex( @" (/d{4})-(/d{1,2})-(/d{1,2}) " ,RegexOptions.Compiled);
Match match = reg.Match(s);
int year = int .Parse(match.Groups[ 1 ].Value);
int month = int .Parse(match.Groups[ 2 ].Value);
int day = int .Parse(match.Groups[ 3 ].Value);
DateTime time = new DateTime(year,month,day);
Console.WriteLine(time);
Console.ReadLine();
}
从上例可以看出,第一个括号对包涵的组被自动编号为1,后面的括号依次编号为2、3……
{
string s = " 2005-2-21 " ;
Regex reg = new Regex( @" (?<2>/d{4})-(?<1>/d{1,2})-(?<3>/d{1,2}) " ,RegexOptions.Compiled);
Match match = reg.Match(s);
int year = int .Parse(match.Groups[ 2 ].Value);
int month = int .Parse(match.Groups[ 1 ].Value);
int day = int .Parse(match.Groups[ 3 ].Value);
DateTime time = new DateTime(year,month,day);
Console.WriteLine(time);
Console.ReadLine();
}
再看上例,我们用 (?< 数字 >) 的方式手工给每个括号对的组编号,(注意我定义 1 和 2 的位置时不是从左到右定义的)
通过以上三例,我们知道了给 Regex 定义 Group 的三种办法以及相应的引用组匹配结果的方式。
然后,关于组定义,还有两点请注意:
1 、因为括号用于定义组了,所以如果要匹配 "(" 和 ")" ,请使用 "/(" 和 "/)"( 关于所有特殊字符的定义,请查看相关 Regex expression 帮助文档 ) 。
2 、如果定义 Regex 时,使用了 ExplicitCapture 选项,则第二个例子不会成功,因为此选项要求显式定义了编号或名字的组才捕获并保存结果,如果你没有定义 ExplicitCapture 选项,而有时又定义了类式于 (A|B) 这样的部分在表达式,而这个 (A|B) 你又并不想捕获结果,那么可以使用“不捕获的组”语法,即定义成 (?:) 的方式,针对于 (A|B), 你可以这样来定义以达到不捕获并保存它到 Group 集合中的目的-- (?:A|B) 。
上面内容仅讨论了一般的组,组还有很多的花样,很多高级的功能,下一篇将试图带您体验一番其中洞天。
反向引用
反向引用,指把匹配出来的组引用到表达式本身其它地方,比如,在匹配 HTML 的标记时,我们匹配出一个 <a>, 我们要把匹配出来的 a 引用出来,用来找到 </a> ,这个时候就要用到反向引用。
语法
a 、反向引用编号的组,语法为 /number
b 、反向引用命名的组,语法为 /k<name>
举例
a 、匹配成对的 HTML 标签
b 、匹配两个两个重叠出现的字符
{
string s = " aabbc11asd " ;
Regex reg = new Regex( @" (/w)/1 " );
MatchCollection matches = reg.Matches(s);
foreach (Match m in matches)
Console.WriteLine(m.Value);
Console.ReadLine();
}
返回结果为 aa bb 11
辅助匹配组
以下几种组结构,括号中的 Pattern 都不作为匹配结果的一部分进行保存
1 、正声明 (?=)
涵义:括号中的模式必须出现在声明右侧,但不作为匹配的一部分
{
string s = " C#.net,VB.net,PHP,Java,JScript.net " ;
Regex reg = new Regex( @" [/w/#]+(?=/.net) " ,RegexOptions.Compiled);
MatchCollection mc = reg.Matches(s);
foreach (Match m in mc)
Console.WriteLine(m.Value);
Console.ReadLine();
// 输出 C# VB JScript
}
可以看到匹配引擎要求匹配.net,但却不把.net 放到匹配结果中
2 、负声明 (?!)
涵义:括号中的模式必须不出现在声明右侧
下例演示如何取得一个 <a> 标签对中的全部内容,即使其中包含别的 HTML tag 。
{
string newsContent = @" url:<a href=""1.html""><img src=""1.gif"">test<span style=""color:red;"">Regex</span></a>. " ;
Regex regEnd = new Regex( @" </s*a[^>]*>([^<]|<(?!/a))*</s*/a/s*> " ,RegexOptions.Multiline);
Console.WriteLine(regEnd.Match(newsContent).Value);
// Result: <a href="1.html"><img src="1.gif">test<span style="color:red;">Regex</span></a>
Console.ReadLine();
}
3、反向正声明 (?<=)
涵义:括号中的模式必须出现在声明左侧,但不作为匹配的一部分
4 、反向负声明 (?<!)
涵义:括号中的模式必须不出现在声明左侧
非回溯匹配
语法: (?>)
涵义:该组匹配后,其匹配的字符不能通过回溯用于后面的表达式的匹配。呵呵,光看这句话肯定搞不懂,我当初为了搞懂这个也花了不少的时间,还是通过实例来说明吧:
"www.csdn.net" 可以通过 @"/w+/.(.*)/./w+" 来匹配,却不能通过 @"/w+/.(?>.*)/./w+" 来匹配!为什么呢?
原因是正则匹配是贪婪的,匹配时它会尽可能多的匹配最多的结果,所以,上例两个正则式中的 .* 都会把 csdn.net 匹配完, 这个时候,第一个表达式在开始匹配时发现 /./w+ 没得字符给它匹配了,所以它会进行回溯,所谓回溯,就是把 .* 匹配的结果往回推,回推留出来的字符再用来匹配 /./w+, 直到 /./w+ 匹配成功,整个表达式返回成功的匹配结果。而第二个表达式,因使用的是非回溯匹配,所以, .* 匹配完后,不允许通过回溯来匹配 /./w+ ,所以整个表达式匹配失败。
请注意,回溯匹配是很浪费资源的一种匹配方式,所以,请尽量避免您的正则式要通过回溯来成功匹配,如上例, 可以换成@"/w+/.([^/.]+/.)+/w+" +" 。
Lazy 匹配
语法: ??,*?,+?,{n}?,{n,m}?
涵义:简单说,后面的这个 ?(lazy 符 ) 告诉正则引擎,它前面的表达式匹配到最短的匹配项就不用匹配下去了,如 ?? , ? 本身匹配 0-1 个匹配项,那么 ?? 就取最短的,匹配 0 个项就不匹配下去了,同理, *? 匹配 0 个, +? 匹配 1 个, {n}? 匹配 n 个, {n,m}? 匹配 n 个。当用 @”/w*?” 匹配 ”abcd” 时,会有 五次 成功匹配, 每次都匹配的结果都是空字符串 , 为什么会是 5 次呢 ,这是因为正则引擎在匹配一个表达式时是一个字符一个字符对比下去的,每成功匹配一次,就前进一下。
判断表达式
语法:
1 、 A|B ,这个是最基本的, A 或者 B ,其实这个不能算判断
2 、 (?(expression)yes-expression|no-expression), 其中 no-expression 为可选项,意为,如果 expression 成立,则要求匹配 yes-expression, 否则要求匹配 no-expression
3 、 (?(group-name)yes-expressioin|no-expression), 其中 no-expression 为可选项,意为,如果名为 group-name 的组匹配成功,则要求匹配 yes-expression, 否则要求匹配 no-expression
判断表达式还是很好理解的,唯有一点要注意: @"(?(A)A|B)" 不能匹配 "AA", 为什么呢 ? 要怎么样写才能匹配呢,大家先想想……
我们应该这样写 Regex: @”(?(A)AA|B)” ,请注意,判断式中的内容并不会做为 yes-expression 或 no-expression 表达式的一部分。
.net 的正则引擎工作特点
.net 的正则引擎工作方式大多数和我们“想当然”的方式一样,只是有几点要注意:
1 、 .NET Framework 正则表达式引擎尽可能的匹配多的字符(贪婪)。正是由于这一点,所以,不要用 @"<.*>(.*)</.*>" 这样的正则式来试图找出一个 HTML 文档中的所有 innerText 。(我也正是在网上看到有人这样写正则式才决定要写《正则表达式 高级技巧》的,呵呵)
2 、 .NET Framework 正则表达式引擎是回溯的正则表达式匹配器,它并入了传统的非确定性有限自动机 (NFA) 引擎(例如 Perl 、 Python 使用的引擎)。这使其有别于更快的、但功能更有限的纯正则表达式确定性有限自动机 (DFA) 引擎。 .NET Framework 正则表达式引擎尽量匹配成功,所以,当 @"/w+/.(.*)/./w+" 中的 .* 把 www. .csdn.net 中的 .csdn.net 都匹配完了,让后面的 /./w+ 没得字符去匹配时,引擎会进行回溯,以得到成功的匹配。
NET Framework 正则表达式引擎还包括了一组完整的语法,让程序员能够操纵回溯引擎。包括:
“惰性”限定符: ?? 、 *? 、 +? 、 {n,m}? 。这些惰性限定符指示回溯引擎首先搜索最少数目的重复。与之相反,普通的“贪婪的”限定符首先尝试匹配最大数目的重复。
从右到左匹配。这在从右到左而非从左到右搜索的情况下十分有用,或者在从模式的右侧部分开始搜索比从模式的左侧部分开始搜索更为有效的情况下十分有用。
3 、 .NET Framework 正则表达式引擎在 (expression1|expression2|expression3) 这样情况下, expression1 总是最先得到尝试,再依次是 expression2 和 expression3
{
string s = " THIN is a asp.net developer. " ;
Regex reg = new Regex( @" (/w{2}|/w{3}|/w{4}) " ,RegexOptions.Compiled | RegexOptions.IgnoreCase);
MatchCollection mc = reg.Matches(s);
foreach (Match m in mc)
Console.WriteLine(m.Value);
Console.ReadLine();
}
输出结果是 : ‘ TH’ ‘IN’ ‘is’ ‘as’ ‘ne’ ‘de’ ‘ve’ ‘lo’ ‘pe’
附表
转义符 | 说明 |
一般字符 | 除 .$ ^ { [ ( | ) * + ? / 外,其他字符与自身匹配。 |
/a | 与响铃(警报) /u0007 匹配。 |
/b | 在正则表达式中, /b 表示单词边界(在 /w 和 /W 之间),不过,在 [] 字符类中, /b 表示退格符。在替换模式中, /b 始终表示退格符。 |
/t | 与 Tab 符 /u0009 匹配。 |
/r | 与回车符 /u000D 匹配。 |
/v | 与垂直 Tab 符 /u000B 匹配。 |
/f | 与换页符 /u000C 匹配。 |
/n | 与换行符 /u000A 匹配。 |
/e | 与 Esc 符 /u001B 匹配。 |
/040 | 将 ASCII 字符匹配为八进制数(最多三位);如果没有前导零的数字只有一位数或者与捕获组号相对应,则该数字为后向引用。例如,字符 |
/x20 | 使用十六进制表示形式(恰好两位)与 ASCII 字符匹配。 |
/cC | 与 ASCII 控制字符匹配;例如, |
/u0020 | 使用十六进制表示形式(恰好四位)与 Unicode 字符匹配。 |
/ | 在后面带有不识别为转义符的字符时,与该字符匹配。例如, /* 与 /x2A 相同。 |
字符类 | 说明 |
. | 匹配除 /n 以外的任何字符。如果已用 Singleline 选项做过修改,则句点字符可与任何字符匹配。 |
[ aeiou ] | 与指定字符集中包含的任何单个字符匹配。 |
[^ aeiou ] | 与不在指定字符集中的任何单个字符匹配。 |
[0-9a-fA-F] | 使用连字号 (–) 允许指定连续字符范围。 |
/p{ name } | 与 {name} 指定的命名字符类中的任何字符都匹配。支持的名称为 Unicode 组和块范围。例如, Ll 、 Nd 、 Z 、 IsGreek 、 IsBoxDrawing 。可以使用 GetUnicodeCategory 方法找到某个字符所属的 Unicode 类别。 |
/P{ name } | 与在 {name} 中指定的组和块范围不包括的文本匹配。 |
/w | 与任何单词字符匹配。等效于 Unicode 字符类别 [/p{Ll}/p{Lu}/p{Lt}/p{Lo}/p{Nd}/p{Pc}/p{Lm}] 。如果用 ECMAScript 选项指定了符合 ECMAScript 的行为,则 /w 等效于 [a-zA-Z_0-9] 。 |
/W | 与任何非单词字符匹配。等效于 Unicode 字符类别 [^/p{Ll}/p{Lu}/p{Lt}/p{Lo}/p{Nd}/p{Pc}/p{Lm}] 。如果用 ECMAScript 选项指定了符合 ECMAScript 的行为,则 /W 等效于 [^a-zA-Z_0-9] 。 |
/s | 与任何空白字符匹配。等效于 Unicode 字符类别 [/f/n/r/t/v/x85/p{Z}] 。如果用 ECMAScript 选项指定了符合 ECMAScript 的行为,则 /s 等效于 [ /f/n/r/t/v] 。 |
/S | 与任何非空白字符匹配。等效于 Unicode 字符类别 [^/f/n/r/t/v/x85/p{Z}] 。如果用 ECMAScript 选项指定了符合 ECMAScript 的行为,则 /S 等效于 [^ /f/n/r/t/v] 。 |
/d | 与任何十进制数字匹配。对于 Unicode 类别的 ECMAScript 行为,等效于 /p{Nd} ,对于非 Unicode 类别的 ECMAScript 行为,等效于 [0-9] 。 |
/D | 与任何非数字匹配。对于 Unicode 类别的 ECMAScript 行为,等效于 /P{Nd} ,对于非 Unicode 类别的 ECMAScript 行为,等效于 [^0-9] 。 |
断言 | 说明 |
^ | 指定匹配必须出现在字符串的开头或行的开头。。 |
$ | 指定匹配必须出现在以下位置:字符串结尾、字符串结尾处的 /n 之前或行的结尾。 |
/A | 指定匹配必须出现在字符串的开头(忽略 Multiline 选项)。 |
/Z | 指定匹配必须出现在字符串的结尾或字符串结尾处的 /n 之前(忽略 Multiline 选项)。 |
/z | 指定匹配必须出现在字符串的结尾(忽略 Multiline 选项)。 |
/G | 指定匹配必须出现在上一个匹配结束的地方。与 Match.NextMatch() 一起使用时,此断言确保所有匹配都是连续的。 |
/b | 指定匹配必须出现在 /w (字母数字)和 /W (非字母数字)字符之间的边界上。匹配必须出现在单词边界上,即出现在由任何非字母数字字符分隔的单词中第一个或最后一个字符上。 |
/B | 指定匹配不得出现在 /b 边界上。 |
限定符 | 说明 |
* | 指定零个或更多个匹配;例如 /w* 或 (abc)* 。等效于 {0,} 。 |
+ | 指定一个或多个匹配;例如 /w+ 或 (abc)+ 。等效于 {1,} 。 |
? | 指定零个或一个匹配;例如 /w? 或 (abc)? 。等效于 {0,1} 。 |
{ n } | 指定恰好 n 个匹配;例如 (pizza){2} 。 |
{ n ,} | 指定至少 n 个匹配;例如 (abc){2,} 。 |
{ n , m } | 指定至少 n 个但不多于 m 个匹配。 |
*? | 指定尽可能少地使用重复的第一个匹配(等效于 lazy * )。 |
+? | 指定尽可能少地使用重复但至少使用一次(等效于 lazy + )。 |
?? | 指定使用零次重复(如有可能)或一次重复 (lazy ?) 。 |
{ n }? | 等效于 {n} (lazy {n}) 。 |
{ n ,}? | 指定尽可能少地使用重复但至少使用 n 次 (lazy {n,}) 。 |
{ n , m }? | 指定介于 n 次和 m 次之间、尽可能少地使用重复 (lazy {n,m}) 。 |
--完--