当想匹配比如手机号、邮箱等特殊形式的字符串时,大家往往都会想到正则表达式,不过当脑海中浮现出那一串神奇的既没有注释,甚至没有空格的字符串时,内心又会充满恐惧。此时你可能会动用搜索引擎,搜到别人写的那一串神奇字符,拷贝到自己的代码中,稍经测试,发现竟然能用,不禁发出一阵牛逼,然后就去写别的代码逻辑去了。再也不想看这一串字符。
是不是你也有过上述经历呢,对正则表达式怀有很大的畏惧之情,觉得自己无法驾驭它,但有时候又实在离不开它。下面我会简单介绍下正则表达式的由来,和驾驭它的正确姿势。本篇会议 JS 端的正则表达式为例子来演示。
由来
正则表达式诞生于 1951 年,它起源于对形式语言的数学研究。1968 年开始,正则表达式广泛应用于编辑器的字符匹配,和编译器中的词法分析。Ken Tompson 实现了第一个切实可用的匹配器,后来被用在了 Unix 的 grep 搜索工具中,g/re/p 全称 Global search for Regular Expression and Print matching lines。真正诞生更现代化的正则表达式实现是在 19 世纪 80 年代的 Perl 语言中。现在正则表达式已被各种语言广泛支持,比如 Java、Python、JavaScript 等。
由于正则表达式趋向于极致的简洁,甚至不惜容忍含义模糊,而且它不支持注释和空白,所有部分都紧密排列在一起,导致我们难以理解也属正常,但尽管有这些缺点,正则表达式依然被广泛的应用着。
举个例子
我们要提取一个 http 链接的 scheme 和 domain,那正则表达式如何写呢?我们可以简化成如下
1 | let parse_url = /^(https?):\/{0,3}([0-9.\-A-Za-z]+)(?::\d+)?.*$/ |
1 | parse_url.exec('http://www.kujiale.com:80/college') |
下面我们一步步来分析该正则表达式
^
$
表示匹配开始和结尾,它是一个锚,只有开头和结尾处满足该正则的字符串才匹配,而如果没有该锚,意味着,如果字符串中间部分满足该正则也会被匹配到。
比如当我们不用 ^
符号时,同样执行上述代码,如果此时来匹配 xhttp://www.kujiale.com:80/college
则一样会成功匹配,而使用了 ^
则会匹配失败。
注:^
既可以表示开始的锚,也可以用作语意非
。后面会详述。
分组
上面例子中得到的结果为数组,包含了三部分内容,而无其他。在正则中,如果我们想获取其中的部分内容,我们就会使用一对括号来保住所要提取的部分,我们把这部分称为一个分组 (…), 一个分组会复制它所匹配的文本,并将其放到 result 数组里,每个分组会被指定一个编号,1,2,3… 。如果分组有嵌套,则会从按从左到右,从外到里的方式输出。如:
1 | /((ht)(tp))(s)/.exec('https') |
当我们不想捕获某个分组时,我们需要在分组中加上 ?:
表示不要捕获该分组,就像最初的例子中不要捕获端口号那样使用。
1 | /((ht)(?:tp))(s)/.exec('https') |
分组被捕获后,我们可以使用 \
+ 编号来使用该分组。\1 表示分组一,\2 表示分组二,以此类推。比如我们要找一个文本中搜索一对重复的单词
1 | const reg = /([A-Za-z]+)\s+\1/ |
字符集
正则匹配最终匹配的还是字符,所以我们需要表达字符的方式。如果只是匹配一个字符,那很简单,比如
1 | /a/.test('a') true |
我们也可以加上 或
逻辑 |
1 | /a|b/.test('b') true |
如果是一类字符,如果一直用 或
逻辑那也太繁琐了,此时我们可以使用 []
,比如匹配所有 a 到 z 的字符
1 | /[a-z]/.test('b') true |
如果想加上 非
的语义,就像我们上面所说,那我们就需要在字符集中加上 ^
符号
1 | /[^ab]/.test('b') false |
由于某些字符集经常用到,所以正则帮我们提取了出了这些常用的字符集,用一些特殊符号来表示
.
匹配除 \n \r
之外的任何单个字符
\d
等价于 [0-9]
\D
等价于 [^0-9]
\w
等价于[0-9A-Z_a-z]
\W
等价于[^0-9A-Z_a-z]
\s
匹配任何不可见字符, 比如空格换行等
\S
匹配任何可见字符
注:当字符集中使用到一些保留字符时,需要使用 \
进行转意
匹配次数
我们除了匹配字符,还得需要定义匹配字符的数量,比如我们要匹配两个字母的单词,不是写成 /\w\w/
而是可以写成 /\w{2}/
字符或字符集后面跟 {n, m} 表示该字符或字符集可重复的次数范围,一样,正则为我们封装了一些常用的次数匹配规则,比如 +, *, ? ,如下表所示。
{n,}
大于等于 n 次
{n}
等于 n 次
+
等价于 {1,}
*
等价于 {0,}
?
等价于 {0,1}
标识
除了上述说的这些,你还可能看到过有些正则表达式后面加了 /g
或 /i
或 /m
。 比较常用的是 /g
,/i
表示忽略字符大小写,/m
表示多行匹配,都不怎么常用。
其中 /g
的使用也有限制,比如 test
、search
、和 RegExp 的 exec
方法会忽略掉 /g
标识。/g
可以用在 string.match 和 string.replace 方法中, 如下例子:
1 | "[22].[44].[33].".match(/\d+/g) // 输出 ["22", "44", "33"] |
写在最后
我们再回过头来看下这个正则,是不是也觉得不难了。
1 | /^(https?):\/{0,3}([0-9.\-A-Za-z]+)(?::\d+)?.*$/ |
(https?) 表示捕获该部分,s 可有可无。\/{0, 3},表示匹配 /
,0 到 3 个都满足。后面就是匹配 域名、端口和后面的 path、query 部分。另外以上例子部分使用 javascript 语言,java 的正则表达式和 js 本质没有区别,但要特别注意 Java 中的转译需要用两个斜杆 \\
。