正则指引
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第一部分

第1章 字符组

1.1 普通字符组

字符组(Character Class)在有的资料中,写作Character Set,所以也有人翻译为“字符类”或者“字符集”。不过在计算机术语中,“类”是和“对象”相关的,“字符集”常常表示Character Set(比如GBK、UTF-8之类),所以本书中没有采用这两个名字。是正则表达式最基本的结构之一,要理解正则表达式的“灵活”,认识它是第一步。

顾名思义,字符组就是一组字符,在正则表达式中,它表示“在同一个位置可能出现的各种字符”,其写法是在一对方括号[和]之间列出所有可能出现的字符,简单的字符组比如[ab][314][#.?]在解决一些常见问题时,使用字符组可以大大简化操作,下面举“匹配数字字符”的例子来说明。

字符可以分为很多类,比如数字、字母、标点等。有时候要求 “只出现一个数字字符”,换句话说,这个位置上的字符只能是012、…、89这10个字符之一。要进行这种判断,通常的思路是:用10个条件分别判断字符是否等于这10个字符,对10个结果取“或”,只要其中一个条件成立,就返回True,表示这是一个数字字符,其伪代码如例1-1所示。

例1-1判断数字字符的伪代码

      charStr == "0" || charStr == "1" … || charStr == "9"

注:因为正则表达式处理的都是“字符串”(String)而不是“字符”,所以这里假设变量charStr(虽然它只包含一个字符)也是字符串类型,使用了双引号,在有些语言中字符串也用单引号表示。

这种解法的问题在于太烦琐——如果要判断是否是一个小写英文字母,就要用||连接26个判断;如果还要兼容大写字母,则要连接52 个判断,代码长到几乎无法阅读。相反,用字符组解决起来却异常简单,具体思路是:列出可能出现的所有字符(在这个例子里就是10个数字字符),只要出现了其中任何一个,就返回True。例1-2 给出了使用字符组判断的例子,程序语言使用Python。

例1-2用正则表达式判断数字字符

      re.search("[0123456789]", charStr) != None

re.search()是Python提供的正则表达式操作函数,表示“进行正则表达式匹配”;charStr仍然是需要判断的字符串,而[0123456789]则是以字符串形式给出的正则表达式,它是一个字符组,表示“这里可以是012、…、89中的任意一个字符。只要charStr与其中任何一个字符相同(或者说“charStr可以由[0123456789]匹配”),就会得到一个MatchObject对象(这个对象暂时不必关心,在第21页会详细讲解);否则,返回None。所以判断结果是否为None,就可以判断charStr是否是数字字符。

当今流行的编程语言大多支持正则表达式,上面的例子在各种语言中的写法大抵相同,唯一的区别在于如何调用正则表达式的功能,所以用法其实大同小异。例1-3列出了常见语言中的表示,如果你现在就希望知道语言的细节,可以参考本书第三部分的具体章节。

例1-3用正则表达式判断数字字符在各种语言中的应用客观地说,Perl是正则表达式处理最方便的编程语言,考虑到今天使用Perl的人数,以及Perl程序员一般都熟练掌握正则表达式的现实,本书没有给出Perl语言的例子。

      .NET(C#)
      //能匹配则返回true,否则返回false
      Regex.IsMatch(charStr, "[0123456789]");
      Java
      //能匹配则返回true,否则返回false
      charStr.matches("[0123456789]");
      JavaScript
      //能匹配则返回true,否则返回false
      /[0123456789]/.test(charStr);
      PHP
      //能匹配则返回1,否则返回0
      preg_match("/[0123456789]/", charStr);
      Python
      #能匹配则返回RegexObject,否则返回None
      re.search("[0123456789]", charStr)
      Ruby
      #能匹配则返回0,否则返回nil
      charStr =~ /[0123456789]/

可以看到,不同语言使用正则表达式的方法也不相同。如果仔细观察会发现Java、.NET、Python、PHP中的正则表达式,都要以字符串形式给出,两端都有双引号";而Ruby和JavaScript中的正则表达式则不必如此,只在首尾有两个斜线字符/,这也是不同语言中使用正则表达式的不同之处。不过,这个问题现在不需要太关心,因为本书中大部分例子以Python程序来讲解,下面讲解关于Python的基础知识,其他语言的细节留到后文会详细介绍。

1.2 关于Python的基础知识

本书选择使用Python语言来演示实际的匹配结果,因为它能在多种操作系统中运行,安装也很方便;另一方面,Python是解释型语言,输入代码就能看到结果,方便动手实践。考虑到不是所有人都熟悉Python,这里专门用一节来介绍。

如果你的机器上没有安装Python,可以从http://python.org/download/下载,目前Python有2和3两个版本,本书的例子以2版本为准本书写作时,2.x最新的版本为2.7,示例以此为准。Python 3虽然已经正式发行,但相对2.x变化较大,而2.x较为流行,所以采用了2.x版本。关于2.x和3.x的差别,在Python一章有详细介绍。。请选择自己平台对应的程序下载并安装(目前MacOS、Linux的各种发行版一般带有Python,具体可以在命令行下输入python,看是否启动对应的程序)。

然后可以启动Python,在MacOS和Linux下是输入python,会显示出Python提示符,进入交互模式,如图1-1(Linux下的提示符与MacOS下的差不多,所以此处不列出);而在Windows下,需要在“开始”菜单的“程序”中,选择Python目录下的Python(command line),如图1-2所示。

图1-1 MacOS下的Python提示符

图1-2 Windows下的Python提示符

Python中常用的关于正则表达式的函数是re.search(),使用它必须首先导入正则表达式对应的包(package),也就是输入下面的代码。

      #导入正则表达式对应的包
      import re

通常的用法是提供两个参数:re.search(pattern, string),其中pattern是字符串形式提供的正则表达式,string 是需要匹配的字符串;如果能匹配,则返回一个MatchObject(详细介绍请参考第241 页,暂时可以不必关心),这时提示符会显示类似<_sre.SRE_Match object at 0x0000000001D8E578>之类的结果;如果不能匹配,结果是None(这是Python中的一个特殊值,类似其他某些语言中的Null),不会有任何显示。图1-3演示了运行Python语句的结果。

图1-3 观察re.search()匹配的返回值

注:>>>是等待输入的提示符,以>>>开头的行,之后文本是用户输入的语句;其他行是系统生成的,比如打印出语句的结果(在交互模式下,匹配结果会自动输出,便于观察;真正程序运行时不会如此)。

为讲解清楚、形象、方便,本书中的程序部分需要做两点修改。

第一,因为暂时还不需要关心匹配结果的细节,只关心有没有结果,所以在re.search()之后添加判断返回值是否为None,如果为True,则表示匹配成功,否则返回False表示匹配失败。为节省版面,尽可能用注释表示这类匹配结果,如# => True或者 # => False,附在语句之后。

第二,目前我们关心的是整个字符串是否能由正则表达式匹配。但是,在默认情况下re.search(pattern,string)只判断string的某个子串能否由pattern匹配,即便pattern只能匹配string的一部分,也不会返回None。为了测试整个string能否由pattern匹配,在pattern两端加上^和$。^和$是正则表达式中的特殊字符,它们并不匹配任何字符,只是表示“定位到字符串的起始位置”和“定位到字符串的结束位置”(原理如图1-4 所示,如果你现在就希望详细了解这两个特殊字符,可以参考第61页),这样就保证;只有在整个string都可以由pattern匹配时,才算匹配成功,不返回None,如例1-4所示。

图1-4 ^[0123456789]$的匹配

例1-4使用^和$测试string由pattern完整匹配

      # 只要字符串中包含数字字符,就可以匹配
      re.search("[0123456789]", "2") != None # => True
      re.search("^[0123456789]$", "12") != None        #  => False
      re.search("[0123456789]", "a2") != None          #  => True
      # 整个字符串就是一个数字字符,才可以匹配
      re.search("[0123456789]", "2") != None           #  => True
      re.search("^[0123456789]$", "12") != None        #  => False
      re.search("^[0123456789]$", "a2") != None        #  => False

1.3 普通字符组(续)

介绍完关于Python的基础知识,继续讲解字符组。字符组中的字符排列顺序并不影响字符组的功能,出现重复字符也不会影响,所以[0123456789]完全等价于 [9876543210][1029384756][9988876543210]

不过,代码总是要容易编写,方便阅读,正则表达式也是一样,所以一般并不推荐在字符组中出现重复字符。而且,还应该让字符组中的字符排列更符合认知习惯,比如[0123456789]就好过[0192837465]。为此,正则表达式提供了-范围表示法(range),它更直观,能进一步简化字符组。

所谓“-范围表示法”,就是用[x-y]的形式表示xy整个范围内的字符,省去一一列出的麻烦,这样[0123456789]就可以表示为[0-9]。如果你觉得这不算什么,那么确实比[abcdefghijklmnopqrstuvwxyz]简单太多了。

你可能会问,“-范围表示法”的范围是如何确定的?为什么要写作[0-9],而不写作[9-0]

要回答这个问题,必须了解范围表示法的实质。在字符组中,-表示的范围,一般是根据字符对应的码值(Code Point,也就是字符在对应编码表中的编码的数值)来确定的,码值小的字符在前,码值大的字符在后。在ASCII编码中(包括各种兼容ASCII的编码中),字符0的码值是48(十进制),字符9的码值是57(十进制),所以[0-9]等价于[0123456789];而[9-0]则是错误的范围,因为9的码值大于0,所以会报错。程序代码见例1-5。

例1-5 [0-9]是合法的,[9-0]会报错

      re.search("^[0-9]$", "2") != None       #  => True
      re.search("^[9-0]$", "2") != None
      Traceback (most recent call last):
      error: bad character range

如果知道0~9的码值是48~57,a~z的码值是97~122,A~Z的码值是65~90,能不能用[0-z]统一表示数字字符、小写字母、大写字母呢?

答案是:勉强可以,但不推荐这么做。根据惯例,字符组的范围表示法都表示一类字符(数字字符是一类,字母字符也是一类),所以虽然[0-9][a-z]都是很好理解的,但[0-z]却很难理解,不熟悉ASCII编码表的人甚至不知道这个字符组还能匹配大写字母,更何况,在码值48到122之间,除去数字字符(码值48~57)、小写字母(码值97~122)、大写字母(码值65~90),还有不少标点符号(参见表1-1),从字符组[0-z]中却很难看出来,使用时就容易引起误会,例1-6所示的程序就很可能让人莫名其妙。

表1-1 ASCII编码表(片段)

例1-6 [0-z]的奇怪匹配

      re.search("^[0-z]$", "A") != None        #  => True
      re.search("^[0-z]$", ":") != None        #  => True

在字符组中可以同时并列多个“-范围表示法”,字符组[0-9a-zA-Z]可以匹配数字、大写字母或小写字母;字符组[0-9a-fA-F]可以匹配数字,大、小写形式的a~f,它可以用来验证十六进制字符,代码见例1-7。

例1-7 [0-9a-fA-F]准确判断十六进制字符

      re.search("^[0-9a-fA-F]$", "0") != None   #  => True
      re.search("^[0-9a-fA-F]$", "c") != None   #  => True
      re.search("^[0-9a-fA-F]$", "i") != None   #  => False
      re.search("^[0-9a-fA-F]$", "C") != None   #  => True
      re.search("^[0-9a-fA-F]$", "G") != None   #  => False

在不少语言中,还可以用转义序列\xhex来表示一个字符,其中\x是固定前缀,表示转义序列的开头,num是字符对应的码值(Code Point,详见第127页,下文用☞127表示),是一个两位的十六进制数值。比如字符A的码值是41(十进制则为65),所以也可以用\x41表示。

字符组中有时会出现这种表示法,它可以表现一些难以输入或者难以显示的字符,比如\x7F;也可以用来方便地表示某个范围,比如所有ASCII字符对应的字符组就是[\x00-\x7F],代码见例1-8。这种表示法很重要,在第120页还会讲到它,依靠这种表示法可以很方便地匹配所有的中文字符。

例1-8 [\x00-\x7F]准确判断ASCII字符

      re.search("^[\x00-\x7F]$", "c") != None   #  => True
      re.search("^[\x00-\x7F]$", "I") != None   #  => True
      re.search("^[\x00-\x7F]$", "0") != None   #  => True
      re.search("^[\x00-\x7F]$", "<") != None   #  => True

1.4 元字符与转义

在上面的例子里,字符组中的横线-并不能匹配横线字符,而是用来表示范围,这类字符叫做元字符(meta-character)。字符组的开方括号[、闭方括号]和之前出现的^$都算元字符。在匹配中,它们有着特殊的意义。但是,有时候并不需要表示这些特殊意义,只需要表示普通字符(比如“我就想表示横线字符-”),此时就必须做特殊处理。

先来看字符组中的-,如果它紧邻着字符组中的开方括号[,那么它就是普通字符,其他情况下都是元字符;而对于其他元字符,取消特殊含义的做法都是转义,也就是在正则表达式中的元字符之前加上反斜线字符\

如果要在字符组内部使用横线-,最好的办法是将它排列在字符组的最开头。[-09]就是包含三个字符-、09的字符组;[0-9]是包含0~9这10个字符的字符组,[-0-9]则是由“-范围表示法”0-9和横线-共同组成的字符组,它可以匹配11个字符,例1-9说明了使用横线-的各种情况。

例1-9 –出现在不同位置,含义不同

      #作为普通字符
      re.search("^[-09]$", "3") != None       #  => False
      re.search("^[-09]$", "-") != None       #  => True
      #作为元字符
      re.search("^[0-9]$", "3") != None       #  => True
      re.search("^[0-9]$", "-") != None       #  => False
      #转义之后作为普通字符
      re.search("^[0\\-9]$", "3") != None      #  => False
      re.search("^[0\\-9]$", "-") != None      #  => True

仔细观察会发现,在正文里说“在正则表达式中的元字符之前加上反斜线字符\”,而在代码里写的却不是[0\-9],而是[0\\-9]。这并不是输入错误。

因为在这段程序里,正则表达式是以字符串(String)的方式具体来说,在Java、PHP、Python、.NET等语言中,正则表达式都是以字符串的形式给出的,在Ruby和JavaScript中则不是这样。详细的说明,请参考第96页。提供的,而字符串本身也有关于转义的规定(你或许记得,在字符串中有\n\t之类的转义序列)。上面说的“正则表达式”,其实是经过“字符串转义处理”之后的字符串的值,正则表达式[0\-9]包含6个字符:[0\-9],在字符串中表达这6个字符;但是在源代码里,必须使用7个字符: \需要转义成\\,因为处理字符串时,反斜线和之后的字符会被认为是转义序列(Escape Sequence),比如\n\t都是合法的转义序列,然而\-不是。

这个问题确实有点麻烦。正则表达式是用来处理字符串的,但它又不完全等于字符串,正则表达式中的每个反斜线字符\,在字符串中(也就是正则表达式之外)还必须转义为\\。所以之前所说的是“正则表达式[0\-9]”,程序里写的却是[0\\-9],这确实有点麻烦。

不过,Python提供了原生字符串(Raw String),它非常适合于正则表达式:正则表达式是怎样,原生字符串就是怎样,完全不需要考虑正则表达式之外的转义(只有双引号字符是例外,原生字符串内的双引号字符必须转义写成\")。原生字符串的形式是r"string",也就是在普通字符串之前添加r,示例代码如例1-10。

例1-10原生字符串的使用

      #原生字符串和字符串的等价
      r"^[0\-9]$" == "^[0\\-9]$"              #  => True
      #原生字符串的转义要简单许多
      re.search(r"^[0\-9]$", "3") != None      #  => False
      re.search(r"^[0\-9]$", "-") != None      #  => True

原生字符串清晰易懂,省去了烦琐的转义,所以从现在开始,本书中的Python示范代码都会使用原生字符串来表示正则表达式。另外,.NET和Ruby中也有原生字符串,也有一些语言并没有提供原生字符串(比如Java),所以在第6章(☞93)会专门讲解转义问题。不过,现在只需要知道Python示范代码中使用了原生字符串即可。

继续看转义,如果希望在字符组中列出闭方括号],比如[012]345],就必须在它之前使用反斜线转义,写成[012\]345];否则,结果就如例1-11所示,正则表达式将]与最近的[匹配,这个表达式就成了“字符组[012]加上4 个字符 345]”,它能匹配的是字符串 0345]1345]2345],却不能匹配]。

