正規表示式從入門到入坑

2022-07-18 18:00:20

正規表示式從入門到入坑

入坑前先介紹兩個輔助網站:
正規表示式測試網站:https://regex101.com
正規表示式思維導圖:https://regexper.com


正則基礎(入門)

1、元字元

進入正題,我們先去了解最基本的字元及其初步應用。

元字元 描述
\ 將下一個字元標記為一個特殊字元、或一個原義字元、或一個 向後參照、或一個八進位制跳脫符。
^ 匹配輸入字串的開始位置。如果設定了 RegExp 物件的 Multiline 屬性,^ 也匹配 '\n' 或 '\r' 之後的位置。
$ 匹配輸入字串的結束位置。如果設定了RegExp 物件的 Multiline 屬性,$ 也匹配 '\n' 或 '\r' 之前的位置。
\b 匹配一個單詞邊界,也就是指單詞和空格間的位置。
\B 匹配非單詞邊界。
\d 匹配一個數位字元。等價於 [0-9]。
\D 匹配一個非數位字元。等價於 [^0-9]。
\f 匹配一個換頁符。
\n 匹配一個換行符。
\r 匹配一個回車符。
\s 匹配任何空白字元,包括空格、製表符、換頁符等等。等價於 [ \f\n\r\t\v]。
\S 匹配任何非空白字元。等價於 [^ \f\n\r\t\v]。
\w 匹配字母、數位、下劃線。等價於'[A-Za-z0-9_]'。
\W 匹配非字母、數位、下劃線。等價於 '[^A-Za-z0-9_]'。

不運用起來的知識都不是自己的知識,所以看完總得寫點例子建立思維記憶。
比如:
1、匹配有hello開頭的字串:

let str = "hello world";
// 方法一
let reg = /^hello/;
reg.test(str); //true
// 方法二
let reg2 = /\bhello/;
reg2.test(str); //true

這麼一看\b和^好像功能差不多,其實不然,我們看下一個例子:

let str = "say hello";
let reg = /^hello/;
reg.test(str); //false
let reg2 = /\bhello/;
reg2.test(str); //true

可以看出\b並不是匹配開頭,它匹配的是單詞邊界。

2、匹配1開頭的11位數位的手機號碼:

let phone = "13388882983";
let reg = /^1\d\d\d\d\d\d\d\d\d\d$/

3、匹配8的數位、字母和下劃線組成的密碼:

let password = "A_1234_b"
let reg = /^\w\w\w\w\w\w\w\w$/

2、重複限定符

匹配每一個數位都得寫一個/d,程式碼怎麼可以這麼冗餘,我們追求的是優雅,那該怎麼寫呢?我們先看下面的限定符。

語法 描述
* 匹配前面的子表示式零次或多次。* 等價於{0,}。
+ 匹配前面的子表示式一次或多次。+ 等價於 {1,}。
? 匹配前面的子表示式零次或一次。? 等價於 {0,1}。
{n} n 是一個非負整數。匹配確定的 n 次。
{n,} n 是一個非負整數。至少匹配n 次。
{n,m} m 和 n 均為非負整數,其中n <= m。最少匹配 n 次且最多匹配 m 次。配n 次。

看完我們對剛剛的正則進行一點點優雅的改造。
1、匹配8的數位、字母和下劃線組成的密碼:

let password = "A_1234_b"
let reg = /^\w{8}$/

但是產品覺得限制8位元太不靈活了,它要8-15位,好,滿足它:

let password = "A_1234_b"
let reg = /^\w{8,15}$/

2、匹配以a開頭的,中間帶有一個或多個b,0個或多個c結尾的字串:

let reg = /^ab+c*$/;
let str = "abbc";
reg.test(str);  //true
let str2 = "abb";
reg.test(str2); //true
let str3 = "acc";
reg.test(str3); //false

3、區間 []

