字符组

1. 普通字符组

字符组(Character Class)是正则表达式最基本的结构之一。

注:正则表达式处理的都是“字符串”而不是“字符”。

正则表达式判断数字字符

/[0123456789]/.test(charStr);

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

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

/^[0-9]$/.test('2'); // true
/^[9-0]$/.test('2'); // SyntaxError: Invalid regular expression: /^[9-0]$/: Range out of order in character class

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

答案是:勉强可以,但是不推荐这么做。

根据惯例,字符组的范围表示法都表示一类字符(数字字符是一类、字母字符也是一类),
所以虽然[0-9]、[a-z]都是很好理解,但[0-z]却很难理解,不熟悉ASCII编码表的人甚至不知道这个字符组还能匹配大写字符,
除此之外,在码值48-122,除去数字字符、小写字母和大写字母,还有不少其他的字符。

ascii.png

不推荐的用法:

/^[0-z]$/.test('A'); // true /^[0-z]$/.test(':'); // true

在字符组中可以并列使用多个“范围表示法”,字符组[0-9a-zA-Z]可以匹配数字、小写字母和大写字母。

/^[0-9a-zA-Z]$/.test('A'); // true

在JavaScript中,还可以使用转义序列(\xhex)来表示一个字符,其中\x是固定前缀,表示转义序列的开头,
num是字符对应的码值,是一个两位的十六进制数值。比如字符A的码值是41,所以也可以用\x41来表示。

字符组中也可以这样使用,用来表现一些难以输入或者难以显示的字符,比如\x7F。
也可以方便的表示某个范围,比如所有ASCII字符对应的字符组就是[\x00-\x7F]。
依靠这种表示法可以很方便地匹配所有的中文字符。

/^[\x00-\x7F]$/.test('c'); // true /^[\x00-\x7F]$/.test('I'); // true /^[\x00-\x7F]$/.test('0'); // true /^[\x00-\x7F]$/.test('<'); // true

2. 元字符与转义字符

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

给定一个一个由个字符构成的字符串,要判断这两个字符是否都是数字字符。可以用[0-9][0-9]匹配。
但是如果需要判断第一个字符不是数字字符,第二个字符才是数字字符,应该如何处理?
数字字符的匹配很好处理,但是不是数字就很难办,这时,就应当使用排除性数组。

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

(1)使用排除型数组

/^[^0-9][0-9]$/.test('A8'); // true /^[^0-9][0-9]$/.test('x6'); // true

(2)排除型数组必须匹配一个字符

/^[^0-9][0-9]$/.test('8'); // false /^[^0-9][0-9]$/.test('A8'); // true

(3)排除型字符组中,紧跟在^之后的不是-不是元字符

  1. 匹配一个-、0、9之外的字符
/^[^-09]$/.test('-'); // false /^[^-09]$/.test('8'); // true
  1. 匹配一个0~9之外的字符
/^[^0-9]$/.test('-'); // true /^[^0-9]$/.test('8'); // false

(4)排除型字符组的转义

  1. 匹配一个0、1、2之外的字符
/^[^012]$/.test('^'); // true /^[0^12]$/.test('^'); // true /^[\^012]$/.test('^'); // true

3. 字符组简记法

用[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)”。

/^\d/.test('8'); // true /^\d/.test('a'); // false
/^\w/.test('8'); // true /^\w/.test('a'); // true /^\w/.test('_'); // true
/^\s/.test(' '); // true /^\s/.test('\t'); // true /^\s/.test('\n'); // true

一般来说,单词字符似乎只包含大小写字母,但是字符组简记法中的“单词字符”不只有大小写单词,还包括数字字符和下划线_。
尤其是_需要注意,进行数据验证时,有可能只允许输入“数字和字母”,这时不要偷懒用\w验证,而忽略\w能匹配下划线,
使用\w匹配并不严格,[0-9a-zA-Z]才是准确的选择。

“空白字符”可以是空格字符、制表符\t、回车符\r、换行符\n等。
这也提醒我们必须注意,匹配看到的“空白字符”可能不是空格字符,因此,使用\s才是准确的选择。

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

(1)字符组简记法与普通字符组混用

  1. 用在普通字符组内部
/^[\da-zA-Z]$/.test('8'); // /^[\da-zA-Z]$/.test('a'); // /^[\da-zA-Z]$/.test('c'); //
  1. 用在排除字符组内部
/^[^\w]$/.test('8'); // false /^[^\w]$/.test('-'); // true /^[^\w]$/.test(','); // true

相对于\d、\w、\s这三个普通字符组简记法,正则表达式也提供了对应排除字符组的简记法。
\D、\W、和\S,和普通字符组简记法相似,只是将小写字母替换为大写。这些简记法匹配的字符互补。
\s能匹配的字符,\S一定不能匹配;\w能匹配的字符,\w一定不能匹配;\d能匹配的字符,\D一定不能匹配。

(2)\D、\W、\S的使用

  1. \d和\D
/^\d$/.test('8'); // true /^\D$/.test('8'); // false /^\d$/.test('a'); // false /^\D$/.test('a'); // true
  1. \w和\W
/^\w$/.test('c'); // true /^\W$/.test('c'); // false /^\w$/.test('!'); // false /^\W$/.test('!'); // true
  1. \s和\S
/^\s$/.test('\t'); // true /^\S$/.test('\t'); // false /^\s$/.test('0'); // false /^\S$/.test('0'); // true

妥善使用这种互补的属性,可以得到一些巧妙的结果,最简单的应用就是字符组[\s\S]。
\s和\S组合在一起,匹配的就是“所有的字符(任意字符)”。
许多语言中的正则并没有直接提供匹配“任意字符”的表示法,所以[\s\S]、[\w\W]、[\d\D],
虽然看起来比较古怪,但确实可以匹配任意字符。

许多关于正则的文档说,点号 . 能匹配“任意字符”。默认情况下,点号其实不能匹配换行符。

关于字符组简记法,最后补充两点:

  1. 如果字符组出现字符组简记法,最好不要出现单独的-,否则可能会引起错误。例如[\d-a]。

  2. 以上说的\d、\w、\s的匹配规则,都是针对ASCII编码而言的,也叫ASCII匹配规则。
    目前一些语言中的正则已经支持了Unicode符,那么数字字符、单词字符、空白字符的范围,
    已经不限于ASCII编码中的字符。

4. 字符组运算

上面介绍了字符组的基本功能,还有一些语言为字符组提供了更强大的功能,可以再字符组内进行集合运算。

如果要匹配所有的元音字母(只考虑小写字母的情况),可以用[aeiou],
但是要匹配所有的辅音字母却没有方便的方法,最直接的写法是[b-df-hj-np-tv-z],不但繁琐而且难理解。
其实,只要从26的字母中减去元音字母,剩下的就是21个辅音字母。

"a".matches("^[[a-z]&&[^aeiou]]$"); // false "b".matches("^[[a-z]&&[^aeiou]]$"); // true

上面是java的写法,不用尝试了,JS不支持上述写法,只能最繁琐的方式匹配。

/^[b-df-hj-np-tv-z]$/.test('a'); // false /^[b-df-hj-np-tv-z]$/.test('b'); // true

5. 总结

本篇文章总结了正则表达式中字符组的运用,是作者的读书笔记,内容来源于《正则指引》。