正则表达式包会教程

冬青 | 2019-06-18 | 浏览量:
JavaScript

当想匹配比如手机号、邮箱等特殊形式的字符串时,大家往往都会想到正则表达式,不过当脑海中浮现出那一串神奇的既没有注释,甚至没有空格的字符串时,内心又会充满恐惧。此时你可能会动用搜索引擎,搜到别人写的那一串神奇字符,拷贝到自己的代码中,稍经测试,发现竟然能用,不禁发出一阵牛逼,然后就去写别的代码逻辑去了。再也不想看这一串字符。

是不是你也有过上述经历呢,对正则表达式怀有很大的畏惧之情,觉得自己无法驾驭它,但有时候又实在离不开它。下面我会简单介绍下正则表达式的由来,和驾驭它的正确姿势。本篇会议 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
2
3
parse_url.exec('http://www.kujiale.com:80/college')
会得到一个数组,["http://www.kujiale.com:80/college", "http", "www.kujiale.com"]
数组的首位是整个输入,第二位和第三位则为我们想要的内容。

下面我们一步步来分析该正则表达式

^ $ 表示匹配开始和结尾,它是一个锚,只有开头和结尾处满足该正则的字符串才匹配,而如果没有该锚,意味着,如果字符串中间部分满足该正则也会被匹配到。

比如当我们不用 ^ 符号时,同样执行上述代码,如果此时来匹配 xhttp://www.kujiale.com:80/college 则一样会成功匹配,而使用了 ^ 则会匹配失败。

注:^ 既可以表示开始的锚,也可以用作语意。后面会详述。

分组

上面例子中得到的结果为数组,包含了三部分内容,而无其他。在正则中,如果我们想获取其中的部分内容,我们就会使用一对括号来保住所要提取的部分,我们把这部分称为一个分组 (…), 一个分组会复制它所匹配的文本,并将其放到 result 数组里,每个分组会被指定一个编号,1,2,3… 。如果分组有嵌套,则会从按从左到右,从外到里的方式输出。如:

1
2
/((ht)(tp))(s)/.exec('https')
输出:["https", "http", "ht", "tp", "s"]

当我们不想捕获某个分组时,我们需要在分组中加上 ?: 表示不要捕获该分组,就像最初的例子中不要捕获端口号那样使用。

1
2
/((ht)(?:tp))(s)/.exec('https')
输出:["https", "http", "ht", "s"]

分组被捕获后,我们可以使用 \ + 编号来使用该分组。\1 表示分组一,\2 表示分组二,以此类推。比如我们要找一个文本中搜索一对重复的单词

1
2
3
const reg = /([A-Za-z]+)\s+\1/
reg.test('aaa bbb') false
reg.test('aaa aaa') true

字符集

正则匹配最终匹配的还是字符,所以我们需要表达字符的方式。如果只是匹配一个字符,那很简单,比如

1
/a/.test('a')  true

我们也可以加上 逻辑 |

1
2
/a|b/.test('b') true
/a|b/.test('a') true

如果是一类字符,如果一直用 逻辑那也太繁琐了,此时我们可以使用 [] ,比如匹配所有 a 到 z 的字符

1
2
/[a-z]/.test('b') true
/[ab]/.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 的使用也有限制,比如 testsearch、和 RegExp 的 exec 方法会忽略掉 /g 标识。/g 可以用在 string.match 和 string.replace 方法中, 如下例子:

1
2
3
4
5
"[22].[44].[33].".match(/\d+/g)  // 输出 ["22", "44", "33"]
"[22].[44].[33].".match(/\d+/) // 输出 ["22"]

"aaaa".replace(/a/, 'b') // 输出 "baaa"
"aaaa".replace(/a/g, 'b') // 输出 "bbbb"

写在最后

我们再回过头来看下这个正则,是不是也觉得不难了。

1
/^(https?):\/{0,3}([0-9.\-A-Za-z]+)(?::\d+)?.*$/

(https?) 表示捕获该部分,s 可有可无。\/{0, 3},表示匹配 /,0 到 3 个都满足。后面就是匹配 域名、端口和后面的 path、query 部分。另外以上例子部分使用 javascript 语言,java 的正则表达式和 js 本质没有区别,但要特别注意 Java 中的转译需要用两个斜杆 \\

评论,需翻墙