心静自然凉~

一杯清茶、二盏淡酒、邀三五知己,诉七八句心语,道九分珍重,怀十分真诚,或伤感、或快乐,无现实之隔阂 、无世俗之势利,如此人生,不亦乐乎!

导航

正则表达式

Posted on 2004-11-10 13:11  Leo.Zhu  阅读(163)  评论(0编辑  收藏  举报
正则表达式

一、简介

正则表达式这个名词,相信很多人都听说过,这个名词最早起源于1956 年, 一位叫 Stephen Kleene 的美国数学家在 McCulloch 和 Pitts 早期工作的基础上,发表了一篇标题为“神经网事件的表示法”的论文,引入了正则表达式的概念。正则表达式就是用来描述他称为“正则集的代数”的表达式,因此采用“正则表达式”这个术语。

随后,发现可以将这一工作应用于使用Ken Thompson 的计算搜索算法的一些早期研究,Ken Thompson是Unix 的主要发明人。正则表达式的第一个实用应用程序就是 Unix 中的qed 编辑器。

Q: 正则表达式,能够为我们做什么呢?

A: 基于文本的编辑器和搜索工具中的一个重要部分。正则表达式可以让用户通过使用一系列的特殊字符构建匹配模式,然后把匹配模式与数据文件、程序输入以及WEB页面的表单输入等目标对象进行比较,根据比较对象中是否包含匹配模式,执行相应的程序。

下面我们就一步一步的结合它的语法,来介绍正则表达式的使用。

二、初次接触正则表达式

我们先来了解正则表达式的一些基本概念。正则表达式作为一种表示语言,其定义了自己的一套描述方式,来描述各种各样的字符类。下面摘取msdn中的一段定义。(ms-help://MS.VSCC/MS.MSDNVS.2052/cpgenref/html/cpconcharacterclasses.htm)

字符转义表
字符类
含义

.
与除 \n 以外的任何字符匹配。如果通过 Singleline 选项(请参阅正则表达式选项)进行了修改,则句点字符与任何字符匹配。

[aeiou]
与指定字符集中包含的任何单个字符匹配。

[^aeiou]
与不在指定字符集中的任何单个字符匹配。

[0-9a-fA-F]
使用连字号 (–) 允许指定连续字符范围。

\p{name}
与 name 指定的命名字符类中的任何字符匹配。支持的名称为 Unicode 组和块范围。例如 Ll£?Nd£?Z£?IsGreek£?IsBoxDrawing。

\P{name}
与在 {name} 中指定的组和块范围中未包含的文本匹配。

\w
与任何单词字符匹配。等效于 Unicode 字符类别
[\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Nd}\p{Pc}]。如果通过 ECMAScript 选项指定了符合 ECMAScript 的行为,则 \w 等同于 [a-zA-Z_0-9]。

\W
与任何非单词字符匹配。等效于 Unicode 类别 [^\p{Ll}\p{Lu}\p{Lt}\p{Lo}\p{Nd}\p{Pc}]。如果通过 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 的 \p{Nd} 和非 Unicode 的 [0-9] 以及 ECMAScript 行为一样。

\D
与任何非数字匹配。与 Unicode 的 \P{Nd} 和非 Unicode 的 [^0-9] 以及 ECMAScript 行为一样。


上表列举了,正则表达式中最最基本的语法定义,了解这些,我们已经可以定义一些简单的规则了,例如:

1. 匹配所有的字符

当然是什么都不用写(@_@)

2. 匹配所有的英文字符

a) \w

b) [a-zA-Z_0-9]

3. 匹配十进制数字

a) \d

b) [0-9]

看上面的例子,是不是觉得很简单呢,不过,到目前为止,这样写出来的规则,还有一个很大的缺陷,就是没有声明匹配字符的个数?

Q: 我希望要匹配的字符为5个英文字母

A: ???

光了解上面的知识是,无法解决这个的L。那正则表达式中是如何解决这个问题的呢,我们来看下面这个表:

(ms-help://MS.VSCC/MS.MSDNVS.2052/cpgenref/html/cpconquantifiers.htm)

限定符表
限定符
说明

*
指定零个或更多个匹配;例如 \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})。


上表中列出了,正则表达式的限定方式,配合这些字符的使用,我们就可以很方便的编写更为强劲的正则表达式了。

例如:

1. 匹配零个或多个所有的字符

*

2. 匹配一个或多个所有字符

+

3. 匹配零个或多个所有的英文字符

\w*

4. 匹配一个或多个所有的英文字符

