1.匹配固定单个字符
所有的 单个 大小写字母、数字,以及后面将要讲述的特殊字符,都是一个
正则表达式,它们只能匹配单个字符,且这个字符与它本身相同,例如,对于表达式“i” :
2.匹配任意单个字符
“.”可以匹配任意的 单个 字符、英文字母、数字,以及它本身。我们现在结
可以看到,对于表达式“regular.”来说,前面“regular”部分是一个全字匹配,
只能固定的匹配“regular”字符串;后面的“.”部分,可以匹配任意单个字符(包含字符“.”本身)。
“.”可以连续使用,比如,我们可以写出“.e..”这样一个正则表达式。它将匹配所有:
前面有任意一个字符,紧跟着一个 e,随后又跟着任意两个字符的文本。
NOTE:很多情况下, “.”不匹配换行。但在 RegexTester 这个工具中,它匹配
换行,所以,这里有个值得注意的匹配,就是 回车符+exp,我在上面的代码中用绿色粗体标识出了(回车符难以标识)。
3.匹配“.”元字符
有的时候,我们不想让“.”去匹配任何的字符,仅仅想让它匹配“.”这一单个字符,也就是仅匹配它本身,此时,可以使用“\.”来对它进行转义。
表达式“r\.”仅仅匹配了字符串“r.” ,没有匹配下面的 “r1” 。
表达式“r.”则会匹配“r1” 、 “r.” 、以及“re” 。
NOTE:如果要匹配“\”,可以使用“\\”来对它进行转义。 后面还会介绍更多需要转义的字符。
4.匹配字符组
4.1 字符组的基本语法
“.”过于灵活了,它可以匹配几乎所有的单个字符。有的时候,我们只希望匹配有限个字符中的某一个。这个时候,可以使用字符组。
假设有这样一种情况,我们希望验证某个单词是不是拼写正确,比如说 “head” 是一个单词, “heat”也是一个单词,但“heay”就不是一个单词,所以, “hea”后面要么出现“d” ,要么“t” ,而如果我们使用“.”来进行匹配,那么不管是“heay”也好, “heas”也罢,都会被匹配(被认为是正确的单词)。
正则表达式提供 字符组 来解决这一问题,对于上例, “hea”后面仅可以匹配“d”或者“t”的情况,它的语法是 “[dt]” 。中括号是特殊标记,用以划定属于组内的字符的界限,它所代表的含义是:“匹配d 或者 t”。
NOTE:字符组 虽然由多个字符构成,但它仍只匹配 单个 字符,而字符组能够匹配的单个字符,即是它定义中的字符( “[]”内的字符) 。 “[]”本身不进行字符匹配,它仅仅划定字符组边界。
4.2 在字符组中使用字符区间
现在假设我们需要匹配一组文件名,它们的名称为 city0.jpg、city1.jpg...city9.jpg。
根据前面介绍的内容,我们很容易写出这样的表达式: “city[0123456789].jpg” 。
没错!这样写法的确可以达到我们要的效果。如果说写 10 个阿拉伯数字你觉得并不困难,那么如果要匹配这样的文件名呢?a_1.jpg、b_1.jpg、c_1.jpg...z_1.jpg。这次,你的表达式变成这样了:“[abcdefghijklmnopqrstuvwxyz]_1\.jpg” ,哇!看着就难受。
正则表达式提供了字符区间来简化这一写法,它的语法是: “起始字符-结束字符” 。对于上面匹配阿拉伯数字的例子,它的写法是[0-9]。
NOTE:不一定非要将匹配写成“[0-9]” ,完全可以根据需要写成“[0-3]”,它将仅匹配“0,1,2,3”这个区间。起始字符 和 结束字符,依据的是它的 ASCⅡ值的大小,即是说,将会匹配其ASCⅡ码位于 起始字符 和 结束字符 的 ASCⅡ之间的所有字符(包含起始、结束字符)。
另外,如果起始字符的 ASCⅡ值大于结束字符的 ASCⅡ值,例如,如果你写成“[3-0]”,则会出错,导致匹配失败。
NOTE:如果要在字符组(“[”“]”内)中匹配“-” ,需要使用转义符,写法是“\-” ;
而在“[” “]”以外, “-”变成了一个普通字符,无需再进行转义。
同样的道理,我们可以写出“[a-z]”来匹配所有的小写字母, “[A-Z]”匹配所有的大写字母,这里就不再举例子了。
现在假设需要在 HTML 中匹配所有的 RGB 颜色,我们知道,在 Web 中,颜色通常表示为诸如“#FF00CC”这样的值,那么结合上面讲述的内容,我们可以像这样使用匹配:
NOTE:之前说了,我们默认 Ignore Case 是为 True 的,也就是说忽略大小写。因此,本例中的表达式也可以写作: “#[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F]”
4.3 反义字符组
有的时候,我们需要匹配“除了某些字符以外”的其他字符,这时候,我们可以使用反义字符组,其语法是: “[^字符集合]” 我们在以上面的例子为例,现在我们匹配“city1.jpg、city2.jpg、city3.jpg”以外的文件名。
5.匹配特殊字符
5.1 匹配元字符
我们先对元字符作一个定义:元字符是在正则表达式中具有特殊含义的字符。如果我们之前已经介绍过的, “.”就是一个元字符,它用来匹配任意单个字符。当我们要匹配字符“.”本身的时候,需要使用“\”来对它进行转义: “\.”
很容易就看出“\”也是一个元字符,它叫做转义符。所以,当我们需要匹配字符“\”的时候,就需要对它进行转义: “\\” 。
回想一下之前讲述的字符组,我们知道“[”和“]”也是元字符,当我们需要匹配“[”和“]”字符的时候,需要分别写作: “\[”和“\]” 。
举个例子,加入我们需要匹配 “City[0].Name” ,却写成了下面这样:
因为没有对“[” 、 “]”和“.”进行转义,所以“[0]”被当作字符组来解释,因为字符组中只有一个字符“0” ,所以,它仅能匹配一个“0” ,所以“City[0]”相当于“City0” 。又因为没有对“.”进行转义,所以它可以匹配 它本身 以及 “_” 。
正确的写法:City\[0\]\.Name
5.2 匹配空字符
我想先介绍一下回车换行的由来。通常,当我们在键盘上敲击一下回车键时,不管光标此时在哪里,总是会新起一行,然后将光标位于新行的首位置。这在计算机上看起来一气呵成,用一个符号来表示就 OK 了,可在正则表达式中,以及很多语言中(比如 VBScript),却被表示成了两个动作,一个叫“回车” (Carriage Return), 一个叫“换行” (Line Feed), 在语言, 比如VBScript
中,就表示成了:Chr(13)&Chr(10)。这与打印机的工作原理有关,大家知道,打印机先于计算机键盘很多年,是键盘的雏形,在打印机上换行时,将进行两个动作:
1、将打印头换到下一行;
2、将打印头返回到新行的行首位置。也就分别对应了现在的“换行”和“回车” 。
在正则表达式中,比较常用的三类空白字符如下表所示:
元字符 | 匹配描述 |
\r | 回车 |
\n | 换行 |
\t | Tab 键 |
\f | 换页 |
\v | 垂直 Tab |
这种情况使得书写表达式时,变得稍有不便,例如,如果我们想匹配一个换行的效果,我们需要将表达式写成“\r\n” 。然后,我们在 IE 和 Firefox 中用 javascript 分别做个测试,却地发现对于 IE6(NOTE:IE7 我没有试过)来说“\r\n”可以匹配一个换行,而在 Firefox 中,只用一个“\n”就可以了,使用“\r\n”则无法匹配。
对于我们的测试工具 RegexTester 来说,它也是只使用“\n”来匹配一个换行的效果。
5.3 匹配特定字符类型
结合“匹配元字符”和“匹配空字符”这两个小节,我们发现这样一个规律:
z 对于“.”和“[”等来说,它们本身就是 元字符,而当给它们前面加上转义字符“\”的时候,它们才代表一个普通字符: “\.”匹配字符“.” , “\[”匹配字符“[” 。
z 对于“r”和“n”等来说,它们本身只是 普通字符,而只有当加上转义字符“\”的时候(变成了“\r”和“\n”),它们才代表着元字符:“\r”匹配空字符回车, “\n”匹配空字符换行。
下面将讨论的特定字符类型,就属于上面的第二种情况。
5.3.1 匹配数字类型
元字符 | 匹配描述 |
\d | 所有单个数字,与 [0-9] 相同 |
\D | 所有非数字,与 [^0-9] 相同 |
NOTE:不管 Ignore Case 是否设置为 True,在这种情况下, “\d”与“\D”总是区分大小写的,下面将介绍的也是一样。
5.3.2 匹配字母、数字、下划线
不管是在程序命名中,还是文件命名中,这一类字符集都是最常见的,那就是:所有大小写字母、数字 以及 下划线,其正则表达式为“[a-zA-Z0-9_]”。
正则表达式中可以使用“\w ”来代表这一匹配;类似于上一节介绍的,使用“\W”来匹配所有不属于这一字符集的其他字符: “[^a-zA-Z0-9_]” 。
元字符 | 匹配描述 |
\w | 所有单个大小写字母、数字、下划线,与 [a-zA-Z0-9_] 相同 |
\W | 所有单个非大小写字母、非数字、非下划线,与 [^a-zA-Z0-9_] 相同 |
5.3.3 匹配空字符
元字符 | 匹配描述 |
\s | 所有单个空字符,与 [\f\n\r\t\v] 相同 |
\S | 所有单个非空字符,与 [^\f\n\r\t\v] 相同 |
匹配多个字符
应该了解,上面所介绍的不管简单也好,复杂也好,都只是匹配单个字符,如果需要匹配一个很长的字符串,而组成这个字符串的每个字符都比较复杂(没有诸如\d 这样的简写方式),那么,可以想象,一个表达式会多么复杂。
回顾一下匹配 Web 中颜色的例子,我们的正则表达式写法是这样的: “#[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]” 。应该想到,如果有办法确定匹配的单个字符的个数就好了。
本章中,将讨论使用正则表达式来进行多个字符的匹配。
正则表达式中,可以在 单个字符(比如“j”)、字符组(比如“[abcde]”)、特定字符类型(比如“\d”)、单个任意字符(即“.”) 后面加“+” ,来表示匹配一个或多个(换言之,至少一个)字符组成的字符串。
拿上面的例子来说,对于 “j+” 有:
对于 “[abcde]+”有:
对于其他两个就不再进行说明了,可以自行做测试。
我们现在考虑一个实际的例子,我的电子邮件的写法:jimmy_dev@163.com ,如果没有本章的知识,匹配邮件地址是不可能的,因为,我们无法确定的知道邮件地址的长度。现在,如果我们要匹配这个邮件地址,结合之前的知识 和 本小节的新内容,很容易得出这样的表达式:“\w+@\w+\.\w+” 。
我们发现,这样的匹配并不完整,对于多级域名的匹配不完整。我们回想一下之前的内容,很容易想到,我们需要将“.”也加入到字符组中,最后,我们将“\w+”改写成“[\w\.]+” 。
前面说过: “-”只在字符组“[]”区间内才是一个元字符,在“[]”以外是普通字符。在此处, “.”在“[]”区间以外才是元字符,用以匹配任意单个字符,而在“[]”区间以内,它就变成了一个普通字符。所以, “[\w\.]”可以简写成“[\w.]”(写成“[\w\.]也不会出错”)。
这次,可以看到能够正确地匹配多级域名了。
2.匹配零个或多个字符
让我们再次回到刚才匹配城市图片的例子(字符组和反义字符组那一节),我们知道,不管是使用“city[0-3]\.jpg”还是“city[^0-3]\.jpg” , “city”与“.jpg”之间总是要出现一个字符的,而有的时候,我们允许它们之间可以不出现字符,比如说:现在我要求可以匹配 “city.jpg” ,那么,该如何完成呢?
正则表达式中,可以在 单个字符(比如“j”)、字符组(比如“[abcde]”)、特定字符类型(比如“\d”)、单个任意字符(即“.”) 后面加“*” ,来表示匹配零个或多个字符组成的字符串。可以看出,其使用方法与“+”完全相同。
对于上面的例子,我们可以写出下面的表达式:
我们看到,表达式“city\d*\.jpg”匹配这样的字符串:以“city”开头,后面紧跟零个多个“0-9”的数字字符,然后,以“.jpg”结果。
我们再次回到上面的匹配 Email 的例子中。表达式是这样的: “[\w.]+@[\w.]+\.\w+” ,看上去似乎没有问题,但是,我们发现对于 “.jimmy.dev@hotmail.com”这样的地址,它也能够匹配。而我们知道, “.”不应该出现在邮件的第一个字符位置上。
结合本节的知识,我们知道,Email的起首一定是 一个或多个 字母或数字组合,而后面可以是 零个或多个 字母数字与“.”的组合。所以,我们修改匹配为如下:
尽管这次对正确的 Email 地址部分进行了匹配, 但由于“.jimmy.dev@hotmail.com”是作为一个整体出现,而它并不是一个合法的地址,所以我们通常希望的操作是完全不去匹配它,即认为它并不是一个有效 Email,而不是去匹配它合法的部分。如何解决这个问题,留待后面字符边界说明。
对于“flowers+”来说,它不能匹配“flower” ,因为它只能匹配“flower”后面有一个或多个“s”的单词,于是,“flowers”和“flowersss”都可以被匹配。
对于“flowers*”来说,它虽然能够匹配“flower”和“flowers” ,但是一样可以匹配
“flowersss” 。
正则表达式中,使用“?”来匹配零个或一个字符。其使用方式与“+”和“*”相同。
对于本例而言,其使用方法如下:
很明显,本章的所讲述的多字符匹配中,“+” 、 “*” 、 “?”都是元字符,如果要对它们进行匹配,需要使用“\”进行转义: “\+” 、 “\*” 、 “\?” 。
4.匹配指定数目字符
尽管“?” 、 “+” 、 “*”这三个元字符的出现解决了很多问题,但是,它们并不完善:1.没有办法指定最多匹配多少个字符,比如说,我们要匹配手机号码,那么应该是 11 个数字,而“+”和“*”会匹配尽可能多的数字,不管是 17 个还是 18 个都认为是正确的。2.没有办法指定最少匹配多少个字符, “+” 、 “*” 、 “?” ,所能提供的最少字符,要么零个,要么一个。
4.1 匹配固定数目的字符
正则表达式中,可以在 单个字符(比如“j”)、字符组(比如“[abcde]”)、特定字符类型(比如“\d”)、单个任意字符(即“.”) 后面加“{数字}” ,来表示匹配零个或多个字符组成的字符串。
例如:使用“\d{3}”,可以匹配从 000 到 999,这 1000 个数。而使用“a{6}” ,则可以匹配“aaaaaa”(也只能匹配它,因为“a”是固定字符)。
现在我们考虑一个更复杂的例子,假如我们要匹配手机号码,那么它的规则是:首位为“1” ,第二位为“3或者 5” ,后面 9 位任意数字。那么它的匹配应该是这样的:
4.2 匹配区间以内数目的字符
我们再次考虑 000-999 的匹配:“\d{3}”,尽管它没有错,但它只能匹配精确地匹配 3位:000、001、002 … 100、101 … 999。而通常,我们需要对于 0、10、99 这样的数也能够匹配,这时,就需要指定可以匹配 1 到3 位的数字。
正则表达式中,使用“{最小数目,最大数目}”的语法来实现,它的使用方式与上一节介绍的匹配固定数目字符的语法相同。
NOTE:我们发现了两个问题:1、我们不希望 “1234” 被匹配,然而,它被分成两部分“123”和“4”进行了匹配,如何不让它被匹配;2、为什么要被分成 123 和4,而不是 1 和234?我们留待后面讨论。
注意两个特例:
z 最小数目可以是 0,所以 “{0,1}”,相当于 “?”。
z 如果不限制最大数目,可以将最大数目设为空,所以“\d{1,}”相当于“+”;而“{0,}”相当于“*”。
“{”和“}”也是元字符,当我们需要对它们进行匹配的时候,使用“\”进行转义:“\{”和“\}”。
5.贪婪匹配和惰性匹配
5.1 贪婪匹配、惰性匹配概述
我们首先从字面意思上来理解一下贪婪匹配和惰性匹配:
z 贪婪匹配(greedy):它会匹配尽可能多的字符。它首先看整个字符串,如果不匹配,对字符串进行收缩;遇到可能匹配的文本,停止收缩,对文本进行扩展,当发现匹配的文本时,它不着急将该匹配保存到匹配集合中,而是对文本继续扩展,直到无法继续匹配 或者 扩展完整个字符串,然后将前面最后一个符合匹配的文本(也是最长的)保存起来到匹配集合中。所以说它是贪婪的。
z 惰性匹配(lazy):它会匹配尽可能少的字符,它从第一个字符开始找起,一旦符合条件,立刻保存到匹配集合中,然后继续进行查找。所以说它是懒惰的。
光看上面的定义,我们很难有一个生动的认识,现在假设我们要匹配下面 <b>和</b> 之间的文本。为了做演示,尽管不符合 HTML 的定义,我们再加入一段<b>和</c>之间的文本:
这样匹配显然不是我们的初衷,它仅找到了一个匹配,而通常情况下,我们希望得到的是<b>junior</b> 和 <b>living</b> 两个匹配。
解决的办法,就是上面说到的惰性匹配,它的语法如下表所示:
贪婪匹配 | 惰性匹配 | 匹配描述 |
? | ?? | 匹配0个或 1 个 |
+ | +? | 匹配1个或多个 |
* | *? | 匹配0个或多个 |
{n} | {n}? | 匹配n个 |
{n,m} | {n,m}? | 匹配n个或 m 个 |
{n,} | {n,}? | 匹配n个或多个 |
对于本例,当我们使用惰性匹配时:
5.4 值得注意的两个匹配模式
现在请回顾一下上面贪婪、惰性匹配语法的表,有两个匹配模式比较有意思: 一个是“{n}”,对于这种形式的匹配,由于它精确地要求匹配 n 个字符,所以无所谓贪婪还是惰性,尽管“{n}?”也是正确的匹配写法,但它的匹配结果总是与“{n}”相同。
还有一个就是“??”,它看上去比较古怪且不好理解,因为通常我们使用贪婪匹配的时候都是匹配多个,也就是“*”或者“+”之类的匹配,而这里是 0 个或1 个,它的贪婪与惰性匹配又是如何呢?我们还是看范例来说明:
我们来分析一下,在这个匹配中,匹配是这样的:首先需要匹配“flower”字符串,然后,可以有 0 个或者 1 个“s”,按上面讲述的,贪婪匹配匹配尽可能多的,所以 flowers 被匹配了。
再来看看“??”这种匹配。
这次只匹配了“flower”,我想现在已经不用过多解释,你对这个怪异的匹配语法已经明白了。惰性匹配发现“flower”满足匹配 0 个的条件,于是将它保存到匹配结果集,然后重新进行匹配查找,直到字符串结束。
匹配边界
回忆一下刚才邮件地址的匹配,表达式“\w+[\w.]*@[\w.]+\.\w+”匹配了不合法的邮件地址“.jimmy_dev@163.com”中合法的部分,而通常,我们希望它完全不去匹配它。这时,就有一个匹配边界的问题,我们希望:“\w+”必须出现在字符串的首位置,也就是字符串的边界。
邮件的例子稍显复杂,我们再看一个更简单的情况:
可见,通常情况下,我们只希望匹配 cat ,而不希望匹配 scattered 中出现的 cat。
1.匹配单词边界
正则表达式中,可以在 字符 前加“\b”,来匹配其 后面 的字符位于字符串首位的字符。
NOTE:以后提到 字符,指:单个字符(比如“j”)、字符组(比如“[abcde]”)、特定字符类型(比如“\d”)、转义过的特殊字符“\[” 或者 单个任意字符(即“.”)。
我们来看一个最简单的例子:
如同定义的那样:“\bz”匹配所有位于字符串首位的“z”,而对位于字符串中间的“z”则不进行匹配(“JimmyZhang”中的“Z”)。不过,我们很少如此简单地使用边界匹配,而是将它加在一个表达式前面。
现在回到本章开头的例子,修改表达式,让它只匹配单词 cat:
这一次,匹配正确,有了刚才单个字符的例子,现在这个表达式很好理解了:“\b”只规定了“c”这个字符必须出现在字符串首位,接下来需要出现字符“a”、“t”。这两个字符的匹配与“\bc”无关,它们属于固定字符匹配的范畴。关于固定字符匹配,可见本文第一章。
但是这个匹配还存在问题,如果我们将上面的例子稍微做下修改,将“scattered”单词分成“s”和“cattered”:
可以看到,我们仅仅将 “scattered” 拆写成 “s” 和 “cattered” ,就又错误地匹配了 “cattered” ,因为它符合我们前面所说的匹配规则。
可见,如果需要仅匹配“cat”这个单词,我们还需要规定“t”必须出现在单词的末尾。
正则表达式中,可以在 字符 后加“\b”,来匹配其 前面 的字符位于字符串末位的字符。
可以看到, “cat\b” 不能匹配“cattered”之中的“cat”,却匹配了 “scat” 中的“cat”。
这个匹配的过程就不再说明了,与上面类似。
显然,为了精确地匹配“cat”,我们需要在前后都加上字符边界,“\bcat\b”。
如果我简单地说:“\bcat\b”匹配完整地一个“cat”单词,那相当于什么也没说。我们综合上面讲述的,来分析一下它是如何匹配的:首先,“c”必须出现在字符串首位;然后,紧跟一个“a”字符;最后,它必须以“t”结束。
2.边界及其相对性
2.1 边界的定义
讲了这么多,还漏掉了一个重要的内容:究竟什么才算边界? 通常情况下,以 空格、段落首行、段落末尾、逗号、句号 等符号作为边界,值得注意的是,分隔符“-”也可以作为边界。
这是什么原因呢?其实很好理解,从“-”的字面意思:分隔符,大致就可以想到了。实际上,在英语中,它是用来做单词分隔的。
NOTE:
这里有个重要的搜索引擎优化常识,大家注意到本文档的命名,我采用的是:
Regular-Expression-Tutorial.pdf,为什么不用下划线分隔,命名成Regular_Expression_Tutorial.pdf 呢? 因为当搜索引擎看到“-”的时候,会把它视为一个空格“ ” ,而看到下划线“_”的时候,会把它视为空字符“” ,实际上,下划线的正确叫法是“连字符” 。于是,当我命名为 Regular-Expression-Tutorial.pdf 时,搜索引擎看到的是:Regular Expression Tutorial.pdf,而当我命名成Regular_Expression_Tutorial.pdf 时,搜索引擎看作 RegularExpressionTutorial.pdf 。
可以看出,正则表达式在字符边界问题上 对“-”的处理方式 与 搜索引擎相同。
2.2 边界的相对性
请牢牢记住边界的这个特点:
z 当你对一个普通字符,比如“s”,设定边界的时候,它的边界是诸如空格、分隔符、逗号、句号等。
z 当你对一个边界,比如分隔符“-”或者“,”等,设定边界的时候,它的边界是普通字符。
我们先看第一种情况:
这个和我们上面将的例子相似,和我们预想的一样,下面 x 被匹配了,因为“-”可以作为边界。
我们再看另一个例子:
与上面唯一不同的是:这次我们匹配逗号“,”,而它本身也是一个边界,结果与上面完全相反。可见,对于“,”而言,它的边界是一个普通字母。
边界的相对性是很重要的,因为我们很多时候需要匹配诸如“<”这样的字符。
3.匹配非单词边界
和上面 匹配特定类型字符有些相似,有了“\b”,自然有“\B”,它用来匹配不在边界的字符。
我们继续拿上面的例子做示范,来看看“\Bcat”匹配的效果:
它的匹配规则是这样的:字符“c”必须出现,但是不能位于字符串首位;随后跟两个固定字符“a”和“t”。
我们再对上面例子进行扩展:
看了这么多例子,现在不用我讲你也应该明白了:首先,必须出现字符“c”,且不能位于字符串首位;接着,“c”后面必须出现字符“a”;最后,必须出现字符“t”,且它不能位于字符串的末尾。
3.匹配文本边界
有的时候,我们想要匹配的字符串必须位于全部文本的首位,比如说 XML文件的声明 “<?xml version="1.0" encoding="UTF-8" ?>”;有的时候,需要匹配的字符串位于全部文本的末尾,比如</html>。对于这种匹配,上面介绍的单词边界匹配就无能为力了。
3.1 匹配文本首
在正则表达式中,可以在 匹配模式 的第一个字符前添加 “^”,以匹配 满足模式且位于全部文本之首的字符串。可以将它的匹配方式理解成这样:1、假设不存在“^”,进行一个正常匹配,将所有匹配的文本保存到匹配集合中;2、在匹配集合中寻找位于 所搜索的文本 首位的匹配;3、从匹配集合中删除其他匹配,仅保留该匹配 我们依然是从简单的例子看起:“^city\d?\.jpg”。
按照之前说的,它的匹配过程是这样:
1. 假设匹配模式是 city\d?\.jpg,对文本进行匹配。
2. 一共找到 6 个符合模式的文本:第一行 和 第二行的 city.jpg, city1,jpg 及
city9.jpg
3. 从所有匹配的文本中筛选出位于文本首位的匹配文本:即第一行的 city.jpg,删除所有其他匹配。
这里有个值得注意的地方,如果我们在第一行的 city.jpg 中添第几个空格,就破坏了这个匹配。
可见,没有找到任何匹配,所以,我们进行文本边界匹配时,通常还需要添加对空字符的处理:
3.2 匹配文本末
有匹配文本首的语法,自然有匹配文本末的语法。
在正则表达式中,可以在 匹配模式 的最后一个字符后添加 “$”,以匹配 满足模式且位于全部文本之末的字符串。
它的匹配方式 与 匹配文本首“^”相似,这里就不再详细说明了,只给出一个例子:
回顾下之前介绍的,可以看出:“\b”和“\B”是对 匹配模式(表达式) 中某个字符出现的进行位置(单词首位还是末位)进行限制。“^”和“$” 是对 整个待搜索文本 的 匹配模式(表达式) 出现位置(文本首位还是文本末位)进行限制。它们的关系是一小一大。
匹配子模式
可以看出,我们之前介绍的所有匹配模式(例如“+”、“*”、“{n,m}”),都是针对于某种 单个字符 的。
考虑这样一个例子:我们需要将 HTML 中两个或以上的“<br />”、“<br/>”、“<br>”全部替换成一个“<br />”。按照之前的例子,我们只能写出这样的表达式:
可以看到,匹配结果并不是我们想要的,这是因为 “{2,}”限制的是它之前的单个字符,在本例中也就是“>”的出现次数,而我们希望的,是整个“<br>”、“<br/>”或“<br />”。
1.子模式
在正则表达式中,可以使用“(”和“)”将模式中的子字符串括起来,以形成一个子模式。
将子模式视为一个整体时,那么它就相当于一个单个字符。
就本例而言,我们希望子模式就是 “<br\s*/>”,按上面的定义,我们重新写模式:
这次匹配了正确的文本,我们可以将匹配过程理解成这样:子模式“(<br\s*/?>)”首先匹配所有“<br>”、“<br/>”或“<br />”;然后,将每一个匹配结果视为一个整体(相当于单个字符);接着,匹配这个整体连续出现两次或以上的文本。
2.“或”匹配
有的时候,我们要取消某段文字中的加粗、斜体等效果,我们想匹配所有的“<b>”、 “</b>” 或者 “<i>”、“</i>”,然后把它们替换成空,仅利用之前的知识,我们只能进行两次匹配和替换,一次是“</?b>”,一次是“</?i>”。
在正则表达式中,可以使用“|”将一个表达式拆分成两部分“reg1|reg2”,它的意思是:
匹配所有符合表达式reg1 的文本 或者 符合表达式 reg2 的文本。
对于本节提出的问题,可以这样进行解决:
3.在子模式中使用“或”匹配
从上面的定义应该可以看出,“|”分隔的是整个表达式,而有的时候,我们希望分隔的是一个表达式的一部分,比如说,我们想要匹配 “1900”到“2099”的所有年份。
可以看到,表达式“19|20\d{2}”要么匹配“19”,要么匹配“20\d{2}”。而我们希望的是匹配“19\d{2}”或者“20\d{2}”,当然,我们可以改写上面的表达式为“19\d{2}|20\d{2}”来完成,但是,利用本章所讲述的子模式,可以更加简洁地完成这个过程。
这次,我们得到了想要的结果,使用子模式可以简化匹配表达式。
NOTE:有点类似于数学中的提取公因式: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]) 拆分为下面三个子模式:
5. 当月份是 2的时候,天数是 28; Reg:2-([1-9]\b|1\d|2[0-8])
6. 1、3、5、7、 8、 10、 12 月, 天数是 31; Reg: ([13578]|1[02])-([1-9]\b|[12]\d|3[01]) 7. 4、6、9、11月,天数是 30; Reg:([469]|11)-([1-9]\b|[12]\d|30)
NOTE:注意上面日期部分的匹配,分成了两部分,月和日;对于月来说,如果我们要匹配大月(31 天的月),写法是:[13578]|1[0-2];而日期部分,比如说要匹配 31天,它又由三部分组成:[1-9]表示 1 号到9 号;[12]\d 表示 10 号到29 号;3[01]表示 30 号到31 号。
还有个地方需要注意:单词边界问题,如果你这样写表达式:2-([1-9]|1\d|2[0-8]),对于 2-29 这样不应该匹配的日期,会匹配它合法的部分 2-29,因为2-2满足2-[1-9]。回顾下我之前讲述的内容,我们还必须规定,当天数是个位数时,它必须处于单词边界[1-9]\b。
组合一下,得到的月份和天数的模式是:
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))
后向引用
1.理解后向引用
我们还是一如既往地从最简单的开始。假设我们要进行这样一个匹配:找出
下面文本中所有重复的单词,以便日后进行替换。
Is the cost of of gasline going up?
我们看到:“of of”重复了,我们需要找出它:
很显然,匹配结果满足了我们的要求,在这里使用全字匹配是为了后面好说明。
现在,如果 up 也重复出现了,句子变成这样:
Is the cost of of gasline going up up?
我们就需要改写表达式成这样:
NOTE:我们可以使用更简洁的表达式:((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}
可以看到,对这个句子来说,这样匹配没有问题。但是我们匹配的文本往往比这个复杂,让我们给上面的文本再添一句话。
不幸的是,对于下面不应该匹配的“up of”也进行了匹配。而我们需要的是这样一种匹配:
当前面是“of”的时候,后面跟的也是“of”;当前面是“up”时,后面跟的也是“up”,只有这样的情况才去匹配它。换言之,后面所需要匹配的内容是前面的一个引用。
正则表达式中,使用“\数字”来进行后向引用,数字表示这里引用的是前面的第几个子模式。
我们回头在看本节第一个例子,我们也可以这样写:
这里,表达式“(of) \1”使用了后向引用,意思是:“\1”代表前面第一个匹配了的子模式的内容,对于本例,因为前面子模式只可能匹配“of”,那么“\1”等价于“of”,整个表达式相当于“of of”。
现在,我们使用后向引用,对刚才出现“up of”问题的表达式加以纠正: