瞎說系列之正規表示式入門

軒少發表於2019-02-03

前言

一直以來也沒有寫文章的習慣,前不久在公司內部進行技術分享之後,發現寫技術文章還是非常重要的,因此將之前分享過的內容整理出來,寫成此文章。之後會不定期更新瞎說系列教程,敬請期待。如有不妥,歡迎大家不吝賜教。

簡介

正規表示式,又稱規則表示式。(在程式碼中常簡寫為regex)。正規表示式通常被用來檢索,替換那些符合某個模式的文字。

正規表示式是對字串(包括普通字元(例如:a到z之間的字母)和特殊字元(又稱為元字元))操作的一種邏輯公式。用事先定義好的一些特定字元,以及這些特定字元的組合,組成一個“規則字串”,這個規則字串用來表達對字串的一種過濾邏輯。正規表示式是一種文字模式,模式描述在搜尋文字時要匹配的一個或多個字串。

語法

下面詳細的介紹正規表示式的語法。

字元類

元字元是擁有特殊意義的字元。

常用元字元
字元 等價 含義
. [^\n\r] 匹配任何單個字元,除了換行符和回車符。
\w [a-zA-Z_0-9] 匹配任何單詞字元(數字,字母,下劃線)
\W [^a-zA-Z_0-9] 匹配任何非單詞字元
\d [0-9] 匹配數字。
\D [^0-9] 匹配非數字。
\s [\n\f\r\t\v\x0B] 匹配空白字元。(包括空格符,製表符,回車符,換行符,垂直換行符,換頁符)。
\n \n 匹配換行符。
\f \f 匹配換頁符。
\r \r 匹配回車符。
\t \t 匹配製表符。
\v \v 匹配垂直製表符。
\ \ 轉義字元,轉義後面字元所代表的含義(比如\*匹配的是*)
| | 表示字元匹配是或的關係(比如x|y匹配的是x或y中的一個字元)。
\0 Null 匹配null

換行符和回車符的區別可以參考阮一峰的文章

量詞

量詞用來表示匹配的數量。

字元 等價 含義
n? n{0,1} 匹配任何包含0個或1個的字串(最多有一個)。
n+ n{1,} 匹配任何包含至少1個的字串(至少一個)。
n* n{0,} 匹配任何0個或多個的字串。
n{x} n{x} 匹配包含x個n的序列的字串。
n{x,} n{x,} 匹配包含至少x個n的序列的字串。
n{x,y} n{x,y} 匹配出現x次但是不超過y次的n的序列的字串。
定位符

定位符顧名思義用來確定位置的字元。

字元 含義
^ 單獨使用表示匹配表示式的開始。
$ 匹配表示式的結尾。
\b 匹配一個單詞字元的邊界。單詞字元後面或前面不與另外的單詞字元相鄰,可以理解為匹配一個單詞的開始或者結束。(比如:/\bx/可以匹配s x)。
\B 匹配非單詞的邊界。可以理解為查詢不處在單詞的開始或者結束的位置。
標誌字元
字元 含義
g 匹配全域性。
i 不區分大小寫。
m 多行搜尋。
範圍類
字元 含義
[0-9] 匹配0到9之間的任意一個數字
[a-z] 匹配a到z之間的任意一個字元。
[A-Z] 匹配A到Z之間的任意一個字元。
[^0-9] 匹配不在0到9之間的任意一個字元。
其他規則
字元 含義
(pattern) 匹配pattern並獲取這一匹配,即捕獲組。
(?:pattern) 匹配pattern但不獲取匹配結果,即非捕獲組。
(?=pattern) 正向肯定預查,在任何匹配pattern的字串開始處匹配查詢字串。這是一個非捕獲組匹配。例如/windows(?=95)/可以匹配windows95中的windows,但是不能匹配windows98中的windows。可以理解為匹配(?=pattern)外面且在前面的內容。
(?!pattern) 正向否定預查,在任何不匹配pattern的字串開始處匹配查詢字串。這也是一個非捕獲組匹配。例如/windows(?!95)/可以匹配windows98中的windows,但是不能匹配windows95中的windows
(?<=pattern) 反向肯定預查,與正向肯定預查相似,只是方向相反。正向肯定預查是匹配前面的內容,而反向肯定預查是匹配後面的內容。例如(?<=95)windows能匹配95windows中的windows,但是不能匹配98windows的windows,也不能匹配windows95中的windows。
(?<!pattern) 反向否定預查,與正向否定預查相似,只是方向相反。例如(?<!95)windows能匹配98windows中的windows,但是不能匹配95windows中的windows,也不能匹配windows95中的windows。

