每一次当我需要用到正则表达式的时候,我都会去查一下正则的一些用法、语法等。这让我十分困扰。于是今天我想要去深入学习一下正则表达式的方方面面吧,希望搞懂正则表达式各种符号之间的内在联系,形成知识体系,当下次再遇到正则表达式的时候可以不借助搜索引擎,自己解决。
正则表达式(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/
==> color
和colour
# >=0
元字符*
,匹配可有可无的字符串。
# >=1
元字符+
,表示至少存在一个字符。
# 特定次数
通过{
、}
来设置精确的匹配区间。
- {x}: x次
- {min, max}: 介于min次到max次之间
- {min, }: 至少min次
- {0, max}: 至多max次
2
3
4
5
6
7
# 位置边界
# 单词边界 \b 和 \B
一个比较常见的使用场景就是在文章或句子中的特定单词找出来。如:
The cat scattered his food all over the room.
我想找到 cat
这个单词,但是如果只是使用/cat/
这个正则,就会同时匹配到 cat
和 scattered
这两处文本。这时候我们就需要使用边界正则表达式\b
,其中 b 是 boundary 的首字母。在正则引擎里它其实匹配的是能构成单词的字符(\w)
和不能构成单词的字符(\W)
中间的那个位置。
上面的例子改写成/\bcat\b/
这样就能匹配到 cat
这个单词了。
# 字符串边界 ^ $
元字符 ^
匹配字符串的开头,而 $
匹配字符串的末尾。如:
I am scq000.
I am scq000.
I am scq000.
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.
利用回溯引用我们可以写出:/\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
// ]
}
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'
2
3
如果我们不想某个子表达式被引用,使用**非捕获正则(?:regex)
**就可以避免浪费内存。
var str = "scq000";
str.replace(/(scq00)(?:0)/, "$1,$2");
// 返回scq00,$2
// 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2
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 ]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
其中第三个例子的意思就是说,后文是 abc 的当前位置上去找到 abc
# 后向查找
后向查找与前向查找相反,即向前文查找。通过(?<=regex)
来实现。
如在apple
和people
中找到apple
的 ple 后缀:
/(?<=app)ple/
负后向查找:/(?<!peo)ple/
ps: 从 es2018 之后,chrome 中的正则表达式也支持反向查找了。不过,在实际项目中还需要注意对旧浏览器的支持,以防线上出现 Bug。详情请查看
回溯查找 | 正则表达式 | 记忆方式 |
---|---|---|
引用 | \1 和$1 | |
非捕获组 | (?😃 | |
前向查找 | (?=regxp) | |
负前向查找 | (?!regxp) | |
后向查找 | (?<=regxp) | |
负后向查找 | (?<!regxp) |
# 逻辑处理
我们来梳理一下与或非的逻辑处理吧。
与:正则表达式默认规则就是与
非:这里要注意一下,只有在[]
内部的^
才会表示非的关系;还有就是子表达式中的(?!regxp)
和(?<!regxp)
或:(a|b)