0
点赞
收藏
分享

微信扫一扫

【正则】174-《JavaScript 正则迷你书》知识点小抄本(下)

四、正则表达式回溯法原理

概念理解起来比较容易。
比如用 ​​/ab{1,3}c/​ 去匹配下面两个字符串。

  • 当匹配 ​abbbc​,按顺序匹配,到了第 3 个 ​b​ 后,直接匹配 ​c​,这样就没有回溯。
  • 当匹配 ​abbc​,按顺序匹配,到了第 2 个 ​b​ 后,由于规则是 ​b{1,3}​ ,则会继续往下匹配,然后发现下一位是 ​c​,于是回退到前一个位置,重新匹配,这就是回溯。

另外像 ​/".*"/​​ 来匹配 ​"abc"de​​ 的话,就会有三个回溯情况,为了减少不必要的回溯,我们可以把正则修改为 ​/"[^"]*"/​。

介绍

回溯法,也称试探法,本质上是深度优先探索算法,基本思路是:匹配过程中后退到之前某一步重新探索的过程。

1. 常见的回溯形式

  • 贪婪量词

多个贪婪量词挨着存在,并相互冲突时,会看匹配顺序,深度优先搜索:

  1. "12345".match(/(\d{1,3})(\d{1,3})/);
  2. //  ["12345", "123", "45", index: 0, input: "12345"]
  • 惰性量词

有时候会因为回溯,导致实际惰性量词匹配到的不是最少的数量:

  1. "12345".match(/(\d{1,3}?)(\d{1,3})/);
  2. // 没有回溯的情况 ["1234", "1", "234", index: 0, input: "12345"]

  3. "12345".match(/^\d{1,3}?\d{1,3}$/);
  4. // 有回溯的情况 ["12345", index: 0, input: "12345"]
  • 分支结构

分支机构,如果一个分支整体不匹配,会继续尝试剩下分支,也可以看成一种回溯。

  1. "candy".match(/can|candy/); // ["can", index: 0, input: "candy"]

  2. "candy".match(/^(?:can|candy)$/); // ["candy", index: 0, input: "candy"]

2. 本章小结

简单总结:一个个尝试,直到,要么后退某一步整体匹配成功,要么最后试完发现整体不匹配。

  • 贪婪量词:买衣服砍价,价格高了,便宜点,再便宜点。
  • 懒惰量词:卖衣服加价,价格低了,多给点,再多给点。
  • 分支结构:货比三家,一家不行换一家,不行再换。

五、正则表达式的拆分

拆分正则代码块,是理解正则的关键。

在 JavaScrip 正则表达式有以下结构:

  • 字面量: 匹配一个具体字符,如 ​a​​ 匹配字符 ​a​。
  • 字符组: 匹配一个有多种可能性的字符,如 ​[0-9]​ 匹配任意一个数字。
  • 量词: 匹配一个连续出现的字符,如 ​a{1,3}​​ 匹配连续最多出现 3 次的 ​a​字符。
  • 锚: 匹配一个位置,如 ​^​ 匹配字符串的开头。
  • 分组: 匹配一个整体,如 ​(ab)​​ 匹配 ​ab​ 两个字符连续出现。
  • 分支: 匹配一个或多个表达式,如 ​ab|bc​​ 匹配 ​ab​ 或 ​bc​ 字符。

另外还有以下操作符:

优先级

操作符描述

操作符

1

转义符

\

2

括号和方括号

(...)(?:...)(?=...)(?!...)[...]

3

量词限定符

{m}{m,n}{m,}?*+

4

位置和序列

^$\元字符一般字符

5

管道符