產品想法越來越多,密碼希望只能給使用者設定由大小寫字母和數位組成的8-15位密碼,摸了摸刀柄,決定繼續滿足它。

let reg = /^[A-Za-z0-9]{8,15}$/;
let password = "A123456789b";
reg.test(password);  //true
let password2 = "A_1234_b";
reg.test(password2);  //false

4、條件或

產品給你點了個贊然後提出了手機號碼驗證要優化的想法,調查發現VIP客戶的手機只有13、156、176、186開頭的11位數,要我們進行精確一點匹配。看了一眼它更長的刀,默默的寫下下面的正則:

let reg = /^(13\d|156|176|186)\d{8}$/

產品表示很滿意,結束了它的基本需求。

5、修飾符

標記也稱為修飾符,正規表示式的標記用於指定額外的匹配策略。
標記不寫在正規表示式裡,標記位於表示式之外,格式如下:

/pattern/flags
修飾符 含義 描述
i ignore - 不區分大小寫 將匹配設定為不區分大小寫,搜尋時不區分大小寫: A 和 a 沒有區別。
g global - 全域性匹配 查詢所有的匹配項。
m multiline - 多行匹配 使邊界字元 ^ 和 $ 匹配每一行的開頭和結尾,記住是多行,而不是整個字串的開頭和結尾。
s 特殊字元圓點 . 中包含換行符 \n 預設情況下的圓點 . 是匹配除換行符 \n 之外的任何字元,加上 s 修飾符之後, . 中包含換行符 \n。
let str = 'The fat cat eat the fish on the dish.'
let reg = /the/
str.match(reg); //["the",index:16]

通常正則匹配到第一個就會自動結束,因此我們只能匹配到the fish中的ths就結束了,如果我們希望把後面的"the"也匹配出來呢?這時候我們就要加一個全域性匹配修飾符了。

let str = 'The fat cat eat the fish on the dish.'
let reg = /the/g
str.match(reg); //["the","the"]

要是希望把開頭大寫的"The"也一起匹配出來呢?這時候我們需要再加多一個全域性匹配修飾符i。

let str = 'The fat cat eat the fish on the dish.'
let reg = /the/gi
str.match(reg); //["The","the","the"]

一般我們使用^或$只會匹配文章的開頭和結尾。

let str = 'The fat cat eat the fish on the dish.\nThe cat is beautiful.'
let reg = /^The/g
str.match(reg); //["The"]

但是如果我們需要匹配各個段落的開頭呢?

let str = 'The fat cat eat the fish on the dish.\nThe cat is beautiful.'
let reg = /^The/gm
str.match(reg); //["The","The"]

預設情況下的圓點 . 是匹配除換行符 \n 之外的任何字元。如:

let str = 'The fat cat eat the fish on the dish.\nThe cat is beautiful.'
let reg = /.+/
str.match(reg); //["The fat cat eat the fish on the dish.",...]

我們發現遇到\n的時候會切換了匹配,如果我們想繼續完全匹配下去,需要加上修飾符s。

let str = 'The fat cat eat the fish on the dish.\nThe cat is beautiful.'
let reg = /.+/s
str.match(reg); //['The fat cat eat the fish on the dish.\nThe cat is beautiful.',...]

6、運運算元優先順序

正規表示式從左到右進行計算,並遵循優先順序順序,這與算術表示式非常類似。
相同優先順序的從左到右進行運算,不同優先順序的運算先高後低。下表從最高到最低說明了各種正規表示式運運算元的優先順序順序:

運運算元 描述
\ 跳脫符
(), (?: ), (?=), [] 圓括號和方括號
*, +, ?, {n}, {n,}, {n,m} 限定符
^, $, \任何元字元、任何字元 定位點和序列(即:位置和順序)
| 替換,"或"操作
字元具有高於替換運運算元的優先順序,使得"m|food"匹配"m"或"food"。若要匹配"mood"或"food",請使用括號建立子表示式,從而產生"(m|f)ood"。

