這裏是 Mastering Lookahead and Lookbehind 文章的簡單翻譯,這篇文章是在本身搜索問題的時候stackoverflow上回答問題的人推薦的,看完以爲寫得很不錯。這裏的簡單翻譯是指略去了一些js不具有的內容,再者原文實在是太長了,因此也去掉了一些沒有實質內容的話,同時也加入了不少本身的理解。若是須要深刻理解js的斷言機制,仍是推薦先去看完MDN的基礎再去看這篇文章(http://www.rexegg.com/regex-lookarounds.html)效果會比較好。html
一開始是對零寬斷言的簡單概念介紹,略去。正則表達式
密碼須要知足四個條件:數組
最初的設想就是在字符串的開頭先行檢測四次,每次檢測每一個條件。app
這裏文章用 \A 匹配字符串開頭,用 \z 匹配字符串結尾,和 js 不同,改了一下
第一個條件很簡單:^\w{6,10}$
。加入先行斷言:(?=^\w{6,10}$)
,先行斷言:在字符串開頭的位置後面,是6到10個字符,以及字符串的結尾。編輯器
(at the current position in the string, what follows is the beginning of the string, six to ten word characters, and the very end of the string. )ide
咱們想在字符串的開頭斷言,所以須要用^作一個錨點定位,不須要重複聲明開頭,因此把^從斷言中拿出來:函數
^(?=\w{6,10}$)
留意到,雖然咱們已經用先行斷言檢測了整個字符串,可是咱們的位置尚未變,正則驗證錨點依然停留在字符串的開頭位置,只是作了先行判斷。意味着咱們還能夠繼續檢測整個字符串。性能
檢測小寫字母最容易想到的寫法是 .*[a-z]
,可是這種寫法 .* 一開始就會匹配到字符串的結尾,致使回溯,容易想到的寫法是 .*?[a-z]
這會致使更多的回溯。推薦的寫法是 [^a-z]*[a-z]
(當須要用到包含某些字符時,能夠參考這種通用的寫法),將條件加入先行斷言:(?=[^a-z]*[a-z])
,所以正則變成:優化
^(?=\w{6,10}$)(?=[^a-z]*[a-z])
斷言裏面依然沒有匹配任何字符,兩個斷言的位置是能夠互換的。atom
相似條件二: (?=(?:[^A-Z]*[A-Z]){3})
正則變成了:
^(?=\w{6,10}$)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3})
相似的:(?=\D*\d)
正則變成了:
^(?=\w{6,10}$)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3})(?=\D*\d)
此時,咱們在字符串開頭斷言,並先行檢測了四次判讀了四種條件,依然沒有匹配任何字符,可是驗證了密碼。
檢查完畢後,正則檢測的位置依然停留在字符串開頭,能夠用一個簡單的.*
去匹配整個字符串,由於無論.*
匹配到了什麼,都是通過驗證的。所以:
^(?=\w{6,10}$)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3})(?=\D*\d).*
檢查這個正則裏的先行斷言,能夠留意到\w{6,10}$
這個表達式檢查了字符串的全部字符,所以能夠用他匹配整個字符串而不是用.*
,所以能夠減小一個先行判斷簡化正則:
^(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3})(?=\D*\d)\w{6,10}$
總結這個結果,若是檢查n
個條件,正則至多須要n-1
個先行判斷。甚至可以把幾個先行判斷合併。
實際上,除了\w{6,10}$
恰好匹配了整個字符串外,其餘的幾個先行判斷也能夠經過改寫匹配整個字符串,好比(?=\D*\d)能夠加一個簡單的.*$
匹配到字符串結尾:
^(?=\w{6,10}$)(?=[^a-z]*[a-z])(?=(?:[^A-Z]*[A-Z]){3})\D*\d.*$
此外,爲何要在.*
後面加$
,難道不能匹配到字符串結尾麼?由於點符號不匹配換行符(除非在DOTALL mode
下,即點匹配全部),所以.*
只能匹配到第一行的末尾,若是有換行則沒法匹配到,$
保證了咱們不只到達一行的結尾,也到達了字符串的結尾。
在這個正則表達式裏,開頭的(?=\w{6,10}$)
已經匹配到告終尾,因此後面的$
不是很必要。
在這個例子裏,由於三個先行斷言都沒有改變位置,因此能夠互換。雖然結果沒有影響,可是會影響性能,應該把容易驗證失敗的先行斷言放在前面。
實際上,咱們把^
放在前面就是考慮了這個狀況,由於^
也沒有匹配任何字符移動正則匹配錨點,他也能夠和其餘先行斷言互換,可是這會帶來問題。
首先,在DOTALL mode
下,後行負向斷言(?<!.)
能夠匹配開頭,即前面沒有任何字符,非DOTALL mode
下,有(?<![\D\d])
匹配開頭。
如今假設把^
放在第四個位置,在三個先行斷言後,這時若是第三個斷言失效了,那麼正則引擎會到第二個位置繼續從第一個先行斷言匹配,就這樣不停地改變位置匹配直到所有位置都失敗。雖然只要匹配到^
就不會從其餘位置繼續判斷,可是正則引擎由於提早失敗而沒法到達^
。
放第一位時,除了開頭位置外,其餘位置在第一次匹配^
就失敗了,所以效率高些。
這裏是一些初學者常犯的錯誤。
好比用A(?=5)
匹配AB25
,不理解地方在於先行斷言裏的5
是緊跟A
後的位置,若是要匹配後面的位置,須要用(?=[^5]*5)
。
用A(?=5)(?=[A-Z])
匹配A5B
,依然是位置不變問題,應該是用A(?=5[A-Z])
即上面密碼驗證的例子,即一個字符串知足多個條件。每一個條件都是檢測整個字符串。
好比匹配非Q
字符外的單字字符\w
。有幾種寫法:
(?!Q)\w
\w
匹配了一個字符。這個寫法不只容易理解,也容易附加拓展,好比不包含Q和K,那麼就是:(?![QK])\w`
後行斷言:
\w(?<!Q)
限制標誌(token)的匹配範圍。
舉個例子,若是想要匹配不以{END}開頭的任何字符,能夠用:
(?:(?!{END}).)*
每個.
標誌都被(?!{END})
調整,斷言點標誌不能是{END}
的開頭,這個技巧叫tempered greedy token
另一種方案有點過於複雜,略去。
在第一個#START#
出現後匹配後面的全部字符寫法:
(?<=#START#).*
或者匹配字符串的全部字符,除了#END#
.*?(?=#END#)
兩個斷言能夠合併:
(?<=#START#).*?(?=#END#)
給你一個文件,裏面都是駝峯命名的電影標題,好比HaroldAndKumarGoToWhiteCastle
,爲了方便閱讀,須要在大小寫之間插入空格,下面的正則匹配這些位置:
(?<=[a-z])(?=[A-Z])
在編輯器的正則匹配查找中,能夠用這個去匹配這些位置,並用空格代替。(這裏能想到/[a-z][A-Z]/g
一樣可以查找,可是找到的不是位置,因此替換起來就不是那麼方便了。
相似上面的例子,就能夠分割大小寫之間的位置,在不少語言中,用split函數加上正則能夠返回一個單詞數組。
有時候須要在同一個單詞裏作屢次匹配,舉個例子,想在ABCD
中匹配ABCD,BCD,CD和D,能夠用:
(?=(\w+))
這個還蠻好理解的,會匹配四個位置,"","A",,"","B","","C","","D",""。不過至於說怎麼提取這四個部分,還沒找到合適的方法。
零寬斷言,錨點,邊界在包含標誌的正則表達式中,容許正則引擎返回匹配的字符串。舉個例子(?<=start_)\d+
,正則引擎會返回數字,可是不包括前綴start_
。
下面是一些應用:
即相似密碼驗證例子
相似插入空格例子
相似插入空格例子
同一個單詞裏作屢次匹配例子
零寬斷言有兩個選擇去定位,在文本前和文本後,通常來說,其中一個性能更高。
\d+(?= dollars)
和(?=\d+ dollars)\d+
都匹配100 dallars
中的100
,可是前者性能更佳,由於他只匹配\d+
一次。(這裏寫一下本身對第二個式子的理解,第二個式子實際上是先斷言當前位置的後面是\d+ dollars
,而後匹配斷言中的字符串中的\d+
)。
\d+(?! dollars)
和(?!\d+ dollars)\d+
都匹配100 pesos
中的100
,可是前者性能更佳,同上。
後面還有兩個後行斷言的例子,js不支持就不列舉了。
這些例子的不一樣在於匹配的先後。這裏的說明不是要就糾結於位置,只是可以知道並感受到這樣寫正則的效率,經過練習,會慢慢熟悉這些不一樣並寫出性能更高的正則。
這個部分涉及到的是零寬斷言的嵌套,這裏只說明一下里面舉的例子,由於js不支持後行斷言,這裏講的東西做用就不大了。
匹配下劃線之間的數字:_12_
,有不少方法,文中提出的新方法是:
(?<=_(?=\d{2}_))\d+
即,當前位置前面斷言匹配了下劃線_
,同時下劃線的後面斷言匹配了\d{2}_,即整個後行斷言匹配的是_\d{2}_
,而當前的位置在_
和\d{2}
之間,後面用\d+
匹配數字。
匹配後面至多有一個下劃線的數字:
\d+(?=_(?!_))
還有一種不太優雅的寫法是:\d+(?=(?!__)_)
匹配前面至多有一個下劃線的數字:
(?<=(?<!_)_)\d+
還有一種不太優雅的寫法是:(?<=_(?<!__))\d+
即多個嵌套,這個有點複雜,就是超過一次嵌套,多個條件一塊兒判斷。這裏就不列舉了,能夠看看這個例子:
(?<=(?<!(?<!X)_)_)\d+
表示數字前綴不能是多個下劃線,除了X__
這種狀況。
_rabbit _dog _mouse DIC:cat:dog:mouse
在這個字符串中,DIC後面是容許的動物名,咱們要匹配前面_tokens
中在容許動物名內的。
_(\w+)\b(?=.*:\1\b)
得到_dog
和_mouse
。
翻轉一下:
_(?=.*:(\w+)\b)\1\b
這樣只匹配到了_mouse
這個地方很神奇,稍微講一下。第一個正則還蠻好理解的每次正向斷言都拿前面的\1
捕獲去匹配後面,按從左往右屢次匹配結果到兩個結果。第二個正則就特殊,捕獲是放在正向斷言裏的,正向斷言因爲貪婪匹配會直接到了_mouse
的下劃線後的位置,而後正則引擎跳出正向斷言去匹配\1
,匹配到mouse
成功。匹配結束。這裏的重點是,正則引擎並不能在正向判斷裏面回溯,只要跳出了正向斷言,就不會再進去。所以這裏的正向斷言只會匹配到mouse
。我一開始想到加個非貪婪,那麼就只會匹配到cat了。
匹配一個包含一個單詞的字符串,裏面有一位數字:
^(?=\D*\d)\w+$
這裏須要考慮的問題是^
錨點是否有必要。
這裏的重點在於^
可以減小錯誤的次數,若是沒有^
,正則引擎會在每一個位置都去匹配,只有在全部位置都錯誤後纔會返回錯誤,可是加了^
,只要開頭匹配錯誤引擎就會中止。雖然在匹配成功的狀況下,兩種狀況返回是同樣的,可是在性能上差異卻很大。
不過有時候咱們但願正則引擎匹配多個位置,好比上面的例子:(?=(\w+))
。在ABCD
中匹配了四次,得到了四個咱們想要的結果。
後記提到了上面講到的[^a-z]*[a-z]
優化爲[^a-z]*+[a-z]
,不過一看就知道js不支持,這個的優化點在於,若是發現匹配不成功,有些不夠智能的引擎會回溯前面的非小寫字符,去匹配後面的小寫字母這樣顯而易見的無效回溯。
這篇文章的大體解釋就到這裏,後面須要在瞭解一下關於正則引擎的問題了。
翻譯文章來源:
http://www.rexegg.com/regex-lookarounds.html
本文來源:JuFoFu
本文地址:http://www.cnblogs.com/JuFoFu/p/7719916.html
水平有限,錯誤歡迎指正,轉載請註明出處。