運算子優先順序

正規表示式是從左到右進行計算,並遵循優先順序順序。相同的優先順序從左到右計算,不同優先順序從高到低計算。下表為表示式運算子從高到低的優先順序順序。

字元 含義
\ 轉義字元。
(), (?:), (?=), [] 圓括號和方括號。
*, +, ?, {n}, {n,}, {n,m} 量詞等限定符。
^, $, \任何元字元、任何字元 定位點和序列(即位置和順序)。
| ”或“操作。

貪婪模式和非貪婪模式

正規表示式的貪婪模式和非貪婪模式是相對於量詞這個限定符來說的。預設情況下,正則的所有量詞(限定符)都是貪婪模式,即儘可能多的去匹配字元。而在量詞(限定符)後面加上?就變成了非貪婪模式,即儘可能少的去匹配字元。

舉個?

正規表示式/a{2,5}/,可以匹配aa,aaa,aaaa,aaaaa。這是貪婪模式,即儘可能多的去匹配。

正規表示式/a{2,5}?/,只會匹配aa。這是非貪婪模式,即儘可能少的去匹配。

疑問?

如果正規表示式是<div>.+?</div>cc的話,匹配結果卻是<div>test1</div><div>test2</div>cc。很多人在日常開發中會對這種情況感到困惑。這裡明明是非貪婪模式,為什麼匹配結果不是<div>test1</div>cc呢?這是因為無論是貪婪模式還是非貪婪模式都有一個前提條件:整個表示式必須匹配成功。當表示式匹配到<div>test1</div>時,後面的cc無法匹配成功,只有匹配到<div>test1</div><div>test2</div>時,後面的cc才會匹配成功。

分組

正則的分組主要通過小括號來實現,括號的子表示式作為一個分組,括號後面可以緊跟量詞表示重複的次數。

捕獲組

捕獲性分組,通常由一對小括號加上子表示式組成。正則會把每個分組裡面的內容儲存起來,供後續呼叫。其中由分組捕獲的串會從1開始編號,依次類推。這種引用既可以在表示式內部,也可以在表示式外部。

舉個?
const regex = /(\d{4})-(\d{2})-(\d{2})/
const str = '2019-10-21'
console.log(RegExp.$1) // 2019
console.log(RegExp.$2) // 10
console.log(RegExp.$3) // 21
複製程式碼

捕獲組引用常用來進行替換操作。

const regex = /(\d{4})-(\d{2})-(\d{2})/
const str = '2019-10-21'
const result = str.replace(regex, '$3/$2/$1')
// => 21/10/2019
複製程式碼
巢狀分組的捕獲

在巢狀的分組中是以左括號出現的順序進行捕獲。

舉個?
const regex = /((I) (am) (your) (father))/
const str = 'I am your father'
RegExp.$1 // I am your father
RegExp.$2 // I
RegExp.$3 // am
RegExp.$4 // your
RegExp.$5 // father
複製程式碼
反向引用

捕獲組捕獲到的內容,不僅可以在正規表示式外部通過程式進行引用,也可以在正規表示式內部進行引用,在內部被反向引用的值繼續參與匹配,而在表示式內部進行引用的方式被稱為反向引用。其格式為\數字。反向引用通常是用來查詢或限定重複,限定指定標識配對出現。

反向引用匹配原理

