正则表达式

       (本文是精简自张子阳先生的正则表达式一文,并做部分修改)

       正则表达式总是在你的开发过程中如影随形,不管是进行表单验证,还是高亮显示搜索结果,又或者是进行URL地址映射,总是需要使用它们。几乎所有的语言都对它进行了不同程度的支持,由此,足见其在文本匹配这一领域中的地位。

什么是正则表达式?

       正则表达式也叫做匹配模式(Pattern),它由一组具有特定含义的字符串组成,通常用于匹配和替换文本。正则表达式是大小写敏感的,是顺序敏感的。

匹配固定单个字符      

       所有的单个大小写字母、数字,以及特殊字符,都是一个正则表达式,它们只能匹配单个字符,且这个字符与它本身相同。例如,在源字符串中搜索类似这样的表达式:m;这样将会匹配字符串中所有出现过的 m。

       将多个固定单个字符进行组合,就构成了一个匹配固定字符串的表达式,例如:Jack。

 

匹配任意单个字符

       符号 . 可以匹配任意单个字符。如,英文字母、数字、以及它本身。例如:abc. ,将会匹配 abcX;如果是 .abc. 将会匹配 XabcX。

 

匹配字符组

       有时候,符号 . 过于灵活了,它可以匹配几乎所有的单个字符。有的时候,我们只希望匹配有限个字符中的某一个。这个时候,可以使用字符组。例如,Lov[ef] 表示匹配 Love 或者 Lovf 这两者。中括号是特殊标记,用以划定属于组内的字符的界限。

       字符组虽然由多个字符构成,但它仍只匹配单个字符。

       字符组还可以使用区间表示,例如 [0-9]、[A-Z]、[a-z]、[5-8]、[e-g] 等。它将仅依据起始字符和结束字符的 ASCⅡ值的大小,匹配其ASCⅡ码位于起始字符和结束字符的ASCⅡ之间的所有字符(包含起始、结束字符)。另外,如果起始字符的ASCⅡ值大于结束字符的ASCⅡ值,例如,如果你写成 [3-0],则会出错,导致匹配失败。

 

       参考这样的一个 HTML 中的 RGB 颜色表示:

<BODY BGCOLOR="#336633" TEXT="#FFFFFF">

       最后写出这样的匹配:

#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]

 

反义字符组

       有的时候,我们需要匹配“除了某些字符以外”的其他字符,这时候,我们可以使用反义字符组,其语法是:“[^字符集合]”,例如 [^1-3]。

 

匹配元字符

       元字符是在正则表达式中具有特殊含义的字符。如之前已经介绍过的,“.”就是一个元字符,它用来匹配任意单个字符。当我们要匹配字符“.”本身的时候,需要使用“\”来对它进行转义:“\.”。

       很容易就看出“\”也是一个元字符,它叫做转义符。所以,当我们需要匹配字符“\”的时候,就需要对它进行转义:“\\”。
       回想一下之前讲述的字符组,我们知道“[”和“]”也是元字符,当我们需要匹配“[”和“]”字符的时候,需要分别写作:“\[”和“\]”。

       举个例子,加入我们需要匹配 “City[0].Name”,却写成了下面这样:

City[0].Name

       其实这是匹配了 City0XName 这样的字符串,. 代表了任意一个字符。正确的写法应该是下面这样:

City\[0\]\.Name

 

匹配空字符

       通常,当我们在键盘上敲击一下回车键时,不管光标此时在哪里,总是会新起一行,然后将光标位于新行的首位置。这在计算机上看起来一气呵成,用一个符号来表示就OK了,可在正则表达式中,以及很多语言中(比如VBScript),却被表示成了两个动作,一个叫“回车”(Carriage Return),一个叫“换行”(Line Feed)。

       这与打印机的工作原理有关,打印机先于计算机键盘很多年,是键盘的雏形。在打印机上换行时,将进行两个动作:1、将打印头换到下一行;2、将打印头返回到新行的行首位置。也就分别对应了现在的“换行”和“回车”。
       在正则表达式中,比较常用的三类空白字符如下表所示:

\r 回车
\n 换行
\t Tab 键

 