Js正則常用方法

1、定義正則

定義正則有下面兩種方式:

// 第一種
//RegExp物件。引數就是我們想要制定的規則。
let reg = new RegExp("a");
// 第二種
// 簡寫方法 推薦使用 書寫簡便、效能更好。
let reg = /a/;

2、test()

在字串中查詢符合正則的內容,若查詢到返回true,反之返回false。
例:

let reg = /^ab+c*$/;
let str = "abbc";
reg.test(str);  //true

3、match()

在字串中搜尋複合規則的內容,搜尋成功就返回內容,格式為陣列,失敗就返回null。
例:

let str = 'The fat cat eat the fish on the dish.';
let reg = /the/;
str.match(reg); //["the",index:16,...]

全域性匹配匹配到多個是則在陣列中按序返回,如:

let str = 'The fat cat eat the fish on the dish.';
let reg = /the/g;
str.match(reg); //["the","the"]

4、search()

在字串搜尋符合正則的內容,搜尋到就返回座標(從0開始,如果匹配的不只是一個字母,那隻會返回第一個字母的位置), 如果搜尋失敗就返回 -1 。
例:

let str = 'abc';
let reg = /bc/;
str.search(reg);    //1

5、exec()

和match方法一樣,搜尋符合規則的內容,並返回內容,格式為陣列。

let str = 'The fat cat eat the fish on the dish.';
let reg = /the/;
reg.exec(str); //["the",index:16]

如果是全域性匹配,可以通過while迴圈 找到每次匹配到的資訊。如:

let str = 'The fat cat eat the fish on the dish.';
let reg = /the/g;
let res = "";
while(res = reg.exec(str)){
    console.log(res);
}
/**
 * 匹配到兩次
 * 第一次:
 * [
    0: "the"
    groups: undefined
    index: 16
    input: "The fat cat eat the fish on the dish."
 * ]
 *
 * 第二次:
 * [
    0: "the"
    groups: undefined
    index: 28
    input: "The fat cat eat the fish on the dish."
 * ]
 */

6、replace()

查詢符合正則的字串,就替換成對應的字串。返回替換後的內容。
replace方法接收兩個引數,第一個是正則,第二個是替換字元/回撥方法,我們下面分別舉例說明:
例1:

let str = 'abc';
let reg = /a/;
str.replace(reg,"A");   //"Abc"

例2:

let str = 'abc';
let reg = /a/;
str.replace(reg,function(res){
    console.log(res);   //'a'
    return "A"; //不return則會返回undefine,輸出結果則會變成"undefinedbc"。
});   //"Abc"

除此以外replace還有更深入的用法,會放在後面入坑那裡再說。


正則進階(入坑)

1、零寬斷言

我們先去理解零寬和斷言分別是什麼。
--零寬:就是沒有寬度,在正則中,斷言只是匹配位置,不佔字元,也就是說,匹配結果裡是不會返回斷言本身。
--斷言:俗話的斷言就是「我斷定什麼什麼」,而正則中的斷言,就是說正則可以指明在指定的內容的前面或後面會出現滿足指定規則的內容。
總結:
零寬斷言正如它的名字一樣,是一種零寬度的匹配,它匹配到的內容不會儲存到匹配結果中去,最終匹配結果只是一個位置而已。
javascript只支援零寬先行斷言,而零寬先行斷言又可以分為正向零寬先行斷言,和負向零寬先行斷言。

1、 正向先行斷言(正向肯定預查):
--語法:(?=pattern)
--作用:匹配pattern表示式的前面內容,不返回本身。
我們來舉個栗子:

The fat cat eat the fish on the dish.

我們希望拿到fat前面的字串The。

let str = 'The fat cat eat the fish on the dish.'
let reg = /the(?=\sfat)/gi
str.match(reg);  //["The"]

