正则那些事

正则表达式的快速入门

本文的目标,就是让你能够了解正则是什么,怎么用,并且学的轻松,用的快乐:)

前言

想必有不少人跟我一样,身为一名开发人员,却对正则这东西知之甚少,甚至于完全不会用。我很长一段时间也是,工作也有段时间了,却只会写一些很简单的正则匹配,想写个电话号码匹配,我都要抓耳挠腮半天最后百度,甚是惭愧。 于是乎,我痛下决心要弄懂它。不过学习的过程总是痛苦的,为了监督自己,我觉得边学边写会是个好主意。毕竟,输入的最好方式是输出。

那么,什么是正则呢?

正则表达式描述了一种字符串匹配的模式(pattern),可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。

简单来说,就是用于一种用于描述匹配规则的工具,通常用于检索、替换符合某些规则的文本,在很多场合都用得到,就例如前文提到的电话号码匹配。你实际生活中肯定也用过不少,比如在文件夹检索文件,在页面中查找关键字等,这些都跟正则有关。

假如你写了一个正则表达式,如何验证它的正确性呢?正则表达式引擎通常会提供一个“测试指定的字符串是否匹配一个正则表达式”的方法,比如JavaScript提供了两个方法:patt.test(str)str.match(patt)

请打开浏览器的控制台,将你的的正则表达式(例如/\w{2,10}/)输入到控制台中,然后输入.test(str)并回车,str就是你要测试的字符串,正确的话控制台就会输出true,反之为false。


step1:初窥门径

本文为了方便理解,会结合一个个问题,来循序渐进的讲解正则相关的知识。

问题1:假如你要在一篇文章中找出he的字符串,那么正则怎么写?

这就是最简单的正则表达式,你只需要使用正则表达式he。这样就能找到那些包含he的字符串,不过,很多单词也包含了he这个字符串,例如hello、heart等。如果直接使用he来检索的话,这些单词也会被找出,如果想精准的找出he这个单词的话,那么就该 \b 出场了。

\b 是正则表达式规定的一个特殊代码,也叫元字符,代表着单词的开头或结尾,也就是单词的分界处。虽然通常英文的单词是由空格,标点符号或者换行来分隔的,但是 \b 并不匹配这些单词分隔字符中的任何一个,它只匹配一个位置。

所以这道题的答案是:\bhe\b ,同理,假如你要查找的是hello world,那么就应该用\bhello\bworld\b


元字符

现在,你已经认识了一个元字符,其实正则表达式里还有更多的元字符,将这些组合在一起使用,可以构造出功能更强大、更复杂的正则表达式。那么,先来认识一下它们:

元字符 简单说明
. 匹配除换行符以外的任意字符
\w 匹配字母或数字或下划线或汉字
\s 匹配任意的空白符
\d 匹配数字0-9
\b 匹配单词的开始或结束
\W 匹配任意不是字母或数字或下划线或汉字的字符
\S 匹配任意不是空白符的字符
\D 匹配任意非数字的字符
\B 匹配不是单词开头或结尾的位置
[^x] 匹配除了x以外的任意字符
^ 匹配字符串的开始
$ 匹配字符的的结束

其中s用于匹配任意的空白符,包括空格、制表符(Tab)、换行符、中文全角空格等,[^x]表示不匹配^后面跟着的字符串,^和$则表示匹配字符的的开始和结束,用在正则表达式的开头和结尾,这两个用来用户验证输入的内容时非常合适。

问题2:如何匹配一个类似4 G的由数字、空格、字母组成的字符串呢?

很简单,我们只需要将我们学到的元字符组合起来就行了,答案是:^\d\s\w$


限定符

你现在认识了不少有用的元字符,这些元字符在实际使用的时候,往往不是单个数字或者字母等,可能会重复很多次,这时要怎么办呢?

问题3:假如要匹配一个类似于Leonardo.DiCaprio或者Hugh.Jackman的由两个长度为4到8位的单词以及一个.组成的名字,正则应该怎么写呢?

在这里,我们就要引入一个新的概念,用来表示重复次数的限定符。下面是正则表达式中所有的限定符:

元字符 简单说明
* 重复零次或更多次
+ 重复一次或更多次
? 重复零次或一次
{n} 重复n次
{n,} 重复n次或更多次
{n, m} 重复n到m次

那么就很简单了,长度4到8位的单词可以用 \w{4,8} 来表示,不过.呢,直接使用.是肯定不行的,正则会将它解释成匹配除换行符以外的任意字符, 这时你就需要使用 \ 来取消这些字符的特殊意义。 例如使用 \.\* 来匹配 .*,当然要查找\本身,你可以使用 \\

那么,答案就是 ^\w{4,8}\.\w{4,8}$


step2:小试牛刀

在认识了这么多元字符和限定符之后,或许我们可以来挑战一下实际会用的那些正则了。

问题4:如何匹配一个手机号码呢?

要解决这个问题,我们先要理清楚手机号码由哪几部分组成,手机号首先是由限定的3位数字(号码段)和任意8位数字组成的11位数字,号码段根据运营商分为以下几种(统计于2017.06):

运营商 号码段
移动 134、135、136、137、138、139、147、150、151、152、157、158、159、170、178、182、183、187、188
联通 130、131、132、145、155、156、170、175、176、185、186
电信 133、149、153、170、173、177、180、181、189

由于号码段的存在,我们又得引入一个新的概念——分枝条件,即x|y,它主要用来匹配条件x或y。例如 [s|f]ox 可以匹配sox或fox。

然后我们在结合之前的知识,那么答案就是 ^((13[0-9])|(14[5|7])|(15([0-3]|[5-9]))|(17([0,3,5-8]))|(18([0-3]|[5-9])))\d{8}$ ,看起来很复杂,不过仔细看的话,其实只是简单的排列组合罢了。

