如何優化正則表達式性能?

做者:huangrenhui

出處:https://www.cnblogs.com/huang...java

一.背景

正則表達式是計算機科學的一個概念,不少語言都實現了它。正則表達式使用一些特定的元字符來檢索、匹配以及替換符合規定的字符串。面試

構造正則表達式語法的元字符,由普通字符、標準字符、限定字符(量詞)、定位符(邊界字符)組成,詳情以下 正則表達式

二.正則表達式引擎

正則表達式是一個用正則符號寫出的公式,程序對這個公式進行語法分析,創建一個語法分析樹,再根據這個分析樹結合正則表達式的引擎生成執行程序(這個執行程序咱們把它稱做狀態機,也叫狀態自動機),用於字符匹配。算法

而這裏的正則表達式引擎就是一套核心算法,用於創建狀態機。spring

目前實現正則表達式引擎的方式有兩種:DFA自動機(Deterministic Final Automata 肯定有限狀態自動機)和 NFA(Non deterministic Finite Automaton 非肯定有限狀態自動機)。編程

對比來看,構造 DFA 自動機的代價遠大於 NFA 自動機,但 DFA 自動機的執行效率高於 NFA 自動機。數組

假設一個字符串的長度是 n,若是用 DFA 自動機做爲正則表達式引擎,則匹配的時間複雜度爲 O(n);若是用 NFA 自動機做爲正則表達式引擎,因爲 NFA 自動機在匹配過程當中存在大量的分支和回溯,假設 NFA 的狀態數爲 s,則該匹配算法的時間複雜度爲 O(ns)。intellij-idea

NFA 自動機的優點是支持更多功能。例如:捕獲 group、環視、佔有優先量詞等高級功能。這些功能都是基於子表達式獨立進行匹配,所以在編程語言裏,使用的正則表達式庫都是基於 NFA 實現的。編程語言

那麼 NFA 自動機究竟是怎麼進行匹配的呢?接下來如下面的例子來進行說明:ide

text = "aabcab"
regex = "bc"

NFA 自動機會讀取正則表達式的每個字符,拿去和目標字符串匹配,匹配成功就換正則表達式的下一個字符,反之就繼續和目標字符串的下一個字符進行匹配。

分解一下過程:

1)讀取正則表達式的第一個匹配符和字符串的第一個字符進行比較,b 對 a,不匹配;繼續換字符串的下一個字符,也就是 a,不匹配;繼續換下一個,是 b,匹配;

2)同理,讀取正則表達式的第二個匹配符和字符串的第四個字符進行比較,c 對 c,匹配;繼續讀取正則表達式的下一個字符,然然後面已經沒有可匹配的字符了,結束。

這就是 NFA 自動機的匹配過程,雖然在實際應用中,碰到的正則表達式都要比這複雜,但匹配方法是同樣的。

三.NFA自動機的回溯

用 NFA 自動機實現的比較複雜的正則表達式,在匹配過程當中常常會引發回溯問題。大量的回溯會長時間地佔用 CPU,從而帶來系統性能開銷。以下面例子:

text = "abbc"
regex = "ab{1,3}c"

上面例子,匹配目的比較簡單。匹配以 a 開頭,以 c 結尾,中間有 1-3 個 b 字符的字符串。NFA 自動機對其解析的過程是這樣的:

1)讀取正則表達式第一個匹配符 a 和字符串第一個字符 a 進行比較,a 對 a,匹配;

2)讀取正則表達式第一個匹配符 b{1,3} 和字符串的第二個字符 b 進行比較,匹配。但由於 b{1,3} 表示 1-3 個 b 字符串,NFA 自動機又具備貪婪特性,因此此時不會繼續讀取正則表達式的下一個匹配符,而是依舊使用 b{1,3} 和字符串的第三個字符 b 進行比較,結果仍是匹配。

3)繼續使用 b{1,3} 和字符串的第四個字符 c 進行比較,發現不匹配了,此時就會發生回溯,已經讀取的字符串第四個字符 c 將被吐出去,指針回到第三個字符 b 的位置。

4)那麼發生回溯之後,匹配過程怎麼繼續呢?程序會讀取正則表達式的下一個匹配符 c,和字符串中的第四個字符 c 進行比較,結果匹配,結束。

四.如何避免回溯問題?

既然回溯會給系統帶來性能開銷,那咱們如何應對呢?若是你有仔細看上面那個案例的話,你會發現 NFA 自動機的貪婪特性就是導火索,這和正則表達式的匹配模式息息相關。

1.貪婪模式(Greedy)

顧名思義,就是在數量匹配中,若是單獨使用 +、?、*或(min,max)等量詞,正則表達式會匹配儘量多的內容。

例如,上面那個例子:

text = "abbc"
regex = "ab{1,3}c"

