Lookaround是Perl 5引進的特性,這個特性極大加強了正則表達式的能力,熟練掌握該特性,能夠幫助咱們運用正則表達式解決更復雜的問題。Lookaround有4種類型,下面的定義取自Java API : html
上面定義中的"Zero-width"是理解Lookaround特性的關鍵。經常使用的"^"、"$"、"\b"等Boundary Characters都是"Zero-width Assertions",即不消費字符,但斷定當前位置是否知足特定的要求,Lookaround實際也是"Zero-width Assertions"。Boundary Characters是系統預約義的"Zero-width Assertions",而Lookaround能夠看作用戶自定義的"Zero-width Assertions"。 java
接下來給幾個Lookaround應用的例子。 正則表達式
下面的例子除了Lookaround,主要用的都是一些正則表達式的基本特性,只有兩個可能不太常見的特性: api
先了解這兩個特性對理解後面的例子是有幫助的。 oracle
匹配全數字的字符串,正則表達式很容易寫,"\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會在模式的先後自動添加"^"和"$",因此就不必本身加了;但在有的語言或工具裏,須要本身添加"^"和"$"。
這些要求看似很複雜,實際上倒是異乎尋常地簡單,下面是Java代碼:
Pattern.compile( "(?=.*?[a-z]) # 至少包含小寫字母\n" + "(?=.*?[A-Z]) # 至少包含大寫字母\n" + "(?=.*?[\\d_]) # 至少包含一個數字或_\n" + "(?!\\d|.*\\d$) # 開頭和結尾不容許是數字\n" + "(?!.*?__) # 不容許出現連續的_\n" + "\\w{8,16} # 長度在8到16之間\n", Pattern.COMMENTS);
若是熟悉Lookaround,這個正則表達式是很是容易理解的,註釋已經說明地很清楚了;固然,上面的這個正則表達式不是惟一的寫法,更不是最優的寫法。
再看一個例子,怎麼判斷一個字符串是否包含重複的字符?若是瞭解反向引用,能夠用下面的正則表達式來實現:
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的作法,效率是很是差的,若是在意性能,必定要避免這種作法。
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(" ");
使用Lookaround時必定要注意,不少正則表達式引擎只支持Lookahead,不支持Lookbehind;即便支持Lookbehind,也有限制,通常只能使用固定長度的表達式,不能用"*"或者"+"這些量詞。
還有很重要的一點,Lookaround是Atomic匹配,即一旦Lookaround成功,那麼就不會再對Lookaround作回溯,即便後面的匹配失敗,若是在Lookaround中使用了分組,必定要當心這點。