例1-11 ]出现在不同位置,含义不同

      #未转义的]
      re.search(r"^[012]345]$", "2345") != None        #  => True
      re.search(r"^[012]345]$", "5") != None           #  => False
      re.search(r"^[012]345]$", "]") != None           #  => False
      #转义的]
      re.search(r"^[012\]345]$", "2345") != None       #  => False
      re.search(r"^[012\]345]$", "5") != None          #  => True
      re.search(r"^[012\]345]$", "]") != None          #  => True

除去字符组内部的-,其他元字符的转义都必须在字符之前添加反斜线,[的转义也是如此。如果只希望匹配字符串[012],直接使用正则表达式[012]是不行的,因为这会被识别为一个字符组,它只能匹配012这三个字符中的任意一个;而必须转义,把正则表达式写作\[012],请注意,只有开方括号[需要转义,闭方括号]不需要转义,如例1-12所示。

例1-12取消其他元字符的特殊含义

      re.search(r"^[012]345]$", "3") != None       #  => False
      re.search(r"^[012\\]345]$", "3") != None     #  => True
      re.search(r"^[012]$", "[012]") != None       #  => False
      re.search(r"^\[012]$", "[012]") != None      #  => True

1.5 排除型字符组

在方括号[…]中列出希望匹配的所有字符,这种字符组叫做“普通字符组”,它的确非常方便。不过,也有些问题是普通字符组不能解决的。

给定一个由两个字符构成的字符串 str,要判断这两个字符是否都是数字字符,可以用[0-9][0-9]来匹配。但是,如果要求判断的是这样的字符串——第一个字符不是数字字符,第二个字符才是数字字符(比如A8x6一般来说,计算机中的偏移值都是从0 开始的。此处考虑到叙述自然,使用了“第一个字符”和“第二个字符”的说法,其中“第一个字符”指最左端,也就是偏移值为0的字符;“第二个字符”指紧跟在它右侧,也就是偏移值为1的字符。——应当如何办?数字字符的匹配很好处理,用[0-9]即可;“不是数字”则很难办——不是数字的字符太多了,全部列出几乎不可能,这时就应当使用排除型字符组。

排除型字符组(Negated Character Class)非常类似普通字符组[…],只是在开方括号[之后紧跟一个脱字符^,写作[^…],表示“在当前位置,匹配一个没有列出的字符”。所以[^0-9]就表示“0~9 之外的字符”,也就是“非数字字符”。那么,[^0-9][0-9]就可以解决问题了,如例1-13所示。

例1-13使用排除型字符组

      re.search(r"^[^0-9][0-9]$", "A8") != None        #  => True
      re.search(r"^[^0-9][0-9]$", "x6") != None        #  => True

排除型字符组看起来很简单,不过新手常常会犯一个错误,就是把“在当前位置,匹配一个没有列出的字符”理解成“在当前位置不要匹配列出的字符”两者其实是不同的,后者暗示“这里不出现任何字符也可以”。例1-14很清楚地说明:排除型字符组必须匹配一个字符,这点一定要记住。

例1-14排除型字符组必须匹配一个字符

      re.search(r"^[^0-9][0-9]$", "8") != None     #  => False
      re.search(r"^[^0-9][0-9]$", "A8") != None    #  => True

除了开方括号[之后的^,排除型字符组的用法与普通字符组几乎完全相同,唯一需要改动的是:在排除型字符组中,如果需要表示横线字符-(而不是用于“-范围表示法”),那么-应该紧跟在^之后;而在普通字符组中,作为普通字符的横线-应该紧跟在开方括号之后,如例1-15所示。

例1-15在排除型字符组中,紧跟在^之后的-不是元字符

      #匹配一个-、09之外的字符
      re.search(r"^[^-09]$", "-") != None          #  => False
      re.search(r"^[^-09]$", "8") != None          #  => True
      #匹配一个0~9之外的字符
      re.search(r"^[^0-9]$", "-") != None          #  => True
      re.search(r"^[^0-9]$", "8") != None          #  => False

在排除型字符组中,^是一个元字符,但只有它紧跟在[之后时才是元字符,如果想表示“这个字符组中可以出现^字符”,不要让它紧挨着[即可,否则就要转义。例1-16给出了三个正则表达式,后两个表达式实质是一样的,但第三种写法很麻烦,理解起来也麻烦,不推荐使用。

例1-16排除型字符组的转义

      #匹配一个012之外的字符
      re.search(r"^[^012]$", "^") != None          #  => True
      #匹配4个字符之一:0^12
      re.search(r"^[0^12]$", "^") != None          #  => True
      #^紧跟在[之后,但经过转义变为普通字符,等于上一个表达式,不推荐
      re.search(r"^[\^012]$", "^") != None         #  => True

1.6 字符组简记法

[0-9][a-z]等字符组,可以很方便地表示数字字符和小写字母字符。对于这类常用的字符组,正则表达式提供了更简单的记法,这就是字符组简记法(shorthands)。

常见的字符组简记法有\d\w\s。从表面上看,它们与[…]完全没联系,其实是一致的。其中\d等价于[0-9],其中的d代表“数字(digit)”;\w等价于[0-9a-zA-Z_],其中的w代表“单词字符(word)”;\s 等价于[ \t\r\n\v\f](第一个字符是空格),s表示“空白字符(space)”。例1-17说明了这几个字符组简记法的典型匹配。

例1-17字符组简记法\d、\w、\s

      #如果没有原生字符串,\d就必须写作\\d
      re.search(r"^\d$", "8") != None      #  => True
      re.search(r"^\d$", "a") != None      #  => False
      re.search(r"^\w$", "8") != None     #  => True
      re.search(r"^\w$", "a") != None     #  => True
      re.search(r"^\w$", "_") != None     #  => True
      re.search(r"^\s$", " ") != None     #  => True
      re.search(r"^\s$", "\t") != None    #  => True
      re.search(r"^\s$", "\n") != None    #  => True

一般印象中,单词字符似乎只包含大小写字母,但是字符组简记法中的“单词字符”不只有大小写单词,还包括数字字符和下画线_,其中的下画线_尤其值得注意:在进行数据验证时,有可能只容许输入“数字和字母”,有人会偷懒用\w 验证,而忽略了\w 能匹配下画线,所以这种匹配并不严格,[0-9a-zA-Z]才是准确的选择。

“空白字符”并不难定义,它可以是空格字符、制表符\t,回车符\r,换行符\n等各种“空白”字符,只是不方便展现(因为显示和印刷出来都是空白)。不过这也提醒我们注意,匹配时看到的“空白”可能不是空格字符,因此,\s才是准确的选择。

字符组简记法可以单独出现,也可以使用在字符组中,比如[0-9a-zA-Z]也可以写作[\da-zA-Z],所以匹配十六进制字符的字符组可以写成[\da-fA-F]。字符组简记法也可以用在排除型字符组中,比如[^0-9]就可以写成[^\d][^0-9a-zA-Z_]就可以写成[^\w],代码如例1-18。

例1-18字符组简记法与普通字符组混用

      #用在普通字符组内部
      re.search(r"^[\da-zA-Z]$", "8") != None   #  => True
      re.search(r"^[\da-zA-Z]$", "a") != None   #  => True
      re.search(r"^[\da-zA-Z]$", "C") != None   #  => True
      #用在排除型字符组内部
      re.search(r"^[^\w]$", "8") != None       #  => False
      re.search(r"^[^\w]$", "_") != None       #  => False
      re.search(r"^[^\w]$", ",") != None       #  => True

相对于\d\w\s这三个普通字符组简记法,正则表达式也提供了对应排除型字符组的简记法:\D\W\S——字母完全一样,只是改为大写。这些简记法匹配的字符互补:\s 能匹配的字符,\S一定不能匹配;\w能匹配的字符,\W一定不能匹配;\d能匹配的字符,\D一定不能匹配。例1-19示范了这几个字符组简记法的应用。

例1-19 \D、\W、\S的使用

      #\d\D
      re.search(r"^\d$", "8") != None     #  => True
      re.search(r"^\d$", "a") != None     #  => False
      re.search(r"^\D$", "8") != None     #  => False
      re.search(r"^\D$", "a") != None     #  => True
      #\w\W
      re.search(r"^\w$", "c") != None     #  => True
      re.search(r"^\w$", "!") != None     #  => False
      re.search(r"^\W$", "c") != None     #  => False
      re.search(r"^\W$", "!") != None     #  => True
      #\s\S
      re.search(r"^\s$", "\t") != None    #  => True
      re.search(r"^\s$", "0") != None     #  => False
      re.search(r"^\S$", "\t") != None    #  => False
      re.search(r"^\S$", "0") != None     #  => True

妥善利用这种互补的属性,可以得到一些非常巧妙的效果,最简单的应用就是字符组[\s\S]。初看起来,在同一个字符组中并列两个互补的简记法,这种做法有点奇怪,不过仔细想想就会明白,\s\S 组合在一起,匹配的就是“所有的字符”(或者叫“任意字符”)。许多语言中的正则表达式并没有直接提供“任意字符”的表示法,所以[\s\S][\w\W][\d\D]虽然看起来有点古怪,但确实可以匹配任意字符许多关于正则表达式的文档说:点号.能匹配“任意字符”。但在默认情况下,点号其实不能匹配换行符,具体请参考第84页。

关于字符组简记法,最后需要补充两点:第一,如果字符组中出现了字符组简记法,最好不要出现单独的-,否则可能引起错误,比如[\d-a]就很让人迷惑,在有些语言中,-会被作为普通字符,而在有些语言中,这样写会报错;第二,以上说的\d\w\s的匹配规则,都是针对ASCII编码而言的,也叫ASCII匹配规则。但是,目前一些语言中的正则表达式已经支持了Unicode字符,那么数字字符、单词字符、空白字符的范围,已经不仅仅限于ASCII编码中的字符。关于这个问题,具体细节在后文有详细的介绍,如果你现在就想知道,可以翻到第115页。

1.7 字符组运算

以上介绍了字符组的基本功能,它们在常用的语言中都有提供;还有些语言中为字符组提供了更强大的功能,比如Java和.NET就提供了字符组运算的功能,可以在字符组内进行集合运算,在某些情况下这种功能非常实用。

如果要匹配所有的元音字母(为讲解简单考虑,暂时只考虑小写字母的情况),可以用,但是要匹配所有的辅音字母却没有什么方便的办法,最直接的写法是[b-df-hj-np-tv-z],不但烦琐,而且难理解。其实,从26个字母中“减去”元音字母,剩下的就是辅音字母,如果有办法做这个“减法”,就方便多了。[aeiou]

Java语言中提供了这样的字符组:[[a-z]&&[^aeiou]],虽然初看有点古怪,但仔细看看,也不难理解。[a-z]表示26 个英文字母,[^aeiou]表示除元音字母之外的所有字符(还包括大写字母、数字和各种符号),两者取交集,就得到“26个英文字母中,除去5个元音字母,剩下的21个辅音字母”。

.NET中也有这样的功能,只是写法不一样。同样是匹配辅音字母的字符组,.NET中写作[a-z-[aeiou]],其逻辑是:从[a-z]能匹配的26个字符中,“减去”[aeiou]能匹配的元音字母。相对于Java,这种逻辑更符合直觉,但写法却有点古怪——不是[[a-z]-[aeiou]],而是[a-z-[aeiou]]。例1-20集中演示了Java和.NET中的字符组运算。

例1-20字符组运算

      Java
      "a".matches("^[[a-z]&&[^aeiou]]$");      //  => True
      "b".matches("^[[a-z]&&[^aeiou]]$");      //  => False
      .NET
      Regex.IsMatch("^[a-z-[aeiou]]$", "a");   //  => True
      Regex.IsMatch("^[a-z-[aeiou]]$", "b");   //  => False

1.8 POSIX字符组

前面介绍了常用的字符组,但是在某些文档中,你可能会发现类似[:digit:][:lower:]之类的字符组,看起来不难理解(digit就是“数字”,lower就是“小写”),但又很奇怪,它们就是POSIX字符组(POSIX Character Class)。因为某些语言的文档中出现了这些字符组,为避免困惑,这里有必要做个简要介绍。如果只使用常用的编程语言,可以忽略文档中的POSIX字符组,也可以忽略本节;如果想了解POSIX字符组,或者需要在Linux/UNIX下的各种工具(sed、awk、grep等)中使用正则表达式,最好阅读本节。

之前介绍的字符组,都属于Perl衍生出来的正则表达式流派(Flavor),这个流派叫做PCRE(Per Compatible Regular Expression)。在此之外,正则表达式还有其他流派,比如POSIX(Portable Operating System Interface for uniX),它是一系列规范,定义了UNIX操作系统应当支持的功能,其中也包括关于正则表达式的规范,[:digit:]之类的字符组就是遵循POSIX规范的字符组。

常见的[a-z]形式的字符组,在POSIX规范中仍然获得支持,它的准确名称是POSIX方括号表达式(POSIX bracket expression),主要用在UNIX/Linux系统中。POSIX方括号表达式与之前所说的字符组最主要的差别在于:在POSIX字符组中,反斜线\不是用来转义的。所以POSIX方括号表达式[\d]只能匹配\d两个字符,而不是[0-9]对应的数字字符。

为了解决字符组中特殊意义字符的转义问题,POSIX方括号表达式规定:如果要在字符组中表达字符](而不是作为字符组的结束标记),应当让它紧跟在字符组的开方括号之后,所以[]a]能匹配的字符就是]或a;如果要在字符组中标识字符-(而不是“-范围表示法”),就必须将它放在字符组的闭方括号]之前,所以[a-]能匹配的字符就是a或-。

另一方面,POSIX规范还定义了POSIX字符组(POSIX character class),它大致等于之前介绍的字符组简记法,都是使用类似[:digit:][:lower:]之类有明确意义的记号表示某类字符。

表1-2简要介绍了POSIX字符组,注意表格中与其对应的是ASCII字符组,也就是能匹配的ASCII字符(ASCII编码表中码值在0~127之间的字符)。因为POSIX规范中有一个重要概念:locale(通常翻译为“语言环境”),它是一组与语言和文化相关的设定,包括日期格式、货币币值、字符编码等。POSIX字符组的意义会根据locale的变化而变化,表1-2介绍的只是这些POSIX字符组在ASCII编码中的意义;如果换用其他的locale(比如使用Unicode字符集),它们的意义可能会发生变化,具体请参考第129页。

表1-2 POSIX字符组

注:标记*的字符组简记法并不是POSIX规范中的,但使用很多,一般语言中都提供,文档中也会出现。

POSIX字符组的使用也与PCRE字符组简记法的使用有所不同,主要区别在于,PCRE字符组简记法可以脱离方括号直接出现,而POSIX字符组必须出现在方括号内。所以同样是匹配数字字符,PCRE中可以直接写\d,而POSIX字符组必须写成[[:digit:]]

在本书介绍的6种语言中,Java、PHP、Ruby支持使用POSIX字符组。

在PHP中可以直接使用POSIX字符组,但是PHP中的POSIX字符组只识别ASCII字符,也就是说,任何非ASCII字符(比如中文字符)都不能由任何一个POSIX字符组匹配。

Ruby的情况稍微复杂一点。Ruby 1.8中的POSIX字符组只能匹配ASCII字符,而且不支持[:word:][:ASCII:];Ruby 1.9中的POSIX字符组可以匹配Unicode字符,而且支持[:word:][:ASCII:]

Java中的情况更加复杂。POSIX字符组[[:name:]]必须使用\p{name}的形式,其中name为POSIX字符组对应的名字,比如[:space:]就应当写作\p{Space},请注意第一个字母要大写,其他POSIX字符组都是这样,只有[:xdigit:]要写作\p{XDigit}。并且Java中的POSIX字符组,只能匹配ASCII字符。