Skip to content
大纲

正则基础

TIP

JS 的正则引擎是 NFA----“非确定型有限自动机”----匹配慢,但编译快

正则的适用场

  1. 适合查找匹配复杂的 substring
  2. 能够找到查找“子串”的规律,例如:Vue1.0 的词法解析,就大量运用了正则

基础属性、方法与标识量

  1. lastIndex:下一个匹配的起始位置
  2. flags:返回正则的标识符
  3. ignoreCase:是否忽略大小写
  4. multiline:对匹配的多行字符串看做多行来匹配,尤其对于位置匹配^$
  5. sticky:下一次匹配一定在 lastIndex 位置开始
  6. global: 下次匹配位置可能在 lastIndex 位置开始,也可能在其后面开始
  7. dotAll:是否匹配新行
  8. unicode:/u 一串 Unicode 代码点
  9. compile() 运行期间重新编译正则表达式
  10. exec() 匹配项的搜索,设置 g 标志位时,多次执行 exec 查找同一字符串,查找从 lastIndex 属性指定的位置开始
    1. index 匹配到的字符位于原始字符串的索引值,
    2. input 原始字符串
    3. groups 捕获组对象,
    4. indices 在设置了 d 标志时存在,一个数组包含子字符串的边界,第一个元素为开始索引,第二表示结束索引
  11. match() 匹配并返回匹配结果,matchAll
  12. replace() 用给定的新子串,替换匹配结果
  13. search() 搜索匹配项,返回在支付串中找到的字符索引
  14. test() 会更新 lastIndex,即使再次查找 lastIndex 也不好被重置
  15. split()

正则字符集匹配

字符含义
\w, \W字母、数字、下划线
\d, \D只匹配拉丁字母的数字 或 非数字
\b, \B单词边界,即字与空格间的位置,一边为\w,一边为非\w;左右两边都为\w与\W之间的位置,\w与^之间的位置,\w与$之间的位置
\s, \S\s 是匹配所有空白符,包括换行,等价于 [ \f\n\r\t\v];\S 非空白符,不包括换行
\p{ Unicode 属性值} 或 \p{Unicode 属性名=Unicode 属性值} 或 \p指定 unicode 的匹配范围
^起始位置
$结束位置
.任何单个字符
*匹配前面的字符零次或多次
+匹配前面的字符一次 或 多次
{n} 或 {m,n}匹配前面的字符 n 次 或 m~n 次
[^XXX]不包括括号中的字符集
[xxxx]匹配括号中的任意字符,字符直接是或的关系
[0-9]表示匹配范围
\f换页符 等价于 \x0c
\n换行符 等价于 \x0a
\r回车符 等价于 \x0d
\t制表符 等价于 \x09
\v垂直制表符 等价于 \x0b
其他\x20 空白符 \x0A(16) \011(8) posix 字符类
修饰符/g 全局,/i 不区分大小写,/m 多行匹配,只影响^和$, /s 让.包含换行,/y sticky,/u unicode

基础知识、基本概念

  1. 捕获:(exp) 将相关匹配存储到一个临时缓冲区中,缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。 reg?<name>exp 匹配 exp, 并将捕获文本放到名称为 name 的组里
  2. 非捕获:(?:exp) 在需要分组,但不捕获时使用
  3. 零宽度断言:位置匹配,不消耗字符,也就是说,在一个匹配发生后,匹配字符之后立即开始下一次匹配的搜索,而不是从包含环视的字符之后开始:
    1. ^ 匹配起始位置
    2. $ 匹配结束位置
    3. \b 单词边界
    4. \B 非单词边界
    5. \W 与\w 直接的位置
    6. 正向预查:作为限制条件使用
      1. 正向环视 exp1(?= p) ---- 找 p 前面的 exp1
      2. 反向环视 (?<=p)exp1 ---- 找 p 后面的 exp1
      3. 否定正向环视 exp1(?!p) ---- 找 exp1 后面不是 p
      4. 否定反向环视 (?<!p)exp1 ---- 前面不是 p 的 exp1
      5. 占有字符:子表达式匹配到的是字符内容,而非位置,且在最终结果中;互斥的,即一个字符同一时间只能由一个子表达式匹配
  4. 引用:"$2","$1" 获取捕获分组的第几个匹配子项;"$0"匹配到的整体
  5. 反向引用:使用前面匹配的内容作为正则表达式的一部分 \1
  6. 贪婪量词 *, + 「先下手为强!深度优先搜索」
  7. 惰性量词 ?, *?, +? 「不贪、很知足,但有时为了整体匹配成功,也会进行回溯,再多塞点」
  8. 查找机制:从左到右,可使用贪婪 (*+) 或非贪婪 (*?+?) 匹配,子表达式匹配成功,继续向右,否则进行回溯过程,再尝试匹配
  9. 回溯机制
    1. 确定匹配成功的部分
    2. 正在匹配的部分不成功,即进行回退,默认先回退一步,成功则继续,不成功则继续回退
  10. 分支 |

