正则总结

333 阅读9分钟

==============基础==================

  • 原理

  1. 正则引擎:使正则能执行,同JS引擎

    可以理解为根据你的正则表达式用算法去模拟一台机器,这台机器有很多状态,通过读取待测的字符串,在这些状态间跳来跳去,如果最后停在了“终结状态”(Happy Ending),那么就Say I Do,否则Say You Are a Good Man。如此将一个正则表达式转换为一个可在有限的步数中计算出结果的机器,那么就实现了引擎。

  2. 分类:DFA(Deterministic finite automaton)确定型有穷自动机 (不支持捕获,反向引用和$)
              NFA(Non-deterministic finite automaton)非确定型有穷自动机 (大部分都是NFA)

    “确定型”指,对于某个确定字符的输入,这台机器的状态会确定地从a跳到b;
    “非确定型”指,对于某个确定字符的输入,这台机器可能有好几种状态的跳法;
    “有穷”指,状态是有限的,可以在有限的步数内确定某个字符串是被接受还是发好人卡的;
    “自动机”,可以理解为,一旦这台机器的规则设定完成,就可以自行判断了,不要人看。

  3. 占有字符和零宽度

    3.1 正则眼中的字符串——n个字符,n+1个位置

    3.2 占有字符

          如果一个子正则表达式匹配到的是字符,而不是位置,而且会被保存到最终的结果中,那个这个子表达式就是占有字符的.
          /ha/(匹配ha)就是占有字符的;

    3.3 零宽度(定位符和零宽断言)

          如果一个子正则匹配的是位置,而不是字符,或者匹配到的内容不保存在结果中(其实也可以看做一个位置),那么这个子表达式是零宽度的.
          定位符:^,$,\b
          零宽断言:如/read(?=ing)/(匹配reading,但是只将read放入结果中),其中的(?=ing)就是零宽度的,它本质代表一个位置。

    3.4 区别

          占有字符是互斥的,零宽度是非互斥的。

    也就是一个字符,同一时间只能由一个子表达式匹配,而一个位置,却可以同时由多个零宽度的子表达式匹配。
    举个栗子,比如/aa/是匹配不了a的,这个字符串中的a只能由正则的第一个a字符匹配,而不能同时由第二个a匹配;但是位置是可以多个匹配的,比如/\b\ba/是可以匹配a的,虽然正则表达式里有2个表示单词开头位置的\b元字符,这两个\b是可以同时匹配位置0(在这个例子中)的。

          我们说字符和位置是面向字符串说的,而说占有字符和零宽度是面向正则说的。

  4. 控制权和传动

    控制权是指哪一个正则子表达式(可能为一个普通字符、元字符或元字符序列组成)在匹配字符串,那么控制权就在哪。

    传动是指正则引擎的一种机制,传动装置将定位正则从字符串的哪里开始匹配。

    正则表达式当开始匹配的时候,一般是由一个子表达式获取控制权,从字符串中的某一个位置开始尝试匹配,一个子表达式开始尝试匹配的位置,是从前一子表达匹配成功的结束位置开始的

    举一个栗子,read(?=ing)ing\sbook匹配reading book,我们把这个正则看成5个子表达式read、(?=ing)、ing、\s、book,当然你也可以吧read看做4个单独字符的子表达式,只是我们这里为了方便这么看待。
    read从位置0开始匹配到位置4,
    后面的(?=ing)继续从位置4开始匹配,发现位置4后面确实是ing,于是断言匹配成功,也就是整一个(?=ing)就是匹配了位置4这一个位置而已(这里更能理解什么是零宽了吧),
    然后后面的ing再从位置4开始匹配到位置7,然后\s再从位置7匹配到位置8,最后的book从位置8匹配到位置12,整一个匹配完成

  • 语法

  1. 要用某类常见字符——简单元字符

    元字符包括简单的如\d、\w 和 复杂元字符:{n,m}、(?!exp)等

     .  匹配除了换行符以外的任意字符,也即是[^\n],如果要包含任意字符,可使用(.|\n)
     \w 匹配任意字母、数字或者下划线,等价于[a-zA-Z0-9_]>
     \s 匹配任意空白符,包含换页符\f、换行符\n、回车符\r、水平制表符\t、垂直制表符\v
     \d 匹配数字
     \un匹配n,这里的n是一个有4个十六进制数字表示的Unicode字符
        比如\u597d表示中文字符“好”,超过\uffff编号的字符用ES6的u修饰符你。
    
  2. 要表示出现次数(重复)——限定符

     a*表示字符a连续出现次数 >= 0 次
     a+表示字符a连续出现次数 >= 1 次
     a?表示字符a出现次数 0 或 1 次
     a{5}表示字符a连续出现次数 5 次
     a{5,}表示字符a连续出现次数 >= 5次
     a{5,10}表示字符a连续出现次数为 5到10次 ,包括5和10
    
  3. 匹配位置——定位符和零宽断言

    匹配某个位置的表达式都是零宽的,这是主要包含两部分,一是定位符,匹配一个特定位置,二是零宽断言,匹配一个要满足某要求的位置。

    定位符有以下几个常用的:

     \b 匹配单词边界位置,准确的描述是它匹配一个位置,这个位置前后不全是\w能描述的字符
        所以像\u597d\babc是可以匹配“好abc”的。
     ^  匹配字符串开始位置,也就是位置0
        如果设置了 RegExp 对象的 Multiline 属性,^ 也匹配 '\n' 或 '\r' 之后的位置
     \$ 匹配字符串结束位置
        如果设置了RegExp 对象的 Multiline 属性,$ 也匹配 '\n' 或 '\r' 之前的位置
    

    零宽断言(JS支持的)有以下两个:

     (?=exp)匹配一个位置,这个位置的右边能匹配表达式exp,
     注意这个表达式仅仅匹配一个位置,只是它对于这个位置的右边有要求,而右边的东西是不会被放进结果的,
     比如用read(?=ing)去匹配“reading”,结果是“read”,而“ing”是不会放进结果的
     (?!exp)匹配一个位置,这个位置的右边不能匹配表达式exp
     (?<=exp)匹配一个位置,这个位置的左边能匹配表达式exp
     (?<!exp)匹配一个位置,这个位置的左边不能匹配表达式exp
    
  4. 想表达“或”的意思——字符簇和分歧

    字符簇表示(单个字符或):

    [abc]表示a、b、c这3个字符中的任意一个,如果字母或者数字是连续的,那么可以用-连起来表示,[b-f]代表从b到f这么多字符中任选一个
    [(ab)(cd)]并不会用来匹配字符串“ab”或“cd”,而是匹配a、b、c、d、(、)这6个字符中的任一个

    分歧表示(表达式级):

    ab|cd会匹配字符串“ab”或者“cd”
    会短路,回想下编程语言中逻辑或的短路,所以用(ab|abc)去匹配字符串“abc”,结果会是“ab”,因为竖线左边的已经满足了,就用左边的匹配结果代表整个正则的结果

  5. 想表达“非”的意思——反义

    \W、\D、\S、\B 用大写字母的这几个元字符表示就是对应小写字母匹配内容的反义,这几个依次匹配“除了字母、数字、下划线外的字符”、“非数字字符”、“非空白符”、“非单词边界位置”

    [^aeiou]表示除了a、e、i、o、u外的任一字符,在方括号中且出现在开头位置的^表示排除,如果^在方括号中不出现在开头位置,那么它仅仅代表^字符本身

  6. 整体看待和捕获——分组和后向引用

    圆括号用来分的,括在一对括号里的就是一个分组。

    括号的第一个作用,将括起来的分组当做一个整体看待,所以你可以像对待字符重复一样在一个分组后面加限定符,比如(ABC){5}。

    分组匹配到的内容也就是这个分组捕获到的内容,从左往右,以左括号为标志,每个分组会自动拥有一个从1开始的编号,编号0的分组对应整个正则表达式,JS不支持捕获组显示命名。

    括号的第二个作用,分组捕获到的内容,可以在之后通过\分组编号的形式进行后向引用。比如(ab|cd)123\1可以匹配“ab123ab”或者“cd123cd”,但是不能匹配“ab123cd”或“cd123ab”,这里有一对括号,也是第一对括号,所以编号为捕获组1,然后在正则中通过\1去引用了捕获组1的捕获的内容,这叫后向引用。

    括号的第三个作用,改变优先级,比如abc|de和(abc|d)e表达的完全不是一个意思。

  7. 转义

    任何在正则表达式中有作用的字符都建议转义,哪怕有些情况下不转义也能正确,比如[]中的圆括号、^符号等。

  8. 优先级问题

    优先级从高到低是:

    转义 \
    括号(圆括号和方括号)(), (?:), (?=), []
    字符和位置
    竖线 |

  9. 贪婪和非贪婪

    在限定符中,除了{n}确切表示重复几次,其余的都是一个有下限的范围。

    在默认的模式(贪婪)下,会尽可能多的匹配内容。比如用ab*去匹配字符串“abbb”,结果是“abbb”。

    而通过在限定符后面加问号?可以进行非贪婪匹配,会尽可能少地匹配。用ab*?去匹配“abbb”,结果会是“a”。

    不带问号的限定符也称匹配优先量词,带问号的限定符也称忽略匹配优先量词。

  10. 修饰符(匹配选项)

    加g修饰符:表示全局匹配,模式将被应用到所有字符串,而不是在发现第一个匹配项时停止
    加i修饰符:表示不区分大小写
    加m修饰符:表示多行模式,会改变^和$的行为,上文已述

  • ES5 RegExp

    JS中的正则由引用类型RegExp表示,下面主要就RegExp类型的创建、两个主要方法和构造函数属性来展开,然后会提及String类型上的模式匹配,最后会简单罗列JS中正则的一些局限。
  1. 创建正则表达式

    //字面量的方式创建
    var exp = /pattern/flags; 
    //比如
    var pattern=/\b[aeiou][a-z]+\b/gi; 
    //构造函数创建
    var pattern=new RegExp("\\b[aeiou][a-z]+\\b","gi");
    

    用构造函数创建正则,可能会导致对一些字符的双重转义,在上面的例子中,构造函数中第一个参数必须传入字符串(ES6可以传字面量),所以字符\ 会被转义成\,因此字面量的\b会变成字符串中的\b,这样很容易出错。

  2. RegExp上用来匹配提取的方法——exec()

    var matches=pattern.exec(str);
    

    接受一个参数:源字符串
    返回:结果数组,在没有匹配项的情况下返回null
    结果数组包含两个额外属性,index表示匹配项在字符串中的位置,input表示源字符串,
    结果数组matches第一项即matches[0]表示匹配整个正则表达式匹配的字符串,matches[n]表示于模式中第n个捕获组匹配的字符串。

    要注意的是,第一,exec()永远只返回一个匹配项(指匹配整个正则的),
    第二,如果设置了g修饰符,每次调用exec()会在字符串中继续查找新匹配项,

    不设置g修饰符,对一个字符串每次调用exec()永远只返回第一个匹配项。
    所以如果要匹配一个字符串中的所有需要匹配的地方,那么可以设置g修饰符,然后通过循环不断调用exec方法。

    //匹配所有ing结尾的单词
    var str="Reading and Writing"; 
    var pattern=/\b([a-zA-Z]+)ing\b/g; 
    var matches; 
    while(matches=pattern.exec(str)){ 
     console.log(matches.index +' '+ matches[0] + ' ' + matches[1]);
    }
    //循环2次输出
    //0 Reading Read
    //12 Writing Writ
    
  3. RegExp上用来测试匹配成功与否的方法——test()

    var result=pattern.test(str);
    

    接受一个参数:源字符串
    返回:找到匹配项,返回true,没找到返回false

  4. RegExp构造函数属性

    RegExp构造函数包含一些属性,适用于作用域中的所有正则表达式,并且基于所执行的最近一次正则表达式操作而变化。

     RegExp.input或RegExp["$_"]:最近一次要匹配的字符串
     RegExp.lastMatch或RegExp["$&"]:最近一次匹配项
     RegExp.lastParen或RegExp["$+"]:最近一次匹配的捕获组
     RegExp.leftContext或RegExp["$`"]:input字符串中lastMatch之前的文本
     RegExp.rightContext或RegExp["$'"]:input字符串中lastMatch之后的文本
     RegExp["$n"]:表示第n个捕获组的内容,n取1-9
    
  5. String类型上的模式匹配方法

    上面提到的exec和test都是在RegExp实例上的方法,调用主体是一个正则表达式,而以字符串为主体调用模式匹配也是最为常用的。

    5.1 匹配捕获的match方法

    在字符串上调用match方法,本质上和在正则上调用exec相同。

    加g修饰符,直接返回一个数组

    5.2 返回索引的search方法

    接受的参数和match方法相同,要么是一个正则表达式,要么是一个RegExp对象。

    //下面两个控制台输出是一样的,都是5
    var str="I am reading."; 
    var pattern=/\b([a-zA-Z]+)ing\b/g; 
     
    var matches=pattern.exec(str); 
    console.log(matches.index);
     
    var pos=str.search(pattern); 
    console.log(pos);
    

    5.3 查找并替换的replace方法

    var result=str.replace(RegExp or String, String or Function);
    

    第一个参数(查找):RegExp对象或者是一个字符串
    第二个参数(替换内容):一个字符串或者是一个函数
    返回:替换后的结果字符串,不会改变原来的字符串

    第一个参数是字符串,只会替换第一个子字符串

    第一个参数是正则,指定g修饰符,则会替换所有匹配正则的地方,否则只替换第一处

    第二个参数是字符串

    第二个参数是特殊的字符序列,将正则表达式操作的值插进入:

     $n:匹配第n个捕获组的内容,n取0-9
     $nn:匹配第nn个捕获组内容,nn取01-99
     $`:匹配子字符串之后的字符串
     $':匹配子字符串之前的字符串
     $&:匹配整个模式得字符串
     ?:表示$符号本身
    

    第二个参数是一个函数
    这个函数要返回一个字符串,表示要替换掉的匹配项

    在只有一个匹配项的情况下,会传递3个参数给这个函数:模式的匹配项、匹配项在字符串中的位置、原始字符串

    在有多个捕获组的情况下,传递的参数是模式匹配项、第一个捕获组、第二个、第三个...最后两个参数是模式的匹配项在字符串位置、原始字符串

    5.4 分隔字符串的split

    基于指定的分隔符将一个字符串分割成多个子字符串,将结果放入一个数组

    第一个参数可以是RegExp对象或者是一个字符串(不会被转为正则)
    第二个参数可选指定数组大小,确保数组不会超过既定大小。

  • ES6 RegExp

  1. 构造函数可以传正则字面量

    var pattern = new RegExp(/\d/,"gim");
    
  2. u修饰符

    // \uD83D\uDC2A是一个4字节的UTF-16编码,代表一个字符
    /^\uD83D/u.test('\uD83D\uDC2A')
    // false,加了u可以正确处理
    /^\uD83D/.test('\uD83D\uDC2A')
    // true,不加u,当做两个unicode字符处理
    

    加了u修饰符,会改变一些正则的行为:

    原本只能匹配不大于\uFFFF的字符,加了u修饰符可以匹配任何Unicode字符
    Unicode字符新表示法\u{码点}必须在加了u修饰符后才是有效的
    使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的Unicode字符
    使一些反义元字符对于大于\uFFFF的字符也生效

  3. y修饰符

    y修饰符的作用与g修饰符类似,也是全局匹配,开始从位置0开始,后一次匹配都从上一次匹配成功的下一个位置开始。

    不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始。

    所以/a/y去匹配"ba"会匹配失败,因为y修饰符要求,在剩余位置第一个位置(这里是位置0)开始就要匹配。

    ES6对正则的加强,查看

===============应用====================

  1. 基本匹配

    正则表达式:easy
    源字符串:So easy

    匹配过程:
    首先由正则表达式字符e取得控制权,从字符串的位置0开始匹配,遇到字符串字符‘S',匹配失败,
    然后正则引擎向前传动,从位置1开始尝试,遇到字符串字符‘o',匹配失败,
    继续传动,后面的空格自然也失败,于是从位置3开始尝试匹配,成功匹配字符串字符‘e',
    控制权交给正则表达式子表达式(这里也是一个字符)a,尝试从上次匹配成功的结束位置4开始匹配,成功匹配字符串字符‘a',
    后面一直如此匹配到‘y',然后匹配完成,匹配结果为easy。

  2. 零宽匹配

    正则:^(?=[aeiou])[a-z]+$
    源字符串:apple

    匹配过程:
    首先正则的^(表示字符串开始的位置)获取控制权,从位置0开始匹配,匹配成功,
    控制权交给(?=[aeiou]),这个子表达式要求该位置右边必须是元音小写字母中的一个,
    零宽子表达式相互间不互斥,所以从位置0开始尝试匹配,右侧是字符串的‘a',符合因此匹配成功,所以(?=[aeiou])匹配此处的位置0匹配成功,
    控制权交给[a-z]+,从位置0开始匹配,字符串‘apple'中的每个字符都匹配成功,匹配到字符串末尾,
    控制权交回正则的$,尝试匹配字符串结束位置,成功,至此,整个匹配完成。

  3. $n,表示()内捕获内容

    name = "Doe, John";
    name.replace(/(\w+)\s*, \s*(\w+)/, "$2 $1");
    "John Doe"
    
    name = '"a", "b"';
    name.replace(/"([^"]*)"/g, "'$1'");
    "'a' 'b'"