正则表达式,你别过来~

4/21/2022 JavaScript 4/21/2022

每一次当我需要用到正则表达式的时候,我都会去查一下正则的一些用法、语法等。这让我十分困扰。于是今天我想要去深入学习一下正则表达式的方方面面吧,希望搞懂正则表达式各种符号之间的内在联系,形成知识体系,当下次再遇到正则表达式的时候可以不借助搜索引擎,自己解决。

正则表达式(Regular Expression)其实就是一门工具,目的是为了字符串模式匹配,从而实现搜索和替换功能。它起源于上个 20 世纪 50 年代科学家在数学领域做的一些研究工作,后来才被引入到计算机领域中。从它的命名我们可以知道,它是一种用来描述规则的表达式。而它的底层原理也十分简单,就是使用状态机的思想进行模式匹配。大家可以利用 regexper.com (opens new window) 这个工具很好地可视化自己写的正则表达式:

# 从字符出发

正则表达式的基本组成元素可以分为:字符元字符。字符就是基本的计算机字符编码,如数字,英文字母等。而元字符,就是一些表示特殊语义的字符,如^表示非,|表示或。

# 单个字符

最简单的正则表达式就是简单的数字和字母所组成的。纯粹是一对一的关系。比如想要在apple中找到字母 a,则只需要匹配/a/即可。

如果要匹配特殊字符,比如匹配一个.,这时候就需要对它进行转义。因为.是一个元字符,表示除换行符外的任意字符,那我们就需要使用\.来匹配。

特殊字符 正则表达式 记忆方式
换行符 \n new line
换页符 \f form feed
回车符 \r
空白符 \s
制表符 \t
垂直制表符 \v
回退符 [\b]

# 多个字符

如何去实现一对多的匹配模式呢?在正则表达式中需要通过集合区间和通配符的方式来实现,而集合的定义方式就是使用中括号[],如[123]能同时匹配 1、2、3,[0-9]以及[a-z]等。

在正则表达式中还有一批用来同时匹配多个字符的简易正则:

匹配区间 正则表达式 记忆方式
除了换行符之外的任何字符 .
单个数字, [0-9] \d digit
除了[0-9] \D
包括下划线在内的单个字符,[A-Za-z0-9_] \w word
非单字字符 \W
匹配空白字符,包括空格、制表符、换页符和换行符 \s space
匹配非空白字符 \S

# 循环和重复

循环和重复的去匹配字符。根据循环次数的数量可以分为 0 次、1 次、多次、特定次。

# 0|1

元字符?,可以匹配 0 或者 1 个字符。如/colou?r/ ==> colorcolour

# >=0

元字符*,匹配可有可无的字符串。

# >=1

元字符+,表示至少存在一个字符。

# 特定次数

通过{}来设置精确的匹配区间。

- {x}: x次

- {min, max}: 介于min次到max次之间

- {min, }: 至少min次

- {0, max}: 至多max次
1
2
3
4
5
6
7

# 位置边界

# 单词边界 \b 和 \B

一个比较常见的使用场景就是在文章或句子中的特定单词找出来。如:

The cat scattered his food all over the room.
1

我想找到 cat 这个单词,但是如果只是使用/cat/这个正则,就会同时匹配到 catscattered 这两处文本。这时候我们就需要使用边界正则表达式\b,其中 b 是 boundary 的首字母。在正则引擎里它其实匹配的是能构成单词的字符(\w)和不能构成单词的字符(\W)中间的那个位置。

上面的例子改写成/\bcat\b/这样就能匹配到 cat 这个单词了。

# 字符串边界 ^ $

元字符 ^ 匹配字符串的开头,而 $ 匹配字符串的末尾。如:

I am scq000.
I am scq000.
I am scq000.
1
2
3

若要匹配I am scq000.这个句子。就得使用/^I am scq000\.$/m

这里的m表示多行模式,除此之外还有i不分大小写、g全局模式

# 子表达式

从简单到复杂的正则表达式的演变通常要用到分组、回溯引用和逻辑处理的思想。

# 分组 ( )

