匹配模式
- 1、不区分大小写模式与模式的指定方式
- 2、单行模式
- 3、多行模式
- 4、注释模式
- 5、补充
- 5.1、更多的模式
- 5.2、修饰符的作用范围
- 5.3、失效修饰符
- 5.4、模式与反向引用
- 5.5、冲突策略
- 5.6、哪种方式更好
所谓匹配模式(match mode),指的是匹配时遵循的规则。设置特定的模式,可能会改变对正则表达式的识别,也可能会改变正则表达式中字符的匹配规定。常用的匹配模式一共有4种:
- 不区分大小写模式
- 单行模式
- 多行模式
- 注释模式
1、不区分大小写模式与模式的指定方式
通常,用户关心的只是文本的意义,而不是它的具体形式。比如单词the,在句子中写作the,在句子开头写作The,还可能为了强调写作THE,可是用户不管这些,只希望找到所有的the。为达到这个目的,可以使用第1章介绍的字符组,写作[tT][hH][eE],这样做确实没错,但是如果单词长一些,写起来就很麻烦了,比如tomorrow就要写成[tT][oO][mM][oO][rR][rR][oO][wW]。更重要的是,这样的表达式不够直观,读代码的人很难明白[tT][oO][mM][oO][rR][rR][oO][wW]要匹配的是tomorrow。
为解决这种问题,正则表达式提供了不区分大小写的匹配模式,指定此模式之后,在正则表达式中可以直接写the,就可以匹配the、The、THE等各种大小写形式的the,tomorrow也可以匹配各种形式的tomorrow,大大降低了理解的难度。
在看这个模式的应用实例之前,必须首先了解模式的指定方式。通常,有两种办法指定匹配模式:
- 以模式修饰符指定
- 以预定义的常量作为特殊参数传入来指定
模式修饰符即模式名称对应的单个字符,使用时将其填入特定结构(?modifier)中(其中的modifier为模式修饰符),嵌在正则表达式的开头。
比如不区分大小写的匹配模式对应的模式修饰符是i(case Insensitive),对the指定此模式,完整的正则表达式就是(?i)the。
这个表达式几乎可以原封不动地用在任何语言中(只有JavaScript是例外,JavaScript不支持模式修饰符(?modifier)的记法),其意义都是相同的:对the采用不区分大小写的模式。Python中的结果下例所示。
用模式修饰符指定不区分大小写模式
另一种指定模式的方式是使用预定义的常量作为参数,传入正则函数。在Python中不区分大小写的预定义常量是Re类的静态成员re.IGNORECASE(一般来说,它都是某个类的静态成员),在Java中它属于Pattern类,在.NET中属于RegexOptions,在Ruby中属于Regexp。PHP和JavaScript是例外,它们的做法(其实在Ruby中也可以这么做)是在正则表达式末尾的分隔符(delimeter)之后加上模式对应的字母(比如不区分大小写模式对应的字母是i,则添加字母i)。
下表列出了常用语言中的写法。Golang的语法更加奇怪,似乎完全不支持这种做法,只能在表达式中以修饰符来指定匹配模式。各语言中用预定义常量指定不区分大小写模式的例子参见下例。
各语言中用预定义常量指定不区分大小写模式
比较两种指定形式,模式修饰符较为通用,因为在各种语言中写法基本相同,而预定义常量在不同语言中写法不同。不过,两种形式的效果是相同的:无论以哪种方式,只要指定了不区分大小写模式,正则表达式在匹配时,就不会区分同一个字母的大小写形式((?i)the和(?i)THE是完全等价的)。
之前看到过匹配HTML中tag的例子,比如匹配超链接tag的正则表达式、匹配图片tag及网页标题tag的正则表达式,虽然XHTML规范推荐tag名都用小写字母,但类似<IMG>的tag也很有可能出现,为同时兼容大写字母,可以使用不区分大小写模式,这样比用字符组详细列出字母的大小写形式简单很多,如表所示。
2、单行模式
元字符点号.几乎能匹配任何字符,唯有换行符\n是例外。但是,有时候确实需要匹配“任何字符”,比如在处理HTML源代码时,经常会遇到跨越多行的脚本代码。
正则文档里一般都会说明“点号.不能匹配换行符”,不过许多人并不习惯仔细阅读文档,所以认为点号.能匹配任何字符,当然也就包括换行符,所以直接的想法是用<script\s.*?</script>来匹配。
因为这段JavaScript代码中出现了换行符,所以.*?的匹配最多只能延伸到第一行末尾。之前提到过,可以用[\s\S]之类的字符组匹配“任意字符”,所以正则表达式<script\s[\s\S]*?</script>能解决问题。
不过对大多数人来说,点号更自然,也更简洁,所以正则表达式提供了单行模式。在这种模式下,所有文本似乎只在一行里,换行符是这一行中的“普通字符”,所以可以由点号.匹配。
单行模式对应的模式修饰符是s(Single line),所以如果用模式修饰符,可以在表达式的开头用(?s)指定,因此上面的表达式也可以改写为(?s)<script\s.*?</script>。使用预定义常量的写法见下表。
你可能注意到了,单行模式在不同语言中的称呼很不一样,甚至在同一门语言中,也可能有不同的记法(比如在Python中)。比如在Java和Python中叫作DOTALL(也就是点号通配),这个名字确实更高明(因为常用的模式中还包含“多行模式”,它和“单行模式”没什么联系,但是这两个名字确实很迷惑人)。不过,“单行模式”成了约定俗成的称呼(通用的模式修饰符是s,它难以联系上DOTALL),所以本书还是沿用“单行模式”的说法。
比较奇怪的是Ruby的预定义常量Multiline,它的意思是“多行”,而且它使用的模式修饰符也是m。这确实让人费解,或许本意是“使用点号.的正则表达式可以跨越多行”?不管怎样,请一定记住,Ruby中的“多行模式”实际上是常说的“单行模式”。
如果不想使用“点号+单行模式”的组合(比如JavaScript完全不支持这种模式,想用也没办法),也可以使用[\s\S]之类的字符组,它的确可以匹配任何字符,只是许多人并不习惯这样的写法,“点号+单行模式”的组合看起来更顺眼一些。
3、多行模式
“多行模式”听起来是与“单行模式”对应的,其实这两个模式没有任何联系。
- 单行模式影响的是点号的匹配规则:在默认模式下,点号.可以匹配除换行符之外的任何字符,在单行模式下,点号.可以匹配包括换行符在内的任何字符;
- 多行模式影响的是
^和$的匹配规则:在默认模式下,^和$匹配的是整个字符串的起始位置和结束位置,但在多行模式下,它们也能匹配字符串内部某一行文本的起始位置和结束位置。
假设,需要找到下面文本中所有数字字符开头的行。
解决这个问题,需要定位到每行的起始位置,尝试匹配一个数字字符,如果成功,则匹配之后的整行文本。多行模式的模式修饰符是m(Multiline),所以在表达式的开头用(?m)指定多行模式,这样^可以定位到字符串内部每一行的起始位置;匹配数字字符的表达式是\d,因为没有指定单行模式,点号.不能匹配换行符,.*可以匹配“之后的整行文本”,整个表达式就是(?m)^\d.*,示例见下例。
用模式修饰符指定多行模式
还可以利用多行模式下的$,给每一行的末尾添加句号,如下例所示。
在多行模式下,给行末添加句号
上面的表达式几乎是任何语言中都“通用”的,唯有JavaScript是例外,因为它不支持用模式修饰符指定模式,但是可以使用预定义常量来指定多行模式。在下表中列出了常用语言中预定义常量的写法。
在各种语言中,多行模式对应预定义常量的写法比较统一,都是multiline。值得一提的是Ruby,它默认就采用多行模式,^和$在任何情况下都能匹配文本内部的行起始/结束位置。如果要在Ruby中“摆脱”多行模式,只能用\A替代^,用\Z替代$。
4、注释模式
有时,用到的正则表达式可能非常复杂,不但难以编写和阅读,也难以维护。如果正则表达式也像程序源代码一样,可以添加注释,阅读和维护起来就容易多了。
为解决这个问题,许多语言支持使用(?#comment)的记法添加注释,comment就是注释的内容。所以,上面的表达式^\d.*?$就可以写成这样:
.NET、Python、Ruby、PHP、Objective-C都支持这种记法,Java、JavaScript、Golang不支持。不过,还有一种注释的写法是各种语言都支持的,就是使用注释模式,此时,正则表达式对应的字符串可以跨越很多行。注释模式的代码见下例。
注释模式
在注释模式下,正则表达式内部的空白字符都被忽略(一般来说,主要是ASCII编码中的空白字符,Unicode编码中的空白字符情况不定),注释则以#comment的形式添加在正则表达式内部,每一条注释从#开始,到行末结束。许多文档中都用这种模式来解释复杂的表达式,并且会使用缩进表示层级结构,这样更加方便阅读和维护。比如匹配日期的正则表达式((?x)(\d{4})-(\d{2})-(\d{2})),在注释模式下可以就这样像下例这样展开。
在注释模式下展开复杂正则表达式
注释模式对应的模式修饰符是x(extended mode,扩展模式,但更常见的写法是free-spacing mode,宽松格式模式)。下表介绍了各语言中注释模式的预定义常量。
你可能注意到了,实力同时指定了两种模式:多行模式和注释模式。注释模式的x与多行模式的模式修饰符m,合写作(?mx)。如果需要同时使用多种模式,只要在(?modifier)中将模式修饰符排列起来就可以了。
如果希望同时指定多行模式和注释模式,使用预定义常量该怎么做?答案是,使用位运算符|。通常来说,匹配模式对应的预定义常量都是int类型,所以多个值进行按位与的结果,并不会彼此干扰。比如在Java中,对应的写法就是Pattern.COMMENTS | Pattern.MULTILINE,在.NET、Ruby和Python中也可以这样。
如果是PHP和JavaScript,则在结束的分隔符之后直接并列模式对应的修饰符,比如/regex/mx,这种写法在Ruby中也行得通。
5、补充
5.1、更多的模式
上面提到的4种常用模式是各种语言中通用的,但是匹配模式并不只有这4种,不同的语言提供了不同的模式,它们一般都在预定义常量中,而且不一定有对应的模式修饰符。
比如Java的Pattern还包含其他模式(仅列举两种模式,更多的请参看文档)。
Pattern.UNIX_LINES:在此模式下,^、.、$识别的“行终止符”只有\n,而不能是其他字符(比如\r\n),它对应的模式修饰符是d。Pattern.CANON_EQ:在此模式下,字符的“相等”规则更加灵活,Unicode字符a\u030A与\u00E5也是“相等”的(都是å,也就是拉丁字母a加上分音符,只是前者用两个字符组合,后者用单个字符),此模式可能对性能有很大影响,而且没有对应的模式修饰符。
Python的re也包含了其他模式(仅列举两种模式,更多的请参阅文档)。
re.U或re.UNICODE:在此模式下,\w、\d、\s等字符组简记法的匹配规则会发生改变,比如\w能匹配Unicode中的“单词字符”,包括中文字符,\d也能匹配1、2之类的全角数字字符,它有对应的模式修饰符u。re.A或re.ASCII:因为在Python 3以上的版本中,正则表达式默认采用Unicode匹配规则,如果希望让\d、\w等字符组简记法恢复到ASCII匹配规则,可以使用此模式,它有对应的模式修饰符a。
在.NET和PHP中,也有其他模式可用。不过一般来说,本章介绍的4种模式最为常用,在其他模式中,只有涉及Unicode或者ASCII的模式使用较多,因为它们影响了字符组简记法\d、\s、\w的匹配规则。关于每种语言可用模式的具体信息,请参考该语言对应的章节或文档。
5.2、修饰符的作用范围
常见的模式修饰符是(?modifier)形式的,它表示“从现在开始使用某个模式”。
- 通常的做法是将它写在正则表达式的最开头,表示“整个正则表达式都指定此模式”;
- 如果它出现在正则表达式当中,则表示此模式从这里开始生效;
- 如果模式修饰符出现在某个括号内—比如
((?modifier)…)—那么它的作用范围只限于括号内部,此模式也可以记为(?modifier:…)。
模式修饰符的作用范围见下表所示。
模式修饰符对正则表达式的操控性更强,因为预定义常量指定的匹配模式是对整个表达式生效的。
5.3、失效修饰符
(?modifier)指定了匹配模式开始作用的范围,在正则表达式中,另有一类失效修饰符,它用来“终止”某种模式的作用范围,其形式是(?-modifier),类似(?modifier),只是问号?之后多了一个减号-,表示“取消模式”,也就是某个模式生效到此处为止。
假设,一段文本中包含了许多单词,其中以bar结尾的有foobar、zeebar等,而且大小写不定。
现在希望找出所有这样的foobar和zeebar:不论foo和zee的大小写如何,只要结尾是大写的BAR就可以。FooBAR、ZEEBAR、zeeBAR都符合要求,Foobar、zooBar、feeBAR等则要排除。
你可能会觉得这很简单,要找到的单词,前面部分可能是zee或是foo,不区分大小写则用字符组把大小写形式都列出来即可,后面必定是BAR,所以正则表达式是[fFzZ][oOeE][oOeE]BAR。可是,这个表达式行不通,因为它可以匹配feeBAR,而这并不是我们需要的。
真正要做的,其实是准确划定不区分大小写模式的作用范围即可:匹配前面部分的表达式是(foo|zee),同时必须使用不区分大小写模式;匹配后面部分的BAR则必须停止使用不区分大小写模式。
要满足这个要求,可以使用上一节介绍的((?i)foo|zee)BAR,也可以使用失效修饰符,将表达式写作(?i)(foo|zee)(?-i)BAR,代码见下例。因为Python不支持这种写法,所以使用了Ruby。
不区分大小写模式与反向引用
一般来说,只要语言或工具支持模式修饰符(?modifier),都可以支持失效修饰符(?-modifier),两者配合使用,可以更精确地设定模式的作用范围。不过,Python和JavaScript并不支持(?-modifier)的写法。Golang的文档里虽然没有明确指出支持,但实际代码测试可以支持。
5.4、模式与反向引用
反向引用前面介绍过,它引用之前的表达式匹配的文本。如果之前的表达式使用了某种模式,引用时是否会继承这种模式呢?下面看看实际情况,因为在Python中无法限定模式的作用范围,现在以Java为例,代码见下例。
不区分大小写模式与反向引用
在表达式((?i)abc)\1中,\1引用的是(?i)abc能匹配的文本(在这个例子里就是abc),因为\1不在区分大小写模式的作用范围内,所以无法匹配ABC;而在表达式(?i)(abc)\1中,\1处在区分大小写模式的作用范围内,所以可以成功匹配ABC。
在下例所示的两个表达式中,\1引用的是之前a.匹配的文本,在单行模式下.可以匹配换行符;所以a.匹配的是字符a和换行符\n,因此\1也可以匹配字符a和换行符\n。
单行模式与反向引用
在下例的两个表达式中,\1引用的是之前^a匹配的文本,^a只能匹配行开头的a;但是无论\1是否处在多行模式的作用范围内,它能匹配的不只有行开头的a,而是任何位置的字符a,这一点值得注意。
多行模式与反向引用
5.5、冲突策略
使用(?-modifier)可以终止某个模式的作用范围;但是,模式也可以通过预定义常量来指定,它是对整个表达式生效的。这时候,就可能产生冲突。
如果在正则表达式中使用(?-i)取消不区分大小写的匹配模式,又使用了预定义常量(比如Java中的Pattern.CASE_INSENSITIVE)指定整个表达式采用不区分大小写的模式,这时候情况会是怎样呢?从下例可以看到。
冲突策略
可以看到,模式修饰符具有更高的优先级。严格地说,如果用预定义常量指定了整个正则表达式采用某种模式,同时正则表达式内部使用了关闭该模式的失效修饰符,则失效修饰符之后的部分,都不受预定义常量所指定模式的影响(失效修饰符之前的部分则不受影响)。并不是每种语言的文档都会针对这种情况做详细讲解,但大家的原则一致。下例给出了具体的例子。
更详细的例子
5.6、哪种方式更好
模式修饰符和预定义常量都可以指定匹配模式,哪种方式更好?这个问题没有标准答案。
用模式修饰符指定的好处之一是简洁,因为通用的模式修饰符就只有ixsm,虽然不那么直观,但习惯之后并不难理解;而且,这些记法在各语言中都是通用的;再者,模式修饰符还可以用(?i)、(?-i)来精确控制模式的作用范围,这是预定义常量做不到的;另外,许多正则处理函数只接受字符串形式的正则表达式,此时只能以模式修饰符来标识模式。
使用预定义常量的好处在于,它很形象—“单行模式”“多行模式”的说法确实很让人困惑,尤其它们竟然可以同时使用,互不干扰,而看字面意思分明是互相矛盾的;而且,一些模式并没有对应的修饰符,比如Java中的Pattern.LITERAL(消除所有元字符的特殊含义)、.NET中的RegexOptions.ECMAScript(字符组简记法采用ASCII匹配规则)。
我的建议是,熟练掌握常用的模式,在能够使用模式修饰符的地方,尽量使用修饰符。毕竟,无论使用哪种编程语言,(?i)是熟悉正则表达式的人都能看懂的,但是不是每个人都能记得的,Java中的Pattern.DOTALL等价于.NET中的RegexOptions.Singleline,也不是每个人都能记得,Java中的Pattern.CASE_INSENSITIVE等价于RegexOptions.IgnoreCase,因此,理解起来总是要多点周折。