php实战正则表达式(一):验证手机号

基本验证

即验证字符串是否是11位数字。

表达式

  • [0123456789]{11}
  • [0-9]{11}
  • \d{11}

知识点

字符组:正则表达式中用方括号对[...]表示字符组。字符组表示在同一位置可能出现的字符。
如,[0123456789]表示匹配数字0123456789中的任意一个;[0123abc]匹配数字0123和字母abc中的任意一个。

字符组的范围表示法:字符组中使用短横线([..-..])来表示一段范围的字符。
如,[a-z]表示匹配所有小写英文字母中的任意一个;[a-zA-Z]表示匹配所有小写英文字母和大写英文字母中的一个;[0-9]表示匹配0123456789中的任意一个。
要注意的是,默认范围是起始字符的ACSⅡ码到结束字符的ACSⅡ码之间的字符

字符组简记法:对于一些常用的字符组,正则表达式规定了一些简记符号来表示它们。

  • \d 所有的数字,即[0-9]
  • \D 所有的非数字,与\d互斥
  • \w 所有的单词字符(字符、数字、下划线),即[0-9a-zA-Z_]
  • \W 所有的非单词字符,与\W互斥
  • \s 所有的空白字符,包括空格、制表符、回车符、换行符等空白字符
  • \S 所有的非空白字符,与\s互斥

量词:量词表示它所修饰的对象(如字符、字符组)可能出现的次数。
量词的一般形式是{m,n}(逗号,后面不能有空格),表示它所修饰的字符(或字符组)的出现次数大于等于m次,小于等于n次。特别地

  • {m}表示修饰对象只能出现m次;
  • {0,n}表示修饰的对象最多出现n次,最少出现0次;
  • {m,}表示修饰的对象最少出现m次。

长度真的只能为11?

观察下面的gif中的代码可以看到,当输入的字符串是长度为15的数字时,也可以匹配前面11个数字。甚至输入字符是abcd180123412341234时也可以匹配到11个数字。

这是因为上面的正则表达式的含义是“匹配11个数字”,因此只要输入的字符串中有连续的11个数字就可以匹配成功。要想验证输入的字符串仅仅是手机号,需要使用正则表达式中的字符串起始位置^和字符串结束位置$

表达式

  • ^\d{11}$

知识点

正则表达式中有一些符号匹配的是位置,而不是文本,这类符号叫做锚点(anchor)。^$就是其中两个。

^ 匹配的位置是字符串的开始位置
$ 匹配的位置是字符串的结束位置

更严谨的验证

我们都知道国内常见的手机号都是以130-139,150-153、155-159、180、182、185-189,此外,还有170、176-178等。我们上一节得到的表达式对手机开头并没有进行验证。

表达式

^1(3[0-9]|5[012356789]|8[0256789]|7[0678])\d{8}$

知识点

分组:正则表达式中可以用圆括号对(...)表示一个分组(子表达式),这样在匹配的结果中除了会返回全部匹配到的内容,还会返回每个子表达式各自匹配到的内容。如上图中的表达式执行后的结果,数组的第0个元素为整个正则表达式匹配到的值,第1个元素为圆括号对内的正则匹配到的值。

1
2
3
4
5
6
7
8
9
preg_match('/^1(3[0-9]|5[012356789]|8[0256789]|7[0678])\d{8}$/', '18012341234', $arr);
print_r($arr);
/*
Array
(
[0] => 18012341234
[1] => 80
)
*/

选择结构:圆括号对(...)内的子表达式用竖线|隔开表示不同的选择,圆括号内的整个正则可以匹配任意一个选择。
例如,(3[0-9]|5[012356789]|8[0256789]|7[0678])表示这里匹配的值可以是3[0-9]或者5[012356789]或者8[0256789]或者7[0678]

锦上添花

有些时候,手机号中间会有-符号,变成180-1234-1234的形式,比如现在的iPhone会自动将手机号转为这种格式。

依据到目前为止介绍的一些知识,可以写出下面的正则表达式:

^1(3[0-9]|5[012356789]|8[0256789]|7[0678])-{0,1}\d{4}-{0,1}\d{4}$

其中-{0,1}表示字符-可以出现1次或者不出现,这是我们前面了解过的量词,其实在正则表达式中,对这种常用量词还规定了特殊的记法:

  • ? 相当于{0,1},可以出现0次或1次
  • + 相当于{1,},出现次数大于等于1次
  • * 相当于{0,},出现次数大于等于0次

因此,上面的正则表达式也等价于

^1(3[0-9]|5[012356789]|8[0256789]|7[0678])-?\d{4}-?\d{4}$


但是,上面的表达式除了能匹配180-1234-1234,其实也能匹配180-123412341801234-1234这两种形式。
如果我们只想匹配18012341234180-1234-1234这两种形式,可以使用正则表达式中的反向引用:

^1(3[0-9]|5[012356789]|8[0256789]|7[0678])(-?)\d{4}\2\d{4}$

上面的\2就是反向引用,它是匹配第二个圆括号对(...)匹配到的内容。反向引用的形式是\num

上面的正则表达式中,我们用\2来进行反向引用,然而\1却没有什么用,那么我们可不可以忽略那些不用的分组呢?正则表达式中的非捕获分组可以满足这个需求:

^1(?:3[0-9]|5[012356789]|8[0256789]|7[0678])(-?)\d{4}\1\d{4}$

上面的(?:3[0-9]|5[012356789]|8[0256789]|7[0678])就是非捕获分组。非捕获的形式是(?:...),使用了非捕获分组后,匹配的结果里面不再有该分组匹配到的结果。

上面对分组的引用是基于子表达式的编号,当正则表达式比较复杂或编号太多时要弄清楚每个分组的编号是一件很痛苦的事情。因此,正则表达式提供了命名分组

^1(?:3[0-9]|5[012356789]|8[0256789]|7[0678])(?P<separato>-?)\d{4}(?P=separato)\d{4}$

上面正则表达式中的(?P<separato>-?)就是命名分组。命名分组的形式是(?P<name>...),命名分组的引用使用(?P=name)的形式。

总结

到此为止,一个健壮的验证手机号的正则表达式就完成了。虽然功能很简单,但是还是涉及到了正则表达式中不少的知识点。