常用匹配集合

js
// 匹配中文字符的正则表达式:
const han = /[\u4e00-\u9fa5]/
// 匹配双字节字符 (包括汉字在内):
const hanChar = /[^\x00-\xff]/
js
// 零宽度断言例子
/^(?=D)[E-F]+$/

// 1. 位置开始位置 0;
// 2. 零宽度子表达式之间不互斥,即同一个位置可以由多个零宽度子表达式匹配,所以环视从位置 0 开始尝试匹配,右侧为字符 D;才为匹配成功;
// 3. 因为上个匹配只进行匹配位置,结果不保存到最终结果,如果上个匹配成功的位置为 0,那么下面也是从 0 位置开始匹配。

// 其他例子
/^(?:\/(?=$))?$/i, /^\/dialog(?:\/(?=$))?$/i
/\((?!\?)/g
/tom(?=(and))\1jerry/
/(?=(.+.+))\1+X/
/\d{1,3}(?=(\d{3})+$)/g
/^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{4,8}$/ //只是限定了前面或者后面匹配的规则,而不占用匹配的字符。
/(?<=\s)\d+(?=\s)/ //匹配两边是空白符的数字,不包括空白符

正则实战

js
// 匹配模版中的事件
onRE = /^@|^v-on:/
//
dirRE = process.env.VBIND_PROP_SHORTHAND
  ? /^v-|^@|^:|^\.|^#/
  : /^v-|^@|^:|^#/
// 小括号表达式
stripParensRE = /^\(|\)$/g
// 中括号表达式
dynamicArgRE = /^\[.*\]$/
// 小括号表达式
argRE = /:(.*)$/
bindRE = /^:|^\.|^v-bind:/
propBindRE = /^\./
modifierRE = /\.[^.\]]+(?=[^\]]*$)/g
slotRE = /^v-slot(:|$)|^#/
lineBreakRE = /[\r\n]/
whitespaceRE = /[ \f\t\r\n]+/g
invalidAttributeRE = /[\s"'<>\/=]/
// 这个是否可以优化?前面用到懒惰模式,后面用贪婪模式?这里的的优点?
forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/
forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/

// text 部分的解析
defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g

// 构建出需要的正则
const buildRegex = cached((delimiters) => {
  const open = delimiters[0].replace(regexEscapeRE, '\\$&')
  const close = delimiters[1].replace(regexEscapeRE, '\\$&')
  return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
})

// filter 部分的
validDivisionCharRE = /[\w).+\-_$\]]/

// 高级部分
// 匹配函数定义
fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/
// 匹配函数调用部分
fnInvokeRE = /\([^)]*?\);*$/
// 表达式属性访问路径匹配
simplePathRE =
  /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/

正则特殊情况与注意事项

  1. 对捕获的引用:$1, $2... 或 RegExp.$2 != express,其注意事项
    • 如果表示不是引用,而是表示\和 0 时,需要使用 (?:\1)0 或\1(?:0)
    • 引用不存在的分组:如\2,即是对 2 的转义
    • 分组后面有量词时,则捕获的是最后一次的匹配,如:/(\d)+ \1/.test("12345 5")
  2. 回溯在不同情况下的表现情况:
    • 贪婪模式下:模糊度过高,直接匹配到比较考结束位置了,下一个匹配项无法匹配,则需要向前回溯,如下面横向对比的例子-----使字符组匹配尽量精确,不要范围过大
    • 惰性模式下:匹配过少,后面的匹配不了,前一个匹配要再想后匹配,直到后一个匹配成功或者整体匹配失败
    • 分支结构:多条匹配道路,第一个使整体匹配成功的道路,匹配不成功也要进行多次回溯
  3. 构建正则时考虑的点
    • 准确性,匹配预期的字符串
    • 准确性,不匹配非预期的字符串
    • 可读性和可维护性
    • 提取正则的公共部分,使用反向引用,不断的提取优化
    • 效率
    • 是否需要复杂的正则,是否可以拆分成几个,分段匹配
  4. 常用的字符组:[\u4e00-\u9fa5]