捕獲組在匹配成功時,會將子表示式匹配到的內容,儲存在一個以數字編號的組裡,這時可以通過反向引用的方式,引用這個區域性變數的值。一個捕獲組在匹配成功之前,它的內容是不確定的,一旦匹配成功,它的內容就確定了,反向引用的內容也就是確定的了。

舉個?
const regex = /(\w{3}) is \1/
regex.test('skr is skr') // true
regex.test('krs is krs') // true
regex.test('krs is skr') // false
複製程式碼

在表示式匹配成功之後,\1引用了第一個被分組捕獲的內容即skr或者krs,所以前兩個會匹配成功。

但是,如果編號越界了,則會被當成普通的表示式:

const regex = /(\w{3}) is \\6/
regex.test('skr is skr') // false
regex.test('skr is \6') // true
複製程式碼
非捕獲組

有時我們只想要括號的原始功能,只進行分組,而不進行捕獲,即既不在表示式外部引用,也不在表示式內部反向引用。此時我們可以使用非捕獲分組。語法為(?:p)。

舉個?
const regex = /(?:\d{4})-(\d{2})-(\d{2})/
const date = '2019-10-21'
RegExp.$1 // 10
RegExp.$2 // 21
複製程式碼

在這個例子中使用了非捕獲分組,因此(?:\d{4})不會捕獲任何字串,所以$1為(\d{2})捕獲的內容。

RegExp物件

RegExp建構函式會建立一個正規表示式物件,用於將文字與一個模式匹配。

有兩種方法來建立一個RegExp物件:一個是字面量,另一個是RegExp建構函式。要指示字串,字面量的引數不使用引號,而建構函式的引數使用引號。

當表示式被賦值時,字面量形式提供了正規表示式的編譯狀態。當你在迴圈中使用字面量構造一個正規表示式時,表示式不會在每一次迭代中被重新編譯。而通過建構函式建立的正規表示式提供了執行時編譯。如果你知道表示式模式將會改變,或者你事先不知道什麼模式,而是從另一個來源獲取的,比如使用者的輸入,那麼這些情況都可以使用建構函式模式。

當使用建構函式建立表示式物件時,需要常規的字元轉義(即在前面加上反斜槓\)。下面兩種情況是等價的:

const regexp = new RegExp('\\w+')
const regexp = /\w+/
複製程式碼
RegExp物件屬性
屬性 含義
global RegExp物件是否具有標誌g。
ignoreCase RegExp物件是否具有標誌i。
lastIndex 一個整數,表示下一次匹配的開始位置。
multline RegExp物件是否具有標誌
source 正規表示式的源文字。
RegExp物件方法
exec()

RegExp.prototype.exec():該方法用於檢索字串中正規表示式的匹配。

返回值是一個陣列,其中存放匹配的結果。如果未找到匹配,將返回null。此陣列的第0個元素是與正規表示式相匹配的文字。第1個元素是與RegExpObject的第1個子表示式匹配的文字,第2個元素是與RegExpObject的第2個子表示式匹配的文字,以此類推。除了陣列元素和length屬性外,該方法還返回兩個屬性。index屬性表示的是匹配的文字第一個字元的位置。input屬性表示的是被檢索的字串。在呼叫非全域性的RegExp物件的exec()方法時,返回的陣列與呼叫String.match()返回的陣列是相同的。

但是當RegExpObject時一個全域性正規表示式時,它會在RegExpObject的lastIndex屬性指定的字元處開始檢索字串。當exec()方法找到了相匹配的文字後,它將把RegExpObject的lastIndex屬性設定為匹配文字的最後一個字串的下一個位置,因此可以反覆呼叫exec()來遍歷字串中所有匹配的文字,當exec()再也找不到匹配的文字時,它將返回null,並且lastIndex屬性也會重置為0。

注意:如果在一個字串中完成了一次匹配之後,想要檢索新的字串,必須手動把lastIndex屬性設定為0。

