正则表达式的优化
为了高效地使用正则表达式,首先要理解它的工作原理。
- 编译
当你创建一个正则表达式对象(使用正则直接量或 RegExp 构造函数),浏览器会验证你的表达式,然后把它转化为一个原生代码程序,用于执行匹配工作。如果你把正则对象赋值给一个变量,可以避免重复执行这一步骤。
- 设置起始位置
当正则类进入使用状态,首先要确定目标字符串的起始搜索位置,它是字符串的起始字符,或者由正则表达式的lastIndex 属性指定,但是当它从第四步返回到这里时,此位置则在最后一次的起始位置的下一位字符分类的位置上。
浏览器厂商优化正则表达式引擎的办法是,通过提前决定跳过一些不必要的步骤,来避免大量无意义的工作。例如,如果正则表达式由 ^ 开始,IE和chrome通常会判断字符串的起始位置能否匹配,如果匹配失败,那么可以避免愚蠢地搜索后续位置。另一个例子是匹配第三个字符x的字符串,一个聪明的做法是先找到x ,然后再将起始 位置回退两个字符。
- 匹配每个正则表达式字元
一旦正则表达式知道开始位置,他会逐个检查文本和正则表达式模式。当一个特定字原匹配失败时,正则表达式会试着回溯到之前尝试匹配的位置上,然后尝试其他可能的路径。
- 匹配成功或失败
如果在字符串当前的位置发现了一个完全匹配,那么正则表达式宣布匹配成功。如果正则表达式所有的可能路径都没有匹配到,正则表达式引引擎会回退到第二步,然后从下一个字符重新尝试。当字符串的每个字符串(以及最后一个字符串后面的位置)都经历这个过程,如果还没有成功匹配,那么正则表达式就宣布彻底匹配失败。
- 理解回溯
在大多数现代正则表达式的实现中,回溯时匹配过程的基础组成部分。这很大程度上是正则表达式如此强大且富有表现力的根源。然而,回溯会产生昂贵的计算消耗,一不小心就会失控。尽管回溯只是影响整体性能的其中一环,但理解它的工作原理以及最少地使用它可能是编写高效正则表达式的关键所在。
正则表达式匹配目标字符串的方式
- 从左到右逐个测试表达式。
- 遇到量词(* , + ? 或者 {3,4})需要决定何时尝试匹配更多字符。
- 遇到分支 ( 来自 | 操作符 ),需要从可选项中选择一个尝试匹配
- 分支回溯
// 正则部分
var reg = /a(b|c) de/
var str = "ab ee,ac de"
str.test(str)
该正则表达式匹配 “ac de"。匹配过程开始时,首先会查找a,目标字符串的首字母a符合匹配,于是立即被找到。接下来,子表达式(b|c)提供了两个选择属于分支类型,根据从左到右匹配的原则,选择左边部分b, 在str中第二部分匹配成功,随后继续空格匹配,然后正则部分的d去和str中的e匹配,匹配失败。
此时表达式不能放弃,还有其他情况需要匹配
此时发生回溯,回溯不是从头进行重新匹配,回溯到从第一个分支决策点进行匹配,显然匹配失败,
这时已无更多决策点,说明字符串从第一个位置匹配失败,
将其匹配位置移到第二个字符,和正则中的首字符匹配失败,继续后移,直到下次和正则中的首字符a匹配, 此时字符串str还可以匹配的字符"ac de"
接着正则reg选择左分支b, b和str中的c不匹配,继续回溯到上次决策的位置,匹配右分支c和str中的c匹配,继续匹配空格,d,e 完全匹配 。该正则表达式匹配结束
str.test(str) 返回 true.
- 重复与回溯
var str = "<p>test </p>"+
"<img src='123.png'>"+
"<div>div</div>";
var reg = /<p>.*</p>/i
reg.test(str)
该正则表达式开始从左向右匹配,匹配了< p > ,然后匹配 .* 。点号(.)能匹配除换行符意外的任意字符,贪婪量词(*)表示重复零次或多次----尽可能匹配多次。因为目标字符串中无换行符,那么它会吞并所有字符!因此正则表达式在字符串末尾匹配失败,因为正则表达式并未完成,需要尝试匹配<,
正则表达式并未完成,需要尝试匹配<
表达式每次回溯一个字符 ,继续尝试匹配 < ,直到回溯到 < /div > 标签的首字符 < 。然后匹配 / ,匹配成功,然后匹配p 失败。
正则表达式继续回溯,
并重复这一过程,直到回溯匹配到第一段的< /p >结束。
这里可以看出 (.*)这种贪婪的匹配方式并非我们想要的,所以可以替换为 (非贪婪)量词 (*?) 以匹配单个段落。
如果目标字符串只有一个段落,你会发现贪婪模式和惰性模式正则表达式时等价的。但他们的匹配过程并不相同。
回溯失控
当正则表达式导致你的浏览器假死数秒,数分钟,甚至更长时间,问题很可能是因为回溯失控,例如:
var reg = /<html>[\s\S]*?<head>[\s\S]*?<\/head>[\s\S]*?<body>[\s\S]*?<\/body>[\s\S]*?<\/html>/
这段表达式匹配常规的HTML字符时运行正常,但是当目标字符串缺少一个或多个必要的标签时会变得很糟糕。比如< /html >标签缺失,最后一个[\s\S] * ?将扩展到字符串末尾,此时没有找到< /html >标签,正则表达式依次向前搜索[\s\S] * ?并记住回溯的位置以便后续使用。 正则表达式尝试扩展到倒数第二个 [ \s\S] * ? 用它匹配 < /body >,匹配成功后,继续向后匹配 ,直到< /html > 失败,然后回溯到倒数第三 [\s\S] * ?, 依次类推。
- 解决方案:
- 具体化
例如:在上述的例子中,可以将宽泛的 . * ? 替换成更为具体的 [^"\r\n"]*,就去除了回溯时可能发生的几种情况
- 使用预查和反向引用次数
> 在一些支持原子组的正则表达式引擎中,可以使用原子组使回溯结束的更快。同时可以有效地阻止海量回溯。原子组的语法是 (?> 正则表达式),位于(?>)之间的所有正则表达式都会被认为是一个单一的正则符号。一旦匹配失败,引擎将会回溯到原子组前面的正则表达式部分。
> js 中不支持原子组,可以利用预查过程中一项鲜为人知的行为来模拟原子组:预查也是原子组。(预查也是原子组)它们的区别是,预查作为全局匹配的一部分,并不消耗任何字符;而只是检查自己包含的正则符号在当前字符串位是否匹配。
预查方法 你可以通过把预查的表达式封装在捕获组中并给它添加一个反向引用的方法来避免这一问题
(?= (pattern to make atomic))\1
注意: 如果你的正则表达式包含了多个捕获组,那么你需要使用适当的反向引用次数。
嵌套量词与回溯失控
所谓的嵌套量词需要额外关注且小心使用,以确保不会引发潜在的回溯失控。嵌套量词是指量词出现在一个自身重复量词修饰的组中。
嵌套量词并不会造成性能危害,然而它很容易在尝试匹配字符串的过程中,在内部变量和外部变量之间产生一大堆文本拆解的路径。
更多提高正则表达式效率的方法
- 关注如何让匹配更快失败
- 正则表达式以简单,必需的字元开始
- 使用量词模式,使它们后面的字元互斥
- 减少分支数量,缩小分支范围
- 使用非捕获组
- 只捕获感兴趣的文本以减少后处理
- 暴露必需的字元
- 使用合适的量词
- 把正则表达式赋值给变量并重用它们
- 将复杂的正则表达式拆分为简单的片段
何时不使用正则表达式
当自己小心使用时,正则表达式速度非常快。如果只是搜索字面字符串时,可能就要注意:
// 例如 检查一个字符串是否以分号结尾
endWithSemicolon = /;$/.test(str);
浏览器正则引擎不能意识匹配字符串末尾。它们所做的是逐个测试整个字符串。每当找到一个分号,正则表达式就移动到下一个标记($),检查它是否匹配字符串的末尾。如果不匹配,他会继续搜索匹配项,直到搜索完整个匹配字符串。
在这中情况下,使用简单地检查最后一个字符是否为分号:
endWithSemicolon = str.charAt(str.length - 1) == ";"
使用正则表达式去首尾空白
if(!String.prototype.trim){
String.prototype.trim = function() {
return this.replace(/^\s+/,"").replace(/\s\s*$/,"");
}
}
小结
通过对以上正则的了解
- 回溯即使正则表达式的基本组成功能,也是正则表达式的低效之源。
- 回溯失控发生在正则表达式应该快速匹配的地方,但因为某些特殊的字符串匹配动作导致运行缓慢甚至浏览器崩溃。避免这个问题的办法:使相邻的字元互斥,通常嵌套量词对同一字符串的相同部分多次匹配,通过重复利用预查的原子组去除不必要的回溯。
- 有许多方法可以去除字符串的首位空白,但使用两个简单的正则表达式来处理大量字符串内容能提供一个简洁而跨浏览器的方法,从字符串末尾开始向前循环向前搜索第一个非空白字符,或者将此技术同正则表达式结合起来,会提供一个更好的替代方案,它很少受到字符串长度影响。
网友评论