正則表達式Lookaround特性的應用

1. 介紹

Lookaround是Perl 5引進的特性,這個特性極大加強了正則表達式的能力,熟練掌握該特性,能夠幫助咱們運用正則表達式解決更復雜的問題。Lookaround有4種類型,下面的定義取自Java APIhtml

  • (?=X)    X, via zero-width positive lookahead    
  • (?!X)    X, via zero-width negative lookahead
  • (?<=X) X, via zero-width positive lookbehind
  • (?<!X) X, via zero-width negative lookbehind
兩個方向:Lookahead和Lookbehind,兩種邏輯:Positive和Negative。目前多數正則表達式引擎至少都支持Lookahead,後面的例子用Java來演示,這4種類型Java都支持。

上面定義中的"Zero-width"是理解Lookaround特性的關鍵。經常使用的"^"、"$"、"\b"等Boundary Characters都是"Zero-width Assertions",即不消費字符,但斷定當前位置是否知足特定的要求,Lookaround實際也是"Zero-width Assertions"。Boundary Characters是系統預約義的"Zero-width Assertions",而Lookaround能夠看作用戶自定義的"Zero-width Assertions"。 java

接下來給幾個Lookaround應用的例子。 正則表達式

2. 應用舉例

下面的例子除了Lookaround,主要用的都是一些正則表達式的基本特性,只有兩個可能不太常見的特性: api

  • Reluctant quantifiers,X*? X, zero or more times
  • (?:X) X, as a non-capturing group

先了解這兩個特性對理解後面的例子是有幫助的。 oracle

2.1 匹配否認

匹配全數字的字符串,正則表達式很容易寫,"\d+";可是要匹配不全是數字的字符串,怎麼寫呢?"\D+"是不行的,由於這樣沒法匹配包含數字的串;分析一下,只要串裏包含非數字就能夠,因此能夠寫成".*\D.*",還不算困難。 工具

再看個例子,匹配包含連續數字的字符串,能夠用".*\d\d.*"來實現;那麼怎麼匹配不包含連續數字的字符串呢?仔細找找規律,"\d?(\D+\d?)*"彷佛能夠知足要求,但理解起來就不是那麼容易了。 性能

從這兩個例子看,模式的否認匹配,跟原模式徹底沒有關係,也沒有規律可尋,不一樣的狀況得具體分析。能夠想象,對於更復雜的狀況,否認匹配極可能會更難寫,甚至寫不出來的,或者即便寫出來的,也很是難理解。 code

利用Lookaround特性能夠很容易實現否認匹配,上面例子的Java代碼以下:
htm

Pattern.compile("(?!\\d+$).+");         // 字符串不全是數字
Pattern.compile("(?!.*?\\d\\d).+");     // 不包含連續數字

在模式的起始處,利用"Negative Lookahead"特性定義一個"Assertion",寫起來頗有規律,也很是容易理解。第二個例子裏用了"*?"(Reluctant quantifiers),由於它比默認的"*"(Greedy quantifiers)更符合咱們的意圖,也更高效。 字符串

注意:在作match的時候,Java會在模式的先後自動添加"^"和"$",因此就不必本身加了;但在有的語言或工具裏,須要本身添加"^"和"$"。

2.2 與運算 

下面舉一個驗證密碼例子。出於簡化的目的,只涉及"\w"中的字符,即[a-zA-Z_0-9];爲了便於演示,也不考慮密碼格式的定義是否合理。對密碼的格式的要求以下:

  • 長度在8到16之間
  • 至少包含一個小寫字母
  • 至少包含一個大寫字母
  • 至少包含一個數字或_
  • 開頭和結尾不容許是數字
  • 不容許出現連續的_

這些要求看似很複雜,實際上倒是異乎尋常地簡單,下面是Java代碼:

Pattern.compile(
        "(?=.*?[a-z])       # 至少包含小寫字母\n"       +
        "(?=.*?[A-Z])       # 至少包含大寫字母\n"       +
        "(?=.*?[\\d_])      # 至少包含一個數字或_\n"    +
        "(?!\\d|.*\\d$)     # 開頭和結尾不容許是數字\n"  +
        "(?!.*?__)          # 不容許出現連續的_\n"      +
        "\\w{8,16}          # 長度在8到16之間\n",
        Pattern.COMMENTS);

