Skip to content

Latest commit

 

History

History
118 lines (90 loc) · 4.71 KB

06正则表达式构建.md

File metadata and controls

118 lines (90 loc) · 4.71 KB

正则表达式的构建

平衡法则

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

构建正则前提

正则虽然很强大,但是很多时候并不是万能的。比如匹配没有规律的字符

  • 如果可以使用字符串 API,就不需要使用正则
let str = "2017-11-12"
let reg = /^(\d{4})-(\d{2})-(\d{2})/
console.log(str.match(reg))
//[ '2017-11-12','2017','11','12',index: 0,input: '2017-11-12',groups: undefined ]
//使用字符串api
console.log(str.split("-"))
//["2017","11","12"]
  • 没有必要构建复杂的正则
    • 例如密码匹配问题 reg = /(?!^[0-9]{6,12})(?!^[a-z]{6,12})(?!^[A-Z]{6,12})^[0-9a-zA-Z]{6,12}$/
    • 可以使用多个小正则
let reg1 = /^[0-9]{6,12}/
let reg2 = /^[a-z]{6,12}/
let reg3 = /^[A-Z]{6,12}/
let reg4 = /^[0-9a-zA-Z]{6,12}/
function checkPassword (string) {
  if (reg1.test(string)) return false
  if (reg2.test(string)) return false
  if (reg3.test(string)) return false
  if (!reg4.test(string)) return false
}

准确性

055188888888
0551-88888888
(0551)88888888
  • 区号是 0 开头的 3~4 位数:对应的正则是 0\d{2,3}
  • 号码是非 0 开头的 7~8 位数字,对应的正则是:[1-9]\d{6,7}
  1. 对应的正则:/^0\d{2,3}[1-9]\d{6,7}/
  2. 对应的正则:/^0\d{2,3}-[1-9]\d{6,7}/
  3. 对应的正则:/^\(0\d{2,3}\)[1-9]\d{6,7}/
  • 简写:/^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}/

效率

保证准确性后,是否需要考虑优化?

  • 正则表达式的运行分为如下的阶段
    1. 编译
    2. 设定起始的位置
    3. 尝试匹配
    4. 匹配失败的化,从下一位开始继续第三步
    5. 最终结果:匹配成功或失败
let regex = /\d+/g;
console.log( regex.lastIndex, regex.exec("123abc34def") );
console.log( regex.lastIndex, regex.exec("123abc34def") );
console.log( regex.lastIndex, regex.exec("123abc34def") );
console.log( regex.lastIndex, regex.exec("123abc34def") );
// => 0 ["123", index: 0, input: "123abc34def"]
// => 3 ["34", index: 6, input: "123abc34def"]
// => 8 null
// => 0 ["123", index: 0, input: "123abc34def"]
  • 当生成一个正则时,引擎会对其进行编译。报错与否出现这这个阶段。
  • 当尝试匹配时,需要确定从哪一位置开始匹配。一般情形都是字符串的开头,即第 0 位。
  • 但当使用 testexec 方法,且正则有 g 时,起始位置是从正则对象的 lastIndex 属性开始。
    • 因此第一次 exec 是从第 0 位开始,而第二次是从 3 开始的。
    • 第一次 exec,从 0 开始,去尝试匹配,并且成功地匹配到 3 个数字。此时结束时的下标是 2,因此下一次的起始位置是 3。
    • 第二次,起始下标是 3,但第 3 个字符是 "a",并不是数字。但此时并不会直接报匹配失败,而是移动到下一位置,即从第4位开始继续尝试匹配,但该字符是 b,也不是数字。再移动到下一位,是 c 仍不行,再移动一位是数字 3,此时匹配到了两位数字 34。此时,下一次匹配的位置是 d 的位置,即第 8 位。
    • 第三次,是从第 8 位开始匹配,直到试到最后一位,也没发现匹配的,因此匹配失败,返回 null。同时设置 lastIndex 为 0,即,如要再尝试匹配的话,需从头开始。
  • 匹配出现问题时上面的第三阶段和第四阶段。优化主要针对于这两个阶段

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

  • 看这个例子
    • 使用 /".*"/ 匹配目标字符串 "abc"de 正则引擎会保存多种可能中未尝试过的状态,以便后续的回溯时使用,占用一定的内存
    • 因此需要具体化字符组,消除布标要的字符 "[^*]*"

使用非捕获型分组

  • 括号的作用之一是,可以捕获分组和分支里的数据,那么就需要内存来保存它们。
  • 当我们不需要使用分组引用和反向引用时,此时可以使用非捕获分组。
  • 例如,/^[-]?(\d\.\d+|\d+|\.\d+)$/ 可以修改成 /^[-]?(?:\d\.\d+|\d+|\.\d+)$/

独立出确定的字符

  • 例如 /a+/ 可以写成 /aa+/
  • 由于后者比前者多列确定字符 a,这样会在第四阶段中,加快判断是否匹配失败

提取分支的公共部分

  • 比如,/^abc|^def/ 修改成 /^(?:abc|def)/
  • 比如,/this|that/ 修改成 /th(?:is|at)/
  • 可以减少匹配过程中可消除的重复。

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

  • /red|read/ 可以修改成 /rea?d/.此时分支和量词产生的回溯的成本是不一样的.但这样优化后,可读性会降低的