Bootstrap

正则表达式的效率与优化

以下内容整理自《PHP核心技术与最佳实践》

一、使用字符组代替分支条件

eg. 使用[a-d]表示a~d之间的字母,而不是使用(a|b|c|d)

function regTest($pattern,$str,$cnt){
    $start=microtime(true);
    for ($i=0;$i<$cnt;$i++){
        preg_match($pattern,$str);
    }
    echo 'waste time(s): ',number_format(microtime(true)-$start, 10),'<br>';
}
$cnt=15;//最好设大数,eg.1000
$str='';
for ($i=0;$i<$cnt;$i++){
    $str.='abababcdefg';
}
//方案1:分支条件
regTest("/^(a|b|c|d|e|f|g)+$/",$str,$cnt);
//方案2:字符组
regTest("/^[a-g]+$/",$str,$cnt);
//方案3:同方案2
regTest("/^[abcdefg]+$/",$str,$cnt);

这里写图片描述
可以看出使用字符组比使用分支条件速度快很多。这是由于在匹配单个字符的时候,引擎会把[abc]这样的字符组视为一个元素,而不是3个元素(a、b、c)。整个元素作为匹配迭代的一个单元,不需要进行三次迭代,从而提高匹配效率。

二、优化选择最左端的匹配结果

对于传统NFA引擎来说,因为引擎一旦找到匹配结果就会停下来,而不会去尝试正则表达式的每一种可能(PHP中的preg函数就属于传统型NFA引擎)。

三、标准量词是匹配优先的

若用量词约束某个表达式,那么在匹配成功前,进行的尝试次数有下限和上限。eg.

preg_match('/\w*(\d+)/','copy2003y',$match);

       这条正则表达式匹配的$1结果应该是3。解释如下:当正则引擎用“\w*(\d+)”匹配字符串copy2003y时,会先用“\w*”匹配字符串copy2003y。而“\w*”会匹配字符串copy2003y的所有字符,然后再交给“\d+”匹配剩下的字符串,而剩下的没有了。这时,“\w*”规则会不情愿地吐出一个字符,给“\d+”匹配。同时,在吐出字符之前,记录一个点,这个点就是用于回溯的点。然后“\d+”匹配y,发现不能匹配成功,此时会要求“\w*”再吐出一个字符;“\w*”先记录一个回溯的点,再吐出一个字符。这时,“\w*”匹配结果只有copy200,已经吐出3y。“\d+”再去匹配3,发现匹配成功,会通知引擎,并且直接显示出来。所以,“(\d+)”的结果是3,而不是2003。
       如果改为非贪婪模式呢?“\w*?(\d+)”匹配的结果就应该是2003。由于“\w*?”是非贪婪,正则引擎会用表达式“\w*?”每次仅匹配一个字符串,然后再将控制权交给后面的“(\d+)”匹配下一个字符,同时记录一个点,用于匹配不成功时,返回这里再次匹配。
       尽量以组为单位进行匹配,使用固话分组就能避免无休止的匹配。

四、谨慎用点号元字符,尽可能不用星号和加号这样的任意量词

只要能确定范围(eg.“\w”),就不要用点号;只要能够预测重复次数,就不要用量词。假设一条微博消息的XML正文部分结构如下:

<span class="msg">...</span>

正文中无尖括号,写法如下:

<span class="msg">[^<]{1,200}</span>

或者:

<span class="msg">.*</span>

上述第一种代码的思路要好于第二种,原因如下:
1、使用“[^<]”,保证了文本的范围不会超过下一个小于号所在位置
2、明确长度范围{1,200},依据是一条微博消息大致的字符长度范围是固定的,现在微博字数长度限制是140个字。
同时,能使用懒惰匹配就坚决不用贪婪匹配。

五、尽量使用字符串函数处理代替

使用字符串函数和正则表达式都可以处理字符串,两者相比,字符串函数处理的效率更高。当然,有些情况几乎是非正则表达式不能胜任的,或者不用正则表达式的成本太高,这些情况不得不用正则表达式。

六、合理使用括号

每使用一个普通括号(),而不是非捕获型括号(?:),就会保留一部分内存等着再次访问。

七、起始、行描点优化

能确定起止位置,使用^能提高匹配的速度。同理,使用$标记结尾,正则引擎则会从符合条件的长度处开始匹配,略过目标字符串中许多可能的字符。在写正则表达式时,应该将描点独立出来,例如“^(?:abc|123)”比“^123|^abc”效率高,而“^(abc)”比“(^abc)”效率更高。
这个原则不适用于所有正则引擎。比如在PCRE中,二者效率相当。

八、量词等价转换的效率差异

例如在PHP中,使用“\d\d\d”和“\d{3}”,或者“====”和“={4}”,它们之间的效率几乎没有差别。但是使用其他语言可能就会有比较明显的性能差异了。

九、对大而全的表达式进行拆分
十、使用正则以外的解决方案

eg.
1、 同五;
2、在某项目需要分析PHP代码,分离出对应的函数调用(以及源代码对应的位置)。虽然这些正则表达式也可以实现,但无论从效率还是代码复杂度方面考虑,这都不是最有方法。PHP已经内置解析器的接口PHP Tokenizer。使用PHP Tokenizer能简单、高效、准确地分析出PHP源代码的组成;
3、在解析URL时没必要使用正则表达式,使用parse_url函数即可;
4、在获取HTTP头时,也可以使用get_headers函数;
5、在进行输入校验时,可以使用filter_var函数,如:

filter_var($email,FILTER_VALIDATE_EMAIL);

6、如果在JavaScript里,可以使用DOM代替一些正则匹配。

;