看nginx配置的时候,写表单验证的时候,都会遇到正则表达式。就是用一次看一次,看一次忘一次。为了让自己能从下一次的遗忘中快速回想起来,所以咱就费费键盘写一下🤣

👀本文是总结后的产物,主要参考https://juejin.cn/post/6844903845227659271

1. 正则总表

下表只是为了方便查询。正文从2. 从字符出发开始哦😊

描述 正则表达式 记忆方式/举例
换行符 \\n new line
换页符 \\f form feed
回车符 \\r return
空白符 \\s space
制表符 \\t tab
垂直制表符 \\v vertical tab
回退符 [\\b] backspace,之所以使用[]
符号是避免和\\b
重复
除了换行符之外的任何字符 . 句号,除了句子结束符
单个数字, [0-9] \\d digit
除了[0-9] \\D not digit
包括下划线在内的单个字符,[A-Za-z0-9_] \\w word
非单字字符 \\W not word
匹配空白字符,包括空格、制表符、换页符和换行符 \\s space
匹配非空白字符 \\S not space
集合 [] [123]
只会匹配1, 2, 3,其余字符不会被匹配到
连字符 - /[1-9]/
表示匹配1, 2, 3, …, 9
连字符 - /[1-9]/
表示匹配1, 2, 3, …, 9
匹配0或1次 ?
匹配0次或无数次 *
匹配1次或无数次 +
匹配1次或无数次 +
匹配x次 {x}
匹配介于min次到max次之间 {min, max}
匹配至少min次 {min, }
匹配至多max次 {0, max}
单词边界 \\b boundary
非单词边界 \\B not boundary
字符串开头 ^ 小头尖尖那么大个
字符串结尾 $ 终结者,美国科幻电影,美元符$
多行模式 m标志 multiple of lines
忽略大小写 i标志 ignore case, case-insensitive
全局模式 g标志 global
[^regex]
!
|

2. 从字符出发

咱把正则表达式看成一个肉夹馍,两层饼皮一层肉。肉这部分正是整个正则的精华,它由字符元字符构成:

  • 字符:就是基础的计算机字符,通常正则表达式里使用的就是数字、英文字母
  • 元字符:特殊字符。是一些用来表示特殊语义的字符,比如^表示非,|表示或;
  • 【❗注意】如果想匹配^这个符号,就需要使用\进行转义,写成\^

2.1. 单个字符

单个字符是指正则中的一个字符匹配到目标字符串的一个字符。

这句话有点绕,但可以先看下去,结合后边的多个字符理解。

特殊字符 正则表达式 记忆方式
换行符 \\n new line
换页符 \\f form feed
回车符 \\r return
空白符 \\s space
制表符 \\t tab
垂直制表符 \\v vertical tab
回退符 [\\b] backspace,之所以使用[]
符号是避免和\\b
重复

2.2. 多个字符

多个字符是指正则中的多个字符匹配到目标字符串的一类字符,但这一类字符是单个进行匹配的。

2.2.1. []:集合

