正则表达式的工作原理(三)

135 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第3天,点击查看活动详情

提高正则表达式效率

在讲解完正则表达式的各种匹配模式的工作流程之后,接下来将讨论提高正则表达式的匹配效率的方法,其中大部分已经在关于回溯的讨论中有所涉及。

  1. 更快的失败
    正则表达式慢的原因通常是匹配失败的过程慢,而不是匹配成功过程慢。这是因为如果你使用正则表达式来匹配一个打字符串的小部分,该正则表达式匹配失败的位置比匹配成功的位置要多得多。如果一个修改将正则表达式的匹配过程变快而失败过程变慢,这通常是个失败的修改。

  2. 以简单,必须的字元开始
    最理想的情况是,一个正则表达式的起始标记应当尽可能快速的测试并排除明显不匹配的位置。通常会使用一个锚点^$、特定的字符串或字符类和单词边界。
    如果可能的话,避免以分组或选择字元开头,避免类似 /one|two/ 的顶层分支,因为它强迫正则表达式识别多种起始字元。对于一些对量词比较敏感的浏览器可以进行一些优化,例如:以 \s\s* 替代 \s+ 或 \s{1,}。

  3. 使用量词模式,使他们后面的字元互斥
    当字符与字元相邻或子表达式能够重叠匹配时,正则表达式尝试才接文本的路径数量将增加。为了避免这种情况,尽量具体化你的匹配模式。当你想表达 “[^”\s\n]*” 时不要使用 “.*?” 因为它依赖回溯。

  4. 减少分支数量,缩小分支范围
    分支使用竖线 | 可能要求在字符串的每一个位置上测试所有分支选项。你通常可以通过使用字符集和选项组件来减少对分支的需求,或将分支在正则表达式上的位置推后(允许到达分支前出现一些匹配失败)。

    替换前替换后
    cat|bat[cb]at
    red|readrea?d
    red|rawr(?:ed|aw)
    (.|\r|\n)[\s\S]

    字符集比分支更快,因为它使用位向量(或其他快速实现的方式)而不是回溯。当分支必不可少时,将常用分支放到最前面,如果这样做不影响正则表达式匹配的话。分支选项从左到右依次尝试,一个选项被匹配的机会越大,越希望能够被尽快检测。

  5. 使用非捕获组
    捕获组消耗时间和内存来记录反向引用,并使它保持最新。如果你不需要一个反向引用,可使用非捕获组来避免这些开销,比如用 (?:...) 来替代 (...)。当需要全文匹配的反向引用时,更偏向于把正则表达式包装在一个捕获组里。这不是必要的,因为你可以使用其他方法引用全文匹配,比如: regex.exec() 返回数组的第一项,或在替换字符串中使用 $&

  6. 只捕获感兴趣的文本以减少后处理
    作为上一条的补充,如果你需要引用匹配的一部分,一个采取一切手段捕获这些片段,再使用反向引用来处理。例如,如果你正在编写代码处理一个正则表达式匹配到的引号起来的字符串内容,一个使用 /" ([^"]*)"/ 并使用反向引用处理,而不是使用 /" [^"]*"/ 然后手动从结果中剥离引号。

  7. 暴露必要的字元
    为了帮助正则表达式引擎在优化查询过程中做出明智的决策,可以尝试让它更容易的判断哪些字元是必须的。当字元应用在子表达式或分支中,正则表达式引擎很难判断它们是不是必须的,有些引擎甚至不做这方面的尝试。

  8. 使用合适的量词
    量词 *? 的匹配过程中有较大的区别,即使是处理相同的字符串。使用合适的量词类型可以显著提升性能,尤其是处理长字符串时。

  9. 赋值重用
    将正则表达式赋值给变量可以避免对他们重新编译。

  10. 化繁为简
    避免一个正则表达式中处理太多任务。复杂的搜索问题需要条件逻辑,拆分成两个或者多个正则表达式更容易解决。而且正则表达式的书写也会更加简洁和方便阅读。

何时不使用正则表达式

当你小心使用时,正则表达式速度非常快。然而,当你只是搜索字面字符串时它经常会弄巧成拙,尤其在你事先知道字符串的哪一部分将要被查找时。比如要检查一个字符串是否以分号结尾:

/;$/.test(str);

在浏览器的处理中则是逐个测试整个字符串,每当找到一个分号,正则表达式就移动到下一个标记 $ 检查它是否匹配字符串末尾。这种情况下更好的办法是跳过正则表达式所需的中间步骤:

str.charAt(str.length - 1) === ";";

去除字符串首尾空格

结合所学知识我们利用去除字符串首尾空格来进行实践,去除字符串首尾空格是个简单而常见的任务。正则表达式能让你非常简单的实现 trim 方法,这对一些关系文件尺寸的库而且十分重要。

// 方案一
str.replace(/^\s+|\s+$/g,"")
// 方案二
str.replace(/^\s+/, "").replace(/\s+$/, "");

方案一在处理长字符串时总是比方案二更慢。因为两个分支选项在每个字符串匹配时都要被测试一遍。

// 方案三
str.replace(/^\s*([\s\S]*?)\s*$/, "$1");

方案三的作用是匹配整个字符串,捕获从第一个到最后一个非空白字符串之间的序列。通过把整个字符串替换成反向引用,剩下的就是匹配的结果。但是在捕获组中的惰性量词会导致正则表达式进行大量额外操作,因此在处理较长目标字符串时会变得更慢。

// 方案四
str.replace(/^\s*([\s\S]*\S)?\s*$/, "$1")

方案四能够确保捕获组仍然只匹配最后的非空白字符,末尾的 \S 是必须的。由于正则表达式必须能够匹配全部由空格组成的字符串,整个捕获组通过向末尾增加一个 ? 量词而成为可选组。