2、負向先行斷言(正向否定預查):
--語法:(?!pattern)
--作用:匹配pattern表示式的前面內容,不返回本身。
那如果我們希望拿到不是fat前面的字串The呢?很簡單:

let str = 'The fat cat eat the fish on the dish.'
let reg = /the(?!\sfat)/gi
str.match(reg);  //["the","the"]

3、正向後行斷言(反向肯定預查):
--語法:(?<=pattern)
--作用:匹配pattern表示式的後面的內容,不返回本身。
繼續舉個栗子:

This is the flower cat and the civet cat.

我們希望拿到flower後面的cat。

let str = `This is the flower cat and the civet cat.`
let reg = /(?<=flower\s)cat/
str.match(reg);  //["cat",index:19]

4、 負向後行斷言(反向否定預查)
--語法:(?<!pattern)
--作用:匹配非pattern表示式的後面內容,不返回本身。
那如果我們希望拿到不是flower後面的cat呢?

let str = `This is the flower cat and the civet cat.`
let reg = /(?<!flower\s)cat/
str.match(reg);  //["cat",index:37]

2、捕獲和非捕獲

單純說到捕獲,他的意思是匹配表示式,但捕獲通常和分組聯絡在一起,也就是「捕獲組」。

捕獲組:匹配子表示式的內容,把匹配結果儲存到記憶體中中數位編號或顯示命名的組裡,以深度優先進行編號,之後可以通過序號或名稱來使用這些匹配結果。

而根據命名方式的不同,又可以分為兩種組:

1、數位編號捕獲組:
語法:(exp)
解釋:從表示式左側開始,每出現一個左括號和它對應的右括號之間的內容為一個分組,在分組中,第0組為整個表示式,第一組開始為分組。
舉個例子:

let phone = "020-85653333";
let reg = /(0\d{2})-(\d{8})/;
phone.match(reg);
//輸出結果:
[
    0: "020-85653333",
    1: "020",
    2: "85653333",
    groups: undefined,
    index: 0,
    input: "020-85653333"
]

其實分組個數是2,但是因為第0個為整個表示式本身,因此也一起輸出了。

2、 命名編號捕獲組:
語法:(?exp)
解釋:分組的命名由表示式中的name指定。
比如我們電話匹配加上命名:

let phone = "020-85653333";
let reg = /(?<quhao>0\d{2})-(?<num>\d{8})/;
phone.match(reg);
//輸出結果:
[
    0: "020-85653333",
    1: "020",
    2: "85653333",
    groups: {quhao: "020", num: "85653333"},
    index: 0,
    input: "020-85653333"
]

輸出結構可以看到,groups物件會以命名分組存放對應的匹配資料。

3、非捕獲組:
語法:(?:exp)
解釋:和捕獲組剛好相反,它用來標識那些不需要捕獲的分組,說的通俗一點,就是你可以根據需要去儲存你的分組。
如果我們不想匹配區號,那我們可以:

let phone = "020-85653333";
let reg = /(?:0\d{2})-(\d{8})/;
phone.match(reg);
//輸出結果:
[
    0: "020-85653333",
    1: "85653333",
    groups: undefined,
    index: 0,
    input: "020-85653333"
]

3、反向作用

捕獲會返回一個捕獲組,這個分組是儲存在記憶體中,不僅可以在正規表示式外部通過程式進行參照,也可以在正規表示式內部進行參照,這種參照方式就是反向參照。
根據捕獲組的命名規則,反向參照可分為:
1、數位編號組反向參照:\number
2、命名編號組反向參照:\k<name>
概念都是比較模糊,我們直接舉例說明:
我們有串字元'aabbcddddeffg',需要捕獲兩個連續相同的字母。我們需要解決的關鍵在於怎麼判斷上下兩個字母是相同。

let str = 'aabbcddddeffg';
let reg = /(\w)\1/g;
str.match(reg); // ["aa", "bb", "dd", "dd", "ff"]

這其中的\1是什麼意思呢?其實就是獲取捕獲的第一個分組,下面我們再舉例說明