js
// 基本款
/^\/\*[^/]\*\*\/$/  // 多行注释
/^\/\/[^\n]\*/  // 单行注释
/^\x20\*\/\/[^0-9\n]\*/
/?!^a/ //不以 a 开始
/(?=[a-zA-Z]\d|\d[a-zA-Z]|[a-z][A-Z]|[A-Z][a-z])[\da-zA-Z]{6,12}/g

特殊属性的意义

  • input, RegExp.$_ 整个待匹配字符串
  • leftContext, RegExp['$`'] 上次匹配之前的子字符串
  • lastMatch, RegExp['$&'] 最后匹配的字符串
  • multiline, RegExp['$\*'] 是否所有表达式都使用多行模式的布尔值
  • lastParen, RegExp['$+'] 最后匹配的分组

回溯与优先匹配

  1. 回溯:正则表达式的强大功能中心,它使得表达式强大、灵活、可以匹配非常复杂的模式。同时这种强大需要付出一定的代码;回溯是影响表达式引擎性能的单个最重要的因素。
  2. 优先匹配:
    • 如果匹配到一个位置,需要做尝试匹配或者跳过匹配这样的选择的时候,对于量词匹配,引擎会优先作出进行尝试行为,而忽略量词优先的时候则进行跳过尝试匹配。如 ab?c 匹配 abc 时,b 存在备选状态,如果匹配成功则放弃备选状态;如果匹配的 ac 则先进行匹配尝试,另一种状态放入到备选状态,如果尝试匹配失败,则进行回溯;
    • 放弃量词优先:ab??c,这时先放弃量词优先,跳过了的匹配,先匹配后面的。
  3. 线性比较,非可先限定符不会进行回溯,如{2}, 如果不包含可选限定符或替换构造,则正则能近线性时间运行。
  4. 使用可选限定或替换构造的回溯
  5. 嵌套的可靠限定符的回溯
  6. 控制回溯:非回溯子表达式,后行断言,先行断言
  7. 多选结构,尽量让匹配成功可能性大的情况放在前边,多选状态在每个位置多出多个备选状态,以便回溯 a|b|c|d

优化策略

  1. 不需要捕获的地方使用 (?:expression) 。
  2. 如果括号是非必须的,请不要加括号。
  3. 不要滥用字符数组,比如[.],请直接用. 。
  4. 使用合适的位置匹配,如^ $ ,这会加速定位。
  5. 从两次中提取必须元素,如:x+写成 xx*,a{2,4}写成 aa{0,2}。
  6. 提取多选结构开头的相同字符,如 the|this 改成 th(?:e|is)。(如果你的正则引擎不支持这么使用就改成 th(e|is));尤其是锚点,一定要独立出来,这样很多正则编译器会根据锚点进行特别的优化:^123|^abc 改成^(?:123|abc)。同样的$也尽量独立出来。
  7. 多选结构后边的一个表达式放入多选结构内,这样能够在匹配任何一个多选结构的时候在不退出多选结构的状态下查看后一匹配,匹配失败的更快。这种优化需要谨慎使用。
  8. 忽略优先匹配和优先匹配需要你视情况而定。如果你不确定,请使用匹配优先,它的速度是比忽略优先快的。
  9. 拆分较大正则表达式成一个个小的正则表达式,这是非常有利于提高效率的。
  10. 模拟锚点,使用合适的环视结构来预测合适的开始匹配位置,如匹配十二个月份,可以先预查首字符是否匹配:(?=JFMASOND)(?:Jan|Feb|...|Dec)。这种优化请根据实际情况使用,有时候环视结构开销可能更大。
  11. 很多情况下使用固化分组和占有优先量词能够极大提高速度。
  12. 避免像 (this|that)*这样的几乎无尽的匹配。上边提到的 (...+)*也类似。
  13. 如果能简单的匹配大幅缩短目标字符串,可以进行多次正则匹配,经过实践十分有效。

