教你玩转正则表达式

说起正则表达式,似乎大家都有一种“只可意会,不可言传”的感觉。在网上查正则表达式的解释,许多回答也让新手很头疼,如图

并且对于同样一个功能,不同的人也给出了不同的表达式,例如验证电子邮件是否合法的表达式,就有五花八门不下十种






……

负责任地告诉大家,上面这些表达式或多或少都有不完善的地方,不能很好地起到过滤和验证的作用。至于完善的版本么……我在本文末尾会给出。

以上截图来自于百度知道、百度空间等……(百度还真是Do Evil……)

对新手来讲,这无疑给学习正则表达式带来了很大的困难。

Vkki 认为,这世上没有讲不明白的技术,否则这技术必定会失传。认为说了也只会一知半解的人无非是说的水平不够高罢了。

首先我们来了解一下正则表达式,顾名思义这是一种表达式,与1+1、1>0、a>b?a:b一样,只不过这种表达式表示的是一种pattern,也就是文本的格式。

提前说一下,在某些语言(如PHP、JavaScript)里,正则表达式以字符 “/” 作为起止符,作用就和字符串的引号差不多,例如

1
/\d{2}-\d{5}/

真正的表达式是

1
\d{2}-\d{5}

知道了这点,我们来开始正式学习。


元字符

元字符相当于正则表达式的运算符,地位与算术表达式的加减乘除差不多,与之相对的是普通字符,也就是算术表达式中的运算数。关于元字符微软有一篇文章专门列出了每个字符的作用,不过写的太过纷繁复杂,有很多功能重复的符号,很难记忆,我这里给大家分门别类简化了一下,大家先看下面这张图:

元字符

肯定仍然摸不着头脑吧,没关系我们下面用具体的例子来说。


重复

我们先从 * + ? 这三个字符说起,正则表达式之所以能以很少的字符匹配出大量组合,很大一个原因就是对重复的使用。从上面的表格可以看出这三个字符各自的作用,但到底什么是“0次或n次匹配”呢?我们以php的正则替换为例来看看。

*

1
2
3
4
5
6
7
8
9
10
// * 的使用, * 是重复 0次或 多次
<?php
$str = 'a aa aaa aaaa ab abb abbba abcba';
$str = preg_replace('/ab*/', '◆', $str);
echo $str;
?>
输出:◆ ◆◆ ◆◆◆ ◆◆◆◆ ◆ ◆ ◆◆ ◆cb◆

从上述代码可以看出, ab* 的作用是【匹配a,无论a后面有几个b都匹配】


+

1
2
3
4
5
6
7
8
9
10
// + 的使用, + 是重复 1 次或 多次
<?php
$str = 'a aa aaa aaaa ab abb abbba abcba';
$str = preg_replace('/ab+/', '◆', $str);
echo $str;
?>
输出:a aa aaa aaaa ◆ ◆ ◆a ◆cba

看出区别了吧, ab+ 的作用是【匹配ab,无论ab后面有几个b都匹配】


?

1
2
3
4
5
6
7
8
9
10
// ? 的使用, ? 是重复 0 次或 1 次
<?php
$str = 'a aa aaa aaaa ab abb abbba abcba';
$str = preg_replace('/ab?/', '◆', $str);
echo $str;
?>
输出:◆ ◆◆ ◆◆◆ ◆◆◆◆ ◆ ◆b ◆bb◆ ◆cb◆

到这里已经很明显了,ab? 的作用是【匹配a,如果a后面有b,匹配一次,如果后面仍有b则不再匹配】


{ }

1
2
3
4
5
6
7
8
9
10
// { } 的使用, {n} 是重复 n 次
<?php
$str = 'a aa aaa aaaa ab abb abbba abcba';
$str = preg_replace('/ab{3}/', '◆', $str);
echo $str;
?>
输出:a aa aaa aaaa ab abb ◆a abcba

弄白了上面三个,大括号就很好理解了,就是指定重复的次数,这里的3就代表只匹配【abbb】这种情况。

类似的还有:

  • {2,5} ——- 匹配2、3、4、5次所有情况
  • {2,} ——- 匹配2~n次所有情况

注1:但是没有 {,2} 这种写法哦

注2:个别语言中(如Delphi)使用的是 < > 来实现此功能

注3:很明显,正则表达式大小写敏感


匹配范围

