zoukankan      html  css  js  c++  java
  • 关于正则表达式(转)

    魔芋:学习正则时,了解了正则的大部分语法和用法后,通读了这篇文章,了解了正则的实际运行过程。才知道了正则的回溯会失控。

    不过,只有深入原理才能写出更好的正则代码。




    作者:小胡子哥。

    原文地址:http://www.cnblogs.com/hustskyking/p/how-regular-expressions-work.html


    你写的任何一个正则直接量或者 RegExp 都会被浏览器编译为一个原生代码程序,第一次匹配是从头个字符开始,匹配成功时,他会查看是否还有其他的路径没有匹配到,如果有的话,回退到上一次成功匹配的位置,然后重复第二步操作,不过此时开始匹配的位置(lastIndex)是上次成功位置加 1.这样说有点难以理解,下面写了一个 demo,这个 demo 就是实现一个正则表达式的解析引擎,因为逻辑和效果的表现都太复杂了,所以只做了一个简单的演示:

    Reg:
    /H(i|ello), barret/g

    Str:
    Lalala. Hi, barret. Hello, John

    如果要深入了解正则表达式的内部原理,必须先理解匹配过程的一个基础环节——回溯,他是驱动正则的一个基本动力,也是性能消耗、计算消耗的根源。

    二、回溯

    正则表达式中出现最多的是分支和量词,上面的 demo 中可以很清楚的看到 hi 和 hello 这两个分支,当匹配到第一个字符 h 之后,进入 (i|ello) 的分支选择,首先是进入 i 分支,当 i 分支匹配完了之后,再回到分支选择的位置,重新选择分支。简单点说,分支就是 | 操作符带来的多项选择问题,而量词指的是诸如 *, +?, {m,n} 之类的符号,正则表达式必须决定何时尝试匹配更多的字符。下面结合回溯详细说说分支和量词。

    1. 分支

    继续分析上面那个案例。"Lalala. Hi, barret. Hello, John".match(/H(i|ello), barret/g),首先会查找 H 字符,在第九位找到 H 之后,正则子表达式提供了两个选择 (i|ello),程序会先拿到最左边的那个分支,进入分支后,在第十位匹配到了 i,接着匹配下一个字符,下一个字符是逗号,接着刚才的位置又匹配到了这个逗号,然后再匹配下一个,依次类推,直到完整匹配到整个正则的内容,此时程序会在Hi, barret后面做一个标记,表示在这里进行了一次成功的匹配。但程序到此并没有结束,因为后面加了一个全局参数,依然使用这个分支往后匹配,很显然,到了 Hello 的时候,Hi 分支匹配不了了,于是程序会回溯到刚才我们做标记的位置,并进入第二个分支,从做标记的位置重新开始匹配,依次循环。

    只要正则表达式没有尝试完所有的可选项,他就会回溯到最近的决策点(也就是上次匹配成功的位置)。

    2. 量词

    量词这个概念特别简单,只是在匹配过程中有贪婪匹配和懒惰匹配两种模式,结合回溯的概念理解稍微复杂。还是用几个例子来说明。

    1) 贪婪

    str = "AB1111BA111BA";
    reg = /AB[sS]+BA/;
    console.log(str.match(reg));
    

    首先是匹配AB,遇到了 [sS]+,这是贪婪模式的匹配,他会一口吞掉后面所有的字符,也就是如果 reg 的内容为 AB[sS]+,那后面的就不用看了,直接全部匹配,而往后看,正则后面还有B字符,所以他会先回溯到倒数第一个字符,匹配看是否为 B,显然倒数第一个字符不是B,于是他又接着回溯,找到了B字母,找到之后就不继续回溯了,而是往后继续匹配,此刻匹配的是字符A,程序发现紧跟B后的字母确实是A,那此时匹配就结束了。如果没有看明白,可以再读读下面这个图:

    REG: /AB[sS]+BA/
    MATCH: A               匹配第一个字符
           AB              匹配第二个字符
           AB1111BA111BA   [sS]+ 贪婪吞并所有字符
           AB1111BA111BA   回溯,匹配字符B
           AB1111BA111B    找到字符B,继续匹配A
           AB1111BA111BA   找到字符A,匹配完成,停止匹配
    

    2) 懒惰(非贪婪)

    str = "AB1111BA111BA";
    reg = /AB[sS]+?BA/;
    console.log(str.match(reg));
    

    与上面不同的是,reg 中多了一个 ? 号,此时的匹配模式为懒惰模式,也叫做非贪婪匹配。此时的匹配流程是,先匹配AB,遇到[sS]+?,程序尝试跳过并开始匹配后面的字符B,往后查看的时候,发现是数字1,不是要匹配的内容,继续往后匹配,知道遇到字符B,然后匹配A,发现紧接着B后面就有一个A,于是宣布匹配完成,停止程序。

     REG: /AB[sS]+BA/
    MATCH: A               匹配第一个字符
           AB              匹配第二个字符
           AB              [sS]+? 非贪婪跳过并开始匹配B
           AB1             不是B,回溯,继续匹配
           AB11            不是B,回溯,继续匹配
           AB111           不是B,回溯,继续匹配
           AB1111          不是B,回溯,继续匹配
           AB1111B         找到字符B,继续匹配A
           AB1111BA        找到字符A,匹配完成,停止匹配
    

    如果匹配的内容是 AB1111BA,那贪婪和非贪婪方式的正则是等价的,但是内部的匹配原理还是有区别的。为了高效运用正则,必须搞清楚使用正则时会遇到那些性能消耗问题。

    三、逗比的程序
    //去测试下这句代码
    "TTTTTTTT".match(/(T+T+)+K/);
    //然后把前面的T重复次数改成30
    //P.S:小心风扇狂转,CPU暴涨
    

    我们来分析下上面这段代码,上面使用的都是贪婪模式,那么他会这样做:

      REG: (T+T+)+K
    MATCH: ①第一个T+匹配前7个T,第二个T+匹配最后一个T,没找到K,宣布失败,回溯到最开始位置
           ②第一个T+匹配前6个T,第二个T+匹配最后两个T,没找到K,宣布失败,回溯到最开始位置
           ③...
           ... 接着还会考虑(T+T+)+后面的 + 号,接着另一轮的尝试。
           ⑦...
           ...
    

    这段程序并不会智能的去检测字符串中是否存在 K,如果匹配失败,他会选择其他的匹配方式(路径)去匹配,从而造成疯狂的回溯和重新匹配,结果可想而知。这是回溯失控的典型例子。

    四、前瞻和反向引用
    1. 前瞻和引用

    前瞻有两种,一种是负向前瞻,JS中使用 (?!xxx) 来表示,他的作用是对后面要匹配的内容做一个预判断,如果后面的内容是xxx,则此段内容匹配失败,跳过去重新开始匹配。另一种是正向前瞻,(?=xxx),匹配方式和上面相反,还有一个长的类似的是 (?:xxx),这个是匹配xxx,他是非捕获性分组匹配,即匹配的内容不会创建反向引用。具体内容可以去文章开头提到的文档中查看。

    反向引用,这个在 replace 中用的比较多,在 replace 中:

    字符替换文本
    $1、$2、...、$99与 regexp 中的第 1 到第 99 个子表达式相匹配的文本。
    $&与 regexp 相匹配的子串。
    $`位于匹配子串左侧的文本。
    $'位于匹配子串右侧的文本。
    $$直接量符号。

    而在正则表达中,主要就是 1, 2 之类的数字引用。前瞻和反向引用使用恰当可以大大的减少正则对资源的消耗。举个例子来简单说明下这几个东西:

    问题:使用正则匹配过滤后缀名为 .css 和 .js 的文件。

          如:test.wow.js test.wow.css test.js.js等等。

    有人会立马想到使用负向前瞻,即:

    
    //过滤js文件
    /(?!.+.js$).*/.exec("test.wow.js")
    
    //过滤js和css文件
    /(?!.+.js$|.+.css$).*/.exec("test.wow.js")
    /(?!.+.js$|.+.css$).*/.exec("test.wow.html")
    

    但是你自己去测试下,拿到的结果是什么。匹配非js和非css文件可以拿到正确的文件名,但是我们期望这个表达式对js和css文件的匹配结果是null,上面的表达式却做不到。问题是什么,因为(?!xxx)和(?=xxx)都会消耗字符,在做预判断的时候把 .js 和 .css 给消耗了,所以这里我们必须使用非捕获模式。

    /(?:(?!.+.js$|.+.css$).)*/.exec("test.wow.html");
    /(?:(?!.+.js$|.+.css$).)*/.exec("test.wow.js");
    

    我们来分析下这个正则:

    (?:(?!.+.js$|.+.css$).)*
    ---   ----------------  -
     |                |     |   
     +----------------------+
                 ↓    | 
    非捕获,内部只有一个占位字符
                      |
                      ↓
        负向前瞻以.js和.css结尾的字符串
    

    最后一个星号是贪婪匹配,直接吞掉全部字符。

    这里讲的算是有点复杂了,不过在稍复杂的正则中,这些都是很基础的东西了,想在这方面提高的童鞋可以多研究下。

    2. 原子组

    JavaScript的正则算是比较弱的,他没有分组命名、递归、原子组等功能特别强的匹配模式,不过我们可以利用一些组合方式达到自己的目的。上面的例子中,我们实际上用正则实现了一个或和与的功能,上面的例子体现的还不是特别明显,再写个例子来展示下:

    str1 = "我(wo)叫(jiao)李(li)靖(jing)";
    str2 = "李(li)靖(jing)我(wo)叫(jiao)";
    reg = /(?=.*?我)(?=.*?叫)(?=.*?李)(?=.*?靖)/;
    console.log(reg.test(str1)); //true
    console.log(reg.test(str2)); //true
    

    不管怎么打乱顺序,只要string中包含“我”,“是”,“李”,“靖”这四个字,结果都是true。

    类似(?=xxx)1,就相当于一个原子组,原子组的作用就是消除回溯,只要是这种模式匹配过的地方,回溯时都不会到这里和他之前的地方。上面的程序"TTTTTTTT".match(/(T+T+)+K/);可以通过原子组的方式处理:

    "TTTTTTTT".match(/(?=(T+T+))2+K/);
    

    如此便能彻底消除回溯失控问题。

    五、小结

    关于正则的学习,重点是要多练习多实践,并且多尝试用不同的方案去解决一个正则问题,一个很典型的例子,去除字符串首尾的空白,尝试用5-10种不同的正则去测试,并思考哪些方式的效率最高,为什么?通过这一连串的思考可以带动学习的兴趣,提高学习效率~


    **

  • 相关阅读:
    ABAP接口用法
    监听textarea数值变化
    The first step in solving any problem is recognizing there is one.
    Wrinkles should merely indicate where smiles have been.
    God made relatives.Thank God we can choose our friends.
    Home is where your heart is
    ABAP跳转屏幕
    Python 工具包 werkzeug 初探
    atom通过remote ftp同步本地文件到远程主机的方法
    Mongodb学习笔记一
  • 原文地址:https://www.cnblogs.com/moyuling/p/9184401.html
Copyright © 2011-2022 走看看