做者:wanago翻译:疯狂的技术宅javascript
原文:https://wanago.io/2019/09/23/...
未经容许严禁转载前端
前文:java
正则表达式能够解决许多问题,但也有多是使咱们头痛的根源。 最近 Cloudfare 的一次停机事故就是因为正则表达式致使全球大量机器上的 CPU 峰值飙升至100%。在本文中,咱们将会学习须要注意的状况,例如灾难性的回溯。为了帮助咱们理解问题,还分析了贪婪和懒惰量词以及为何 lookahead 可能会有所帮助。程序员
有些人遇到问题时会想:“我知道,我将使用正则表达式。”如今他们有两个问题了。Jamie Zawinski面试
正则表达式引擎很是复杂。尽管咱们能够用 regexp 创造奇迹,但须要考虑可能会遇到的一些问题。因此须要更深刻地研究如何去执行某些正则表达式。正则表达式
在本系列文章的前几部分中,咱们使用了 +
之类的量词。它告诉引擎至少匹配一个。express
const expression = /e+/; expression.test('Hello!'); // true expression.test('Heeeeello!'); // true expression.test('Hllo!'); // false
让咱们仔细看看第二个例子: /e+/.test('Heeeeello!')
。咱们可能想知道用这个表达式匹配多少个字母。segmentfault
因为默认状况下量词是贪婪的,所以咱们会匹配尽量多的字母。能够用 match函数来确认这一点。浏览器
'Heeeeello!'.match(/e+/); // ["eeeee", index: 1, input: "Heeeeello!", groups: undefined]
另外一个不错的例子是处理一些 HTML 标签:安全
const string = 'Beware of <strong>greedy</strong> quantifiers!'; /<.+>/.test(string); // true
最初的猜想多是它与 <strong>
之类的东西匹配。不彻底是!
string.match(/<.+>/); // ["<strong>greedy</strong>" (...) ]
如你所见,贪婪的量词与最长的字符串匹配!
在本系列中,咱们还将介绍 ?
量词。这意味着匹配零或一次。
function wereFilesFound(string) { return /[1-9][0-9]* files? found/.test(string); } wereFilesFound('0 files found'); // false wereFilesFound('No files found'); // false wereFilesFound('1 file found'); // true wereFilesFound('2 files found'); // true
有趣的是,经过将其添加到贪婪的量词中,咱们告诉它重复尽量少的次数,所以使其变得懒惰。
const string = 'Beware of <strong>greedy</strong> quantifiers!'; string.match(/<.+?>/); // ["<strong>", (...) ]
要了解量词如何影响正则表达式的行为,咱们须要仔细研究被称为回溯的过程。
先让咱们看一下这段看似清白的代码!
const expression = /^([0-9]+)*$/;
乍一看,它能够成功检测到一系列数字。让咱们分解一下它的工做方式。
expression.test('123456789!');
[0-9]+
。它是贪婪的,因此它会首先尝试匹配尽量多的数字。首先匹配的是 123456789
而后引擎尝试应用 *
量词,但没有其余数字了
$
符号,因此咱们但愿字符串以数字结尾—— !
符号不会发生这种状况[[0-9]+]
中匹配的字符数量减小了。它匹配 12345678
。而后使用 *
量词,所以 ([0-9]+)*
产生两个子字符串:12345678
和 9
$
匹配失败[0-9]+
匹配的位数来保持回溯 上述过程会产生多种不一样的组合。
咱们的字符串以 !
符号结尾。所以,正则表达式引擎尝试回溯,直到在提供的字符串的末尾找到数字为止。
[12345678][9]! [1234567][89]! [1234567][8][9]! [123456][789]! [123456][7][89]! [123456][78][9]!
通过了大量的计算,可是没有找到匹配的结果。这可能会致使性能大幅降低。若是使用很是长的字符串,浏览器可能会挂起,从而破坏用户体验。
经过将贪婪量词更改成惰性量词,有时能够提升性能,可是这个特定的例子并不属于这种状况。
要解决上述问题,最直接方法是彻底重写正则表达式。上面的解决方案并不老是很容易,并且有可能会形成很大的痛苦。解决上述问题的方法是使用先行断言(lookahead)。
在最基本的形式中,它声明 x 仅会在其后跟随 y 时才匹配。
const expression = /x(?=y)/; expression.test('x'); // false expression.test('xy'); // true
咱们将其称为正向先行断言。仅当 x 后面不跟随 y 时,用负向先行断言匹配 x
const expression = /x(?!y)/; expression.test('x'); // true expression.test('xy'); // false
先行断言很酷的地方在于它是原子性的。在知足条件后,引擎将不会回溯并尝试其余排列。
咱们在这里须要涉及到的的另外一个问题是回溯引用。
const expression = /(a|b)(c|d)\1\2/;
上面的 \1
表示第一个捕获组的内容,而 \2
表示第二个捕获组的内容。
expression.test('acac'); // true expression.test('adad'); // true expression.test('bcbc'); // true expression.test('bdbd'); // true expression.test('abcd'); // false
咱们能够结合使用先行断言和回溯引用来处理回溯问题:
const expression = /^(?=([0-9]+))\1*$/
这看起来很复杂。让咱们对它进行分解。
(?=([0-9]+))
寻找最长的数字字符串,由于 +
是贪婪的(?=([0-9]+))\1
的回溯引用指出,先行查找的内容须要出如今字符串中因为上述全部缘由,咱们能够安全地测试很长的字符串,而不会产生性能问题。
const expression = /^(?=([0-9]+))\1*$/; expression.test('5342193376141170558801674478263705216832 D:'); //false expression.test('7558004377221767420519835955607645787848'); // true
在本文中,咱们更深刻地研究了量词。能够将它们分为贪婪和懒惰两种量词,而且它们可能会对性能产生影响。咱们还讨论了量词可能致使的另外一个问题:灾难性回溯。咱们还学习了如何使用 先行断言(lookahead) 来改善性能,而不只仅是去重写表达式。有了这些知识,咱们能够编写更好的代码,避免出现Cloudflare这样的问题。