下面我们来看 [ ] 这个符号。这个符号的作用是描述一个字符集,限定了正则表达式必须在这个字符集内进行匹配。有这么几种用法:

  • [A-Z]: 匹配所有大写字母
  • [a-z]: 匹配所有小写字母
  • [0-9]: 匹配所有数字
  • [Vk1]: 匹配所有字符(’V’、’k’、’1’)
  • [Pa]: 上面4种任意组合,如 [A-G2-9l-tK] 代表大写字母A-G、K;小写字母l-t;数字2-9
  • [^Pa]: 匹配不属于Pa的任何字符。Pa含义同上。

还是结合例子来看:

1
2
3
4
5
6
7
8
9
<?php
$str = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
$str = preg_replace('/[A-G2-9l-tK]/', '◆', $str);
echo $str;
?>

输出:abcdefghijk◆◆◆◆◆◆◆◆◆uvwxyz◆◆◆◆◆◆◆HIJ◆LMNOPQRSTUVWXYZ1◆◆◆◆◆◆◆◆0

很直观,就不多解释了。

转义字符

上图是一些转义字符,可以用他们来替换后面的等效写法,使表达式更简洁。

关于 . \S \w 似乎都能代表任意字符,他们有什么区别呢?

  • 【 . 】表示的范围最大,其次是【\S】,【\w】最小

  • 前两者不仅包含数字和字母,也包含了!@#$%^*&()这类符号汉字


或者

下面我们来说说 | 这个符号。这个符号的含义是或者。例如,’z|food’ 匹配“z”或“food”。’(z|f)ood’ 匹配“zood”或“food”。

x|y 和[xy]的区别在于,它可以指定“字符串”,例如 dog|cat 就会匹配dog或cat,而[dogcat]只会匹配’d’,’o’,’g’,’c’,’a’,’t’。可能大家还是搞不明白区别,我们还是以例子来看。

1
2
3
4
5
6
7
8
<?php
$str = 'There is a dog |and a cat.';
echo preg_replace('/[dogcat]/', '◆', $str);
//echo "</br>";
echo preg_replace('/[dog|cat]/', '◆', $str);
//echo "</br>";
echo preg_replace('/dog|cat/', '◆', $str);
?>
输出:
There is ◆ ◆◆◆ |◆n◆ ◆ ◆◆◆.
There is ◆ ◆◆◆ ◆◆n◆ ◆ ◆◆◆.
There is a ◆ |and a ◆.

可以看到:

  • 在 [ ] 内部使用 | 符号时它实际上是被当做字符处理的。

  • 单独使用 | 符号时,匹配的是其左右两边的整个“字符串”。


边界

边界

所谓边界,实际上就是一个字符串或其中单词的开头或结尾,正则表达式使用这个概念来实现匹配特定的字符串。大家从上图可以看到例子。

和元字符一样,我们一个一个来看。


^ 与 $

^ 符号表示匹配字符串开始的位置,其作用就是只匹配第一个符合要求的元素。

$ 符号表示匹配字符串结束的位置,其作用就是只匹配最后一个符合要求的元素。

结合起来看下例子:

1
2
3
4
5
6
7
8
<?php
$str = 'baby baby baby Oh~Oh~Oh~';
echo preg_replace('/^baby/', '◆', $str);
//echo "</br>";
echo preg_replace('/Oh~$/', '◆', $str);
//echo "</br>";
echo preg_replace('/(^baby)|(Oh~$)/', '◆', $str);
?>
输出: 
◆ baby baby Oh~Oh~Oh~
baby baby baby Oh~Oh~◆
◆ baby baby Oh~Oh~◆

注:^ 符号只作用于其右边的字符串。$ 符号只作用与其左边的字符串


\b

先理解一个概念:单词边界

所谓单词边界就是 空格 或 标点 这类能分隔开一个个单词的字符。

我们直接来看例子。

1
2
3
4
5
6
7
8
9
10
11
<?php
$str = 'baby babymy babymy~ mybaby mybaby~ mybabymy~';
echo preg_replace('/\bbaby/', '◆', $str); //左边界
//echo "</br>";
echo preg_replace('/baby\b/', '◆', $str); //右边界
//echo "</br>";
echo preg_replace('/\bbaby\b/', '◆', $str); //左右边界
?>
输出: 
◆ ◆my ◆my~ mybaby mybaby~ mybabymy~
◆ babymy babymy~ my◆ my◆~ mybabymy~
◆ babymy babymy~ mybaby mybaby~ mybabymy~

这个例子信息量比较大,大家仔细看看。

通俗来说,左边界就是匹配以 baby 开头的 左边是单词边界 的字符串。

右边界就是匹配以 baby 结尾的 右边是单词边界 的字符串。