性能提升

  • 优化尝试(比较)次数与回溯次数
  • 减少回溯次数【减少循环查找同一个字符次数】
  • 测试与优化工具:regexbuddy
  • 使用正确的边界匹配器(^、$、\b、\B 等),限定搜索字符串位置
  • 使用具体的元字符、字符类(\d、\w、\s 等) ,[^]少用”.”字符
  • 使用正确的量词(+、*、?、{n,m}),如果能够限定长度,匹配最佳
  • 使用非捕获组、原子组,减少没有必要的字匹配捕获用 (?😃
  • 进行分组匹配 | + g 模式
  • 常用优化手段:
    • 精确字符组匹配范围
    • 当不需要分组或反向引用时,使用非捕获型分组
    • 独立出确定字符,加快判断是否匹配失败,进而加快移位的速度
    • 提取分支公共部分,建设分支个数

TIP

使用 search, test, match, exec, split, replace 进行正则验证,其中 search 与 match 会把字符串参数转换成正则,所以要加转义

正则例子

javascript
/<div[^>]+>[^<]*(?=<p)[^>]+>[^<]*(?=<\/p>)/  //找到 div 内的 p 标签
  /^([01][0-9]|[2][0-3]):[0-5][0-9]$/  // 时:分
  /^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[1-5][0-9])$/  //时:分:秒
  /^[0-9]{4}-(0[1-9]|[1][0-2])-(0[1-9]|[12][0-9]|3[01])$/ 日期
  /[^\\:*<>|"?\r\n/]/ //不包含特殊字符
  /([^\\:*<>|"?\r\n/]+\\)*/
  /([^\\:*<>|"?\r\n/]+)?/
  "12345678".replace(/(?=(\d{3})+$)/g, ","); //后面跟随三个数字,没有排除开始位
  "123456789".replace(/(?!^)(?=(\d{3})+$)/g, ",");
  "12345678 123456789".replace(/(?!\b)(?=(\d{3})+\b)/g, ","); //用空格时的数字
  /(?<=\d)(?=(\d{3})+$)/g;
  Num.toFixed(2).replace(/\B(?=(\d{3})+\b)/, ",").replace(/^/,"$$ ");
  IPv4 /^((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])$/
  /(?=.*[0-9])^[0-9A-Za-z]{6,12}$/ //必须包含一个字符(数字) + 密码长度 6-12 位数字或字母
  /(?=.*[0-9])(?=.*[a-z])^[0-9A-Za-z]{6,12}$/ //必须包含小写字母与数字 6-12 位数字或字母
  /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/  //密码长度 6-12 位数字或字母,即 不能全是数字 或 不能全是大写或小写字母

横向例子对比:

javascript
/id=".*"/  //贪婪模式,会持续匹配到最后一个“结束

/id=".*?"/  //惰性匹配,存在回溯次数过多的问题

/id="[^"]*"/  //用否定字符组匹配

优化的例子:缩短了引擎从开始工作到反馈匹配结果(成功/失败)的时间

javascript
/^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}$/

/^(0\d{2,3}|0\d{2,3}-|\(0\d{2,3}\))[1-9]\d{6,7}$/ //提取公共部分

/^(0\d{2,3}-?|\(0\d{2,3}\))[1-9]\d{6,7}$/ //优化:

// 实数
/^[+-]?(\d+\.\d+|\d+|\.\d+)$/
/^([+-])?(\d+\.\d+|\d+|\.\d+)$/
/^[+-]?(\d+)?(\.)?\d+$/
  1. 写个函数,判断一个字符串是否为手机靓号,手机靓号条件:有 3 个连续相同的数字如 '111' 或者 有 4 个依次递增 1 的数字 '1234'【编程】

  2. 疑问:[\d\D]表示的范围 与[\s\S] 整体表示的范围是否一致?

  3. /(?=.*[0-9])^/两个锚点在一起,其的作用?

一段简单的 Vue1.0 的 text 解析的代码

正则的综合运用

js
export function parseText(text) {
  // ...
  var tokens = []
  var lastIndex = (tagRE.lastIndex = 0)
  var match, index, html, value, first, oneTime
  while ((match = tagRE.exec(text))) {
    index = match.index
    if (index > lastIndex) {
      tokens.push({
        value: text.slice(lastIndex, index)
      })
    }
    html = htmlRE.test(match[0])
    value = html ? match[1] : match[2]
    first = value.charCodeAt(0)
    oneTime = first === 42 // *
    value = oneTime ? value.slice(1) : value
    tokens.push({
      tag: true,
      value: value.trim(),
      html: html,
      oneTime: oneTime
    })
    lastIndex = index + match[0].length
  }
  if (lastIndex < text.length) {
    tokens.push({
      value: text.slice(lastIndex)
    })
  }
  cache.put(text, tokens)
  return tokens
}