2.3 匹配多个字符之一
问题描述
创建一个正则表达式来匹配calendar的所有常见的错误拼写形式,使你能够在一份文档中找到这个单词而无需依赖作者的拼写能力。在每个元音位置都允许使用a或者e。创建另外一个正则表达式来匹配一个单个的十六进制字符。再创建一个正则表达式来匹配不属于十六进制字符的单个字符。
本节中的这个问题用于解释一个重要的、经常使用的正则结构—字符组(character class)。
解决方案
错误拼写的calendar
c[ae]l[ae]nd[ae]r
正则选项:无
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
十六进制字符
[a-fA-F0-9]
正则选项:无
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
非十六进制字符
[^a-fA-F0-9]
正则选项:无
正则流派:.NET、Java、JavaScript、PCRE、Perl、Python、Ruby
讨论
使用方括号的表示法被称作是一个字符组(character class)。一个字符组匹配在一个可能的字符列表中的单个字符。在第一个正则表达式中的3个字符组,每组可以匹配一个a或是一个e。它们彼此之间是独立的。当你使用calendar来测试这个正则表达式的时候,第一个字符组匹配a,第二个字符组匹配e,第三个字符组会匹配a。
在一个字符组中,只有4个字符拥有特殊功能:\、^、-和]。如果你使用的是Java或者 .NET,那么左方括号[在字符组中也是一个元字符。反斜杠总是会对紧跟其后的字符进行转义,这与它在字符组之外的作用一样。被转义的字符可以是单个字符,也可以是表示某个范围的开始或结束。另外4个元字符只有当它们被放置到特定位置时才拥有特殊含义。如果不放置在具有特殊含义的位置,那么可以不必在字符组中进行转义,直接把它们作为字面字符来使用。例如,‹[][^-]›就可以在本书中除JavaScript以外所有流派中实现此用法。JavaScript将‹[]›视为空字符组,所以任何情况下都无法匹配。虽然如此,我们还是推荐你始终对这些元字符进行转义,因此前面的正则表达式应该始终使用‹[][^-]›形式。在使用中始终对元字符进行转义会使你的正则表达式更加容易让人理解。
所有其他字符均为字面量,并且可以直接添加到字符组中。正则表达式‹[$()*+.?{|}›匹配方括号中9个字符中的任意一个。这9个字符仅在字符组外有特殊含义。字符组内它们仅视为字面文本。将它们转义只会使正则表达式更加难以阅读。
字母数字字符则不能使用反斜杠来转义。如果这样,要么会出现一个错误,要么会创建一个正则表达式记号(也就是在正则表达式中含有特殊含义的语法符号)。在前面的实例2.2中,我们讨论了一些其他正则表达式记号,其中提到了它们可以在字符组内使用。所有这些记号都由反斜杠和一个字母组成,有时候后面还会跟一些其他字符。因此,‹[\r\n]›会匹配一个回车符(\r)或者换行符(\n)。
如果紧跟着左括号后面是一个脱字符(^)的话,那么就会排除整个字符组。也就是说它会匹配该字符组以外的任意字符。
在本书介绍的所有正则表达式流派中,不包含换行符的排除型字符组都会匹配换行符。请确保你的正则表达式不会无意中匹配多行文本。
连字符(-)被放到两个字符之间的时候就会创建一个范围(range)。该范围包含连字符之前的字符、连字符之后的字符,以及按照字母表顺序位于这两个字符之间的所有字符。要想知道一个范围中到底包含了哪些字符,请查看ASCII或者Unicode字符表。‹[A-z]›包含在ASCII表中大写A到小写z之间的所有字符。注意这个范围中会包含一些标点符号,因此可以使用‹[A-Z[]^_`a-z]›来更加清晰地匹配相同的字符集合。我们推荐你所创建的范围只位于两个数字,或者两个同是大写或者小写的字母之间。
反向的范围,如‹[z-a]›,是不允许的。
变体
简写
由反斜杠和一个字母组成的6个正则表达式记号会构成简写(shorthand)字符组:‹\d›、‹\D›、‹\w›、‹\W›、‹\s›和‹\S›。你可以在字符组之内或者之外使用这些简写。每个小写的简写字符都拥有一个对应的大写简写字符,其含义正好相反。
‹\d›和‹[\d]›都会匹配单个数字。‹\D›会匹配不是数字的任意字符,所以同‹[^\d]›是等价的。
下面是我们使用简写‹\d›重写本例前面“十六进制字符”正则表达式:
[a-fA-F\d]
正则选项:无
正则流派:.NET、Java、PCRE、Perl、Python、Ruby
‹\w›匹配单个的单词字符(word character)。所谓单词字符指的是能够出现在一个单词中的字符。这包括了字母、数字和下划线。这里所选的字符集合看起来可能有些怪异,但是之所以这样选的原因是这些字符正好是在编程语言中的标识符中通常允许使用的字符。‹\W›则会匹配不属于上述字符集合的任意字符。
在Java 4至Java 6、JavaScript、PCRE和Ruby中,‹\w›总是与‹[a-zA-Z0-9]›的含义完全相同。而在 .NET中,它包含来自所有其他字母表(西里尔语、泰语等)的字母和数字。在Java 7中,只有设置了UNICODE\CHARACTER_CLASS标志才会包含其他字母表的字符。在Python 2.x中,只有当你在创建正则表达式时设置了UNICODE或U标志时,才会包含其他字母表的字符。在Python 3.x中,默认包含其他字母表的字符,但可以使用ASCII或A标志使‹\w›只匹配ASCII编码。在Perl 5.14中,/a(ASCII)标志使‹\w›等同于‹[a-zA-Z0-9_]›,而/u(Unicode)标志则加入所有Unicode字母,/l(local)则使‹\w›依所处地区而定。在Perl 5.14之前的版本中,以及在Perl 5.14中使用/d(default)或者未使用/adlu中任意标志时,如果目标文本或正则表达式编码为UTF-8,或者正则表达式包含了255以上码位(如‹\x{100}›)或Unicode属性(如‹\p{L}›),则‹\w›自动包含Unicode字母;否则,‹\w›默认仅匹配ASCII。
在上述这些流派中,‹\d› 遵循与‹\w›相同的规则。在.NET中,其他字母表中的数字总是会被包含进来,而在Python中则依据UNICODE和ASCII标志,以及使用的是Python 2.x还是3.x。在Perl 5.14中,依据/adlu标志。而Perl更早的版本中,依据目标文本和正则表达式的编码,以及正则表达式是否有任何Unicode记号。
‹\s›匹配任意的空白字符(whitespace character)。其中包括了空格、制表符和换行符。‹\S›会匹配‹\s›不能匹配的任意字符。在.NET和JavaScript中,‹\s›也会匹配根据Unicode标准被定义为空白符号的字符。在Java、Perl和Python中,‹\s›遵循与‹\w›和‹\d›相同的规则。需要注意的是,JavaScript对于‹\s›使用Unicode,而对于‹\d›和‹\w›则使用ASCII标准。当我们还要考虑‹\b›的时候,就会遇到更多的不一致性。‹\b›不是一个简写字符组,而是一个字符边界。虽然你可以期望当‹\w›支持Unicode的时候‹\b›也应该支持Unicode,而当‹\w›只支持ASCII的时候,‹\b›也应该是只使用ASCII,然而事实上却不总是如此。在实例2.6中的“单词字符”小节中会介绍更多的细节。
不区分大小写
(?i)[A-F0-9]
正则选项:无
正则流派:.NET、Java、XRegExp、PCRE、Perl、Python、Ruby
(?i)[^A-F0-9]
正则选项:无
正则流派:.NET、Java、XRegExp、PCRE、Perl、Python、Ruby
不区分大小写也会影响字符组,可以使用一个外部选项来设置(参见实例3.4),也可以在正则表达式内采用模式修饰符来设置(参见实例2.1)。上面给出的这两个正则表达式与最初的解答是等价的。
JavaScript也采用相同的规则,但是它并不支持‹(?i)›。要在JavaScript中把一个正则表达式设置为不区分大小写,就需要在创建的时候设置/i标志。或者使用支持正则表达式前设置模式修饰符的JavaScript XRegExp库。
流派特有的特性
.NET字符组补集
[a-zA-Z0-9-[g-zG-Z]]
正则选项:无
正则流派:.NET 2.0或以后版本
这个正则表达式匹配一个十六进制数字,但是采用了一种迂回的方式。这里的基字符组可以匹配任意字母数字字符,但是后面的一个嵌套组则减去了从g到z的所有字母。这个嵌套组必须出现在基组的最后,紧跟在一个连字号之后:‹[class- [subtract]]›。
当采用了Unicode的属性、区块和字母表时,字符组补集(subtraction)会尤为有用。举例来说,‹\p{IsThai}›会匹配在Thai语区块中的任意字符。‹\P{N}›则匹配不拥有Number属性的任意字符。把二者使用字符组差集组合起来,‹[\p{IsThai}-[\P{N}]]›就可以匹配10个泰语数字的任意字符。实例2.7中介绍了Unicode属性更多细节。
Java字符组并集、交集和补集
Java允许把一个字符组嵌套在另外一个组中。如果嵌套的组是直接包含的,则结果是两个字符组的并集(union)。可以嵌套任意多个组。正则表达式‹[a-f[A-F][0-9]]›和‹[a-f[A-F[0-9]]]›使用了字符组并集。它们匹配一个十六进制数字,与不包含额外方块的形式相同。
正则表达式‹[\w&&[a-fA-F0-9\s]]›使用字符组交集匹配十六进制数字。如果举办一个正则表达式混淆大赛,那么上面这个式子很可能会得奖。这里的基本字符组‹\w›可以匹配任意单词字符。嵌套组‹[a-fA-F0-9\s]›则会匹配任意十六进制数字与任意空白字符。最后所得到的组则是这两个组的交集(intersection),只会匹配十六进制数字。因为基组并不匹配空白字符,而嵌套组不能匹配‹[g-zG-Z_]›,因此在最后的字符组中会去掉它们,只保留十六进制数字。
‹[a-zA-Z0-9&&[^g-zG-Z]]›使用字符组补集匹配一个十六进制数字,它同样也采用了一种迂回的方式。这里的基本字符组‹[a-zA-Z0-9]›可以匹配任意字母数字字符,而随后使用了一个嵌套组‹[^g-zG-Z]›来排除从g到z的所有字母。这个嵌套组必须是一个排除型字符组,并且要紧跟在两个&符号之后:‹[class&&[^subtract]]›。
字符组交集和补集在使用到Unicode的属性、区块和字母表时,是非常有用的。依上所述,‹\p{InThai}›会匹配在Thai语区块中的任意字符,而‹\p{N}›则会匹配拥有Number属性的任意字符。那么,使用正则字符组交集‹[\p{InThai}&&[\p{N}]]›就可以匹配10个泰语数字中的任意数字。