进阶正则表达式
本文同步自我的博客园:http://www.cnblogs.com/hustskyking/ 关于正则表达式,网上可以搜到一大片文章,我之前也搜集了一些资料,并做了排版整理,可以看这篇文章http://www.cnblogs.com/hustskyking/archive/2013/06/04/RegExp.html,作为基础入门讲解,这篇文章说的十分到位。 记得最开始学习正则,是使用 php 做一个爬虫程序。为了获取指定的信息,必须用一定的方式把有规律的数据匹配出来,而正则是首选。下面是当时写的爬虫程序的一个代码片段: $regdata = "/<font size="3">((?<bf>[^<]*)<br />){0,1}⊙(?<bs>.{12})S*s/"; //获取页面 $html = file_get_contents('http://www.qnwz.cn/html/daodu/201107/282277.html'); $html = iconv("GBK","UTF-8",$html); if ($html == '') { die("<hr />出错:【错】无法打开《青年文摘》页面<hr />"); } //匹配页面信息 preg_match_all($regdata,$html,$mdata); print_r($mdata); 当时写代码还真是欢乐多,什么都不懂,什么都是新知识,学起来津津有味。我觉得学习知识一定要把握最基本的原理,先把一个知识的大概轮廓搞清楚,然后学习怎么去使用他,完了就是深入学习,了解底层基础实现。很多人解决问题都是靠经验,这个当然很重要,但如果我们弄懂了一项技术最底层的实现,完全可以靠自己的推断分析出问题的根源。我对一些公司的招聘要求特别不满,说什么要三年五年Javascript编程经验云云,经验当然和时间成正相关,但是对于那些没有三年五年工作经验却照样能够解决实际的人呢?算是小小的吐槽吧,下面进入正题。 一、正则表达式的工作机制画了一个草图,简单的说明了下正则表达式的工作原理。 +--------+ | 编译 | +--------+ | ↓ +----------------+ | 设置开始位置 |←---------+ +----------------+ ↑ | | ↓ 其 | +----------------+ 他 | | 匹配 & 回溯 | 路 | +----------------+ 径 | | | ↓ | +----------------+ | | 成功 or 失败 |---------→+ +----------------+ 你写的任何一个正则直接量或者 RegExp 都会被浏览器编译为一个原生代码程序,第一次匹配是从头个字符开始,匹配成功时,他会查看是否还有其他的路径没有匹配到,如果有的话,回退到上一次成功匹配的位置,然后重复第二步操作,不过此时开始匹配的位置(lastIndex)是上次成功位置加 1.这样说有点难以理解,下面写了一个 demo,这个 demo 就是实现一个正则表达式的解析引擎,因为逻辑和效果的表现都太复杂了,所以只做了一个简单的演示: http://qianduannotes.duapp.com/demo/regexp/index.html 如果要深入了解正则表达式的内部原理,必须先理解匹配过程的一个基础环节——回溯,他是驱动正则的一个基本动力,也是性能消耗、计算消耗的根源。 二、回溯正则表达式中出现最多的是分支和量词,上面的 demo 中可以很清楚的看到 hi 和 hello 这两个分支,当匹配到第一个字符 h 之后,进入 (i|ello) 的分支选择,首先是进入 i 分支,当 i 分支匹配完了之后,再回到分支选择的位置,重新选择分支。简单点说,分支就是 1. 分支继续分析上面那个案例。 只要正则表达式没有尝试完所有的可选项,他就会回溯到最近的决策点(也就是上次匹配成功的位置)。 2. 量词量词这个概念特别简单,只是在匹配过程中有贪婪匹配和懒惰匹配两种模式,结合回溯的概念理解稍微复杂。还是用几个例子来说明。 1) 贪婪 str = "AB1111BA111BA"; reg = /AB[sS]+BA/; console.log(str.match(reg)); 首先是匹配AB,遇到了 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中使用 反向引用,这个在 replace 中用的比较多,在 replace 中:
而在正则表达中,主要就是 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+))2+K/); 如此便能彻底消除回溯失控问题。 五、小结关于正则的学习,重点是要多练习多实践,并且多尝试用不同的方案去解决一个正则问题,一个很典型的例子,去除字符串首尾的空白,尝试用5-10种不同的正则去测试,并思考哪些方式的效率最高,为什么?通过这一连串的思考可以带动你学习的兴趣,也会让你成长的比较快~ (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |