深刻JS正則先行斷言

這裏是 Mastering Lookahead and Lookbehind 文章的簡單翻譯,這篇文章是在本身搜索問題的時候stackoverflow上回答問題的人推薦的,看完以爲寫得很不錯。這裏的簡單翻譯是指略去了一些js不具有的內容,再者原文實在是太長了,因此也去掉了一些沒有實質內容的話,同時也加入了不少本身的理解。若是須要深刻理解js的斷言機制,仍是推薦先去看完MDN的基礎再去看這篇文章(http://www.rexegg.com/regex-lookarounds.html)效果會比較好html



一開始是對零寬斷言的簡單概念介紹,略去。正則表達式

先行斷言例子:簡單密碼驗證

密碼須要知足四個條件:數組

  1. 6到10個單字字符 \w
  2. 至少包含一個小寫字母 [a-z]
  3. 至少包含三個大寫字母 [A-Z]
  4. 至少包含一個數字 \d

最初的設想就是在字符串的開頭先行檢測四次,每次檢測每一個條件。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。有幾種寫法:

  1. 字符減法,[\w-[Q]](js不支持)
  2. [_0-9a-zA-PR-Z]
  3. [^\WQ]
    先行斷言寫法:(?!Q)\w
    在先行斷言當前位置後面不是Q後,\w匹配了一個字符。這個寫法不只容易理解,也容易附加拓展,好比不包含Q和K,那麼就是:
(?![QK])\w`

後行斷言:

\w(?<!Q)

Tempering the scope of a token 標誌範圍調整

限制標誌(token)的匹配範圍。
舉個例子,若是想要匹配不以{END}開頭的任何字符,能夠用:

(?:(?!{END}).)*

每個.標誌都被(?!{END})調整,斷言點標誌不能是{END}的開頭,這個技巧叫tempered greedy token
另一種方案有點過於複雜,略去。

Delimiter 分隔符

在第一個#START#出現後匹配後面的全部字符寫法:

(?<=#START#).*

或者匹配字符串的全部字符,除了#END#

.*?(?=#END#)

兩個斷言能夠合併:

(?<=#START#).*?(?=#END#)

Inserting Text at a Position 在位置插入文本

給你一個文件,裏面都是駝峯命名的電影標題,好比HaroldAndKumarGoToWhiteCastle,爲了方便閱讀,須要在大小寫之間插入空格,下面的正則匹配這些位置:

(?<=[a-z])(?=[A-Z])

在編輯器的正則匹配查找中,能夠用這個去匹配這些位置,並用空格代替。(這裏能想到/[a-z][A-Z]/g一樣可以查找,可是找到的不是位置,因此替換起來就不是那麼方便了。

Splitting a String at a Position 在某位置分割字符串

相似上面的例子,就能夠分割大小寫之間的位置,在不少語言中,用split函數加上正則能夠返回一個單詞數組。

Finding Overlapping Matches 查找重疊匹配

有時候須要在同一個單詞裏作屢次匹配,舉個例子,想在ABCD中匹配ABCD,BCD,CD和D,能夠用:

(?=(\w+))

這個還蠻好理解的,會匹配四個位置,"","A",,"","B","","C","","D",""。不過至於說怎麼提取這四個部分,還沒找到合適的方法。

Zero-Width Matches 0寬度匹配

零寬斷言,錨點,邊界在包含標誌的正則表達式中,容許正則引擎返回匹配的字符串。舉個例子(?<=start_)\d+,正則引擎會返回數字,可是不包括前綴start_
下面是一些應用:

Validation 驗證

即相似密碼驗證例子

Inserting 插入

相似插入空格例子

Splitting 分割

相似插入空格例子

Overlapping Matches 重疊匹配

同一個單詞裏作屢次匹配例子

Positioning the Lookaround 零寬斷言定位

零寬斷言有兩個選擇去定位,在文本前和文本後,通常來說,其中一個性能更高。

Lookahead 先行斷言

\d+(?= dollars)(?=\d+ dollars)\d+都匹配100 dallars中的100,可是前者性能更佳,由於他只匹配\d+一次。(這裏寫一下本身對第二個式子的理解,第二個式子實際上是先斷言當前位置的後面是\d+ dollars,而後匹配斷言中的字符串中的\d+)。

Negative Lookahead 先行負向斷言

\d+(?! dollars)(?!\d+ dollars)\d+都匹配100 pesos中的100,可是前者性能更佳,同上。

後面還有兩個後行斷言的例子,js不支持就不列舉了。
這些例子的不一樣在於匹配的先後。這裏的說明不是要就糾結於位置,只是可以知道並感受到這樣寫正則的效率,經過練習,會慢慢熟悉這些不一樣並寫出性能更高的正則。

Lookarounds that Look on Both Sides: Back to the Future

這個部分涉及到的是零寬斷言的嵌套,這裏只說明一下里面舉的例子,由於js不支持後行斷言,這裏講的東西做用就不大了。
匹配下劃線之間的數字:_12_,有不少方法,文中提出的新方法是:

(?<=_(?=\d{2}_))\d+

即,當前位置前面斷言匹配了下劃線_,同時下劃線的後面斷言匹配了\d{2}_,即整個後行斷言匹配的是_\d{2}_,而當前的位置在_\d{2}之間,後面用\d+匹配數字。

Compound Lookahead and Compound Lookbehind 複合先行和複合後行

在標誌後至多有一個字符

匹配後面至多有一個下劃線的數字:

\d+(?=_(?!_))

還有一種不太優雅的寫法是:\d+(?=(?!__)_)

標誌前至多有一個字符

匹配前面至多有一個下劃線的數字:

(?<=(?<!_)_)\d+

還有一種不太優雅的寫法是:(?<=_(?<!__))\d+

Multiple Compounding 多重複合

即多個嵌套,這個有點複雜,就是超過一次嵌套,多個條件一塊兒判斷。這裏就不列舉了,能夠看看這個例子:

(?<=(?<!(?<!X)_)_)\d+

表示數字前綴不能是多個下劃線,除了X__這種狀況。

The Engine Doesn't Backtrack into Lookarounds……because they're atomic

_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了。

Fixed-Width, Constrained-Width and Infinite-Width Lookbehind 負向斷言,略去

Lookarounds (Usually) Want to be Anchored

匹配一個包含一個單詞的字符串,裏面有一位數字:

^(?=\D*\d)\w+$

這裏須要考慮的問題是^錨點是否有必要。
這裏的重點在於^可以減小錯誤的次數,若是沒有^,正則引擎會在每一個位置都去匹配,只有在全部位置都錯誤後纔會返回錯誤,可是加了^,只要開頭匹配錯誤引擎就會中止。雖然在匹配成功的狀況下,兩種狀況返回是同樣的,可是在性能上差異卻很大。

One Exception: Overlapping Matches

不過有時候咱們但願正則引擎匹配多個位置,好比上面的例子:(?=(\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

水平有限,錯誤歡迎指正,轉載請註明出處。

相關文章
相關標籤/搜索