let reg = /(\d)(\d)\d\1\2/;
let str =  'a12312b';
reg.test(str); //true
// 第一個(\d)捕獲匹配到了1,這時候會存在記憶體,\1=1,
// 第二個(\d)捕獲匹配到了2,\2=2,
// 此時正規表示式的可以解讀成/12\d12/
let str1 =  'a12345b';
reg.test(str1); //false

4、貪婪和非貪婪

1、貪婪匹配:
當正規表示式中包含能接受重複的限定符時,通常的行為是(在使整個表示式能得到匹配的前提下)匹配儘可能多的字元,這匹配方式叫做貪婪匹配。
貪婪匹配是重複限定符( *, +, ?, {n}, {n,}, {n,m} )特有的。
舉個例子:

let phone = "aibicidiei";
let reg = /a\w*i/g;
phone.match(reg);   //["aibicidiei"]

"ai"其實已經可以滿足匹配規則,但是在貪婪模式上它並不滿足,而是匹配到了最大能匹配的字元"aibicidiei"。

2、懶惰(非貪婪):
有貪婪模式那必然也有非貪婪模式。
特性:當正規表示式中包含能接受重複的限定符時,通常的行為是(在使整個表示式能得到匹配的前提下)匹配儘可能少的字元,這匹配方式叫做懶惰匹配。
懶惰量詞是在貪婪量詞後面加個?,如:

let phone = "aibicidiei";
let reg = /a\w*?i/g;
phone.match(reg);   //["ai"]

5、反義

語法:[^]
用得不多,就簡單提及一下,如不想匹配abc這三個字元:

let reg = /[^abc]/

6、replace

上文又提及過replace第二個引數可以是字串或者函數。
字串的時候,它有幾個特定字元。

字元 描述
$ 匹配字串左邊的字元
$' 匹配字串右邊的字元
$& 與正則相匹配的字串
$i (i:1-99) 匹配結果中對應的分組匹配結果

看著有點抽象,寫個程式碼就一目瞭然了。

'abc'.replace(/b/,"$");     //acc
'abc'.replace(/b/,"$`");    //aac
let str = '正規表示式從入門到入坑';
str.replace(/正規表示式/,'{$&}');   //"{正規表示式}從入門到入坑"
let str2 = 'xyz';
str2.replace(/(x)(y)(z)/,"$3$2$1"); //"zyx"

第二個引數是函數,且正則使用分組捕獲的時,函數會3個引數分別是:
0、匹配到的子字串;
1、匹配到的子串的索引位置;
2、源字串本身;

let str = "This is the flower cat and the civet cat.";
let reg = /cat/g;
str.replace(reg,function(){
    console.log(arguments);
    return "tiger";
})
// 第一次列印結果:
[
    0: "cat",
    1: 19,
    2: "This is the flower cat and the civet cat."
]
// 第二次列印結果:
[
    0: "cat",
    1: 37,
    2: "This is the flower cat and the civet cat."
]

當正則使用了分組捕獲時,函數引數依次是:
0、匹配到的子字串;
1、第一個分組項(如存在多個分組會按序緊跟返回);
(總分組項+2)、匹配到的子串的索引位置;
(總分組項+3)、源字串本身;
例:

let str = 'abc';
let reg = /(a)(b)(c)/g;
str.replace(reg,function(){
    console.log(arguments);
    return arguments[3]+arguments[2]+arguments[1];  //cba
})
// arguments列印結果:
[
    0: "abc",
    1: "a",
    2: "b",
    3: "c",
    4: 0,
    5: "abc"
]
// 等價於
str.replace(reg,"$3$2$1");

完結撒花,寫了這麼多其實只是正則的基本語法,只有在實際專案上運用上了才能見識到它的靈活性和博大精深,語法不多,但是用法卻很多,說一句花裡胡哨不為過,入坑之後的填坑就全靠自己了。