若是熟悉Lookaround,這個正則表達式是很是容易理解的,註釋已經說明地很清楚了;固然,上面的這個正則表達式不是惟一的寫法,更不是最優的寫法。

2.3 反向引用和分組

再看一個例子,怎麼判斷一個字符串是否包含重複的字符?若是瞭解反向引用,能夠用下面的正則表達式來實現:

Pattern.compile(
        ".*?        # 第一個重複字母前面的部分\n"    +
        "(.)        # 重複字母第一次出現\n"         +
        ".*?        # 重複字母間的部分\n"           +
        "\\1        # 重複字母第二次出現\n"         +
        ".*         # 重複字母第二次出現後的部分\n",
        Pattern.COMMENTS);
即便不加註釋,這個正則表達式也不難理解。那麼它的否認匹配,判斷一個字符串不包含重複字符的正則表達式怎麼寫呢?仔細考慮了一下,獲得下面的寫法:

Pattern.compile(
        "(?:            # 非捕獲分組,該分組中只包含一個字符\n" +
        "   (.)         # 一個字符的分組\n"                 +
        "   (?!.*?\\1)  # 該字符不能在後面的字符串中出現\n"    +
        ")+             # 全部的字符\n",
        Pattern.COMMENTS);

舉這個例子,主要是爲了說明Lookaround中可使用反向引用;不只如此,在Lookaround中實際可使用任意合法的正則表達式。並且,在Lookaround中還能夠定義分組,雖然Lookaround是"Zero-width Assertions",可是能夠在Lookaround中定義長度不爲零的分組。

上面的不包含重複字符的正則表達式,有一個經常使用的小技巧,"(?:(.)(X))+",其中"X"是一個Lookaround的表達式,這種對單個字符作約束的方式,在不少狀況下都會很用。可是,這種不指定位置,對全部字符都作Lookaround的作法,效率是很是差的,若是在意性能,必定要避免這種作法。

2.4 Lookaround嵌套

Lookaound表達式裏能夠是用任意正則表達式,因此咱們能夠在Lookaround中嵌套Lookaround表達式,這些表達式都是對同一個位置作約束。

好比有這麼個字符串"John has 2,000 dollars,Paul has $1,500,George has $1,200,Ringo has $1,600",如今要在","後添加空格,可是數字裏的","後不添加。下面嵌套的Lookaround能夠知足要求:

Pattern.compile(
        "(?<=,              # 前面是逗號,即在逗號的後面\n" +
        "   (?!             # Negative Lookahead\n"    +
        "       (?<=\\d,)   # 逗號前面是數字\n"           +
        "       (?=\\d)     # 逗號後面是數字\n"           +
        "   )               # \n"                      +
        ")                  # \n",
        Pattern.COMMENTS)
       .matcher(s)
       .replaceAll(" ");
Lookaround是"Zero-width",因此找到位置,直接用空格替換就是了。上面的表達式有"與"和"非的關係",根據德摩根定律, NOT (a AND b) === (NOT a OR NOT b) ,因此也能夠用下面的表達式來實現:

Pattern.compile(
        "(?<=,              # 前面是逗號,即在逗號的後面\n" +
        "   (?:             # Positive Lookahead\n"    +
        "       (?<!\\d,)   # 前面不是數字\n"            +
        "       |           # 或\n"                    +
        "       (?!\\d)     # 後面不是數字\n"            +
        "   )               # \n"                      +
        ")                  # \n",
        Pattern.COMMENTS)
       .matcher(s)
       .replaceAll(" ");

3. 其餘

使用Lookaround時必定要注意,不少正則表達式引擎只支持Lookahead,不支持Lookbehind;即便支持Lookbehind,也有限制,通常只能使用固定長度的表達式,不能用"*"或者"+"這些量詞。

還有很重要的一點,Lookaround是Atomic匹配,即一旦Lookaround成功,那麼就不會再對Lookaround作回溯,即便後面的匹配失敗,若是在Lookaround中使用了分組,必定要當心這點。

相關文章
相關標籤/搜索