我们在做一道类似的题目巩固一下。

问题5:如何匹配一个可能是XXX-XXXXXXX、XXX-XXXXXXXX、XXXX-XXXXXXX、XXXX-XXXXXXXX、XXXXXXX和XXXXXXXX格式的固定电话呢?

我们将问题整理一下,座机可能有区号(3-4位),也可能没有区号,号码也可能是7位或者8位,那么总共有3+7、3+8、4+7、4+8、7位、8位的6种情况。

首先,我们可以使用\d{3,4}来表示3-4位的区号,然后有区号后面可以会跟一个-,当然也可以没有区号,那么就变成(\d{3,4}-)?,7-8位的号码很好表示,即\d{7,8},再将两者进行组合,答案就出来了:^(\d{3,4}-)?\d{7,8}$


step3:小有所成

到这为止,你已经算是完成了入门准备,后面将会涉及到更多复杂的正则表达式。

前面我们已经提到了如何重复单个字符(直接在字符后面加上限定符就行了),但如果想要重复多个字符又该怎么办?你可以用小括号来指定子表达式(也叫做分组),然后你就可以指定这个子表达式的重复次数了,当然你也可以对子表达式进行其它一些操作。

那么如何创建一个子表达式呢,默认情况下,一对括号之内的字符就是一个子表达式,它会自动拥有一个组号,规则是:从左向右,以分组的左括号为标志,第一个出现的分组的组号为1,第二个为2,以此类推。然后,我们就可以开始学习关于分组的语法了。

后向引用用于重复搜索前面某个分组匹配的文本。例如,\1代表分组1匹配的文本。难以理解?请看示例:\b(\w+)\b\s+\1\b可以用来匹配重复的单词,像go go, 或者di di。

不过你也可以自己指定子表达式的组名。要指定一个子表达式的组名,可以使用这样的语法:(?exp)或者(?‘name’exp),这样exp的组名就被指定为name了。在引用这个分组时,需要使用\k,那么上一个例子也可以写作\b(?\w+)\b\s+\k\b。当然,关于分组还有很多特定用途的语法。下面列出了最常用的一些:

分类 代码/语法 说明
捕获 (exp) 匹配exp,并捕获文本到自动命名的组里
捕获 (?exp) 匹配exp,并命名为name,也可写作(?‘name’exp)
捕获 (?:exp) 匹配exp,不捕获匹配的文本,也不给此分组分配组号
零宽断言 (?=exp) 匹配exp前面的位置
零宽断言 (?<=exp) 匹配exp后面的位置
零宽断言 (?!exp) 匹配后面跟的不是exp的位置
零宽断言 (?<!exp) 匹配前面不是exp的位置
注释 (?#comment) 用于提供注释让人阅读

对于前两个语法,前面都已经解释过了,第三个 ((?:exp)) 不会改变正则表达式的处理方式,只是这样的组匹配的内容不会被捕获成组,也不会拥有组号。

“那么,我为什么要这么做呢?”——好问题,你觉得为什么呢?

接下来的四个用于查找在某些内容(但并不包括这些内容)之前或之后的东西,也就是说它们像\b、^、$那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。最好还是拿例子来说明吧:

(?=exp)也叫零宽度正预测先行断言,它断言自身出现的位置的后面能匹配表达式exp。比如\b\w+(?=ing\b),匹配以ing结尾的单词的前面部分(除了ing以外的部分),如查找I’m singing while you’re dancing.时,它会匹配sing和danc。

(?<=exp)也叫零宽度正回顾后发断言,它断言自身出现的位置的前面能匹配表达式exp。比如(?<=\bre)\w+\b会匹配以re开头的单词的后半部分(除了re以外的部分),例如在查找reading a book时,它匹配ading。

同理,(?!exp)也叫零宽度负预测先行断言,它断言自身出现的位置的后面能不匹配表达式exp。例如:\d{3}(?!\d)匹配三位数字,而且这三位数字的后面不能是数字;\b((?!abc)\w)+\b匹配不包含连续字符串abc的单词。

同理,(?<!exp)也叫零宽度负回顾后发断言,它断言此位置的前面不能匹配表达式exp:(?<![a-z])\d{7}匹配前面不是小写字母的七位数字。

问题6:如何匹配一个不包含属性的简单HTML标签内里的内容?

这道题有点难度,我们一点点来做。首先,一个HTML标签一般长这样:\<div\>abc\</div\>,我们要拿到的就是里面的abc。我们再仔细看,标签前面是一个<div>,后面跟着<\/div>,div的部分是一样的,这里可以使用分组的知识。

我们先将前后两个部分写出来:(?<=<\w+>)和(?=<\/\w+>),再引入分组的概念,得到 (?<=<\w+>)和(?=<\/\1>)。 最后,再将标签和内容组合起来:(?<=<\w+>).*(?=<\/\1>),这样就得到我们的答案了。


补充

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。以这个表达式为例:a.*b,它将会匹配最长的以a开始,以b结束的字符串。如果用它来搜索aabab的话,它会匹配整个字符串aabab。这被称为贪婪匹配。

有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。前面给出的限定符都可以被转化为懒惰匹配模式,只要在它后面加上一个问号?。这样 .*? 就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。现在看看懒惰版的例子吧:

a.*?b 就会去匹配最少的字符串,以a开始,以b结束的字符串。如果把它应用于aabab的话,它会匹配aab(因为它是从左往右匹配,得到第一个符合规则的就不会继续匹配了)。

如果你看到了这里,恭喜你,你已经完成了正则表达式的修炼!你可以去其他人面前露一手了。

参考资料

 
comments powered by Disqus