就是在貪婪模式下,NFA自動機讀取了最大的匹配範圍,即匹配 3 個 b 字符。匹配發生了一次失敗,就引發了一次回溯。若是匹配結果是「abbbc」,就會匹配成功。

text = "abbbc"
regex = "ab{1,3}c"

2.懶惰模式(Reluctant)

在該模式下,正則表達式會盡量少地重複匹配字符,若是匹配成功,它會繼續匹配剩餘的字符串。

例如,上面的例子的字符後面加一個「?」,就能夠開啓懶惰模式。

text = "abc"
regex = "ab{1,3}?c"

匹配結果是「abc」,該模式下 NFA 自動機首先選擇最小的匹配範圍,即匹配 1 個 b 字符,所以就避免了回溯問題。

3.獨佔模式(Possessive)

同貪婪模式同樣,獨佔模式同樣會最大限度地匹配更多內容;不一樣的是,在獨佔模式下,匹配失敗就會結束匹配,不會發生回溯問題。

仍是上面的例子,在字符後面加一個「+」,就能夠開啓獨佔模式。

text = "abbc"
regex = "ab{1,3}+c"

結果是不匹配,結束匹配,不會發生回溯問題。

因此綜上所述,避免回溯的方法就是:使用懶惰模式或獨佔模式。

前面講述了「Split() 方法使用了正則表達式實現了其強大的分割功能,而正則表達式的性能是很是不穩定的,使用不恰當會引發回溯問題。」,好比使用了 split 方法提取域名,並檢查請求參數是否符合規定。

split 在匹配分組時遇到特殊字符產生了大量回溯,解決辦法就是在正則表達式後加一個須要匹配的字符和「+」解決了回溯問題:

\\?(([A-Za-z0-9-~_=%]++\\&{0,1})+)

五.正則表達式的優化

1.少用貪婪模式:多用貪婪模式會引發回溯問題,可使用獨佔模式來避免回溯。

2.減小分支選擇:分支選擇類型 「(X|Y|Z)」 的正則表達式會下降性能,在開發的時候要儘可能減小使用。若是必定要用,能夠經過如下幾種方式來優化:

1)考慮選擇的順序,將比較經常使用的選擇項放在前面,使他們能夠較快地被匹配;

2)能夠嘗試提取共用模式,例如,將 「(abcd|abef)」 替換爲 「ab(cd|ef)」 ,後者匹配速度較快,由於 NFA 自動機會嘗試匹配 ab,若是沒有找到,就不會再嘗試任何選項;

3)若是是簡單的分支選擇類型,能夠用三次 index 代替 「(X|Y|Z)」 ,若是測試話,你就會發現三次 index 的效率要比 「(X|Y|Z)」 高一些。

3.減小捕獲嵌套 :

捕獲組是指把正則表達式中,子表達式匹配的內容保存到以數字編號或顯式命名的數組中,方便後面引用。通常一個()就是一個捕獲組,捕獲組能夠進行嵌套。

非捕獲組則是指參與匹配卻不進行分組編號的捕獲組,其表達式通常由(?:exp)組成。

在正則表達式中,每一個捕獲組都有一個編號,編號 0 表明整個匹配到的內容。能夠看看下面的例子:

public static void main(String[] args) {
        String text = "<input high=\"20\" weight=\"70\">test</input>";
        String reg = "(<input.*?>)(.*?)(</input>)";
        Pattern p = Pattern.compile(reg);
        Matcher m = p.matcher(text);
        while (m.find()){
            System.out.println(m.group(0));//整個匹配到的內容
            System.out.println(m.group(1));//<input.*?>
            System.out.println(m.group(2));//(.*?)
            System.out.println(m.group(3));//(</input>)
        }

    }
=====運行結果=====
<input high="20" weight="70">test</input>
<input high="20" weight="70">
test
</input>

若是你並不須要獲取某一個分組內的文本,那麼就使用非捕獲組,例如,使用 「(?:x)」 代替 「(X)」 ,例以下面的例子:

public static void main(String[] args) {
        String text = "<input high=\"20\" weight=\"70\">test</input>";
        String reg = "(?:<input.*?>)(.*?)(?:</input>)";
        Pattern p = Pattern.compile(reg);
        Matcher m = p.matcher(text);
        while (m.find()) {
            System.out.println(m.group(0));//整個匹配到的內容
            System.out.println(m.group(1));//(.*?)
        }

    }
=====運行結果=====
<input high="20" weight="70">test</input>
test

近期熱文推薦:

1.600+ 道 Java面試題及答案整理(2021最新版)

2.終於靠開源項目弄到 IntelliJ IDEA 激活碼了,真香!

3.阿里 Mock 工具正式開源,幹掉市面上全部 Mock 工具!

4.Spring Cloud 2020.0.0 正式發佈,全新顛覆性版本!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

以爲不錯,別忘了隨手點贊+轉發哦!

相關文章
相關標籤/搜索