Skip to content
本页目录

构建

一、平衡法则

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

二、构建正则前提

是否能使用正则

是否有必要使用正则

  • 日期提取年月日
js
const regex = /(\d{4})-(\d{2})-(\d{2})/
const str = "2023-09-18"

str.match(regex)

console.log(RegExp.$1) // "2023"
console.log(RegExp.$2) // "09"
console.log(RegExp.$3) // "18"

采用 split 替换

js
const str = "2023-09-18"

console.log(str.split("-"))

是否有必要构造一个复杂的正则

  • 密码匹配问题

要求密码长度 6-12 位,由数字、小写字符和大写字母组成,但必须至少包括 2 种字符

/((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[AZ]))^[0-9A-Za-z]{6,12}$/;

改用多个小正则替换

js
const regex1 = /^[0-9A-Za-z]{6,12}$/;
const regex2 = /^[0-9]{6,12}$/;
const regex3 = /^[A-Z]{6,12}$/;
const regex4 = /^[a-z]{6,12}$/;
function checkPassword (string) {
  if (!regex1.test(string)) return false;
  if (regex2.test(string)) return false;
  if (regex3.test(string)) return false;
  if (regex4.test(string)) return false;
  return true;
}

三、准确性

所谓准确性,就是能匹配预期的目标,并且不匹配非预期的目标

主要的解决思路是: 针对每种情形,分别写出正则,然用分支把它们合并在一起,再提取分支公共部分,就能得到准确的正则

匹配固定号码

js
055188888888
0551-88888888
(0551)88888888
  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}$/
  4. 明确形成关系, 三者形成或的关系, 可以构建分支 /^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}$/
  5. 化简得到 /^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/

上面的正则构建过程略显罗嗦,但是这样做,能保证正则是准确的。

匹配浮点数

js
1.23+1.23-1.23
10+10-10
.2+.2-.2
  1. 符号部分 [+-], 整数部分 \d+, 小数部分 \.\d+, 写出如下正则 /^[+-]?(\d+)?(\.\d+)?$/, 但是会匹配空字符 "", 因为目标字符串不是要求每部分都是可选的
  2. 要匹配 1.23、+1.23、-1.23: /^[+-]?\d+\.\d+$/
  3. 要匹配 10、+10、-10: /^[+-]?\d+$/
  4. 要匹配 .2、+.2、-.2: /^[+-]?\.\d+$/
  5. 提取公共部分得到 /^[+-]?(\d+\.\d+|\d+|\.\d+)$/

四、效率

保证了准确性后,才需要是否要考虑要优化。大多数情形是不需要优化的,除非运行的非常慢。

正则表达式的运行分为如下的阶段:

  1. 编译
  2. 设定起始位置
  3. 尝试匹配
  4. 匹配失败的话, 从下一位开始继续第 3 步
  5. 最终结果: 匹配成功或失败

效率的瓶颈主要出现在上面的第 3 阶段和第 4 阶段

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

匹配 123"abc"456 时 采用 /"[^"]*"/ 代替 /".*"/

使用非捕获型分组

括号的作用之一是,可以捕获分组和分支里的数据。那么就需要内存来保存它们

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

例如,/^[-]?(\d\.\d+|\d+|\.\d+)$/ 可以修改成:/^[-]?(?:\d\.\d+|\d+|\.\d+)$/

独立出确定字符

例如,/a+/ 可以修改成 /aa*/, 确定了字符"a", 在第四步中, 加快判断是否匹配失败, 进而加快移位速度。

提取分支公共部分

比如,/^abc|^def/ 修改成 /^(?:abc|def)/

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

/red|read/ 可以修改成 /rea?d/

此时分支和量词产生的回溯的成本是不一样的。但这样优化后,可读性会降低的。