左右边界就是匹配 baby 这个单词(左右都是空格或标点)


\B

非单词边界,作用上图有说,我们也直接来看例子,重点看与上面那个的区别。

1
2
3
4
5
6
7
8
9
10
11
<?php
$str = 'baby babymy babymy~ mybaby mybaby~ mybabymy~';
echo preg_replace('/\Bbaby/', '◆', $str); //左边界
//echo "</br>";
echo preg_replace('/baby\B/', '◆', $str); //右边界
//echo "</br>";
echo preg_replace('/\Bbaby\B/', '◆', $str); //左右边界
?>
输出: 
baby babymy babymy~ my◆ my◆~ my◆my~
baby ◆my ◆my~ mybaby mybaby~ my◆my~
baby babymy babymy~ mybaby mybaby~ my◆my~

通俗来说,左边界就是匹配以 baby 开头的,左边不是单词边界 的字符串。

右边界就是匹配以 baby 结尾的,右边不是单词边界 的字符串。

左右边界就是匹配包含 baby 这四个字母 且其左右都不是单词边界 的字符串。


贪婪匹配与非贪婪匹配

正则表达式默认是贪婪匹配,如果要变成非贪婪只需在 *、+、?、{n}、{n,} 或 {n,m}后面加上 即可,我们直接看例子:

1
2
3
4
5
6
7
8
9
<?php
$str = '<html><head><title>标题</title></head><body>内容</body></html>';
echo preg_replace('/<.*>/', '◆', $str); //贪婪模式
//echo "</br>";
echo preg_replace('/<.*?>/', '◆', $str); //非贪婪模式
?>
输出: 
◆
◆◆◆标题◆◆◆内容◆◆

可以看到贪婪模式下在查找所有字符时是尽可能多的找,所以 ‘.’ 代表了第一个 ‘<’ 和最后一个 ‘>’ 之间的所有字符而将他们全部替换成了一个。

非贪婪模式就是尽可能少的找,很好理解,不赘述了。


子表达式

用括号 ( ) 括起来的式子叫子表达式,为了理解它到底是什么东东,我们先来看一个栗子:

1
2
3
4
5
6
7
<?php
$str = 'one two three ten';
echo preg_replace('/t\w+/', '◆', $str);
?>

为了巩固前面所学的知识,大家先来猜猜这个例子中的正则表达式 t\w+ 是干什么用的?

t的作用就是匹配t,\w作用是匹配数字或字母或下划线,+代表一个或多个,合起来看就是匹配t开头的单词。

输出: one ◆ ◆ ◆

没错吧,下面我们把表达式改一下,加上括号:

1
2
3
4
5
6
7
<?php
$str = 'one two three ten';
echo preg_replace('/(t)(\w+)/', '◆', $str);
?>
输出: one ◆ ◆ ◆

什么什么?没变化?哈哈,这里确实没变化,只不过把t和后面的部分分成了两个子表达式,他们合在一起的作用还是一样的。你问我那子表达式的作用是什么?接着往下看:

1
2
3
4
5
6
7
<?php
$str = 'one two three ten';
echo preg_replace('/(t)(\w+)/', '[\1-\2:\0]', $str);
?>
输出: one [t-wo:two] [t-hree:three] [t-en:ten]

什么什么?理解不了?这个[\1-\2:\0]里的\1就代表第一个子表达式,\2代表第二个,\0代表所有子表达式合起来的结果。

所以比如说我们知道很多邮箱(如Vkki@gmail.com),需要他们的用户名(Vkki)写在右上角。就可以用子表达式进行提取啦~


至此我们已经学完了绝大部分正则表达式的用法,现在我们回过头来看看第一张图,有没有想吐槽的冲动?Vkki 反正是有用【 _@_@_ 】这个“电子邮件地址”吐槽他的冲动……

我们来看看电子邮件地址应该是什么样的。

电子邮件地址的格式一般为:

用户名@域名
  • 用户名是用户自己定义的部分,可以包含数字、字母、连字符和下划线。
  • 域名由字母、数字及连字符组合而成,但开头及结尾均不能含有“ - ”,且“ - ”不能连续出现。

要求还真是高啊,这里奉上 Vkki 版的验证电邮的正则表达式:

[\w\-]+@[a-zA-Z\d]((\-)?[a-zA-Z\d])*(\.[a-zA-Z\d]((\-)?[a-zA-Z\d])*)*(\.[a-zA-Z]{2,4})

复杂吧,这才是满足要求的正则表达式。