日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

深入JS正则先行断言

發布時間:2025/3/15 javascript 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 深入JS正则先行断言 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

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



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

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

密碼需要滿足四個條件:

  • 6到10個單字字符 \w
  • 至少包含一個小寫字母 [a-z]
  • 至少包含三個大寫字母 [A-Z]
  • 至少包含一個數字 \d
  • 最初的設想就是在字符串的開頭先行檢測四次,每次檢測每個條件。

    條件一

    這里文章用 \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. )

    我們想在字符串的開頭斷言,因此需要用^做一個錨點定位,不需要重復聲明開頭,所以把^從斷言中拿出來:

    ^(?=\w{6,10}$)

    留意到,雖然我們已經用先行斷言檢測了整個字符串,但是我們的位置還沒有變,正則驗證錨點依然停留在字符串的開頭位置,只是做了先行判斷。意味著我們還可以繼續檢測整個字符串。

    條件二

    檢測小寫字母最容易想到的寫法是 .*[a-z],但是這種寫法 .* 一開始就會匹配到字符串的結尾,導致回溯,容易想到的寫法是 .*?[a-z] 這會導致更多的回溯。推薦的寫法是 [^a-z]*[a-z](當需要用到包含某些字符時,可以參考這種通用的寫法),將條件加入先行斷言:(?=[^a-z]*[a-z]) ,因此正則變成:

    ^(?=\w{6,10}$)(?=[^a-z]*[a-z])

    斷言里面依然沒有匹配任何字符,兩個斷言的位置是可以互換的。

    條件三

    類似條件二: (?=(?:[^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。有幾種寫法:

  • 字符減法,[\w-[Q]](js不支持)
  • [_0-9a-zA-PR-Z]
  • [^\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

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

    轉載于:https://www.cnblogs.com/JuFoFu/p/8267184.html

    新人創作打卡挑戰賽發博客就能抽獎!定制產品紅包拿不停!

    總結

    以上是生活随笔為你收集整理的深入JS正则先行断言的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。