正则表达式在各种程序中都有着难以置信的作用,但并不是所有的开发人员都知道这一点。正则表达式可以看做一种有特定功能的小型编程语言:在大的字符串表达式中定位一个子字符串。它不是一种新技术,最初它是在UNIX环境中开发的,与Perl一起使用得比较多。Microsoft把它移植到Windows中,到目前为止在脚本语言中用得比较多。但System. Text.Regular- Expressions命名空间中的许多.NET类都支持正则表达式。.NET Framework的各个部分都使用正则表达式,例如,在ASP.NET的验证服务器控件中就使用了正则表达式。
许多人都不太熟悉正则表达式语言,所以本节将主要解释正则表达式和相关的.NET类。如果您很熟悉正则表达式,就可以跳过本节,学习.NET基类的引用。注意,.NET正则表达式引擎是为兼容Perl 5的正则表达式而设计的,但有一些新特性。
正则表达式语言是一种专门用于字符串处理的语言。它包含两个功能:
一组用于标识字符类型的转义代码。您可能很熟悉DOS表达式中的*字符表示任意子字符串(例如,DOS命令Dir Re*会列出所有名称以Re开头的文件)。正则表达式使用与*类似的许多序列来表示“任意一个字符”、“一个单词”、“一个可选的字符”等。
一个系统。在搜索操作中,它把子字符串和中间结果的各个部分组合起来。
使用正则表达式,可以对字符串执行许多复杂而高级的操作,例如:
区分(可以是标记或删除)字符串中所有重复的单词,例如,把The computer books books转换为The computer books。
把所有单词都转换为标题格式,例如把this is a Title转换为This Is A Title。
把长于3个字符的所有单词都转换为标题格式,例如把this is a Title转换为This is a Title。
确保句子有正确的大写形式。
区分URI的各个元素(例如http://www.wrox.com,提取出协议、计算机名、文件名等)。
当然,这些都是可以在C#中用System.String和System.Text.StringBuilder的各种方法执行的任务。但是,在一些情况下,还需要编写相当多的C#代码。如果使用正则表达式,这些代码一般可以压缩为几行代码。实际上,是实例化了一个对象System.Text.Regular Expressions. RegEx(甚至更简单:调用静态的RegEx()方法),给它传送要处理的字符串和一个正则表达式(这是一个字符串,包含用正则表达式语言编写的指令),就可以了。
正则表达式字符串初看起来像是一般的字符串,但其中包含了转义序列和有特定含义的其他字符。例如,序列\b表示一个字的开头和结尾(字的边界),如果要表示正在查找以字符th开头的字,就可以编写正则表达式\bth(即序列字边界是– t – h)。如果要搜索所有以th结尾的字,就可以编写th\b(序列t – h–字边界)。但是,正则表达式要比这复杂得多,例如,可以在搜索操作中找到存储部分文本的工具性程序。本节仅介绍正则表达式的功能。
假定应用程序需要把US电话号码转换为国际格式。在美国,电话号码的格式为314-123-1234,常常写作(314)123-1234。在把这个国家格式转换为国际格式时,必须在电话号码的前面加上+1(美国的国家代码),并给区号加上括号:+1(314) 123-1234。在查找和替换时,这并不复杂,但如果要使用String类完成这个转换,就需要编写一些代码(这表示,必须使用System.String上的方法来编写代码),而正则表达式语言可以构造一个短的字符串来表达上述含义。
所以,本节只有一个非常简单的示例,我们只考虑如何查找字符串中的某些子字符串,无须考虑如何修改它们。
下面将开发一个小示例,执行并显示一些搜索的结果,说明正则表达式的一些特性,以及如何在C#中使用.NET正则表达式引擎。这个示例文档中使用的文本是引自另一本有关ASP.NET的Wrox Press书籍(《ASP.NET 2.0高级编程》,清华大学出版社翻译出版):
string Text =
@"This comprehensive compendium provides a broad and thorough investigation of all
aspects of programming with ASP.NET. Entirely revised and updated for the 2.0
Release of .NET, this book will give you the information you need to master ASP.NET
and build a dynamic, successful, enterprise Web application.";
注意:
不考虑换行,则上面的表达式是合法的C#代码—— 说明了使用字符串时应在前面加上符号@。
我们把这个文本称为输入字符串。为了说明正则表达式.NET类,我们先进行一次纯文本的搜索,这次搜索不带任何转义序列或正则表达式命令。假定要查找所有的字符串ion,把这个搜索字符串称为模式。使用正则表达式和上面声明的变量Text,编写出下面的代码:
string Pattern = "ion";
MatchCollection Matches = Regex.Matches(Text, Pattern,
RegexOptions.IgnoreCase |
RegexOptions.ExplicitCapture);
foreach (Match NextMatch in Matches)
{
Console.WriteLine(NextMatch.Index);
}
在这段代码中,使用了System.Text.RegularExpressions命名空间中Regex类的静态方法Matches()。这个方法的参数是一些输入文本、一个模式和RegexOptions枚举中的一组可选标志。在本例中,指定所有的搜索都不应区分大小写。另一个标记ExplicitCapture 改变了收集匹配的方式,对于本例,这样可以使搜索的效率更高,其原因详见后面的内容(尽管它还有这里没有介绍的其他用法)。Matches()返回MatchCollections对象的引用。匹配是一个技术术语,表示在表达式中查找模式实例的结果,用System.Text.RegularExpressions.Match来代表。因此,我们返回一个包含所有匹配的MatchCollection,每个匹配都用一个Match对象来表示。在上面的代码中,只是在集合中迭代,使用Match类的Index属性,返回输入文本中匹配所在的索引。运行这段代码,将得到3个匹配。表8-4描述了RegexOptions枚举的一些选项。
表 8-4
|
成 员 名 |
说 明 |
|
CultureInvariant |
指定忽略字符串的文化背景 |
|
ExplicitCapture |
修改收集匹配的方式,确保把明确指定的匹配作为有效的搜索结果 |
|
IgnoreCase |
忽略输入字符串的大小写 |
|
IgnorePatternWhitespace |
在字符串中删除未转义的空白,使注释用英镑符号或短横线符号指定 |
|
Multiline |
修改字符^和$,把它们应用于每一行的开头和结尾,而不仅仅应用于整个字符串的开头和结尾 |
|
RightToLeft |
从右到左地读取输入字符串,而不是从左到右地读取(适合于一些亚洲语言或其他以这种方式读取的语言) |
|
Singleline |
指定句点的含义(.),它原来表示单行模式,现在改为匹配每个字符 |
除了一些新的.NET基类外,其他内容都不是新的。但正则表达式的功能主要取决于模式字符串。原因是模式字符串不仅仅包含纯文本。如前所述,它还可以包含元字符和转义序列,其中元字符是给出命令的特定字符,而转义序列的工作方式与C#的转义序列相同,它们都是以反斜杠\开头的字符,具有特殊的含义。
例如,假定要查找以n开头的字,就可以使用转义序列\b,它表示一个字的边界(字的边界是以字母数字表中的某个字符开头,或者后面是一个空白字符或标点符号)。可以编写如下代码:
string Pattern = @"\bn";
MatchCollection Matches = Regex.Matches(Text, Pattern,
RegexOptions.IgnoreCase |
RegexOptions.ExplicitCapture);
注意字符串前面的符号@。要在运行时把\b传递给.NET正则表达式引擎,反斜杠\不应被C#编译器解释为转义序列。如果要查找以序列ion结尾的字,可以使用下面的代码:
string Pattern = @"ion\b";
如果要查找以字母a开头,以序列ion结尾的所有字 (在本例中仅有一个匹配application)就必须在上面的代码中添加一些内容。显然,我们需要一个以\ba开头,以ion\b结尾的模式,但中间的内容怎么办?需要告诉应用程序在a和ion中间的内容可以是任意长度的任意字符,只要这些字符不是空白即可。实际上,正确的模式如下所示。
string Pattern = @"\ba\S*ion\b";
使用正则表达式要习惯的一点是,对像这样怪异的字符序列见怪不怪。但这个序列的工作是非常逻辑化的。转义序列\S表示任何不是空白的字符。*称为数量词,其含义是前面的字符可以重复任意次,包括0次。序列\S*表示任意个不是空白的字符。因此,上面的模式匹配于以a开头,以ion结尾的任何单词。
表8-5是可以使用的一些主要的特定字符或转义序列,但这个表并不完整,完整的列表请参考MSDN文档。
表 8-5
|
符 号 |
含 义 |
示 例 |
匹配的示例 |
|
^ |
输入文本的开头 |
^B |
B,但只能是文本中的第一个字符 |
|
$ |
输入文本的结尾 |
X$ |
X, 但只能是文本中的最后一个字符 |
|
. |
除了换行字符(\n)以外的所有单个字符 |
i.ation |
isation、ization |
|
* |
可以重复0次或多次的前导字符 |
ra*t |
rt、rat、raat和raaat等 |
|
+ |
可以重复1次或多次的前导字符 |
ra+t |
rat、raat和 raaat等(但不能是rt) |
|
? |
可以重复0次或1次的前导字符 |
ra?t |
只有rt 和 rat匹配 |
|
\s |
任何空白字符 |
\sa |
[space]a、\ta、\na (\t 和 \n 与C#的\t 和 \n含义相同) |
|
\S |
任何不是空白的字符 |
\SF |
aF、rF、cF、但不能是\tf |
|
\b |
字边界 |
ion\b |
以ion结尾的任何字 |
|
\B |
不是字边界的位置 |
\BX\B |
字中间的任何X |
如果要搜索一个元字符,也可以通过带有反斜杠的转义字符来表示。例如,.(一个句点)表示除了换行字符以外的任何字符,而\.表示一个点。
可以把替换的字符放在方括号中,请求匹配包含这些字符。例如,[1|c]表示字符可以是l或c。如果要搜索map或man,可以使用序列ma[n|p]。在方括号中,也可以指定一个范围,例如[a-z]表示所有的小写字母,[A-E]表示A到E之间的所有大写字母,[0-9]表示一个数字。如果要搜索一个整数(该序列只包含0到9的字符),就可以编写[0-9]+(注意,使用+字符表示至少要有这样一个数字,但可以有多个数字,所以9、 83和 854等都是匹配的)。
本节编写一个示例RegularExpressionsPlayaround,看看正则表达式的工作方式。
该示例的核心是一个方法WriteMatches(),它把MatchCollection中的所有匹配以比较详细的方式显示出来。对于每个匹配,它都会显示该匹配在输入字符串中的索引、匹配的字符串和一个略长的字符串,其中包含匹配和输入文本中至多10个外围字符,其中至多有5个字符放在匹配的前面,至多5个字符放在匹配的后面(如果匹配的位置在输入文本的开头或结尾5个字符内,则结果中匹配前后的字符就会少于5个)。换言之,如果要匹配的单词是messaging,靠近输入文本末尾的匹配应是“and messaging of d”,匹配的前后各有5个字符,但位于输入文本的最后一个字上的匹配就应是"g of data" —— 匹配的后面只有一个字符。因为在该字符的后面是字符串的结尾。这个长字符串可以更清楚地表明正则表达式是在什么地方查找到匹配的:
static void WriteMatches(string text, MatchCollection matches)
{
Console.WriteLine("Original text was: \n\n" + text + "\n");
Console.WriteLine("No. of matches: " + matches.Count);
foreach (Match nextMatch in matches)
{
int Index = nextMatch.Index;
string result = nextMatch.ToString();
int charsBefore = (Index < 5) ? Index : 5;
int fromEnd = text.Length - Index - result.Length;
int charsAfter = (fromEnd < 5) ? fromEnd : 5;
int charsToDisplay = charsBefore + charsAfter + result.Length;
Console.WriteLine("Index: {0}, \tString: {1}, \t{2}",
Index, result,
text.Substring(Index - charsBefore, charsToDisplay));
}
}
在这个方法中,处理过程是确定在较长的子字符串中有多少个字符可以显示,而无需超出输入文本的开头或结尾。注意在Match对象上使用了另一个属性Value,它包含标识该匹配的字符串。而且,RegularExpressionsPlayaround只包含名为Find1、Find2等的方法,这些方法根据本节中的示例执行某些搜索操作。例如,Find2在字开头处查找以a开头的字符串:
static void Find2()
{
string Text =
@"This comprehensive compendium provides a broad and thorough investigation of all
aspects of programming with ASP.NET. Entity revised and updated for the 1.1
Release of .NET, this book will give you the information you need to master ASP.NET
And build a dynamic, successful, enterprise Web application.”;
string pattern = @"\ba";
MatchCollection matches = Regex.Matches(text, pattern,
RegexOptions.IgnoreCase);
WriteMatches(text, matches);
}
下面是一个简单的Main()方法,可以编辑并选择一个Find<n>()方法:
static void Main()
{
Find1();
Console.ReadLine();
}
这段代码还使用了命名空间RegularExpressions:
using System;
using System.Text.RegularExpressions;
运行带有Find1()方法的示例,得到如下所示的结果:
RegularExpressionsPlayaround
Original text was:
This comprehensive compendium provides a broad and thorough investigation of all
aspects of programming with ASP.NET. Entity revised and updated for the 1.1
Release of .NET, this book will give you the information you need to master ASP.NET
And build a dynamic, successful, enterprise Web application.
No. of matches: 1
Index: 291, String: application, Web application.
正则表达式的一个很好的特性是可以把字符组合起来,其方式与C#中的复合语句一样。在C#中,可以把任意数量的语句放在花括号中,把它们组合在一起。其结果就像一个复合语句那样。在正则表达式模式中,也可以把任何字符组合起来(包括元字符和转义序列),像处理一个字符那样处理它们。唯一的区别是要使用圆括号,而不是花括号,得到的序列称为一个组。
例如,模式(an)+定位序列an的任意重复。量词+只应用于它前面的一个字符,但因为我们把字符组合起来了,所以它现在把重复的an作为一个单元来对待。(an)+应用到输入文本bananas came to Europe late in the annals of history上,会从bananas中选择出anan。另一方面,如果使用an+,则程序将从annals中选择ann,从bananas中选择出两个an。表达式(an)+可以提取出an、anan、ananan等,而表达式an+可以提取出an、ann、annn等。
注意:
在上面的示例中,为什么(an)+从banana中选择的是anan,而没有把单个的an作为一个匹配?因为匹配是不能重叠的。如果有可能重叠,在默认情况下就选择最长的匹配。
但是,组的功能要比这强大得多。在默认情况下,把模式的一部分组合为一个组时,就要求正则表达式引擎按照这个组来匹配,或按照整个模式来匹配。换言之,可以把组当作一个要匹配的模式来返回,如果要把字符串分解为各个部分,这种模式就是非常有效的。
例如,URI的格式是<protocol>://<address>:<port>,其中端口是可选的。它的一个示例是http://www.wrox.com:4355。假定要从一个URI中提取协议、地址和端口,而且紧邻URI的后面可能有空白(但没有标点符号),就可以使用下面的表达式:
\b(\S+)://(\S+)(?::(\S+))?\b
该表达式的工作方式如下:首先,前导和尾部的\b序列确保只需要考虑完全是字的文本部分,在这个文本部分中,第一组(\S+)://会选择一个或多个不是空白的字符,其后是://。在HTTP URI的开头会选择出http://。花括号表示把http存储为一个组。后面的序列(\S+)则在上述URI中选择www.wrox.com,这个组在遇到词的结尾(结束\b)时或标记另一个组的冒号(:)时结束。
下一个组选择端口(本例是:4355)。后面的?表示这个组在匹配中是可选的,如果没有:xxxx,也不会妨碍匹配的标记。这是非常重要的,因为端口号在URI中一般不指定,实际上,在大多数情况下,URI是没有端口号的。但是,事情会比较复杂。我们希望指定冒号可以出现,也可以不出现,但不希望把这个冒号也存储在组中。为此,可以嵌套两个组:内部的(\S+)组选择冒号后面的内容(本例中是4355),外面的组包含内部的组,前面是一个冒号,该组又在序列?:的后面。这个序列表示该组不应保存(只需要保存4355,不需要保存:4355)。不要把这两个冒号混淆了,第一个冒号是序列?:的一部分,表示不保存这个组,第二个冒号是要搜索的文本。
在下面的字符串上运行该模式,得到的匹配是http://www.wrox.com。
Hey I've just found this amazing URI at http:// what was it – oh yes http://www.wrox.com
在这个匹配中,找到了刚才提及的3个组,还有第四个组表示匹配本身。理论上,每个组都可以选择0次、1次或多次匹配。单个的匹配就称为捕获。在第一个组(\S+)中,有一个捕获http,第二个组也有一个捕获www.wrox.com,但第三个组没有捕获,因为在这个URI中没有端口号。
注意,该字符串包含第二个http://。虽然它匹配于第一个组,但不会被搜索出来,因为整个搜索表达式不匹配于这部分文本。
前面没有介绍使用组和捕获的任何C#示例,下面提到的.NET类RegularExpressions就通过Group和Capture类支持组和捕获。GroupCollection 和 CaptureCollection分别表示组和捕获的集合,Match类有一个方法Groups(),它返回相应的GroupCollection对象,Group类也相应地执行一个方法Captures(),它返回CaptureCollection对象。这些对象之间的关系如图8-3所示。

图 8-3
把一些字符组合起来后,每次都会返回一个Group对象。如果只是希望把一些字符组合起来,作为搜索模式的一部分,实例化对象就会浪费相当大的系统开销。对于单个的组,可以用以字符序列?:开头,禁止实例化对象,就像URI示例那样。而对于所有的组,可以在RegEx.Matches()方法上指定RegExOptions.ExplicitCaptures标志,如同前面的示例那样。