匹配特定字符类型

       结合“匹配元字符”和“匹配空字符”发现这样一个规律:

  • 对于“.”和“[”等来说,它们本身就是元字符,而当给它们前面加上转义字符“\”的时候,它们才代表一个普通字符
  • 对于 "r" 和 "n" 等来说,它们本身只是普通字符,而只有当加上转义字符“\”的时候(变成了“\r”和“\n”),它们才代表着元字符

匹配数字类型:

\d 所有单个数字,与 [0-9] 相同
\D 所有非数字,与 [^0-9] 相同

 

匹配字母、数字、下划线:

       不管是在程序命名中,还是文件命名中,这一类字符集都是最常见的,那就是:所有大小写字母、数字以及下划线,其正则表达式为:

[a-zA-Z0-9_]

       正则表达式中可以使用“\w ”来代表这一匹配:

\w 所有单个大小写字母、数字、下划线,与 [a-zA-Z0-9_] 相同
\W 所有单个非大小写字母、非数字、非下划线,与 [^a-zA-Z0-9_] 相同

 

匹配空字符

\s 所有单个空字符,与 [\f\n\r\t\v] 相同
\S 所有单个非空字符,与 [^\f\n\r\t\v] 相同

 

 

匹配多个字符
       上面所介绍的不管简单也好,复杂也好,都只是匹配单个字符,如果需要匹配一个很长的字符串,而组成这个字符串的每个字符都比较复杂(没有诸如\d这样的简写方式),那么,可以想象,一个表达式会多么复杂。
       回顾一下匹配颜色的例子,我们的正则表达式写法是这样的:#[0-9a-f][0-9a-f] [0-9a-f][0-9a-f][0-9a-f][0-9a-f]。应该想到,如果有办法确定匹配的单个字符的个数就好了。这里将讨论使用正则表达式来进行多个字符的匹配。

 

1. 匹配一个或多个

       正则表达式中,可以在单个字符、字符组、特定字符类型、单个任意字符后面加“+”,来表示匹配一个或多个(即 1-n 个)字符组成的字符串。例如,[abc]+ 表示 a 或 b 或 c 字符,一个或多个。

       现在考虑一个实际的例子,例如电子邮件的格式。QQ邮箱是QQ号码开头的固定格式,而一般的邮箱前半部分是字母数字下划线。

QQ 电子邮箱:\d+@qq.com

一般电子邮箱:\w+@\w+\.\w+  // 但不能匹配多级域名的地址,例如:app.press.Jack@hotmail.com

一般电子邮箱:[\w\.]+@\w+\.\w+  // 可以匹配上述多级域名的电子邮件地址

       “-”在字符组“[]”区间内才是一个元字符,在“[]”以外是普通字符。“.”在“[]”区间以外才是元字符,而在“[]”区间内,它就变成了一个普通字符。所以,[\w\.] 可以简写成 [\w.]。因此,上述一般电子邮箱地址可以简写为:

[\w.]+@[\w.]+\.\w+

 

2. 匹配零个或多个字符

       正则表达式中,可以在 单个字符、字符组、特定字符类型后面加“*”来表示匹配零个或多个字符组成的字符串。使用方法与“+”完全相同,仅限定范围不同。

       再次回到上面的匹配 Email 的例子。表达式是这样的:[\w.]+@[\w.]+\.\w+,看上去似乎没有问题,但是,我们发现对于 “.” 开头的地址它也能够匹配。而“.” 不应该出现在邮件的第一个字符位置上。

       Email 起首一定是一个或多个字母或数字组合,而后面可以是零个或多个字母数字与“.”的组合。所以,修改匹配为如下:

\w+[\w.]*@[\w.]+\.\w+ // 这个写法已经比较完善,但仍然有问题,后面会解释

 

3. 匹配零个或一个字符

       例如,对于一个英文单词 pen,或 pens 我们都认为是正确的书写,但 penss 就是错误的。对于这样的匹配,前面的 “+” 或 “*” 都无法做到。

       正则表达式中,使用 “?” 来匹配零个或一个字符。其使用方式与 “+” 和 “*” 相同。

pens?  // 能匹配 pen、pens

 

 

匹配指定数量字符

       尽管 “?”、“+”、“*” 这三个元字符的出现解决了很多问题,但是,它们并不完善:

  1. 没有办法指定最多匹配多少个字符,比如手机号码最多 11 位。
  2. 没有办法指定最少匹配多少个字符,“+”、“*”、“?”,所能提供的最少字符,要么零个,要么一个。

 

1. 匹配固定数目的字符

       正则表达式中,可以在 单个字符、字符组、特定字符类型、单个任意字符后面加 “{数字}” 来表示匹配零个或多个字符组成的字符串。

\d{3}  // 可以匹配 000 - 999,但必需是一个 3 位的百位数

a{6}  // 只能匹配 aaaaaa

       假如,手机号码规则为 1 开头,11 位数字:

1\d{10}

 

2. 匹配区间以内数目的字符

       针对上述固定数目的指定,它不够灵活。例如在 0 - 999 之间的数,固定写法就不可行(\d{3} 必需是个3位数)。

       正则表达式中,使用 “{最小数目,最大数目}” 的语法来实现,使用方式与固定数目字符的语法相同。

\d{1,3}  // 匹配 0 - 999 之间整数。 但这样的写法仍然有问题,例如 1234 也会被匹配。那是因为 1234 被看作了 2 部分。

       注意两个特例:

  • {0,1} 相当于 ?
  • 如果不限制最大数目,可以将最大数目设为空,所以 \d{1,} 相当于“+”;而 {0,} 相当于“*”

 

 

贪婪匹配和惰性匹配

1. 贪婪匹配、惰性匹配概述

  • 贪婪匹配(greedy):匹配尽可能多的字符。首先看整个字符串,如果不匹配,对字符串进行收缩;遇到可能匹配的文本,停止收缩,对文本进行扩展,当发现匹配的文本时,它不着急将该匹配保存到匹配集合中,而是对文本继续扩展,直到无法继续匹配或者扩展完整个字符串,然后将前面最后一个符合匹配的文本(也是最长的)保存起来到匹配集合中。所以说它是贪婪的。(看整体-收缩-看整体-收缩,直到有有可能匹配时停止收缩,而对文本进行向后扩展延伸的搜索)
  • 惰性匹配(lazy):匹配尽可能少的字符。它从第一个字符开始找起,一旦符合条件,立刻保存到匹配集合中,然后继续进行查找。所以说它是懒惰的。

       光看上面的定义,很难有一个生动的认识,现在假设我们要匹配下面 <b> 和 </b> 之间的文本。为了做演示,尽管不符合 HTML 的定义,我们再加入一段<b>和</c>之间的文本:

You are a <b>software</b> engineer <b>working</b> in <b>Shanghai</c>.

       现在使用这个正则表达式:<b>.*</b>,会得到这样的结果(划线处):

You are a <b>software</b> engineer <b>working</b> in <b>Shanghai</c>.

       而我们往往期望得到的是:<b>software</b> 和 <b>working</b>,解决办法就是用惰性匹配。

惰性匹配语法

贪婪匹配

惰性匹配

匹配描述

? ?? 匹配 0 个或 1 个
+ +? 匹配 1 个或 多 个
* *? 匹配 0 个或 多 个
{n} {n}? 匹配 n 个
{n,m} {n,m}? 匹配 n 个或 m 个
{n,} {n,}? 匹配 n 个或 多 个

       上面的例子,若使用惰性匹配,将得到期望的结果:

<b>.*?</b>  // 将得到结果:<b>software</b>、<b>working</b>

 

2. 贪婪匹配的匹配过程

You are a <b>software</b> engineer <b>working</b> in <b>Shanghai</c>. // 不匹配,收缩

ou are a <b>software</b> engineer <b>working</b> in <b>Shanghai</c>.  // 不匹配,收缩

u are a <b>software</b> engineer <b>working</b> in <b>Shanghai</c>.  // 不匹配,收缩

……(略)

<b>software</b> engineer <b>working</b> in <b>Shanghai</c>.  // 可能匹配,扩展

<b>software</b> engineer <b>working</b> in <b>Shanghai</c>.  // 可能匹配,扩展

……(略)

<b>software</b> engineer <b>working</b> in <b>Shanghai</c>.  // 找到一个匹配,并不保存到结果集,继续扩展

<b>software</b> engineer <b>working</b> in <b>Shanghai</c>. // 继续扩展

……(略)

<b>software</b> engineer <b>working</b> in <b>Shanghai</c>. // 又找到一个匹配,仍继续扩展

……(略)

<b>software</b> engineer <b>working</b> in <b>Shanghai</c>. // 字符串结束

// 返回最后一次匹配时的结果:<b>software</b> engineer <b>working</b>

 

3. 惰性匹配的匹配过程

       惰性匹配,以此搜索过程是和贪婪匹配相似的,区别在于一旦匹配成功,就立刻保存到结果集中。

You are a <b>software</b> engineer <b>working</b> in <b>Shanghai</c>. // 不匹配,继续

You are a <b>software</b> engineer <b>working</b> in <b>Shanghai</c>. // 不匹配,继续

You are a <b>software</b> engineer <b>working</b> in <b>Shanghai</c>. // 不匹配,继续

……(略)

<b>software</b> engineer <b>working</b> in <b>Shanghai</c>. // 可能匹配,扩展

<b>software</b> engineer <b>working</b> in <b>Shanghai</c>. // 可能匹配,扩展

……(略)

<b>software</b> engineer <b>working</b> in <b>Shanghai</c>. // 找到一个匹配,保存到结果集,继续

engineer <b>working</b> in <b>Shanghai</c>.  // 不匹配,继续

……(略)

<b>working</b> in <b>Shanghai</c>. // 又找到一个匹配,保存到结果集

……(略)

 

       回顾先前 \d{1,3} 匹配数字的例子。对于 1234,当使用 \d{1,3} 时,进行的是贪婪匹配,它首先找到 123(因为 1234 不符合),之后的“4”也符合,所以,找到的匹配是“123”和“4”。
       当我们使用 \d{1,3}? 匹配上面的例子,对于 1234,这次是惰性匹配。首先,发现“1”符合,将“1”保存到匹配集合中;随后,依次发现“2”、“3”、“4”符合,并依次保存到结果集中,最后,我们得到了四个匹配“1”、“2”、“3”、“4”。

 

4. 值得注意的两个匹配模式

       回顾一下先前贪婪、惰性匹配语法的表,有两个匹配模式比较有意思:

       {n}:由于它精确地要求匹配 n 个字符,所以无所谓贪婪还是惰性,尽管 {n}? 也是正确的匹配写法,但匹配结果总是相同。

         ??:它看上去比较古怪且不好理解,因为通常我们使用贪婪匹配的时候都是匹配多个,也就是“*”或者“+”之类的匹配,而这里是 0 个或 1 个,它的贪婪与惰性匹配又是如何呢?

Text:These flowers are for you.
RegEx:flowers?
Result:flowers

       在这个匹配中,匹配是这样的:首先需要匹配 flower 字符串,然后,可以有 0 个或者 1 个 s,贪婪匹配匹配尽可能多的,所以 flowers 被匹配了。

 

Text:These flowers are for you.
RegEx:flowers??
Result:flower

       这次只匹配了 flower,我想现在已经不用过多解释,你对这个怪异的匹配语法已经明白了。惰性匹配发现 flower 满足匹配 0 个的条件,于是将它保存到匹配结果集,然后重新进行匹配查找,直到字符串结束。

 

匹配边界

       回忆一下刚才邮件地址的匹配,表达式 \w+[\w.]*@[\w.]+\.\w+ 匹配了不合法的邮件地址“.jimmy_dev@163.com”中合法的部分,而通常,我们希望它完全不去匹配它。这时,就有一个匹配边界的问题,我们希望:\w+ 必须出现在字符串的首位置,也就是字符串的边界。

       邮件的例子稍显复杂,我们再看一个更简单的情况:

Text:The cat scattered its food all over the room.
RegEx:cat
Result:The cat scattered its food all over the room.

       可见,通常情况下,我们只希望匹配 cat ,而不希望匹配 scattered 中出现的 cat。

 

1. 匹配单词边界

       正则表达式中,可以在字符前加 \b 来匹配其后面的字符位于字符串首位的字符。

Text:Attention: Zhang is the first name of JimmyZhang.
RegEx:\bz
Result:Attention: Zhang is the first name of JimmyZhang.

       \bz 匹配所有位于字符串首位的 z,而对位于字符串中间的 z 则不进行匹配(JimmyZhang 中的 Z)。不过,我们很少如此简单地使用边界匹配,而是将它加在一个表达式前面。

Text:The cat scattered its food all over the room.
RegEx:\bcat
Result:The cat scattered its food all over the room.

       这一次,匹配正确,有了刚才单个字符的例子,现在这个表达式很好理解了:\b 只规定了 c 这个字符必须出现在字符串首位,接下来需要出现字符 at。这两个字符的匹配与 \bc 无关,它们属于固定字符匹配的范畴。

       但是这个匹配还存在问题,如果我们将上面的例子稍微做下修改,将 scattered 单词分成 s 和 cattered:

Text:The cat s cattered its fod all over the room.
RegEx:\bcat
Result:The cat s cattered its food all over the room.

       可以看到,又错误地匹配了 cattered,因为它符合我们前面所说的匹配规则。可见,如果需要仅匹配 cat 这个单词,我们还需要规定 t 必须出现在单词的末尾。

 

       正则表达式中,可以在字符后加 \b 来匹配其前面的字符位于字符串末位的字符。

Text:The cat s cattered its food all over the room. The cat scat tered its food all over the room.
RegEx:cat\b
Result:The cat s cattered its food all over the room. The cat scat tered its food all over the room.

       可以看到,cat\b 不能匹配 cattered 之中的 cat,却匹配了 scat 中的 cat。这个匹配的过程与上面类似。

 

       显然,为了精确地匹配 cat,我们需要在前后都加上字符边界,\bcat\b:

Text:The cat scat tered its food all over the room. The cat s cattered its food all over the room.
RegEx:\bcat\b
Result:The cat scat tered its food all over the room. The cat s cattered its food all over the room.

       来分析一下它是如何匹配的:首先,c 必须出现在字符串首位;然后,紧跟一个 a 字符;最后,它必须以 t 结束。

 

2. 边界及其相对性

       通常情况下,以空格、段落首行、段落末尾、逗号、句号 等符号作为边界,值得注意的是,分隔符“-”也可以作为边界。

Text:The cat s-cat-tered its food all over the room.
RegEx:\bcat\b
Result:The cat s-cat-tered its food all over the room.

       这是什么原因呢?其实很好理解,从“-”的字面意思:分隔符,大致就可以想到了。在英语中,它是用来做单词分隔的。
       这里有个重要的搜索引擎优化常识,大家注意到本文档的命名:Regular-Expression-Tutorial.pdf,为什么不用下划线分隔呢?因为当搜索引擎看到“-”的时候,会把它视为一个空格“ ”,而看到下划线“_”的时候,会把它视为空字符“”。实际上,下划线的正确叫法是“连字符”。于是,当命名为 Regular-Expression-Tutorial.pdf 时,搜索引擎看到的是:Regular Expression Tutorial.pdf,而当命名成 Regular_Expression_Tutorial.pdf 时,搜索引擎看作 RegularExpressionTutorial.pdf。可以看出,正则表达式在字符边界问题上对“-”的处理方式与搜索引擎相同。

 

       请牢牢记住边界的这个特点:

  • 当你对一个普通字符,比如 s 设定边界的时候,它的边界是诸如空格、分隔符、逗号、句号等。
  • 当你对一个边界,如分隔符“-”或者“,”等,设定边界的时候,它的边界是普通字符。

       我们先看第一种情况:

Text:aaaaxaaaa aaa-x-aaa
RegEx:\bx\b
Result:aaaaxaaaa aaa-x-aaa

 

       我们再看另一个例子:

Text:aaaa,aaaa aaa-,-aaa
RegEx:\b,\b
Result:aaaa,aaaa\ aaa-,-aaa

       与上面唯一不同的是:这次我们匹配逗号“,”,而它本身也是一个边界,结果与上面完全相反。可见,对于“,”而言,它的边界是一个普通字母。

       边界的相对性是很重要的,因为我们很多时候需要匹配诸如“<”这样的字符。

 

3. 匹配非单词边界

       和上面匹配特定类型字符有些相似,有了 \b,自然有 \B,它用来匹配不在边界的字符。
       我们继续拿上面的例子做示范,来看看 \Bcat 匹配的效果:

Text:The cat scat tered its food all over the room. The cat s cattered its food all over the room.
RegEx:\Bcat
Result:The cat scat tered its food all over the room. The cat s cattered its food all over the room.

       它的匹配规则是这样的:字符 c 必须出现,但是不能位于字符串首位;随后跟两个固定字符 at。

 

Text:The cat scat tered its food all over the room. The cat s cattered its food all over the room. The cat scattered its food all over the room.

RegEx:\Bcat\B

Result::The cat scat tered its food all over the room. The cat s cattered its food all over the room. The cat scattered its food all over the room.

       看了这么多例子,现在不用我讲你也应该明白了:首先,必须出现字符 c,且不能位于字符串首位;接着,c 后面必须出现字符 a;最后,必须出现字符 t,且它不能位于字符串的末尾。

 

4. 匹配文本边界

       有的时候,我们想要匹配的字符串必须位于全部文本的首位,比如说 XML 文件的声明 <?xml version="1.0" encoding="UTF-8" ?>。有的时候,需要匹配的字符串位于全部文本的末尾,比如 </html>。对于这种匹配,上面介绍的单词边界匹配就无能为力了。

4.1 匹配文本首
       在正则表达式中,可以在匹配模式的第一个字符前添加“^”,以匹配满足模式且位于全部文本之首的字符串。

       可以将它的匹配方式理解成这样:

  1. 假设不存在“^”,进行一个正常匹配,将所有匹配的文本保存到匹配集合中;
  2. 在匹配集合中寻找位于所搜索的文本首位的匹配;
  3. 从匹配集合中删除其他匹配,仅保留该匹配。

       我们依然从简单的例子看起:

Text:

city.jpg、city1.jpg are all beautiful pictures except city9.jpg

city.jpg、city1.jpg are all beautiful pictures except city9.jpg


RegEx:^city\d?\.jpg


Result:

city.jpg、city1.jpg are all beautiful pictures except city9.jpg

city.jpg、city1.jpg are all beautiful pictures except city9.jpg

       按照之前说的,它的匹配过程是这样:假设匹配模式是 city\d?\.jpg,对文本进行匹配;一共找到 6 个符合模式的文本;从所有匹配的文本中筛选出位于文本首位的匹配文本:即第一行的 city.jpg;删除所有其他匹配。
       这里有个值得注意的地方,如果我们在第一行的 city.jpg 中添第几个空格,就破坏了这个匹配:

Text:    city.jpg、city1.jpg are all beautiful pictures except city9.jpg
RegEx:^city\d?\.jpg
Result: city.jpg、city1.jpg are all beautiful pictures except city9.jpg

       可见,没有找到任何匹配,所以,我们进行文本边界匹配时,通常还需要添加对空字符的处理:

Text:   city.jpg、city1.jpg are all beautiful pictures except city9.jpg
RegEx:^\s*city\d?\.jpg
Result:   city.jpg、city1.jpg are all beautiful pictures except city9.jpg

 

4.2 匹配文本末
       在正则表达式中,可以在匹配模式的最后一个字符后添加“$”,以匹配满足模式且位于全部文本之末的字符串。

Text:
city.jpg、city1.jpg are all beautiful pictures except city9.jpg

city.jpg、city1.jpg are all beautiful pictures except city9.jpg

RegEx:city\d?\.jpg\s*$

 

Result:

city.jpg、city1.jpg are all beautiful pictures except city9.jpg

city.jpg、city1.jpg are all beautiful pictures except city9.jpg

       可以看出:\b 和 \B 是对匹配模式(表达式) 中某个字符出现的位置(单词首位还是末位)进行限制。“^”和“$” 是对整个待搜索文本的匹配模式(表达式) 出现位置(文本首位还是文本末位)进行限制。它们的关系是一小一大。

 

 

匹配子模式
       可以看出,我们之前介绍的所有匹配模式(例如“+”、“*”、“{n,m}”),都是针对于某种单个字符的
       考虑这样一个例子:我们需要将 HTML 中两个或以上的 <br />、<br/>、<br> 全部替换成一个 <br />。按照之前的例子,我们只能写出这样的表达式:

Text:This is the first line.<br> This is the second line.<br><br/><br /> This is the third line.<br>>>>>
RegEx:<br\s*/?>{2,}
Result:This is the first line.<br> This is the second line.<br><br/><br /> This is the third line.<br>>>>>

       可以看到,匹配结果并不是我们想要的,这是因为 {2,} 限制的是它之前的单个字符,在本例中也就是“>”的出现次数,而我们希望的,是整个 <br>、<br/> 或 <br />。

 

1. 子模式

       在正则表达式中,可以使用 “(” 和 “)” 将模式中的子字符串括起来,以形成一个子模式。将子模式视为一个整体时,那么它就相当于一个单个字符。
       就本例而言,我们希望子模式就是 <br\s*/>,按上面的定义,我们重新写模式:

Text:This is the first line.<br> This is the second line.<br><br/><br /> This is the third line.<br>>>>>
RegEx:(<br\s*/?>){2,}
Result:This is the first line.<br> This is the second line.<br><br/><br /> This is the third line.<br>>>>>

       这次匹配了正确的文本,我们可以将匹配过程理解成这样:子模式 (<br\s*/?>) 首先匹配所有 <br>、<br/> 或 <br />;然后,将每一个匹配结果视为一个整体(相当于单个字符);接着,匹配这个整体连续出现两次或以上的文本。

 

2. "或" 匹配

       有的时候,我们要取消某段文字中的加粗、斜体等效果,我们想匹配所有的 <b>、</b> 或者 <i>、</i>,然后把它们替换成空,仅利用之前的知识,我们只能进行两次匹配和替换,一次是 </?b>,一次是 </?i>。

       在正则表达式中,可以使用“|”将一个表达式拆分成两部分 reg1|reg2,它的意思是:匹配所有符合表达式 reg1 的文本或者符合表达式 reg2 的文本。
       对于本节提出的问题,可以这样进行解决:

Text:The <b>text of</b> this row is bold. The <i>text of</i> this row is italic.
RegEx:</?i>|</?b>
Result:The <b>text of</b> this row is bold. The <i>text of</i> this row is italic.

 

3. 在子模式中使用“或”匹配

       从上面的定义应该可以看出,“|”分隔的是整个表达式,而有的时候,我们希望分隔的是一个表达式的一部分,比如说,我们想要匹配 1900 到 2099 的所有年份。

Text:1932 is supposed to be matched as a whole, but it is matched only part of it. 2055 is mathced in the right way. 3019 is out of range, but it's still matched partly.
RegEx:19|20\d{2}
Result:1932 is supposed to be matched as a whole, but it is matched only part of it. 2055 is mathced in the right way. 3019 is out of range, but it's still matched partly.

       可以看到,表达式 19|20\d{2} 要么匹配 19,要么匹配 20\d{2}。而我们希望的是匹配 19\d{2} 或者 20\d{2},当然,我们可以改写上面的表达式为 19\d{2}|20\d{2} 来完成,但是,利用本章所讲述的子模式,可以更加简洁地完成这个过程:

Text:1932 is supposed to be matched as a whole, but it is matched only part of it. 2055 is mathced in the right way. 3019 is out of range, but it's still matched partly.
RegEx:(19|20)\d{2}
Result:1932 is supposed to be matched as a whole, but it is matched only part of it. 2055 is mathced in the right way. 3019 is out of range, but it's still matched partly.

       这次,我们得到了想要的结果,使用子模式可以简化匹配表达式。这有点类似于数学中的提取公因式:

2*3 + 7*3 = (2+7)*3 --> 19\d{2}|20\d{2} = (19|20)\d{2}

 

4. 嵌套子模式

       子模式可以再继续嵌套子模式,产生更加功能强大的匹配能力

       比如,我们要匹配 1900 年 1 月 1 日到 2000 年 1 月 1 日除过闰年外的所有正确日期。我们先对这个匹配模式做一个分析:

  1. 首位可以是19也可以是20:Reg:19|20
  2. 当是 19 的时候,后面可以是 00 到 99 中任意数字:RegEx:19\d{2}|20
  3. 当是 20 的时候,只能匹配 00:Reg:19\d{2}|2000
  4. 月份可以是 1 到 9,或者 10 到 12:Reg:(19\d{2}|2000)-([1-9]|1[0-2])

       因为天数与月份相关,所以将 ([1-9]|1[0-2]) 拆分为下面三个子模式:

  1. 当月份是 2 的时候,天数是 28:Reg:2-([1-9]\b|1\d|2[0-8])
  2. 1、3、5、7、8、10、12 月的天数是 31:Reg:([13578]|1[02])-([1-9]\b|[12]\d|3[01])
  3. 4、6、9、11 月的天数是 30:Reg:([469]|11)-([1-9]\b|[12]\d|30)

       组合一下,得到的月份和天数的模式是:

2-([1-9]\b|1\d|2[0-8])|([13578]|1[02])-([1-9]\b|[12]\d|3[01])|([469]|11)-([1-9]\b|[12]\d|30)

       再结合上面年的部分,得到最终的结果:

(19\d{2}|2000)-(2-([1-9]\b|1\d|2[0-8])|([13578]|1[02])-([1-9]\b|[12]\d|3[01])|([469]|11)-([1-9]\b|[12]\d|30))

 

Text:These dates are matched: 1900-1-1、1928-2-28、1931-11-30、2000-1-1、1999-10-30 These dates are not matched: 1900-1-32、1928-2-29、2000-01-1、1982-12-08
RegEx:(19\d{2}|2000)-(2-([1-9]\b|1\d|2[0-8])|([13578]|1[02])-([1-9]\b|[12]\d|3[01])|([469]|11)-([1-9]\b|[12]\d|30))
Result:These dates are matched: 1900-1-11928-2-281931-11-302000-1-11999-10-30 These dates are not matched: 1900-1-32、1928-2-29、2000-01-1、1982-12-08

 

 

后向引用

1. 理解后向引用

       我们还是一如既往地从最简单的开始。假设我们要进行这样一个匹配:找出下面文本中所有重复的单词,以便日后进行替换。

Is the cost of of gasline going up?

       我们看到:of of 重复了,我们需要找出它:

Text:Is the cost of of gasline going up?
RegEx:of of
Result:Is the cost of of gasline going up?

       很显然,匹配结果满足了我们的要求,在这里使用全字匹配是为了后面好说明。

 

       现在,如果 up 也重复出现了,句子变成这样:

Is the cost of of gasline going up up?

       我们就需要改写表达式成这样:

Text:Is the cost of of gasline going up up?
RegEx:(of|up) (of|up)
Result:Is the cost of of gasline going up up?

       我们可以使用更简洁的表达式:((of|up)\b ??){2},但是为了后面好说明,这里我们还是使用全字匹配。关于这个表达式,首先记住,\b 只是对边界进行限制,不匹配任何字符。如果写做 ((of|up)\b ){2},则无法匹配 "up up",因为它要求 up 后面必须出现一个空格“ ”,而本句中,up 后面紧跟了一个问号。
如果写成 ((of|up)\b ?){2},因为是贪婪匹配,如果 of 后出现空格,就会匹配之。两个 of 的匹配将变成 "of of "。通常,我们会替换它成一个 "of",这样,就会出现 "ofgasline" 的情况。
       最终,我们把贪婪匹配改成惰性匹配:((of|up)\b ??){2}

 

       正则表达式中,使用“\数字”来进行后向引用,数字表示这里引用的是前面的第几个子模式。

Text:Is the cost of of gasline going up?
RegEx:(of) \1
Result:Is the cost of of gasline going up?

       表达式“(of) \1”使用了后向引用,意思是:“\1”代表前面第一个匹配了的子模式的内容,对于本例,因为前面子模式只可能匹配“of”,那么“\1”等价于“of”,整个表达式相当于“of of”。

 

Text:Is the cost of of gasline going up up? Look up of the TV, your mobile phone is there.
RegEx:(of|up) \1
Result:Is the cost of of gasline going up up? Look up of the TV, your mobile phone is there.

       (of|up) \1 的含义是:如果子模式1 匹配了 of,那么 \1 就代表 of;如果子模式1 匹配了 up,那么 \1 就代表 up,整个表达式相当于 of of|up up。


       在我们所讨论的简单情况下,已知的重复单词是确定的,我们可以找到后向引用的等价表达式。但通常情况下,我们不知道哪些单词重复,这时候,就只能使用后向引用来完成:

Text:Is the cost of of gasline going up up? Look up of the TV, your mobile phone is there. You are the best of the the best.
RegEx:(\w+) \1
Result:Is the cost of of gasline going up up? Look up of the TV, your mobile phone is there. You are the best of the the best.

       这次,我想你应该可以想出 (\w+) \1 所代表的含义。

 

2. 后向引用的一个常见应用

       匹配重复单词是后向引用的一个较为常见的应用,还有一个应用是匹配有效的HTML标记:

Text:<h1>This is a valid header</h1> <h2>This is not valid.</h3>
RegEx:<h([1-6])>.*?</h\1>
Result:<h1>This is a valid header</h1> <h2>This is not valid.</h3>

       可以看到,它只匹配了符合 HTML 语法的文本,对于前后不一致的文本没有进行匹配。

 

3. .Net中的后向引用

       经常写正则表达式的朋友们应该都有体会:对于复杂的匹配,尤其是子模式嵌套子模式的情况,常常判断不清楚后向引用匹配的到底是前面哪个子模式,比如:我们写的“\4”,期望它去匹配第4个子模式,但往往它匹配了第3个或者第5个。

       如果能有名字命名子模式就好了,这样,在后向引用中,我们可以使用这个名字,而不用根据数字去判断。在 .Net 中使用正则表达式进行查找时,给子模式命名的语法是:?<name>,后向引用的语法是:\k<name>。

       我们改写前面的范例:

Text:<h1>This is a valid header</h1> <h2>This is not valid.</h3>
RegEx:<h(?<sub>[1-6])>.*?</h\k<sub>>
Result:<h1>This is a valid header</h1> <h2>This is not valid.</h3>

       可以看到,我们给子模式 ([1-6]) 起了个名字 sub,然后在后向引用中使用了它 \k<sub>,\k<sub> 就相当于 \1。

 

 

文本替换

1. 使用后向引用进行文本替换

       迄今为止,我们所了解的正则表达式都是用于匹配文本,而如果我们对匹配了的文本不能进行替代,那它也就没有什么用了。

       所以,正则表达式的三部曲应该是:1、查找;2、引用匹配了的文本(后向引用);3、有选择地替换文本。

       大部分语言的正则表达式实现,在查找中,使用后向引用来代表一个子模式,其语法是“\数字”;而在替换中,其语法是“$数字”。

1.1 高亮显示文本
       假设我需要高亮显示所有h1中的文本,我们就可以使用后向引用来完成:

Text:<h1>This is a valid header</h1> <h2>This is not valid.</h3>
RegEx:<h1>(.*?)</h1>
Replace:<h1 style="background:#ff0">$1</h1>
Result:<h1 style="background:#ff0">This is a valid header</h1><h2>This is not valid.</h3>

       在这个例子中,“$1”代表了<h1></h1>之间的文本。

 

1.2 替换电话号码格式
       我们的电话格式通常都是:(区号)电话,比如说:(029)8401132;现在假设我们要求把文本中所有的电话格式都改为:029-8401132,我们可以这样做:

Text:(020)82514769 (021)83281314 (029)88401132
RegEx:\((\d{3})\)(\d{8})
Replace:$1-$2
Result:020-82514769 021-83281314 029-88401132

       如果这篇文章你是从头看到这里,相信这个表达式对你来说没有任何难度,需要留意的是这里对元字符“(”和“)”进行了转义,并且,在替换结果中,我们要求它不出现。

 

2. .Net 中的文本替换

       同后向引用在查找时的情况类似,在 .Net 中,在替换时也可以对后向引用进行命名。在 .Net 中使用正则表达式进行替换时,给子模式命名的语法是:?<name>,后向引用的语法是:${name}。
       我们修改高亮显示h1文本的范例:

Text:<h1>This is a valid header</h1> <h2>This is not valid.</h3>
RegEx:<h1>(?<sub>.*?)</h1>
Replace:<h1 style="background:#ff0">${sub}</h1>
Result:<h1 style="background:#ff0">This is a valid header</h1><h2>This is not valid.</h3>

 

 

预查和非获取匹配

1. 理解非获取匹配

       假设,我们有下面这样一段文本,而我们想要获取的 Windows 的所有版本,我们可以这样写:

Text:Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.Windows 95 and Windows 98 are the successor.Then Windows 2000 and Windows Xp appeared.Windows Vista is the Latest version of the family.


RegEx:Windows [\w.]+\b


Result:Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.Windows 95 and Windows 98 are the successor.Then Windows 2000 and Windows Xp appeared.Windows Vista is the Latest version of the family.

       这时,如果我们想将所有的 Windows,全部换成简写 Win,并去掉 Windows 与版本号之间的空格,我们则需要使用后向引用:

Text:Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.Windows 95 and Windows 98 are the successor.Then Windows 2000 and Windows Xp appeared.Windows Vista is the Latest version of the family.


RegEx:Windows ([\w.]+\b)
Replace:Win$1


Result:Win1.03 and Win2.0 fisrt Released in 1985 and 1987 respectively.Win95 and Win98 are the successor.Then Win2000 and WinXp appeared.WinVista is the Latest version of the family.

       我们首先查看一下表达式的区别,为了要使用后向引用,我们用“(”和“)”把“[\w.]+\b”包起来,使它成为一个子模式。我们知道,只有这样,才可以用 $1 去引用它,这里,我们发现使用子模式的一个作用:系统会在幕后将所有的子模式保存起来,以供后向引用使用(包含查找时的后向引用和替换时的后向引用)。

 

       而很多时候,我们添加一个子模式,并不是为了在后向引用中获取它,我们或许是出于匹配需要,或许简单的只是为了使表达式更清晰。正则表达式中,可以在子模式内部前面加“?:”来表示这个子模式是一个 非获取匹配,非获取匹配不会被保存,不能在后向引用中获取。

       对于本例,我们来测试一下:

Text:Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.Windows 95 and Windows 98 are the successor.Then Windows 2000 and Windows Xp appeared.Windows Vista is the Latest version of the family.


RegEx:Windows (?:[\w.]+\b)
Replace:Win$1


Result:Win$1 and Win$1 fisrt Released in 1985 and 1987 respectively.Win$1 and Win$1 are the successor.Then Win$1 and Win$1 appeared.Win$1 is the Latest version of the family.

       我们看到,由于子模式没有被保存,所以 $1 被当作一个普通字符进行了处理。可见,所谓非获取匹配,意思就是说它只进行匹配,并不保存结果供以后引用。

 

2. 正向预查

       看到这里,你可能觉得 非获取匹配 没有什么实际用途。
       现在,我们假设需要仅匹配 Windows,不匹配后面的版本号,并且要求 Windows 后面的版本号只能是数字类型,换言之,XP 和 Vista 不能被匹配,该如何做?
       按照现有的知识,我们大概只能写出这样的表达式:

Text:Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.Windows 95 and Windows 98 are the successor.Then Windows 2000 and Windows Xp appeared.Windows Vista is the Latest version of the family.


RegEx:Windows [\d.]+\b

Result:Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.Windows 95 and Windows 98 are the successor.Then Windows 2000 and Windows Xp appeared.Windows Vista is the Latest version of the family.

       的确,它是匹配了 数字类型 版本的Windows,但是,它不符合我们的要求。因为,我们要求不能匹配版本号。

       在正则表达式中,可以使用正向预查来解决这个问题。本例中,写法是:Windows( ?= [\d.]+\b)。它的语法是在子模式内部前面加“?=”,表示的意思是:首先,要匹配的文本必须满足此子模式前面的表达式(本例,"Windows ");其次,此子模式不参与匹配。

Text:Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.Windows 95 and Windows 98 are the successor.Then Windows 2000 and Windows Xp appeared.Windows Vista is the Latest version of the family.


RegEx:Windows( ?=[\d.]+\b)


Result:Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.Windows 95 and Windows 98 are the successor.Then Windows 2000 and Windows Xp appeared.Windows Vista is the Latest version of the family.

       这次,你大概了解了“非获取匹配”这五个汉字的含义,它们仅仅起一个限制作用,不参与匹配。你可以将正向预查理解成为自定义的边界(\b),这个边界位于表达式末。

       反言之,你可以将位于表达式末的 \b 理解成非获取匹配的一个特例:(?=[ ,.\r\n<>;\-])。注意,这里没有写全边界符号。

Text:aaaax-aaa aaaaxaaaa
RegEx:x(?=[,.\r\n<>;\-])
Result:aaaax-aaa aaaaxaaaa

       你也可以这样理解上面的匹配过程:

  1. 先进行普通匹配:x 后必须跟子模式中定义的字符 1 个,可见,只有第一段字符串中的 x- 符号要求
  2. 然后从匹配文本中将子模式内的文本排除掉,即排除 "-"

 

3. 反向预查

       正向预查类似于自定义的位于文本末的字符边界。那么自然应该有位于文本首的情况,比如说,我们要匹配下面文本中属于 CNY 的金额:

Text:CNY: 128.04 USD: 22.5 USD: 23.5 HKD: 1533.5 CNY: 23.78
RegEx:CNY: \d+\.\d+
Result:CNY: 128.04 USD: 22.5 USD: 23.5 HKD: 1533.5 CNY: 23.78

       与上面类似,我们现在要求仅匹配金额,而不匹配前面的 “CNY:”

       正则表达式中,可以使用反向预查来解决这个问题。本例中,写法是:(?<=CNY: )\d+\.\d+
       反向预查的语法是在子模式内部前面加“?<=”,表示的意思是:首先,要匹配的文本必须满足此子模式后面的表达式(本例,“\d+.\d+”);其次,此子模式不参与匹配。

Text:CNY: 128.04 USD: 22.5 USD: 23.5 HKD: 1533.5 CNY: 23.78
RegEx:(?<=CNY: )\d+\.\d+
Result:CNY: 128.04 USD: 22.5 USD: 23.5 HKD: 1533.5 CNY: 23.78

 

4. 正向、反向预查组合

       我们可以将正向预查和反向预查组合起来使用,比如我们想获取所有 head 之间的文本,就可以这么写:

Text:<h1>This is header.</h2> <h2>This is header,too.</h2> <span>This is not a header.</span>
RegEx:(?<=<h(?<number>[1-6])>).*?(?=</h\k<number>>)
Result:<h1>This is header.</h2> <h2>This is header,too.</h2> <span>This is not a header.</span>

       注意,这里综合应用了前面的知识,将首尾不一致的 <h1></h2> 做了过滤。

 

5. 负正向预查、负反向预查

5.1 负正向预查
       如同 \b 有与之相对的 \B 一样,正向预查也有它的逆过程,称之为负正向预查。在正则表达式中,可以在子模式内部前面加 “?!” 来形成一个 负正向预查,它的效果与 “?=” 相反。

Text:Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.Windows 95 and Windows 98 are the successor.Then Windows 2000 and Windows Xp appeared.Windows Vista is the Latest version of the family.
RegEx:Windows(?! [\d.]+\b)
Result:Windows 1.03 and Windows 2.0 fisrt Released in 1985 and 1987 respectively.Windows 95 and Windows 98 are the successor.Then Windows 2000 and Windows Xp appeared.Windows Vista is the Latest version of the family.

       从结果我们看到,它匹配了后面不是数字版本的 Windows。

 

5.2 负反向预查
       在正则表达式中,可以在子模式内部前面加 “?<!” 来形成一个负反向预查,它的效果与“?<=” 相反。

Text:CNY: 128.04 USD: 22.5 USD: 23.5 HKD: 1533.5 CNY: 23.78
RegEx:(?<!CNY: )\b\d+\.\d+
Result:CNY: 128.04 USD: 22.5 USD: 23.5 HKD: 1533.5 CNY: 23.78

       这次,它匹配所有前面不是 CNY 的金额,也就是 HKD 和 USD 后面的数字。

posted on 2013-05-09 15:53  SkySoot  阅读(1225)  评论(1编辑  收藏  举报

导航