做者: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這樣的問題。