正規表達式(Regular Expression),有時候會稱呼正則表達式,通常縮寫會寫成 regex
或 regexp
。
正規表達式的概念並不僅限於 JS,而是適用於所有的文字相關的內容,是個我覺得即使不寫程式的人也要學的超好用文字匹配方式。
儘管不同環境會有一些進階語法的差異,但基本語法大都一樣。
許多進階一點的文字編輯器包含 Sublime,Notepad++ 等等在做搜尋的時候都有「使用正規表達式語法」之類的選項可以選,讓你透過正規表達式進行搜尋或取代。
先說明正規表達式是在做什麼。
Ticket001
Ticket002
假設想要在一大篇文章找出上面的幾個字串,如果有點概念的人可能會想到模糊搜尋(Fuzzy Search),即使用具有任意字元(Wildcard)、佔位符的搜尋語法,假設以 %
代表任意字元,那我們可能會搜尋 Ticket00%
,這樣就可以找到上面兩個字串。
模糊搜尋可以說是使用門檻比較低,但可能不夠精確,正規表達式做的就是更精確的文字內容匹配。
Ticket#001
Ticket234
像這兩個要用模糊搜尋來抓出來就相對不容易,以 regex 的角度,可以寫做 ^Ticket#?\d+$
。
我們可以口語地解釋這段表達式:字串起始為 Ticket,後面出現一次或不出現 #,再接上一個或多個數字,結束字串。
這就是正規表達式能做到的事情。
其他更多正規表達式能使用的字元建議參照 Wiki,如果有不懂的也可以問 ChatGPT 並讓他附上語句說明(像上面這樣口語化解釋),ChatGPT 在樣本足夠和狀況描述清楚的情況下,寫出來的正規表達式通常都蠻精準的(只是有時候會寫的比較侷限,所以還是要看得懂才能自己改)。
這才是這篇的主題,畢竟是 JS 的系列文章。
正規表達式在 JS 中也是以物件的形式去儲存表達式本身。
宣告方式有以下兩種:
let reg = /^Ticket#?\d+$/;
console.log(typeof reg);//"object"
let reg2 = new RegExp("^Ticket#?\d+$");
console.log(typeof reg2);//"object"
console.log(reg.prototype == reg2.prototype);//true
兩者宣告出來的物件是一樣的,差別在於第一種是使用定值宣告,效能會比第二種好一點(但一般情況也不會因為這個造成效能瓶頸啦),第二種就是比較靈活。
正規表達式一旦宣告後就無法被修改表達式內容,如果要修改,只能創建新的表達式,但如果只是要查看內容,那還是做得到的。
let reg = /^Ticket#?\d+$/;
console.log(reg.source);//"^Ticket#?\d+$"
console.log(reg.toString());//"^Ticket#?\d+$"
JS 中正規表達式物件支持的物件方法有幾種:
true
,否則回傳 false
。
const regex1 = new RegExp('foo');
const str1 = 'table football, foosball';
regex1.test(str1);//true
null
。Flag
一併使用。其餘還有一些基於 String
方式提供相容性的如 [Symbol.match]
,[Symbol.matchAll]
,[Symbol.replace]``[Symbol.search]``[Symbol.split]
。
使用上都和 String
一樣,只是允許了尋找模式、取代模式的字串變為正規表達式寫法,提供更大的靈活性。
Flag
是 JS 中對正規表達式物件的搜尋方式擴充的稱呼,在其他的語言不一定是使用此種實作。
要為一個正規表達式物件指定 Flag
,必須要在建構的時候設定,物件建立後所有 Flag
相關屬性都是唯讀,如果修改會無效。
const regex = /abc/g; //可以這樣宣告
const regex1 = new RegExp("abc","g");//或這樣宣告
console.log(regex.global);//true
regex.global = false; //無效
console.log(regex.global);//false
目前提供的共有六種 Flag。
g
global,全域匹配,允許字串在搜尋範圍內找到多次結果,而非僅第一次匹配。
正規表達式的物件上有另一個重要的屬性 : lastIndex
。
在提到 g 前得先提到這個屬性,這個屬性表示下一次執行方法的時候,該從哪索引開始執行。方法的執行結果會和這個屬性息息相關,方法執行時同時也會根據結果對這個屬性做對應的操作。
配上 exec()
使用會讓 exec()
的搜尋變成逐次迭代的效果,每次呼叫方法時,若符合同時會把 lastIndex
標到符合字尾的下一個位置,如果不符合則將 lastIndex
設為 0。(test()
也是一樣的行為,後面不會重複,以 exec()
為代表說明)
const regex1 = new RegExp('foo', 'g');//等同於 /foo/g
const str1 = 'table football, foosball';
regex1.test(str1);//true
console.log(regex1.lastIndex);//9
regex1.test(str1);//true
console.log(regex1.lastIndex);//19
regex1.test(str1);//false
console.log(regex1.lastIndex);//0
regex1.test(str1);//true
console.log(regex1.lastIndex);//9
i
ignore case,忽略大小寫。預設行為在進行字串比對會是大小寫嚴格比對方式,若有加上 i
,則變為大小寫寬鬆模式。
const regex1 = /foo/;
const regex2 = /foo/i;
const str1 = 'table foo';
const str2 = 'table Foo';
console.log(regex1.test(str1));//true
console.log(regex1.test(str2));//false
console.log(regex2.test(str1));//true
console.log(regex2.test(str2));//true
m
multiline 多行模式,讓字元 $
和 ^
兩個用於聲明匹配模式的首尾限制字符,改為匹配對象的每行的開始與結束,而非表達整段字串的開頭與結尾。
const regex1 = /^foo$/;//表示輸入字串必須完全剛好是 foo,不多不少
const regex2 = /^foo$/m;//只要輸入字串有一行內容僅有 foo 即可
const str1 = `table foo
foo`;
console.log(regex1.test(str1));//false
console.log(regex2.test(str1));//true
u(ES 6)
Unicode 模式,在這個模式下允許正規表達式正確的去匹配帶有 Unicode 的表達式,原本的正規表達式無法處理像 Unicode 這種一個顯示自元帶有多個碼點(code point,也稱碼位)字元。
const str = 'Hello World👋!';
const regex1 = /\p{Emoji}/;
const regex2 = /\p{Emoji}/u;
console.log(regex1.test(str),regex1.lastIndex);//false 0
console.log(regex2.test(str),regex2.lastIndex);//true 0
y(ES 6)
sticky 黏著模式,這個模式下, 匹配會從指定的 lastIndex
開始,從該位置的第一個字開始就必須開始匹配,否則視為匹配失敗,對 lastIndex
做對應操作。
如果同時掛有 g
和 y
標示,則 y
有較高的優先權。
const regex1 = /foo/;
const regex2 = /foo/y;
const str1 = 'table foo';
console.log(regex1.test(str1),regex1.lastIndex);//true 0
console.log(regex1.test(str1),regex1.lastIndex);//true 0
regex2.lastIndex = 6;
console.log(regex2.test(str1),regex2.lastIndex);//true 9
console.log(regex2.test(str1),regex2.lastIndex);//false 0
console.log(regex2.test(str1),regex2.lastIndex);//false 0
s(ES 2018)
dotAll 點任意匹配模式,允許 .
能夠表示包含 \n
換行字元,沒有加這個模式的 .
沒辦法匹配換行。
const regex1 = /foo./;
const regex2 = /foo./s;
const str1 = `table foo
foo`;
console.log(regex1.exec(str1));//null
console.log(regex2.exec(str1));
// ["foo
// "]
正規表達式是個需要一點時間上手的工具,一旦上手,會發現字串的處理上比過往輕鬆許多,甚至連純文字編輯器的操作體驗都會大幅上升。
如果此前沒有接觸過正規表達式,會推薦至少把文件基本語法稍微讀過,了解正規表達式的允許匹配類別(如數字、符號、空格...等等)、數量表達方式(如出現至少一次,出現零次或多次...等等),網路上有很多關於正規表達式的練習或檢測工具,隨機貼一個我找到的給大家參考:練習網站。
語法不用背,只要有大概印象,寫久了就會記起來,重要的是要讀的懂,Happy Regex!