🌰栗子**/[123]/**

只会匹配1, 2, 3,其余字符不会被匹配到👇
2.  正则表达式 - 图1

2.2.2. -:连字符

如果我想匹配所有的数字怎么办呢?从0写到9显然太过低效,所以元字符-就可以用来表示区间范围

🌰栗子**/[1-9]/**
只会匹配1-9,包括0的其余字符不会被匹配到👇
2.  正则表达式 - 图2

2.2.3. ❗注意

2.2.3.1. []中的^:🌰/[^ab]/

^表示一行开头 (后边会介绍到),但是在[]中表示取反

  • /[ab]/:a或者b;
  • /[^ab]/:啥都行,只要不是a或b(anythings except a and b),相当于取反。

2.2.3.2. -[]中第一个字符时:🌰/[-.]/

比如/[-.]/的含义是连字符-或者点符.。 但是,如果当连字符不是第一个字符时,比如[a-z],这就表示是从字母a到字符z。

2.2.4. 特殊字符表

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

3. 次数匹配

一对一和一对多的字符匹配都讲完了。如果要匹配4次a,可以写成/aaaa/。但如果要匹配20次,写20个a就非常麻烦,所以需要使用正则的次数匹配。

3.1. ?:0|1次

元字符?代表了匹配一个字符或0个字符

🌰栗子:匹配color和colour这两个单词
设想一下,如果你要匹配color和colour这两个单词,就需要同时保证u这个字符是否出现都能被匹配到。所以你的正则表达式应该是这样的:/colou?r/

3.2. *: >=0次

元字符*用来表示匹配0个字符或无数个字符。通常用来过滤某些可有可无的字符串。

3.3. +: >=1次

元字符+适用于要匹配同个字符出现1次或多次的情况。

3.4. {}: 特定次数

在某些情况下,我们需要匹配特定的重复次数,元字符{}用来给重复匹配设置精确的区间范围。

  • {x}: x次;
  • {min, max}: 介于min次到max次之间;
  • {min, }: 至少min次;
  • {0, max}: 至多max次。

🌰栗子:匹配3次a
如’a’我想匹配3次,那么我就使用/a{3}/这个正则,或者说’a’我想匹配至少两次就是用/a{2,}/这个正则。

4. 位置边界

4.1. \b:单词边界

边界正则表达式\b,其中b是boundary的首字母。在正则引擎里它其实匹配的是能构成单词的字符(\w)和不能构成单词的字符(\W)中间的那个位置。

🌰栗子:找出特定单词
找到The cat scattered his food all over the room.中的单词cat👇:

我想找到cat这个单词,但是如果只是使用/cat/这个正则,就会同时匹配到cat和scattered这两处文本。这时候我们就需要使用边界正则表达式\b
改写成/\bcat\b/这样就能匹配到cat这个单词了。

4.2. ^$:字符串边界

  • 元字符^:用来匹配字符串的开头;
  • 元字符$:用来匹配字符串的末尾;
  • ❗注意:在长文本里,如果要排除换行符的干扰,我们要使用多行模式

4.3. img:模式匹配

  • 元字符i:忽略大小写;
  • 元字符m:多行模式;
  • 元字符g:找到所有符合的匹配.

🌰栗子:长文本排除换行符的干扰
试着匹配I am scq000这个句子:

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

我们可以使用/^I am scq000\.$/m这样的正则表达式,其实m是multiple line的首字母。正则里面的模式除了m外比较常用的还有ig。前者的意思是忽略大小写,后者的意思是找到所有符合的匹配。

4.4. 小结

边界和标志 正则表达式 记忆方式
单词边界 \\b boundary
非单词边界 \\B not boundary
字符串开头 ^ 小头尖尖那么大个
字符串结尾 $ 终结者,美国科幻电影,美元符$
多行模式 m标志 multiple of lines
忽略大小写 i标志 ignore case, case-insensitive
全局模式 g标志 global

5. 子表达式

字符匹配我们介绍的差不多了,更加高级的用法就得用到子表达式了。通过嵌套递归和自身引用可以让正则发挥更强大的功能。

从简单到复杂的正则表达式演变通常要采用分组、回溯引用和逻辑处理的思想。利用这三种规则,可以推演出无限复杂的正则表达式。

5.1. ():分组

其中分组体现在:所有以()元字符所包含的正则表达式被分为一组,每一个分组都是一个子表达式,它也是构成高级正则表达式的基础。如果只是使用简单的(regex)匹配语法本质上和不分组是一样的,如果要发挥它强大的作用,往往要结合回溯引用的方式。

5.2. 回溯引用

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

5.2.1. 🌰栗子:匹配两个连续相同的单词

找到Hello what what is the first thing, and I am am scq000.中的`两个连续相同的单词👇:

利用回溯引用,我们可以很容易地写出/\b(\w+)\s\1/这样的正则。

5.2.2. 替换字符串

回溯引用在替换字符串中十分常用,语法上有些许区别,用$1,$2...来引用要被替换的字符串。下面以js代码作演示:

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

5.2.3. (?:regex):非捕获正则

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

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

5.3. 回溯引用的适用范围

有时,我们需要限制回溯引用的适用范围。那么通过前向查找和后向查找就可以达到这个目的。

5.3.1. (?=regex):前向查找

前向查找(lookahead)是用来限制后缀的。凡是以(?=regex)包含的子表达式在匹配过程中都会用来限制前面的表达式的匹配。

🌰栗子:**happy happily**两个单词:

  • 我想获得以happ开头的副词,那么就可以使用happ(?=ily)来匹配。
  • 如果我想过滤所有以happ开头的副词,那么也可以采用负前向查找的正则happ(?!ily),就会匹配到happy单词的happ前缀。

5.3.2. (?<=regex):后向查找

后向查找(lookbehind)是通过指定一个子表达式,然后从符合这个子表达式的位置出发开始查找符合规则的字串。

🌰栗子:apple和people都包含ple这个后缀,那么如果我只想找到apple的ple,该怎么做呢?
我们可以通过限制app这个前缀,就能唯一确定ple这个单词了:/(?<=app)ple/

其中(?<=regex)的语法就是我们这里要介绍的后向查找。regex指代的子表达式会作为限制项进行匹配,匹配到这个子表达式后,就会继续向后查找。
另外一种限制匹配是利用(?<!regex)语法,这里称为负后向查找。与正前向查找不同的是,被指定的子表达式不能被匹配到。于是,在上面的例子中,如果想要查找apple的ple也可以这么写成/(?<!peo)ple/

5.3.3. 小结

回溯查找 正则 记忆方式
引用 \\0,\\1,\\2 和 $0, $1, $2 转义+数字
非捕获组 (?:) 引用表达式(()), 本身不被消费(?),引用(:)
前向查找 (?=) 引用子表达式(()),本身不被消费(?), 正向的查找(=)
前向负查找 (?!) 引用子表达式(()),本身不被消费(?), 负向的查找(!)
后向查找 (?<=) 引用子表达式(()),本身不被消费(?), 后向的(<,开口往后),正的查找(=)
后向负查找 (?<!) 引用子表达式(()),本身不被消费(?), 后向的(<,开口往后),负的查找(!)

6. 与或非:逻辑处理

6.1. 与

在正则里面,默认的正则规则都是与的关系所以这里不讨论。

6.2. [^regex]!:非

而非关系,分为两种情况:一种是字符匹配,另一种是子表达式匹配。
在字符匹配的时候,需要使用^这个元字符。
在这里要着重记忆一下:只有在[]内部使用的^才表示非的关系。子表达式匹配的非关系就要用到前面介绍的前向负查找子表达式(?!regex)或后向负查找子表达式(?<!regex)

6.3. |:或

或关系,通常给子表达式进行归类使用。比如,我同时匹配a,b两种情况就可以使用(a|b)这样的子表达式。

6.4. 小结

逻辑关系 正则元字符
[^regex]
!
&#124;

7. 在JS中的应用

7.1. reg.test()

正则表达式本身有一个test的方法,这个方法只能测试是否包含,返回一个bool变量。

  1. const r = /\d{3}/;
  2. const a = '123';
  3. const b = '123abc';
  4. const c = 'abc';
  5. r.test(a); // true
  6. r.test(b); // true
  7. r.test(c); // false

7.2. str.match()

test()不同,不只是返回bool变量,它会返回你所匹配到的内容

  1. const r = /compus/;
  2. const reg = /m+/
  3. const s = 'compus, I know something about you';
  4. r.test(s); // true
  5. s.match(r); // ['compus', index: 0, input: 'compus, I know something about you', groups: undefined]
  6. s.match(reg); // ['m', index: 2, input: 'compus, I know something about you', groups: undefined]

等等,好像有点问题,明明something中也有一个m,为什么最后一个返回的是[“m”]?这不科学。

好吧,实际上,match()返回了第一个可以匹配的序列。想要实现之前的效果,就要用到前边介绍的模式匹配:

  1. const reg = /m+/g
  2. const s = 'compus, I know something about you';
  3. s.match(reg); // ['m', 'm']

但是还有一个问题,上边说到分组,那么match会返回分组吗?

  1. var str = "Here is a Phone Number 111-2313 and 133-2311"
  2. var sr = /(\d{3})[-.]\d{4}/
  3. var srg = /(\d{3})[-.]\d{4}/g
  4. console.log(str.match(sr)); // ['111-2313', '111', index: 23, input: 'Here is a Phone Number 111-2313 and 133-2311', groups: undefined]
  5. console.log(str.match(srg)); // ['111-2313', '133-2311']

所以结论是: 当使用了全局模式g的时候,不会返回分组,而是全部的匹配结果;如果没有使用g,会将匹配到的结果和分组以数组的形式返回。

7.3. reg.exec()

从字面意思来看,正则表达式的执行方法。 这个方法可以实现匹配全局,并返回分组的结果。
reg.exec()每次调用,返回一个匹配的结果,匹配结果和分组以数组的形式返回,不断的调用即可返回下一个结果,直到返回null.

  1. var str = "Here is a Phone Number 111-2313 and 133-2311" ;
  2. var srg = /(\d{3})[-.]\d{4}/g;
  3. var result = srg.exec(str);
  4. while(result !== null) {
  5. console.log(result);
  6. result = srg.exec(str);
  7. }

运行结果:
2.  正则表达式 - 图3

7.4. str.split()

现在来到了更强的功能上,先说下split,我们知道split是将字符串按照某个字符分隔开,比如有以下一段话,需要将其分割成单词。

  1. var s = "unicorns and rainbows And, Cupcakes"

分割成单词,首先想到的是空格隔开,于是可以用下面方式实现:

  1. var result = s.split(' ');
  2. var result1 = s.split(/\s/);
  3. //完全一样的效果
  4. //["unicorns", "and", "rainbows", "And,", "Cupcakes"]

嗯,这样体现不出来正则的强大,而且最主要的是没有实现要求。因为还有一个”And,”。所以要用正则了,匹配条件是逗号或者空格:

  1. result = s.split(/[,\s]/); // ["unicorns", "and", "rainbows", "And", "", "Cupcakes"]

结果仍然和需要的有出入,因为多了一个””。 我们并不是想让它分割的依据是逗号或者空格,依据应该是逗号或空格所在的连续序列。 在原来的基础上加一个+,改成/[,\s]+/

  1. result = s.split(/[,\s]+/);
  2. // ["unicorns", "and", "rainbows", "And", "Cupcakes"]

7.4.1. 单词分割

好了,拓展一下,实现一个段落的单词分割,一个正则表达式就是

  1. result = s.split(/[,.!?\s]+/)

当然,有个最简单的方法,我们可以这样去做

  1. result = s.split(/\W+/);

接着,如果我们想将一个段落的句子都分隔开,一个可以实现的表达式就是

  1. result = s.split(/[.,!?]+/)

最后,有一个小需求,就是在分割句子的同时,还想把相应的分隔符保留下来。

  1. var s =
  2. "Hello,My name is Vincent. Nice to Meet you!What's your name? Haha."

这是一个小小的ponit,记住如果想要保留分隔符,只要给匹配的内容分组即可

  1. var result = s.split(/([.,!?]+)/)
  2. //["Hello", ",", "My name is Vincent", ".", " Nice to Meet you", "!", "What's your name", "?", " Haha", ".", ""]

7.5. str.replace()

replace也是字符串的方法,它的基本用法是str.replace(reg,replace|function),第一个参数是正则表达式,代表匹配的内容,第二个参数是替换的字符串或者一个回掉函数。
注意,replace不会修改原字符串,只是返回一个修改后的字符串。除此外,正则表达式如果没有使用**g**标志,也和match一样,只匹配或替换第一个

7.5.1. 最简单的替换

  1. // 🌰替换一个序列中的元音字母(aeiou),将其替换成一个double。 比如x->xx
  2. var s = "Hello,My name is Vincent."
  3. var result = s.replace(/([aeiou])/g,"$1$1")
  4. //"Heelloo,My naamee iis Viinceent."

注意❗:

  • 第二个参数必须是字符串;
  • 注意不要忘记加g.

7.5.2. 第二个参数传function

先看一个最简单的示例:

  1. var s = "Hello,My name is Vincent. What is your name?"
  2. var newStr = s.replace(/\b\w{4}\b/g,replacer)
  3. console.log(newStr)
  4. function replacer(match) {
  5. console.log(match);
  6. return match.toUpperCase();
  7. }
  8. /*
  9. name
  10. What
  11. your
  12. name
  13. Hello,My NAME is Vincent. WHAT is YOUR NAME?
  14. */

所以,函数的参数是匹配到的内容,返回的是需要替换的内容。好了,基本示例解释了基本用法,那么之前讨论的分组怎么办?如何实现分组呢?

  1. //分组
  2. function replacer(match,group1,group2) {
  3. console.log(group1);
  4. console.log(group2);
  5. }

如果正则表达式分组处理,那么在回调函数中,函数的第二个、第三参数就是group1,group2。这样子,就可以做很多神奇的事情.

7.5.3. 综合练习

判断一个字符串中出现次数最多的字符,并统计次数。

  1. var s = 'aaabbbcccaaabbbaaa';
  2. var a = s.split('').sort().join(""); //"aaaaaaaaabbbbbbccc"
  3. var ans = a.match(/(\w)\1+/g);
  4. ans.sort(function(a,b) {
  5. return a.length - b.length;
  6. })
  7. console.log('ans is : ' + ans[ans.length-1])