Skip to content

正则表达式基础

作者:Atom
字数统计:1.9k 字
阅读时长:6 分钟

经常使用正则表达式, 但是有些细节知识总是没搞清楚, 在看了《JS 正则表达式》后一些知识点豁然开朗, 此处只做记录, 不做展开;

如果要匹配任意字符怎么办

可以使用 [\d\D][\w\W][\s\S][^] 中任何的一个。

贪婪匹配与惰性匹配表示法

通过在量词后面加个问号就能实现惰性匹配

 惰性量词贪婪量词
{m,n}?{m,n}
{m,}?{m,}
???
+?+
\*?\*

位置匹配

avatr

ES6 正则的位置表示方式总共有: ^$\b\B(?=)(?!)(?<=)(?<!)

其中\b\B表示什么位置?

\b\B 用法:

\b 是单词边界,具体就是 \w\W 之间的位置,也包括 \w^ 之间的位置,和 \w\$ 之间的位置。

\B\b 的反面的意思,非单词边界。也即\w\w之间的间隙,或\W\W之间的间隙, 例如在字符串中所有位置中,扣掉 \b,剩下的都是 \B 的。

首先,我们知道,\w 是字符组 [0-9a-zA-Z_] 的简写形式,即 \w 是字母数字或者下划线的中任何一个字 符。而 \W 是排除字符组 [^0-9a-za-z_] 的简写形式,即 \W\w 以外的任何一个字符

js
// 替换所有单词边界为#
const res = '[JS] Lesson_01.mp4'.replace(/\b/g, '#')  // [#JS#] #Lesson_01#.#mp4#

// 替换所有非单词边界为@
const res1 = '[JS] Lesson_01.mp4'.replace(/\B/g, '@')  // @[J@S]@ L@e@s@s@o@n@_@0@1.m@p@4

括号匹配

分组后面有量词的话,分组最终捕获到的数据是最后一次的匹配; 反向引用也是一样的;

js
var regex = /(\d)+/
var string = '12345'
console.log(string.match(regex))
// => ["12345", "5", index: 0, input: "12345"]

匹配回溯

一般语言中(包括 JS)使用的正则引擎是 NFA(非确定型有限自动机),导致匹配回溯的类型主要是三种:

  • 贪婪量词

    少用贪婪量词(. + ? *)等

  • 惰性量词

    虽然惰性量词不贪,但也会有回溯的现象。比如正则是 \d{1,3}?\d{1,3}

  • 分支结构

    分支结构,可能前面的子模式会形成了局部匹配,如果接下来表达式整体不匹配时,仍会继续尝试剩下的分 支。

正则表达式的拆分

表达式结构

avaScript 正则表达式中,都有哪些结构呢? 字符字面量、字符组、量词、锚、分组、选择分支、反向引用。 具体含义简要回顾如下:

结构说明
字面量匹配一个具体字符,包括不用转义的和需要转义的。比如 a 匹配字符 "a", 又比如 \n 匹配换行符,又比如 \. 匹配小数点。
字符组匹配一个字符,可以是多种可能之一,比如 [0-9],表示匹配一个数字。 也有 \d 的简写形式。 另外还有反义字符组,表示可以是除了特定字符之外任何一个字符,比如 [^0-9], 表示一个非数字字符,也有 \D 的简写形式。
量词表示一个字符连续出现,比如 a{1,3} 表示 "a" 字符连续出现 3 次。 另外还有常见的简写形式,比如 a+ 表示 "a" 字符连续出现至少一次。
匹配一个位置,而不是字符。比如 ^ 匹配字符串的开头,又比如 \b 匹配单词边界, 又比如 (?=\d) 表示数字前面的位置。
分组用括号表示一个整体,比如 (ab)+,表示 "ab" 两个字符连续出现多次, 也可以使用非捕获分组 (?:ab)+
分支多个子表达式多选一,比如 abc | bcd,表达式匹配"abc" 或者 "bcd" 字符子串。 反向引用,比如 \2,表示引用第 2 个分组。

操作符优先性

操作符描述操作符优先级
转义符\1
括号和方括号(...)(?:...)(?=...)(?!...)[...]2
量词限定符{m}{m,n}{m,}?\*+3
位置和序列^\$\元字符一般字符4
管道符(竖杠)|5

结构注意点

  • 匹配字符串整体问题

  • 量词连缀问题

  • 元字符转义问题

    所有结构里,用到的元字符总结如下: ^\$.\*+?|\/()[]{}=!:- , 当匹配上面的字符本身时,可以一律转义;

    另外,在 string 中,也可以把每个字符转义,当然,转义后的结果仍是本身; 一般的做法是 string 中的 \ 字符也要转义的。

    js
    var string = `"^$.*+?|\\/[]{}=!:-,"` // 当中的 \ 需要多转义一次
    var string2 = `"\^\$\.\*\+\?\|\\\/\[\]\{\}\=\!\:\-\,"` // 也可以把所有字符全部转义, 但是一般没有这个必要
    console.log(string == string2)
    // => true

    以及, 跟字符组相关的元字符有 []^-。因此在会引起歧义的地方进行转义

正则表达式的构建

接下来的内容中我们将通过以下几个角度来控制正则表达式的构建过程:

  • 平衡法则
  • 构建正则前提
  • 准确性
  • 效率

平衡法则

构建正则需要考虑: 匹配预期的字符、不匹配非预期的字符串、可读性和可维护性、效率

构建正则前提

构建之前需要考虑: 是否能使用正则? 是否有必要使用正则? 是否有必要构建一个复杂的正则?

准确性

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

效率

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

编译 => 设定起始位置 => 尝试匹配 =>匹配失败的话,从下一位开始继续第3步=> 匹配成功或失败

在整个过程中, 导致匹配性能问题的主要分为下面几个部分:

  • 回溯
  • 分组
  • 移位速度
  • 搜索范围
  • 分支

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

因为回溯的存在,需要引擎保存多种可能中未尝试过的状态,以便后续回溯时使用。注定要占用一定的内存 例如匹配字符串 123"abc"456 中的 "abc", 不要使用/".*"/, 最好使用/"[^"]*"/

II.使用非捕获型分组

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

III.独立出确定字符

例如,/a+/ 可以修改成 /aa\*/

IV.提取分支公共部分

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

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

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

表达式编程

正则表达式是匹配模式,不管如何使用正则表达式,万变不离其宗,都需要先“匹配”。 有了匹配这一基本操作后,才有其他的操作:验证切分提取替换。 进行任何相关操作,也需要宿主引擎相关 API 的配合使用。当然,在 JavaScript 中,相关 API 也不多。

相关的 API 主要是下面几组:

  • String#search
  • String#split
  • String#match
  • String#replace
  • RegExp#test
  • RegExp#exec