1. <em id="2qvri"><tr id="2qvri"></tr></em>
      1. 首頁»JavaScript»正則全攻略使用手冊,你確定不進來看看嗎

        正則全攻略使用手冊,你確定不進來看看嗎

        來源:Croc_wend 發布時間:2019-02-25 閱讀次數:

        前言

        正則表達式是軟件領域為數不多的偉大創作。與之相提并論是分組交換網絡、Web、Lisp、哈希算法、UNIX、編譯技術、關系模型、面向對象等。正則自身簡單、優美、功能強大、妙用無窮。

        學習正則表達式,語法并不難,稍微看些例子,多可照葫蘆畫瓢。但三兩篇快餐文章,鮮能理解深刻。再遇又需一番查找,竹籃打水一場空。不止正則,其他技術點同樣,需要系統的學習。多讀經典書籍,站在巨人肩膀前行。

        這里涉及的東西太多,我就著重講日常開發中可能會用到的內容,如果像深入理解的話推薦翻閱書籍《精通正則表達式》

        (所以簡單來說,學習正則就是投入高,收益低)(起初一看簡單易懂,深入了解過后感嘆正則的強大)

        全文略長,可以選擇感興趣的部分看

        1、介紹正則

        正則表達式嚴謹來講,是一種描述字符串結構模式的形式化表達方法。起始于數學領域,流行于 Perl 正則引擎。JavaScript 從 ES 3 引入正則表達式,ES 6 擴展

        對正則表達式支持。

        正則原理

        對于固定字符串的處理,簡單的字符串匹配算法(類KMP算法)相較更快;但如果進行復雜多變的字符處理,正則表達式速度則更勝一籌。那正則表達式具體匹配原理是什么?這就涉及到編譯原理的知識(編譯原理著實是我大三里面最頭疼的課程了)

        正則表達式引擎實現采用一種特殊理論模型:有窮自動機(Finite Automata)也叫有限狀態自動機(finite-state machine)具體的細節見文章底部的參考文檔

        字符組

        字符組 含義
        [ab] 匹配 a 或 b
        [0-9] 匹配 0 或 1 或 2 ... 或 9
        1 匹配 除 a、b 任意字符
        字符組 含義
        d 表示 [0-9],數字字符
        D 表示 [^0-9],非數字字符
        w 表示 [_0-9a-zA-Z],單詞字符,注意下劃線
        W 表示 [^_0-9a-zA-Z],非單詞字符
        s 表示 [ tvnrf],空白符
        S 表示 [^ tvnrf],非空白符
        . 表示 [^nru2028u2029]。通配符,匹配除換行符、回車符、行分隔符、段分隔符外任意字符

        量詞

        匹配優先量詞 忽略優先量詞 含義
        {m,n} {m,n}? 表示至少出現 m 次,至多 n 次
        {m,} {m,}? 表示至少出現 m 次
        {m} {m}? 表示必須出現 m 次,等價 {m,m}
        ? ?? 等價 {0,1}
        + +? 等價 {1,}
        * *? 等價 {0,}

        錨點與斷言

        正則表達式中有些結構并不真正匹配文本,只負責判斷在某個位置左/右側的文本是否符合要求,被稱為錨點。常見錨點有三類:行起始/結束位置、單詞邊界、環視。在 ES5 中共有 6 個錨點。

        錨點 含義
        ^ 匹配開頭,多行匹配中匹配行開頭
        $ 匹配結尾,多行匹配中匹配行結尾
        b 單詞邊界,w 與 W 之間位置
        B 非單詞邊界
        (?=p) 該位置后面字符要匹配 p
        (?!p) 該位置后面字符不匹配 p

        需要注意,\b 也包括 \w 與 ^ 之間的位置,以及 \w 與 $ 之間的位置。如圖所示。

        修飾符

        修飾符是指匹配時使用的模式規則。ES5 中存在三種匹配模式:忽略大小寫模式、多行模式、全局匹配模式,對應修飾符如下。

        修飾符 含義
        i 不區分大小寫匹配
        m 允許匹配多行
        g 執行全局匹配
        u Unicode 模式,用來正確處理大于\uFFFF的 Unicode 字符,處理四個字節的 UTF-16 編碼。
        y 粘連模式,和g相似都是全局匹配,但是特點是:后一次匹配都從上一次匹配成功的下一個位置開始,必須從剩余的第一個位置開始,這就是“粘連”的涵義。
        s dotAll 模式,大部分情況是用來處理行終止符的

        2、正則的方法

        字符串對象共有 4 個方法,可以使用正則表達式:match()、replace()、search()和split()。

        ES6 將這 4 個方法,在語言內部全部調用RegExp的實例方法,從而做到所有與正則相關的方法,全都定義在RegExp對象上。

        String.prototype.match 調用 RegExp.prototype[Symbol.match]

        String.prototype.replace 調用 RegExp.prototype[Symbol.replace]

        String.prototype.search 調用 RegExp.prototype[Symbol.search]

        String.prototype.split 調用 RegExp.prototype[Symbol.split]

        String.prototype.match

        String.prototype.replace

        字符串的replace方法,應該是我們最常用的方法之一了,這里我給詳細的說一下其中的各種使用攻略。

        replace函數的第一個參數可以是一個正則,或者是一個字符串(字符串沒有全局模式,僅匹配一次),用來匹配你想要將替換它掉的文本內容

        第二個參數可以是字符串,或者是一個返回字符串的函數。這里請注意,如果使用的是字符串,JS 引擎會給你一些 tips 來攻略這段文本:

        變量名 代表的值
        $$ 插入一個 "$"。
        $& 插入匹配的子串。
        $` 插入當前匹配的子串左邊的內容。
        $' 插入當前匹配的子串右邊的內容。
        $n 假如第一個參數是 RegExp對象,并且 n 是個小于100的非負整數,那么插入第 n 個括號匹配的字符串。提示:索引是從1開始,注意這里的捕獲組規則

        如果你不清楚捕獲組的順序,給你一個簡單的法則:從左到右數 >>> 第幾個 '(' 符號就是第幾個捕獲組

        (特別適用于捕獲組里有捕獲組的情況)(在函數模式里,解構賦值時會特別好用)

        $`:就是相當于正則匹配到的內容的左側文本

        $':就是相當于正則匹配到的內容右側文本

        $&:正則匹配到的內容

        $1 - $n :對應捕獲組

        如果參數使用的是函數,則可以對匹配的內容進行一些過濾或者是補充

        下面是該函數的參數:

        變量名 代表的值
        match 匹配的子串。(對應于上述的$&。)
        p1,p2, ... 假如replace()方法的第一個參數是一個RegExp 對象,則代表第n個括號匹配的字符串。(對應于上述的$1,$2等。)例如, 如果是用 /(\a+)(\b+)/這個來匹配, p1就是匹配的 \a+, p2 就是匹配的 \b+。
        offset 匹配到的子字符串在原字符串中的偏移量。(比如,如果原字符串是“abcd”,匹配到的子字符串是“bc”,那么這個參數將是1)
        string 被匹配的原字符串。

        一個示例,從富文本里面,匹配到里面的圖片標簽的地址

        可以說,使用函數來替換文本的話,基本上你想干嘛就干嘛

        String.prototype.search

        String.prototype.split

        RegExp.prototype.test

        和String.prototype.search 的功能很像,但是這個是返回布爾值,search返回的是下標,這個從語義化角度看比較適合校檢

        RegExp.prototype.exec

        3、正則常見的使用

        主要內容是ES6 里新增的修飾符(u,y,s)(g,m,i 就不說了)、貪婪和非貪婪模式、先行/后行斷言

        'u' 修飾符

        ES6 對正則表達式添加了u修飾符,含義為“Unicode 模式”,用來正確處理大于\uFFFF的 Unicode 字符。也就是說,會正確處理四個字節的 UTF-16 編碼。少說廢話,看圖

        但是很可惜的是 MDN給出的瀏覽器兼容性如下:(截止至2019.01.24),所以離生產環境上使用還是有點時間

        'y' 修飾符

        除了u修飾符,ES6 還為正則表達式添加了y修飾符,叫做“粘連”(sticky)修飾符。

        y修飾符的作用與g修飾符類似,也是全局匹配,后一次匹配都從上一次匹配成功的下一個位置開始。不同之處在于,g修飾符只要剩余位置中存在匹配就可,而y修飾符確保匹配必須從剩余的第一個位置開始,這也就是“粘連”的涵義。

        var s = 'aaa_aa_a';
        var r1 = /a+/g;
        var r2 = /a+/y;
        
        r1.exec(s) // ["aaa"]
        r2.exec(s) // ["aaa"]
        
        r1.exec(s) // ["aa"]
        r2.exec(s) // null

        上面代碼有兩個正則表達式,一個使用g修飾符,另一個使用y修飾符。這兩個正則表達式各執行了兩次,第一次執行的時候,兩者行為相同,剩余字符串都是_aa_a。由于g修飾沒有位置要求,所以第二次執行會返回結果,而y修飾符要求匹配必須從頭部開始,所以返回null。

        如果改一下正則表達式,保證每次都能頭部匹配,y修飾符就會返回結果了。

        var s = 'aaa_aa_a';
        var r = /a+_/y;
        
        r.exec(s) // ["aaa_"]
        r.exec(s) // ["aa_"]

        上面代碼每次匹配,都是從剩余字符串的頭部開始。

        使用lastIndex屬性,可以更好地說明y修飾符。

        const REGEX = /a/g;
        
        // 指定從2號位置(y)開始匹配
        REGEX.lastIndex = 2;
        
        // 匹配成功
        const match = REGEX.exec('xaya');
        
        // 在3號位置匹配成功
        match.index // 3
        
        // 下一次匹配從4號位開始
        REGEX.lastIndex // 4
        
        // 4號位開始匹配失敗
        REGEX.exec('xaya') // null

        上面代碼中,lastIndex屬性指定每次搜索的開始位置,g修飾符從這個位置開始向后搜索,直到發現匹配為止。

        y修飾符同樣遵守lastIndex屬性,但是要求必須在lastIndex指定的位置發現匹配。

        const REGEX = /a/y;
        
        // 指定從2號位置開始匹配
        REGEX.lastIndex = 2;
        
        // 不是粘連,匹配失敗
        REGEX.exec('xaya') // null
        
        // 指定從3號位置開始匹配
        REGEX.lastIndex = 3;
        
        // 3號位置是粘連,匹配成功
        const match = REGEX.exec('xaya');
        match.index // 3
        REGEX.lastIndex // 4

        實際上,y修飾符號隱含了頭部匹配的標志^。

        /b/y.exec('aba')
        // null

        上面代碼由于不能保證頭部匹配,所以返回null。y修飾符的設計本意,就是讓頭部匹配的標志^在全局匹配中都有效。

        下面是字符串對象的replace方法的例子。

        const REGEX = /a/gy;
        'aaxa'.replace(REGEX, '-') // '--xa'

        上面代碼中,最后一個a因為不是出現在下一次匹配的頭部,所以不會被替換。

        單單一個y修飾符對match方法,只能返回第一個匹配,必須與g修飾符聯用,才能返回所有匹配。

        'a1a2a3'.match(/a\d/y) // ["a1"]
        'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]

        y修飾符的一個應用,是從字符串提取 token(詞元),y修飾符確保了匹配之間不會有漏掉的字符。

        const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y;
        const TOKEN_G  = /\s*(\+|[0-9]+)\s*/g;
        
        tokenize(TOKEN_Y, '3 + 4')
        // [ '3', '+', '4' ]
        tokenize(TOKEN_G, '3 + 4')
        // [ '3', '+', '4' ]
        
        function tokenize(TOKEN_REGEX, str) {
          let result = [];
          let match;
          while (match = TOKEN_REGEX.exec(str)) {
            result.push(match[1]);
          }
          return result;
        }

        上面代碼中,如果字符串里面沒有非法字符,y修飾符與g修飾符的提取結果是一樣的。但是,一旦出現非法字符,兩者的行為就不一樣了。

        tokenize(TOKEN_Y, '3x + 4')
        // [ '3' ]
        tokenize(TOKEN_G, '3x + 4')
        // [ '3', '+', '4' ]

        上面代碼中,g修飾符會忽略非法字符,而y修飾符不會,這樣就很容易發現錯誤。

        很遺憾,這個的瀏覽器兼容性也不咋地

        但是,如果你的項目里有集成了babel,就可以使用以上的兩個修飾符了,他們分別是

        @babel-plugin-transform-es2015-sticky-regex

        @babel-plugin-transform-es2015-unicode-regex

        's' 修飾符

        正則表達式中,點(.)是一個特殊字符,代表任意的單個字符,但是有兩個例外。一個是四個字節的 UTF-16 字符,這個可以用u修飾符解決;另一個是行終止符(line terminator character)。

        所謂行終止符,就是該字符表示一行的終結。以下四個字符屬于”行終止符“。

        • U+000A 換行符(\n)
        • U+000D 回車符(\r)
        • U+2028 行分隔符(line separator)
        • U+2029 段分隔符(paragraph separator)

        雖然這個瀏覽器兼容性也很差,但是我們有方法來模擬它的效果,只是語義化上有點不友好

        /foo.bar/.test('foo\nbar')    // false
        /foo[^]bar/.test('foo\nbar')    // true
        /foo[\s\S]bar/.test('foo\nbar')        // true 我喜歡這種
        貪婪模式和非貪婪模式(惰性模式)

        貪婪模式:正則表達式在匹配時會盡可能多地匹配,直到匹配失敗,默認是貪婪模式。

        非貪婪模式:讓正則表達式僅僅匹配滿足表達式的內容,即一旦匹配成功就不再繼續往下,這就是非貪婪模式。在量詞后面加?即可。

        在某些情況下,我們需要編寫非貪婪模式場景下的正則,比如捕獲一組標簽或者一個自閉合標簽

        這時捕獲到了一組很奇怪的標簽,如果我們的目標是只想捕獲img標簽的話,顯然是不理想的,這時非貪婪模式就可以用在這里了

        只需要在量詞后加 ? 就會啟用非貪婪模式,在特定情況下是特別有效的

        先行/后行(否定)斷言

        有時候,我們會有些需求,具體是:匹配xxx前面/后面的xxx。很尷尬的是,在很久之前,只支持先行斷言(lookahead)和先行否定斷言(negative lookahead),不支持后行斷言(lookbehind)和后行否定斷言(negative lookbehind),在ES2018 之后才引入后行斷言

        名稱 正則 含義
        先行斷言 /want(?=asset)/ 匹配在asset前面的內容
        先行否定斷言 /want(?!asset)/ want只有不在asset前面才匹配
        后行斷言 /(?<=asset)want/ 匹配在asset后面的內容
        后行否定斷言 /(?<!asset)want/ want只有不在asset后面才匹配

        老實說,根據我的經驗,后行斷言的使用場景會更多,因為js 有很多的數據存儲是名值對的形式保存,所以很多時候我們想要通過"name="來取到后面的值,這時候是后行斷言的使用場景了

        先行斷言:只匹配 在/不在 百分號之前的數字

        后行斷言:

        這里引例 @玉伯也叫射雕 的一篇 博文的內容

        這里可以用后行斷言

        (?<=^|(第.+[章集])).*?(?=$|(第.+[章集]))

        “后行斷言”的實現,需要先匹配/(?<=y)x/的x,然后再回到左邊,匹配y的部分。這種“先右后左”的執行順序,與所有其他正則操作相反,導致了一些不符合預期的行為。

        首先,后行斷言的組匹配,與正常情況下結果是不一樣的。

        /(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"]
        /^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]

        上面代碼中,需要捕捉兩個組匹配。沒有“后行斷言”時,第一個括號是貪婪模式,第二個括號只能捕獲一個字符,所以結果是105和3。而“后行斷言”時,由于執行順序是從右到左,第二個括號是貪婪模式,第一個括號只能捕獲一個字符,所以結果是1和053。

        其次,“后行斷言”的反斜杠引用,也與通常的順序相反,必須放在對應的那個括號之前。

        /(?<=(o)d\1)r/.exec('hodor')  // null
        /(?<=\1d(o))r/.exec('hodor')  // ["r", "o"]

        上面代碼中,如果后行斷言的反斜杠引用(\1)放在括號的后面,就不會得到匹配結果,必須放在前面才可以。因為后行斷言是先從左到右掃描,發現匹配以后再回過頭,從右到左完成反斜杠引用。

        另外,需要提醒的是,斷言部分是不計入返回結果的。

        具名組匹配

        ES2018 引入了具名組匹配(Named Capture Groups),允許為每一個組匹配指定一個名字,既便于閱讀代碼,又便于引用。

        上面代碼中,“具名組匹配”在圓括號內部,模式的頭部添加“問號 + 尖括號 + 組名”(?<year>),然后就可以在exec方法返回結果的groups屬性上引用該組名。同時,數字序號(matchObj[1])依然有效。

        具名組匹配等于為每一組匹配加上了 ID,便于描述匹配的目的。如果組的順序變了,也不用改變匹配后的處理代碼。

        如果具名組沒有匹配,那么對應的groups對象屬性會是undefined。

        具名組匹配 × 解構賦值

        具名組引用

        如果要在正則表達式內部引用某個“具名組匹配”,可以使用\k<組名>的寫法。

        4、常用正則

        我這里比較推薦一個正則可視化的網站:https://regexper.com/ 在上面貼上你的正則,會以圖形化的形式展示出你的正則匹配規則,之后我們就可以大致上判斷我們的正則是否符合預期(貌似需要科學上網)

        如果想通過字符串來生成正則對象的話,有兩種方式,一種是字面量方式,另一種是構造函數

        構造函數:new Regexp('content', 'descriptor')

        字面量模式(請做好try-catch處理):

        const input = '/123/g'
        const regexp = eval(input)
        校驗密碼強度

        密碼的強度必須是包含大小寫字母和數字的組合,不能使用特殊字符,長度在8-10之間。

        ^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$

        非全數字 全字母的 6-15位密碼 先行否定斷言

        /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,15}$/
        校驗中文

        字符串僅能是中文。

        ^[\u4e00-\u9fa5]{0,}$
        校驗身份證號碼

        15位

        ^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$

        18位

        ^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}([0-9]|X)$
        校驗日期

        “yyyy-mm-dd“ 格式的日期校驗,已考慮平閏年。

        ^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$
        提取URL鏈接

        下面的這個表達式可以篩選出一段文本中的URL。

        ^(f|ht){1}(tp|tps):\/\/([\\w-]+\.)+[\w-]+(\/[\w- ./?%&=]*)?
        提取圖片標簽的地址

        假若你想提取網頁中所有圖片信息,可以利用下面的表達式。

        /<img [^>]*?src="(.*?)"[^>]*?>/g;
        \*[img][^\>]*[src] *= *[\"\']{0,1}([^\"\'\ >]*)

        5、注意事項

        使用非捕獲型括號

        如果不需要引用括號內文本,請使用非捕獲型括號 (?:...)。這樣不但能夠節省捕獲的時間,而且會減少回溯使用的狀態數量。

        消除不必要括號

        非必要括號有時會阻止引擎優化。比如,除非需要知道 .* 匹配的最后一個字符,否則請不要使用 (.)* 。

        不要濫用字符組

        避免單個字符的字符組。例如 [.] 或 [*],可以通過轉義轉換為 \. 和 \*\。

        使用起始錨點

        除非特殊情況,否則以 .* 開頭的正則表達式都應該在最前面添加 ^。如果表達式在字符串的開頭不能匹配,顯然在其他位置也不能匹配。

        從量詞中提取必須元素

        用 xx* 替代 x+ 能夠保留匹配必須的 “x”。同樣道理,-{5,7} 可以寫作 -----{0,2}。(可讀性可能會差點)

        提取多選結構開頭的必須元素

        用 th(?:is|at) 替代 (?:this|that),就能暴露除必須的 “th”。

        忽略優先還是匹配優先?

        通常,使用忽略優先量詞(惰性)還是匹配優先量詞(貪婪)取決于正則表達式的具體需求。舉例來說,/^.*:/ 不同于 ^.*?:,因為前者匹配到最后的冒號,而后者匹配到第一個冒號。總的來說,如果目標字符串很長,冒號會比較接近字符串的開頭,就是用忽略優先。如果在接近字符串末尾位置,就是用匹配優先量詞。

        拆分正則表達式

        有時候,應用多個小正則表達式的速度比單個正則要快的多。“大而全”的正則表達式必須在目標文本中的每個位置測試所有表達式,效率較為低下。典型例子可以參考前文, 去除字符串開頭和結尾空白。

        將最可能匹配的多選分支放在前頭

        多選分支的擺放順序非常重要,上文有提及。總的來說,將常見匹配分支前置,有可能獲得更迅速更常見的匹配。

        避免指數級匹配

        從正則表達式角度避免指數級匹配,應盡可能減少 + * 量詞疊加,比如 ([^\\"]+)* 。從而減少可能匹配情形,加快匹配速度。

        6、小結

        正則表達式想要用好,需要一定的經驗,個人經驗來看,需要把你想法中的需要寫出來,然后通過搭積木的形式,把一個個小的匹配寫出來,然后再組合出你想要的功能,這是比較好的一種實現方法。

        如果說遇到了晦澀難懂的正則,也可以貼到上面提到的正則可視化網站里,看下它的匹配機制。

        對于前端來說,正則的使用場景主要是用戶輸入的校檢,富文本內容的過濾,或者是對一些url或者src的過濾,還有一些標簽的替換之類的,掌握好了還是大有裨益的,起碼以前雄霸前端的 jQ 的選擇器 sizzle 就是用了大量正則。

        最后,如果大家覺得我有哪里寫錯了,寫得不好,有其它什么建議(夸獎),非常歡迎大家指出和斧正。也非常歡迎大家跟我一起討論和分享!

        QQ群:WEB開發者官方群(515171538),驗證消息:10000
        微信群:加小編微信 849023636 邀請您加入,驗證消息:10000
        提示:更多精彩內容關注微信公眾號:全棧開發者中心(fsder-com)
        網友評論(共0條評論) 正在載入評論......
        理智評論文明上網,拒絕惡意謾罵 發表評論 / 共0條評論
        登錄會員中心
        江苏快3投注技巧