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

前言

正則表達式是軟件領域爲數不多的偉大創作。與之相提並論是分組交換網絡、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

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

下面是字符串對象的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"]

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

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

/(?<=(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 就是用了大量正則。

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

寫在最後

感謝以下參考文檔的作者的分享

精通正則表達式(第三版)
前端正則二三事 @代碼君的自白

ES6 入門 -- 正則的拓展 @阮一峯


  1. ab
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章