[a-zA-Z0-9]+

5. 匹配3个十进制数字

\d{3}

6. 匹配最少3个十进制数字

\d{3,}

7. 匹配3个到6个十进制数字

\d{3,6}

现在我们可以解答上面问题了:

Q: 我希望要匹配的字符为5个英文字母

A: \w{5}

很高兴,我们已解决了上面的问题,不过,新的问题总是在不断的出现。我如何限制匹配字符出现在哪里呢?

Q: 我希望匹配以doc开头的字符串

A: ???

为了解决这个问题,我们先来看看这个表:

(ms-help://MS.VSCC/MS.MSDNVS.2052/cpgenref/html/cpconatomiczero-widthassertions.htm)

原子零宽度断言
断言
说明

^
指定匹配必须出现在字符串的开头或行的开头。有关更多信息,请参阅正则表达式选项中的 Multiline 选项。

$
指定匹配必须出现在以下位置:字符串结尾、字符串结尾的 \n 之前或行的结尾。有关更多信息,请参阅正则表达式选项中的 Multiline 选项。

\A
指定匹配必须出现在字符串的开头(忽略 Multiline 选项)。

\Z
指定匹配必须出现在字符串的结尾或字符串结尾的 \n 之前(忽略 Multiline 选项)。

\z
指定匹配必须出现在字符串的结尾(忽略 Multiline 选项)。

\G
指定匹配必须出现在当前搜索开始的位置(此位置通常是上一次搜索结束位置之后的第一个字符)。例如,请考虑一个由分离的字符组组成的串联字符串,其中每一组的长度都为 n 个字符。在每个字符组中搜索匹配时,如果正则表达式在 0、n、2n、3n 等字符位置找到匹配,则该正则表达式成功。仅当匹配出现在定位组边界上时才会成功。

\b
指定匹配必须出现在 \w(字母数字)和 \W(非字母数字)字符之间的边界上。匹配必须出现在单词边界上,即出现在由空格分隔的单词中第一个或最后一个字符上。

\B
指定匹配不得出现在 \b 边界上。


相信大家都注意到了,在这个表中第一个断言字符就是我们需要的@_@.

例如,^ 指定当前位置在行或字符串的开头。因此,正则表达式 ^FTP 只会返回那些在行的开头出现的字符串“FTP”的匹配项。

看来上面碰到的问题,又可以解决了,让我们一起来解决上面的问题:

Q: 我希望匹配以doc开头的字符串

A: ^doc

以上我们初步了解了什么是正则表达式,已经了解其最基本的语法,当作热身@_@,接下来,才正式进入主题,我们会从第二篇开始深入探讨正则表达式的使用。
在前一篇文章中,介绍了一些初步的正则表达式的基本概念,相信很多人对正则表达式的基本知识有所了解,接下来,我们结合一些实际的编程示例来掩饰说明正则表达式的作用。

首先,我们先看几个实际的例子:

1. 验证输入字符是否全部为英文字符

javascript:

var ex = "^\\w+$";

var re = new RegExp(ex,"i");

return re.test(str);

VBScript

Dim regEx,flag,ex

ex = "^\w+$"

Set regEx = New RegExp

regEx.IgnoreCase = True

regEx.Global = True

regEx.Pattern = ex

flag = regEx.Test( str )

C#

System.String ex = @"^\w+$";

System.Text.RegularExpressions.Regex reg = new Regex( ex ); bool flag = reg.IsMatch( str );

2. 验证邮件格式

C#

System.String ex = @"^\w+@\w+\.\w+$";

System.Text.RegularExpressions.Regex reg = new Regex( ex );

bool flag = reg.IsMatch( str );

3. 更改日期的格式(用 dd-mm-yy 的日期形式代替 mm/dd/yy 的日期形式)

C#

String MDYToDMY(String input)

{

return Regex.Replace(input,

"\\b(?<month>\\d{1,2})/(?<day>\\d{1,2})/(?<year>\\d{2,4})\\b",

"${day}-${month}-${year}");

}

4. 从 URL 提取协议和端口号

C#

String Extension(String url)

{

Regex r = new Regex(@"^(?<proto>\w+)://[^/]+?(?<port>:\d+)?/",

RegexOptions.Compiled);

return r.Match(url).Result("${proto}${port}");

}

这里的例子可能是我们在网页开发中,通常会碰到的一些正则表达式,尤其在第一个例子中,给出了使用javascript,vbScript,C#等不同语言的实现方式,大家不难看出,对于不同的语言来说,正则表达式没有区别,只是正则表达式的实现类不同而已。而如何发挥正则表达式的公用,也要看实现类的支持。

(摘自msdn: Microsoft .NET 框架 SDK 提供大量的正则表达式工具,使您能够高效地创建、比较和修改字符串,以及迅速地分析大量文本和数据以搜索、移除和替换文本模式。ms-help://MS.VSCC/MS.MSDNVS.2052/cpgenref/html/cpconregularexpressionslanguageelements.htm)

下面我们逐个来分析这些例子:

1-2,这两个例子很简单,只是简单的验证字符串是否符合正则表达式规定的格式,其中使用的语法,在第一篇文章中都已经介绍过了,这里做一下简单的描述。

第1个例子的表达式: ^\w+$

^ -- 表示限定匹配开始于字符串的开始

\w – 表示匹配英文字符

+ -- 表示匹配字符出现1次或多次

$ -- 表示匹配字符到字符串结尾处结束

验证形如asgasdfs的字符串

第2个例子的表达式: ^\w+@\w+.\w+$

^ -- 表示限定匹配开始于字符串的开始

\w – 表示匹配英文字符

+ -- 表示匹配字符出现1次或多次

@ -- 匹配普通字符@

\. – 匹配普通字符.(注意.为特殊字符,因此要加上\转译)

$ -- 表示匹配字符到字符串结尾处结束

验证形如dragontt@sina.com的邮件格式





第3 个例子中,使用了替换,因此,我们还是先来看看正则表达式中替换的定义:

(ms-help://MS.VSCC/MS.MSDNVS.2052/cpgenref/html/cpconsubstitutions.htm)

替换
字符
含义

$123
替换由组号 123(十进制)匹配的最后一个子字符串。

${name}
替换由 (?<name> ) 组匹配的最后一个子字符串。

$$
替换单个“$”字符。

$&
替换完全匹配本身的一个副本。

$`
替换匹配前的输入字符串的所有文本。

$'
替换匹配后的输入字符串的所有文本。

$+
替换最后捕获的组。

$_
替换整个输入字符串。


分组构造
(ms-help://MS.VSCC/MS.MSDNVS.2052/cpgenref/html/cpcongroupingconstructs.htm)

分组构造
定义

( )
捕获匹配的子字符串(或非捕获组;有关更多信息,请参阅正则表达式选项中的 ExplicitCapture 选项。)使用 () 的捕获根据左括号的顺序从 1 开始自动编号。捕获元素编号为零的第一个捕获是由整个正则表达式模式匹配的文本。

(?<name> )
将匹配的子字符串捕获到一个组名称或编号名称中。用于 name 的字符串不能包含任何标点符号,并且不能以数字开头。可以使用单引号替代尖括号,例如 (?'name')。

(?<name1-name2> )
平衡组定义。删除先前定义的 name2 组的定义并在 name1 组中存储先前定义的 name2 组和当前组之间的间隔。如果未定义 name2 组,则匹配将回溯。由于删除 name2 的最后一个定义会显示 name2 的先前定义,因此该构造允许将 name2 组的捕获堆栈用作计数器以跟踪嵌套构造(如括号)。在此构造中,name1 是可选的。可以使用单引号替代尖括号,例如 (?'name1-name2')。

(?: )
非捕获组。

(?imnsx-imnsx: )
应用或禁用子表达式中指定的选项。例如,(?i-s: ) 将打开不区分大小写并禁用单行模式。有关更多信息,请参阅正则表达式选项。

(?= )
零宽度正预测先行断言。仅当子表达式在此位置的右侧匹配时才继续匹配。例如,\w+(?=\d) 与后跟数字的单词匹配,而不与该数字匹配。此构造不会回溯。

(?! )
零宽度负预测先行断言。仅当子表达式不在此位置的右侧匹配时才继续匹配。例如,\b(?!un)\w+\b 与不以 un 开头的单词匹配。

(?<= )
零宽度正回顾后发断言。仅当子表达式在此位置的左侧匹配时才继续匹配。例如,(?<=19)99 与跟在 19 后面的 99 的实例匹配。此构造不会回溯。

(?<! )
零宽度负回顾后发断言。仅当子表达式不在此位置的左侧匹配时才继续匹配。

(?> )
非回溯子表达式(也称为贪婪子表达式)。该子表达式仅完全匹配一次,然后就不会逐段参与回溯了。(也就是说,该子表达式仅与可由该子表达式单独匹配的字符串匹配。)




我们还是先简单的了解一下这两个概念:

分组构造:

最基本的构造方式就是(),在左右括号中括起来的部分,就是一个分组;

更进一步的分组就是形如:(?<name> )的分组方式,这种方式与第一种方式的不同点,就是对分组的部分进行了命名,这样就可以通过该组的命名来获取信息;

(还有形如(?= )等等的分组构造,我们这篇的例子中也没有使用到,下次我们在来介绍)

替换:

上面提到了两种基本的构造分组方式()以及(?<name> ),通过这两种分组方式,我们可以得到形如$1,${name}的匹配结果。



这样说,可能概念上还是有些模糊,我们还是结合上面的例子来说:

第三个例子的正则表达式为:\\b(?<month>\\d{1,2})/(?<day>\\d{1,2})/(?<year>\\d{2,4})\\b

(解释一下,为什么这里都是\\一起用:这里是C#的例子,在C#语言中\是转译字符,要想字符串中的\不转译,就需要使用\\或者在整个字符串的开始加上@标记,即上面等价与

@”\b(?<month>\d{1,2})/(?<day>\d{1,2})/(?<year>\d{2,4}\b”)

\b -- 是一种特殊情况。在正则表达式中,除了在 [] 字符类中表示退格符以外,\b 表示字边界(在 \w 和 \W 字符之间)。在替换模式中,\b 始终表示退格符

(?<month>\d{1,2}) – 构造一个名为month的分组,这个分组匹配一个长度为1-2的数字

/ -- 匹配普通的/字符

(?<day>\d{1,2}) --构造一个名为day的分组,这个分组匹配一个长度为1-2的数字

/ -- 匹配普通的/字符

(?<year>\d{2,4}\b”) --构造一个名为year的分组,这个分组匹配一个长度为2-4的数字



这里还不能够看出这些分组的作用,我们接着看这一句

${day}-${month}-${year}

${day} – 获得上面构造的名为day的分组匹配后的信息

- -- 普通的-字符

${month} --获得上面构造的名为month的分组匹配后的信息

- -- 普通的-字符

${year} --获得上面构造的名为year的分组匹配后的信息



举例来说:

将形如04/02/2003的日期使用例3种的方法替换

(?<month>\d{1,2}) 分组将匹配到04由${month}得到这个匹配值

(?<day>\d{1,2}) 分组将匹配到02由${day}得到这个匹配值

(?<year>\d{1,2}) 分组将匹配到2003由${year}得到这个匹配值

了解了这个例子后,我们在来看第4个例子就很简单了。



第4个例子的正则

^(?<proto>\w+)://[^/]+?(?<port>:\d+)?/

^ -- 表示限定匹配开始于字符串的开始

(?<proto>\w+) – 构造一个名为proto的分组,匹配一个或多个字母

: -- 普通的:字符

// -- 匹配两个/字符

[^/] – 表示这里不允许是/字符

+? – 表示指定尽可能少地使用重复但至少使用一次匹配

(?<port>:\d+) – 构造一个名为port的分组,匹配形如:2134(冒号+一个或多个数字)

? – 表示匹配字符出现0次或1次

/ -- 匹配/字符



最后通过${proto}${port}来获取两个分组构造的匹配内容

(有关Regex对象的用法,参考

ms-help://MS.VSCC/MS.MSDNVS.2052/cpref/html/frlrfSystemTextRegularExpressionsRegexMembersTopic.htm)



好了,本次介绍的几个例子,也讲得差不多了,希望大家有所收获,下次,在就一些特殊的要求,进一步探讨正则表达式的实现。
前面的文章中,介绍了正则表达式的基本语法,以及一些简单的例子。但这些并不是我们会遇到的全部问题,有些时候我们不得不编写一些较为复杂的正则表达式来解决我们的实际问题。

这里,我先提几个问题,然后,我们逐个运用正则表达式的知识来解决。

1. 符合两种条件之一,都成立,例如:是纯数字或者纯字符

123(true),hello(true),234.test23(false)

2. 要得到不以数字开头的字符组合

如:How2234do>you234do,希望得到How和you而不是do,do

3. 得到以数字开头的字符组合

上例中,得到do和do

4. 要得到不以数字结尾的字符组合

还是上面的情况,要得到的是Ho,do,yo,do

5. 得到以数字结尾的字符组合

同上例,得到Ho,do,yo,do

6. 不允许字符中ab同时出现

例:nihaoma(true),above(false),agoodboy(true)



下面我们开始着手解决这些问题:

第一个:符合两种条件之一,都成立

这种要求可能代表着一种普遍的要求,我们先来看看这个表

替换构造
替换构造
定义

|
与由|(垂直条)字符分隔的术语中的任何一个术语匹配;例如 cat|dog|tiger。使用最左侧的成功匹配。

(?(expression)yes|no)
如果表达式在此位置匹配,则与“yes”部分匹配;否则,与“no”部分匹配。“no”部分可省略。表达式可以是任何有效的表达式,但它将变为零宽度断言,因此该语法等效于 (?(?=expression)yes|no)。请注意,如果表达式是命名组的名称或捕获组编号,则替换构造将解释为捕获测试(在本表的下一行对此进行了描述)。若要避免在这些情况下产生混淆,则可以显式拼出内部 (?=expression)。

(?(name)yes|no)
如果命名捕获字符串有匹配,则与“yes”部分匹配;否则,与“no”部分匹配。“no”部分可省略。如果给定的名称不与此表达式中使用的捕获组的名称或编号对应,则替换构造将解释为表达式测试(在本表的上一行进行了描述)。


(ms-help://MS.VSCC/MS.MSDNVS.2052/cpgenref/html/cpconalternationconstructs.htm)

在这个表中,我们看到,正则中为了解决这一类问题,定义了|来表示或者的关系,就好像常见的或运算符一样,现在我们来看看如何利用|来解决我们的问题。

1. 先为可选择的表达式撰写表达式:

a) 纯数字 – [0-9]*

b) 纯字母 – [a-zA-Z]*

2. 将可选条件用|连接起来就是我们所需的

^[0-9]*$|^[a-zA-Z]*$

(这里我特别对两个条件加上了^和$限定符,这在验证字符串是否完全符合要求时,是十分必要的,如果不加这两个限定符,有兴趣的朋友可以自己试一下效果。



后面四个问题,其实是一类的,所以我们把它们放在一起处理。接下来我们来解决第二到第四个问题:

首先,我们回顾一下上次介绍的分组构造:

(?= )
零宽度正预测先行断言。仅当子表达式在此位置的右侧匹配时才继续匹配。例如,\w+(?=\d) 与后跟数字的单词匹配,而不与该数字匹配。此构造不会回溯。

(?! )
零宽度负预测先行断言。仅当子表达式不在此位置的右侧匹配时才继续匹配。例如,\b(?!un)\w+\b 与不以 un 开头的单词匹配。

(?<= )
零宽度正回顾后发断言。仅当子表达式在此位置的左侧匹配时才继续匹配。例如,(?<=19)99 与跟在 19 后面的 99 的实例匹配。此构造不会回溯。

(?<! )
零宽度负回顾后发断言。仅当子表达式不在此位置的左侧匹配时才继续匹配。


可以看到,这个表的这四种规则,正好可以解决我们的问题。

@_@先解决我们的问题再说:

第二例:要得到不以数字开头的字符组合

(?<!\d)[a-zA-Z]{2,}

(?<!\d) -- 限定字符的开头不为数字才匹配

[a-zA-Z]{2,} – 描述匹配2个以上的字母

(注:这是取巧的做法,因为,按照我们的逻辑How2234do>you234do中的两个do的o字母也是符合的,不过,这不是我们想要的,当然还有其他的解决办法,可以根据实际的情况来处理,这里是为了讲解这个方法@_@)

第三例:得到以数字开头的字符组合

(?<=\d)[a-zA-Z]+

(?<=\d) – 限定为数字开头的字符才匹配

[a-zA-Z]+ -- 描述匹配1个或多个字母

第四例:要得到不以数字结尾的字符组合

[a-zA-Z]+(?!\d)

[a-zA-Z]+ -- 描述匹配1个或多个字母

(?!\d) – 限定不为数字结尾的字母才匹配

第五例:得到以数字结尾的字符组合

[a-zA-Z]+(?=\d)

[a-zA-Z]+ -- 描述匹配1个或多个字母

(?=\d) – 限定为数字结尾的字母才匹配



第六例:不允许字符中ab同时出现

^(?!.*?ab).*$

(?!.*?ab) – 限定不允许出现ab相连的字符

.* -- 任意字符



介绍到这里,我们这一次提出的问题,也都解决了,例子虽然简单,不过,再复杂的东西也是建立在简单的基础上的。其实写正则表达式的关键就是要善于定制规则,用最简练正确的话来描述,然后用正则表达式的语法写出来,就可以了(@_@)这就要靠大家多积累经验了。