舉個?
var str = "2019.10.21"
var regexp = /\b(\d+)\b/g
console.log( regexp.exec(str) )
console.log( regexp.lastIndex)
console.log( regexp.exec(str) )
console.log( regexp.lastIndex)
console.log( regexp.exec(str) )
console.log( regexp.lastIndex)
console.log( regexp.exec(str) )
console.log( regexp.lastIndex)
// => ["2019", "2019", index: 0, input: "2019.10.21"]
// => 4
// => ["10", "10", index: 5, input: "2019.10.21"]
// => 7
// => ["21", "21", index: 8, input: "2019.10.21"]
// => 10
// => null
// => 0
複製程式碼
test()

RegExp.prototype.test()方法執行一個檢索,用來檢視正規表示式與指定的字串是否匹配。如果正規表示式與指定的字串匹配,則返回True,否則返回false。

舉個?
const regexp = /\d+/
const str = 'abc123'
regexp.test(str) // true
複製程式碼

其他相關API

search()

search()方法用於檢索字串中指定的子字串,或檢索與正規表示式相匹配的子字串。返回第一個與regexp相匹配的子串的起始位置。

注意:search()放不執行全域性匹配,它將忽略標誌g。

舉個?
const regexp = /\d/
const str = 'abc123'
str.search(regexp) // 3
複製程式碼
match()

match()放可在字串內檢索指定的值,或找到一個或多個正規表示式的匹配。返回值是一個陣列,存放匹配結果,該陣列的內容依賴於正規表示式是否有全域性標誌g。

如果regexp沒有標誌g,那麼match()就在string中只執行一次匹配。如果沒有匹配就返回null。如果有匹配,它將返回一個陣列,該陣列的第0個元素存放的是匹配的文字,其餘元素存放的是與正規表示式的子表示式(即捕獲組)相匹配的文字。此外還包含兩個物件屬性,index屬性宣告的是匹配文字的起始字元在字串中的位置,input屬性宣告的是對該字串的引用。

如果regexp具有標誌g,match()將執行全域性檢索,找到字串中所有匹配的子字串。如果沒有找到就返回null。如果找到,就返回一個陣列。陣列中存放的元素是字串中所有匹配到的子串,沒有index和input屬性。

舉個?
const regexp = /(\d{4})-(\d{2})-(\d{2})/
const str = '2019-10-21'
str.match(regexp) // ['2019-10-21','2019','10','21',index: 0,input: '2019-10-21']
複製程式碼
const regexp = /(\d{4})-(\d{2})-(\d{2})/g
const str = '2019-10-21'
str.match(regexp) // ['2019-10-21']
複製程式碼
replace()

replace()方法用於在字串中用一些字元替換另一些字元,或替換一個與正規表示式匹配的子串。返回值是一個被替換後的字串。如果regexp具有標誌g,則會替換所有相匹配的文字,如果沒有標誌g,則只替換第一個匹配到的文字。替換的內容可以是字串,也可以是函式。如果是字串,那麼替換文字中的$具有特殊的意義。

字元 替換文字
$1,$2……,$99 與 regexp 中的第 1 到第 99 個子表示式相匹配的文字。
$& 與 regexp 相匹配的子串。
$` 位於匹配子串左側的文字。
$' 位於匹配子串右側的文字。
$$ 直接量符號。
舉個?
const regexp = /\w/g
const str = 'acdfe'
str.replace(regexp, 'b') // bbbbb
複製程式碼
const regexp = /\w/
const str = 'acdfe'
str.replace(regexp, 'b') // bcdfe
複製程式碼
const regexp = /\w/g
const str = 'acdfe'
str.replace(regexp, function(){
    return 'b'
}) // bbbbb
複製程式碼

總結

很感謝大家花時間把我的文章看完。正規表示式是一門”玄學“,功能非常強大,並且裡面還有很多不為人知的”騷操作“,希望大家能夠繼續探索它的奧祕,多多動手實際操作一下,畢竟紙上得來終覺淺,絕知此事要躬行。最後再給大家推薦一個正規表示式視覺化的網站regexper

相關文章