`

Tips:优先级从上到下,由高到低。

1. 注意要点

  • 匹配字符串整体

不能写成 ​/^abc|bcd$/​​ ,而是要写成 ​/^(abc|bcd)$/​。

  • 量词连缀问题

需要匹配:每个字符是 ​a​​/ ​b​​/ ​c​ 中其中一个,并且字符串长度是 3 的倍数:

不能写成 ​/^[abc]{3}+$/​​ ,而是要写成 ​/([abc]{3})+/​。

  • 元字符转义问题

元字符就是正则中的特殊字符,当匹配元字符就需要转义,如:

^​​、 ​$​​、 ​.​​、 ​*​​、 ​+​​、 ​?​​、 ​|​​、 ​\​​、 ​/​​、 ​(​​、 ​)​​、 ​[​​、 ​]​​、 ​{​​、 ​}​​、 ​=​​、 ​!​​、 ​:​​、 ​-​ 。

  1. // "[abc]" => /\[abc\]/ 或者 /\[abc]/
  2. // "{1,3}" => /\{1\}/ 或者 /\{1}/ 因为不构成字符组

2. 案例分析

  • 身份证号码
  1. /^(\d{15}|\d{17})[\dxX]$/.test("390999199999999999");// true
  • IPV4地址

需要好好分析:

  1. let r = /^((0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])\.){3}(0{0,2}\d|0?\d{2}|1\d{2}|2[0-4]\d|25[0-5])$/

六、正则表达式的构建

正则的构建需要考虑以下几点的平衡:

  • 匹配预期的字符串
  • 不匹配非预期的字符串
  • 可读性和可维护性
  • 效率

我们还需要考虑这么几个问题:

  • 是否需要使用正则

如能使用其他 API 简单快速解决问题就不需要使用正则:

  1. "2019-03-16".match(/^(\d{4})-(\d{2})-(\d{2})/); // 间接获取 ["2019", "03", "16"]
  2. "2019-03-16".split("-"); //  ["2019", "03", "16"]

  3. "?id=leo".search(/\?/); // 0
  4. "?id=leo".indexOf("?"); // 0

  5. "JavaScript".match(/.{4}(.+)/)[1]; // "Script"
  6. "JavaScript".substring(4); // "Script"
  • 是否需要使用复杂正则

/(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/

将这个正则拆分成多个小块,如下:

  1. var regex1 = /^[0-9A-Za-z]{6,12}$/;
  2. var regex2 = /^[0-9]{6,12}$/;
  3. var regex3 = /^[A-Z]{6,12}$/;
  4. var regex4 = /^[a-z]{6,12}$/;
  5. function checkPassword (string) {
  6. if (!regex1.test(string)) return false;
  7. if (regex2.test(string)) return false;
  8. if (regex3.test(string)) return false;
  9. if (regex4.test(string)) return false;
  10. return true;
  11. }

1. 准确性

即需要匹配到预期目标,且不匹配非预期的目标。

  • 匹配固定电话

如需要匹配下面固定电话号码,可以分别写出对应正则:

  1. 055188888888 => /^0\d{2,3}[1-9]\d{6,7}$/
  2. 0551-88888888 => /^0\d{2,3}-[1-9]\d{6,7}$/
  3. (0551)88888888 => /^0\d{2,3}-[1-9]\d{6,7}$/

然后合并:

  1. let r = /^0\d{2,3}[1-9]\d{6,7}$|^0\d{2,3}-[1-9]\d{6,7}$|^\(0\d{2,3}\)[1-9]\d{6,7}$/

然后提取公共部分:

  1. let r = /^(0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/

再优化:

  1. let r = /^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/
  • 匹配浮点数

先确定,符号部分( ​[+-]​​)、整数部分( ​\d+​​)和小数部分( ​\.\d+​)。

  1. 1.23、+1.23、-1.23 => /^[+-]?\d+\.\d+$/
  2. 10、+10、-10 => /^[+-]?\d+$/
  3. .2、+.2、-.2 => /^[+-]?\.\d+$/

整理后:

  1. let r = /^[+-]?(\d+\.\d+|\d+|\.\d+)$/;

  2. // 考虑不匹配 +.2 或 -.2
  3. let r = /^([+-])?(\d+\.\d+|\d+|\.\d+)$/;

  4. // 考虑不匹配 012 这类 0 开头的整数
  5. let r = /^[+-]?(\d+)?(\.)?\d+$/;

2. 效率

正则表达式运行过程:

  1. 编译
  2. 设定起始位置
  3. 尝试匹配
  4. 若匹配失败则返回前一步重新匹配
  5. 返回匹配成功失败的结果

我们常常优化对 ​34​ 步进行优化:

  • 使用具体字符组替代通配符,消除回溯

如 ​/"[^"]*"/​​ 代替 ​/".*?"/​。

  • 使用非捕获型分组

当不需要使用分组引用和反向引用时,此时可以使用非捕获分组。

如 ​/^[-]?(?:\d\.\d+|\d+|\.\d+)$/​​ 代替 ​/^[-]?(\d\.\d+|\d+|\.\d+)$/​。

  • 独立出确定字符

加快判断是否匹配失败,进而加快移位的速度。

如 ​/aa*/​​ 代替 ​/a+/​。

  • 提取分支公共部分

减少匹配过程中可消除的重复。

如 ​/^(?:abc|def)/​​ 代替 ​/^abc|^def/​。

  • 减少分支的数量,缩小它们的范围

如 ​/rea?d/​​ 代替 ​/red|read/​。

七、正则表达式编程

这里要掌握正则表达式怎么用,通常会有这么四个操作:

  • 验证
  • 切分
  • 提取
  • 替换

1. 四种操作

  • 验证

匹配本质上是查找,我们可以借助相关API操作:

  1. // 检查字符串是否包含数字
  2. let r = /\d/, s = "abc123";
  3. !!s.search(r); // true
  4. r.test(s); // true
  5. !!s.match(r); // true
  6. !!r.exec(s); // true
  • 切分
  1. "leo,pingan".split(/,/); // ["leo", "pingan"]

  2. let r = /\D/, s = "2019-03-16";
  3. s.split(r); // ["2019", "03", "16"]
  4. s.split(r); // ["2019", "03", "16"]
  5. s.split(r); // ["2019", "03", "16"]
  • 提取
  1. // 提取日期年月日
  2. let r = /^(\d{4})\D(\d{2})\D(\d{2})$/;
  3. let s = "2019-03-16";

  4. s.match(r); // ["2019-03-16", "2019", "03", "16", index: 0, input: "2019-03-16"]
  5. r.exec(s); // ["2019-03-16", "2019", "03", "16", index: 0, input: "2019-03-16"]
  6. r.test(s); // RegExp.$1 => "2019" RegExp.$2 => "03" RegExp.$3 => "16"
  7. s.search(r);// RegExp.$1 => "2019" RegExp.$2 => "03" RegExp.$3 => "16"
  • 替换
  1. // yyyy-mm-dd 替换成 yyyy/mm/dd
  2. "2019-03-16".replace(/-/g, "/");

2. 相关API注意

  • search​​ 和 ​match​ 参数问题

这两个方法会把字符串转换成正则,所以要加转义

  1. let s = "2019.03.16";
  2. s.search('.'); // 0
  3. s.search('\\.'); // 4
  4. s.search(/\./); // 4
  5. s.match('.'); // ["2", index: 0, input: "2019.03.16"]
  6. s.match('\\.'); // [".", index: 4, input: "2019.03.16"]
  7. s.match(/\./); // [".", index: 4, input: "2019.03.16"]

  8. // 其他不用转义
  9. s.split('.');
  10. s.replace('.', '/');
  • match​ 返回结果的格式问题

match​​ 参数有 ​g​​ 会返回所有匹配的内容,没有 ​g​ 则返回标准匹配格式:

  1. let s = "2019.03.16";
  2. s.match(/\b(\d+)\b/); // ["2019", "2019", index: 0, input: "2019.03.16"]
  3. s.match(/\b(\d+)\b/g); // ["2019", "03", "16"]
  • test​​ 整体匹配时需要使用 ​^​ 和 ​$
  1. /123/.test("a123b"); // true
  2. /^123$/.test("a123b"); // false
  3. /^123$/.test("123"); // true
  • split​ 的注意点

split​ 第二个参数是 结果数组的最大长度

  1. "leo,pingan,pingan8787".split(/,/, 2); // ["leo", "pingan"]

使用正则分组,会包含分隔符:

  1. "leo,pingan,pingan8787".split(/(,)/); // ["leo", ",", "pingan", ",", "pingan8787"]
  • 修饰符

修饰符

描述

g

全局匹配,即找到所有匹配的,单词是 global

i

忽略字母大小写,单词是 ingoreCase

m

多行匹配,只影响 ^ 和 $,二者变成行的概念,即行开头和行结尾。单词是 multiline

文章到这结束,感谢阅读,也感谢老姚大佬的这本书

【正则】174-《JavaScript 正则迷你书》知识点小抄本(下)_正则


举报

相关推荐

0 条评论