1. 第一,能用普通字符串处理的,坚决用普通字符串处理。
  2. 第二,能写注释的正则表达式,一定要写注释。
  3. 第三,能用多个简单正则表达式解决的,一定不要苛求一个复杂的正则表达式。

有这样的一个案例: 常见的密码要求“必须包含数字、小写字母、大写字母、特殊符号中的至少两种,且长度在 8 到 16 之间”。我们当然可以实现一个强大的正则可以解决所有问题,也可以将此需求拆解,分别验证每一个条件,这样的方案虽繁琐但是简洁高效。

拆分后的规则校验可以实现如下:

  1. const numberRegx = /[0-9]/; // 校验数字
  2. const lowerCaseRegx = /[a-z]/; // 小写字母
  3. const upperCaseRegx = /[A-Z]/; // 大写字母
  4. const superCharRegx = /[_-]/; // 特殊字符
  5. const lengthRegx = /.{8,16}/; // 长度校验
  6. /**
  7. word 需要校验的字符串 matchNum 需要匹配的规则次数
  8. */
  9. function passwordCheck(word='',matchNum=2){
  10. let matchRuleFrequency=0;
  11. if(!lengthRegx.test(word)){
  12. return {
  13. result: false,
  14. msg: '输入长度不满足要求'
  15. };
  16. }
  17. const baseRule=[numberRegx,lowerCaseRegx,upperCaseRegx,superCharRegx];
  18. baseRule.forEach(item=>{
  19. if(item.test(word)){
  20. matchRuleFrequency++;
  21. }
  22. })
  23. if(matchRuleFrequency>=matchNum){
  24. return {
  25. result: true,
  26. msg: '校验通过'
  27. };
  28. }else {
  29. return {
  30. result: false,
  31. msg: '密码需要包含大写字母、小写字母、数字和特殊符号中的至少两种'
  32. };
  33. }
  34. }


01 元字符:正则表达式的基础元件

学习正则的一切基础前提是,了解正则的基本构成单元:元字符。

所谓元字符就是那些在正则表达式中表示特定含义的专用字符。

元字符大致分成这几类:

  • 表示单个特殊字符的
  • 表示空白符的
  • 表示某个范围的
  • 表示次数的量词
  • 另外还有表示断言的 正则 - 图1

    单字符

英文的点”.”表示除换行以外的任意单个字符,\d 表示任意单个数字,\w 表示任意数字字母下划线,\s 表示任意空白符。与之相对应的还有 \D、\W 和 \S , \D 表示任意非数字,\W 表示任意非数字字母下划线,\S 表示任意非空白符。

空白符

回车符 \r ,换行符 \n,TAB 制表符 \t 等等,大部分场景使用 \s 就可以满足需求,\s 代表任意单个空白符号。

量词

上述的基础元字符,或者空白元字符都只是用来匹配单个字符,想要匹配多个字符需要通过量词进行。

量词元字符可用的场景通常有: 重复N次,出现0次或1次,最多出现三次……

在正则中,英文的星号(*)代表出现 0 到多次,加号(+)代表 1 到多次,问号(?)代表 0 到 1 次,{m,n}代表 m 到 n 次。

比如,在文本中“颜色”这个单词,可能是带有 u 的 colour,也可能是不带 u 的 color,我们使用 colou?r 就可以表示两种情况了

范围

管道符号 |,用来隔开多个正则,表示满足其中任意一个就行,比如 ab|bc 能匹配上 ab,也能匹配上 bc

中括号[]代表多选一,可以表示里面的任意单个字符。

中括号中,还可以用中划线表示范围,比如 [a-z] 可以表示所有小写字母。如果中括号第一个是脱字符(^),那么就表示非,表达的是不能是里面的任何单个元素。

本节练习题

请写出一个正则,验证如下规则:

  1. 第 1 位固定为数字 1;
  2. 第 2 位可能是 3,4,5,6,7,8,9;
  3. 第 3 位到第 11 位我们认为可能是 0-9 任意数字
  1. const regx = /1[3-9]\d{9}/;
  2. console.log(regx.test('125566')); // false
  3. console.log(regx.test('1255-66'));
  4. console.log(regx.test('1255w66'));
  5. console.log(regx.test('12556666666'));
  6. console.log(regx.test('14556666666')); // true