使用()元字符所包裹的正则表达式被分为一组。每一个分组都是一个子表达式,这也是构成复杂正则表达式的基础。

使用exec或者match方法可以把匹配到的每一个分组都匹配出来。

# 回溯引用

回溯引用指的是模式的后面部分引用前面已经匹配到的子字符串。你可以把它想象成是变量,回溯引用的语法像\1,\2,....,其中\1表示引用的第一个子表达式,\2表示引用的第二个子表达式,以此类推。而\0则表示整个表达式。

比如现在要匹配到以下文本中的两个连续相同的单词:

Hello what what is the first thing, and I am am scq000.
1

利用回溯引用我们可以写出:/\b(\w+)\s\1/

let ss = "Hello what what is the first thing, and I am am scq000.";
for (let i of ss.matchAll(/\b(\w+)\s\1/g)) {
  console.log(i);
  //   [
  //   'what what',
  //   'what',
  //   index: 6,
  //   input: 'Hello what what is the first thing, and I am am scq000.',
  //   groups: undefined
  // ]
  // [
  //   'am am',
  //   'am',
  //   index: 42,
  //   input: 'Hello what what is the first thing, and I am am scq000.',
  //   groups: undefined
  // ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

除此之外,我们在替换字符串的时候也可以用到回溯引用,使用$1$2来表示匹配的分组

var str = "abc abc 123";
str.replace(/(ab)c/g, "$1g");
// 得到结果 'abg abg 123'
1
2
3

如果我们不想某个子表达式被引用,使用**非捕获正则(?:regex)**就可以避免浪费内存。

var str = "scq000";
str.replace(/(scq00)(?:0)/, "$1,$2");
// 返回scq00,$2
// 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2
1
2
3
4

# 向前查找

(?=regex) 包裹的子表达式在匹配过程中都会用来限制前面的表达式的匹配。即向后文查找(前方的路是未知的),我理解的意思就是匹配到该表达式并且当前位置后面的是 regex 串

负前向查找的正则happ(?!ily),就会匹配到 happily 和 happy 中的 happly 单词的 happ 前缀。

如搜索abcdefg中的abc==>abc(?=defg)

let ss = "abcdefg,abcabc";
for (let i of ss.matchAll(/abc(?=defg)/g)) {
  console.log(i);
  // [ 'abc', index: 0, input: 'abcdefg,abcabc', groups: undefined ]
}
for (let i of ss.matchAll(/abc(?=.)/g)) {
  console.log(i);
  //[ 'abc', index: 0, input: 'abcdefg,abcabc', groups: undefined ]
  //[ 'abc', index: 8, input: 'abcdefg,abcabc', groups: undefined ]
}

for (let i of ss.matchAll(/(?=abc)abc/g)) {
  console.log(i);
  //[ 'abc', index: 0, input: 'abcdefg,abcabc', groups: undefined ]
  //[ 'abc', index: 8, input: 'abcdefg,abcabc', groups: undefined ]
  //[ 'abc', index: 11, input: 'abcdefg,abcabc', groups: undefined ]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

其中第三个例子的意思就是说,后文是 abc 的当前位置上去找到 abc

# 后向查找

后向查找与前向查找相反,即向前文查找。通过(?<=regex)来实现。

如在applepeople中找到apple的 ple 后缀:

/(?<=app)ple/
1

负后向查找:/(?<!peo)ple/

ps: 从 es2018 之后,chrome 中的正则表达式也支持反向查找了。不过,在实际项目中还需要注意对旧浏览器的支持,以防线上出现 Bug。详情请查看

回溯查找 正则表达式 记忆方式
引用 \1 和$1
非捕获组 (?😃
前向查找 (?=regxp)
负前向查找 (?!regxp)
后向查找 (?<=regxp)
负后向查找 (?<!regxp)

# 逻辑处理

我们来梳理一下与或非的逻辑处理吧。

:正则表达式默认规则就是与

:这里要注意一下,只有在[]内部的^才会表示非的关系;还有就是子表达式中的(?!regxp)(?<!regxp)

(a|b)