正则匹配

正则引擎 正则的引擎大致可分为两类:DFA和NFA

  1. DFA (Deterministic finite automaton) 确定型有穷自动机
  2. NFA (Non-deterministic finite automaton) 非确定型有穷自动机,大部分都是NFA
这里的“确定型”指,对于某个确定字符的输入,这台机器的状态会确定地从a跳到b,“非确定型”指,对于某个确定字符的输入,这台机器可能有好几种状态的跳法;这里的“有穷”指,状态是有限的,可以在有限的步数内确定某个字符串是否满足条件;这里的“自动机”指,一旦这台机器的规则设定完成,就可以自行判断了,不要人看。
DFA引擎不需要进行回溯,所以匹配效率一般情况下要高,但是它并不支持捕获组,于是也就不支持反向引用和$这种形式的引用,也不支持环视(Lookaround)、非贪婪模式等一些NFA引擎特有的特性。
字符和位置
  • 如果一个子正则表达式匹配到的是字符,而不是位置,而且会被保存到最终的结果中,那个这个子表达式就是占有字符的,比如/ha/(匹配ha)就是占有字符的;
  • 如果一个子正则匹配的是位置,而不是字符,或者匹配到的内容不保存在结果中(其实也可以看做一个位置),那么这个子表达式是零宽度的,比如/read(?=ing)/(匹配reading,但是只将read放入结果中),其中的(?=ing)就是零宽度的,它本质代表一个位置。
占有字符是互斥的,零宽度是非互斥的。一个字符,同一时间只能由一个子表达式匹配,而一个位置,却可以同时由多个零宽度的子表达式匹配。举个栗子,/aa/是匹配不了a的,这个字符串中的a只能由正则的第一个a字符匹配,而不能同时由第二个a匹配;但是位置是可以多个匹配的,比如/\b\b?=a/是可以匹配a的,虽然正则表达式里有3个零宽度的子表达式,是可以同时匹配位置0的。
修饰符
  • i 忽视大小写。
  • g 全局匹配。
    不启用全局匹配时,match方法返回的数组第一项为第一个匹配内容,后续为各个捕获组
    启用全局匹配时会返回所有匹配内容,但不包括捕获组
  • m 将^$变为匹配单行的开始和结尾。(并非有m时才能多行匹配)
简单元字符
  • ^ 脱字符,表示文本的开始(有m时则为行初)
  • $ 表示文本的结束(有m时则为行末)
一般来说,反斜杠把元字符还原为普通字符,但是有一些普通字符,带反斜杠后反而变成了元字符。
  • \b 匹配一个单词边界(boundary,匹配一个位置,该位置前后不全是\w能描述的字符且不是中文。通常匹配单词和空格之间的位置,包括开头结尾)
  • \B 匹配一个非单词边界
  • \d 匹配一个数字字符(digit)
  • \D 匹配一个非数字字符
  • \s 匹配一个空白字符(space,空格和\f\n\r\t\v的超集)
    • 其中\f是换页符,\n是换行符,\r是回车符,\t是水平制表符,\v是垂直制表符,除空格和\n外均为不可打印字符,因此可以认为匹配[\n| ]
  • \S 匹配一个非空白字符
  • \w 匹配一个字母或者一个数字或者一个下划线(word)
  • \W 匹配一个字母、数字和下划线之外的字符
  • .可以匹配换行符之外的任意单个字符。
    (.|\n)[\b\B][\d\D][\s\S][\w\W]是等价的。
量词 (它重复紧贴在它前面的某个集合。)
  • ? 重复零次或者一次
  • + 重复一次或者多次,也就是至少一次
  • * 重复零次或者多次,也就是任意次数
  • {n} 重复n次
  • {n,} 重复n次或者更多次
  • {n,m} 重复n次到m次
转义 任何在正则表达式中有作用的字符都建议转义,哪怕有些情况下不转义也能正确,比如[]中的圆括号、^符号等。
优先级问题 【正则匹配】优先级从高到低是:
  1. 转义 \
  2. 括号(圆括号和方括号)(), (?:), (?=), []
  3. 字符和位置
  4. 竖线 |
贪婪模式与非贪婪模式
  • 紧贴在量词后的?,开启非贪婪模式
    不带问号的限定符也称匹配优先量词,带问号的限定符也称忽略匹配优先量词。
字符组与分歧
  • []中的字符集合只是所有的可选项,最终它只能匹配一个字符。
    此时$ & @ ()等元字符只视为普通字符,不需要转义。
  • ^ 在字符组中表示取反,不再是文本开始的位置了。
  • - 连字符,匹配范围在它的左边字符和右边字符之间(如[0-9])。只有字母和数字可以用连字符。
分歧 :
[(ab)(cd)]并不会用来匹配字符串“ab”或“cd”,而是匹配a、b、c、d、(、)这6个字符中的任一个,也就是想表达“匹配字符串ab或者cd”这样的需求不能这么做,要这么写ab|cd
捕获组与非捕获组 ()将其中的字符集合打包成一个集合,供量词操作。且其内容可以被捕获。
  • 正则内捕获
    使用\数字的形式,从\1开始,分别对应前面的圆括号捕获的内容 (从左向右,从外向内)。这种捕获的引用也叫反向引用
'hello regexA
hello regex
' .match(/<((A|a)pp)>(hello regex)+<\/\1>\2<\/p>\3<\/p>/);

  • 正则外捕获
    RegExp是构造正则的构造函数。
    RegExp会在我们调用了正则表达式的方法后, 自动将最近一次的结果保存在里面,它的实例属性$数字会显示对应的引用。
const r = /^(\d{4})-(\d{1,2})-(\d{1,2})$/ r.exec('2019-10-08') console.log(RegExp.$1)// 2019 console.log(RegExp.$2)// 10 console.log(RegExp.$3)// 08

另外在replace方法中也支持引用
'hello **regex**'.replace(/\*{2}(.*)\*{2}/, '$1'); // "hello regex"

  • 非捕获组
    只要在圆括号内最前面加上?:标识,即不会被捕获,可以节省性能
捕获命名 (ES2018的新特性)
  • 在捕获组内部最前面加上?,它就被命名了。使用\k语法就可以引用已经命名的捕获组。
'hello regex'.match(/<(?[a-zA-Z]+)>.*<\/\k>/);

零宽断言(必须配合圆括号使用) 零宽断言用于匹配一个位置而非字符,因此断言中的内容不会捕获成为结果
  • 零宽肯定先行断言 : 圆括号内最左边加上?=标识。为紧贴在它前面的规则服务。
'CoffeeScript JavaScript javascript'.match(/\b\w{4}(?=Script\b)\w+/); // ["JavaScript", index: 13, input: "CoffeeScript JavaScript javascript", groups: undefined]

  • 零宽肯定后行断言 : 圆括号内最左边加上?<=标识。
  • 零宽否定先行断言 : 圆括号内最左边加上?!标识。
  • 零宽否定后行断言 : 圆括号内最左边加上?标识。

    推荐阅读