02 量词与贪婪:正则的匹配模式

正则中有三种模式:贪婪匹配、非贪婪匹配和独占模式。

贪婪模式,简单说就是尽可能进行最长匹配。非贪婪模式呢,则会尽可能进行最短匹配。

贪婪匹配(Greedy)

在正则中,表示次数的量词默认是贪婪的,在贪婪模式下,会尝试尽可能最大长度去匹配。

看一个例子:在字符串 aaabb 中使用正则 /a*/ 的匹配过程。
63e5c750b66f6eb914c73befdba43f97.jpg
贪婪匹配示例

a* 在匹配开头的 a 时,会尝试尽量匹配更多的 a,直到第一个字母 b 不满足要求为止,匹配上三个 a,后面每次匹配时都得到了空字符串。

非贪婪匹配(Lazy)

在量词后面加上英文的问号 (?)即可将贪婪模式变成非贪婪模式。

案例演示: https://regex101.com/r/XHpDoS/4

10e40baa1194b17dcc57a089524a37bc.png
非贪婪匹配

通过示例图可以看到修改为非贪婪模式后不再是匹配 aaa ,每个 a 都会被单独匹配一次,也会匹配到 a 周围的空白。

所以可以得出总结:3f95a3648980c1eb3c550fb34b46fad1.png

独占模式(Possessive)

不管是贪婪模式,还是非贪婪模式,都需要发生回溯才能完成相应的功能。但是在一些场景下,我们不需要回溯,匹配不上返回失败就好了,因此正则中还有另外一种模式:独占模式。它类似贪婪匹配,但匹配过程不会发生回溯,因此在一些场合下性能会更好。

独占模式和贪婪模式很像,独占模式会尽可能多地去匹配,如果匹配失败就结束,不会进行回溯,这样的话就比较节省时间。具体的方法就是在量词后面加上加号(+)。

96635e198c2ff6cf7b8ea2a0d18f8ecb.png
如上图所示,当用正则: /xy{1,3}+yz/ 检验文本 ‘xyyz’ 时 xy{1,3}+ 将会匹配上 xyy,当使用剩下的规则匹配时,yz 匹配 z 不成功,因此匹配失败。

独占模式性能比较好,可以节约匹配的时间和 CPU 资源,但有些情况下并不能满足需求,要想使用这个模式还要看具体需求,以及项目使用的语言是否支持。

ps: 目前独占模式的支持性不高,Python 和 Go 的标准库目前都不支持独占模式,JavaScript 也不支持。
image.png

正则匹配回溯

一个因为回溯引发的问题的案例: https://zhuanlan.zhihu.com/p/38229530

这个案例有解释了什么是回溯,也阐述了因为回溯可能会引起的实际问题,其他的内容可以不用过多阅读。

对于这个案例,有几个明显的错误是,没有对点号符进行转义,匹配URL中的点直接使用了“ .”,而在正则中,“.”意味着匹配所有字符。此正则中还存在的问题是校验“http”或“https”的规则写的很混乱,可以说这个案例中的正则是相当的“复杂”了 。

(关于回溯这部分需要继续结合正则的工作原理重新整理~~~Todo)

本节练习题

一篇英文文章,除了通过空格分隔的可以认为是单词,引号包裹的部分也被视为是一个单词,编写一个正则,取出文章中的所有单词:

we found “the little cat” is in the hat, we like “the little cat”。

本题解答:https://regex101.com/r/XHpDoS/5

  1. let regx = (\w+)|(“[\w\s]+”);
  2. console.log('we found “the little cat” is in the hat, we like “the little cat”'.match(/(\w+)|(“[\w\s]+”)/gm));

image.png

03 分组与引用:查找和替换

假设我们现在要去查找 15 位或 18 位数字,根据前面学习的知识,使用量词可以表示出现次数,使用管道符号可以表示多个选择,很快就能写出 \d{15}|\d{18}。但是经过测试,发现这个正则并不能很好地完成任务,因为 18 位数字也会匹配上前 15 位。

当把正则的顺序变为 \d{18}|\d{15} 后,正则可以正常使用了,这是因为在大多数正则实现中,多分支选择都是左边的优先。类似的还有可以使用“北京市?” 来实现来查找 “北京” 和 “北京市”。

针对 15 或 18 位数字这个问题,可以看成是 15 位数字,后面 3 位数字有或者没有,这个在逻辑上市没有问题的,对此可以写出正则 \d{15}\d{3}? ,那么这个正则是否正确呢?
image.png

通过第一节我们知道 \d{3} 表示数字出现3次,可以匹配一个长度为3的数字串,? 在第一节当中说了表示的是出现0到1次,而到了第二节在量词之后跟上一个 ? 表示将贪婪模式改为非贪婪模式,因此在现在的写法下\d{15}\d{3}? 表示的含义是前面出现15位数字,后面出现3位数字,即匹配18位长度的数字。

想要让 ? 保持原有的0到1次的含义,我们需要借助其他符号:() 。使用括号将表示“三个数字”的\d{3}这一部分括起来,也就是表示成\d{15}(\d{3})?这样。

括号在正则中的功能就是用于分组。简单来理解就是,由多个元字符组成某个部分,应该被看成一个整体的时候,可以用括号括起来表示一个整体。

分组与编号

括号在正则中可以用于分组,被括号括起来的“子表达式”部分会被保存成一个子组。从正则左起向右数,第几个括号就是第几个分组。87d896f423780c43199222e32c4e428a.png
我们可以写出如图所示的正则,将日期和时间都用括号括起来。这个正则中一共有两个分组,日期是第 1 个,时间是第 2 个。

分组嵌套

前面讲完了子组和编号,但有些情况会比较复杂,比如在括号嵌套的情况里,我们要看某个括号里面的内容是第几个分组。我们只需要数左括号(开括号)是第几个,就可以确定是第几个子组。
083b6a8af68f56f3120b7c8875329340.png
日期分组编号是 1,时间分组编号是 5,年月日对应的分组编号分别是 2,3,4,时分秒的分组编号分别是 6,7,8。

上面这样的情况是所有的括号都保存为子组,有的时候我们也会不保存子组。

不保存子组

在括号里面的会保存成子组,但有些情况下,只想用括号将某些部分看成一个整体,后续不会再用它,类似这种情况,在实际使用时,是没必要保存子组的。这时我们可以在括号里面使用 ?: 不保存子组。

如果正则中出现了括号,那么我们就认为,这个子表达式在后续可能会再次被引用,所以不保存子组可以提高正则的性能。
d6a3d486a8c575bc1961b7db5a153d18.png4b14f91e4307580bb482c58232c3f1fb.png

  1. let reg = /([\u4e00-\u9fa5])[\u4e00-\u9fa5](\1)/g;
  2. let reg1 = /(?:[\u4e00-\u9fa5])[\u4e00-\u9fa5](\1)/g;
  3. let str = '好不好,美不美'

image.png

命名分组

由于通过编号分组得数在第几个位置,后续如果发现正则有问题,改动了括号的个数,还可能导致编号发生变化,因此一些编程语言提供了命名分组(named grouping),这样和数字相比更容易辨识,不容易出错。

命名分组的格式为(?P<分组名>正则),如 (?P\w+)。

并不是所有的语言都支持了这一特性,使用前需要查询确认一下自己使用的语言是否支持。

分组引用

在知道了分组引用的编号 (number)后,大部分情况下,我们就可以使用 “反斜扛 + 编号”,即 \number 的方式来进行引用。

一些在常见的编程语言中,分组查找和替换的引用方式:
c4eef43e2ccf55978b949a194a175594.jpg

分组引用在查找中使用

我们要找重复出现的单词,使用正则可以很方便地使“前面出现的单词再次出现”,可以使用 \w+ 来表示一个单词,(\w+) \1 这个正则表达式即用 \1 表示 (\w+) “再一次出现”。如果使用(\w+)(\w+) 我们知道 \w+ 匹配的是任意字符,(\w+)(\w+)表示的并不是单词重复。

3951b939651d32402e9efe63a83e7de6.png

分组引用在替换中使用

和查找类似,我们可以使用反向引用,在得到的结果中,去拼出来我们想要的结果。

b2465f3f8c50432b622ec8704dc8a214.png

总结与练习

括号可以将某部分括起来,看成一个整体,也可以保存成一个子组,在后续查找替换的时候使用。分组编号是指,在正则中第几个括号内就是第几个分组,而嵌套括号我们只要看左括号是第几个就可以了。如果不想将括号里面的内容保存成子组,可以在括号里面加上?: 来解决。

练习:

有一篇英文文章,里面有一些单词连续出现了多次,我们认为连续出现多次的单词应该是一次,比如:the little cat cat is in the hat hat hat, we like it. 其中 cat 和 hat 连接出现多次,要求处理后结果是:the little cat is in the hat, we like it.

答案是:

  1. let str = 'the little cat cat is in the hat hat hat, we like it.'
  2. let reg = /(\w+)(?:\s+\1)+/gm;

image.png

04 匹配模式:四种匹配模式

所谓匹配模式,指的是正则中一些改变元字符匹配行为的方式,比如匹配时不区分英文字母大小写。常见的匹配模式有 4 种,分别是不区分大小写模式、点号通配模式、多行模式和注释模式。

不区分大小写

当我们把模式修饰符放在整个正则前面时,就表示整个正则表达式都是不区分大小写的。模式修饰符是通过 (? 模式标识) 的方式来表示的。 我们只需要把模式修饰符放在对应的正则前,就可以使用指定的模式了。在不区分大小写模式中,由于不分大小写的英文是 Case-Insensitive,那么对应的模式标识就是 I 的小写字母 i,所以不区分大小写的 cat 就可以写成 (?i)cat。(js中不支持)
image.png

用括号把修饰符和正则 cat 部分括起来,加括号相当于作用范围的限定,让不区分大小写只作用于这个括号里的内容:((?i)cat)。

有一点需要注意,上面讲到的通过修饰符指定匹配模式的方式,在大部分编程语言中都是可以直接使用的,但在 JS 中我们需要使用 /regex/i 来指定匹配模式。在编程语言中通常会提供一些预定义的常量,来进行匹配模式的指定。

总结不区分大小写模式的要点:

  1. 不区分大小写模式的指定方式,使用模式修饰符 (?i);
  2. 修饰符如果在括号内,作用范围是这个括号内的正则,而不是整个正则;
  3. 使用编程语言时可以使用预定义好的常量来指定匹配模式。

    点号通配模式

英文的点(.)可以匹配上任何符号,但不能匹配换行。当我们需要匹配真正的“任意”符号的时候,可以使用 [\s\S] 或 [\d\D] 或 [\w\W] 等。

需要注意的是,JavasScript 不支持此模式,那么我们就可以使用前面说的[\s\S]等方式替代。在 Ruby 中则是用 Multiline,来表示点号通配模式(单行匹配模式),我猜测设计者的意图是把点(.)号理解成“能匹配多行”。

多行匹配模式

单行单配模式

练习

题目: HTML 标签是不区分大小写的,比如我们要提取网页中的 head 标签中的内容,用正则如何实现呢?

答案:/([\s\S])+<\/head>/gi (javaScript) 提取网页标签内容:RegExp.$1

RegExp.$1 获取分组内容
image.png

面试题:

image.png

方法之一:

  1. function isMatch(str) {
  2. if(typeof str !== 'string'){
  3. return false;
  4. }
  5. const reg = /(\(\))|(\[\])|(\{\})/g;
  6. let tempStr = str.replace(reg,'');
  7. while(reg.test(tempStr)) {
  8. tempStr = tempStr.replace(reg,'');
  9. }
  10. return !temp;
  11. }