iT邦幫忙

2024 iThome 鐵人賽

DAY 24
0
JavaScript

Don't make JavaScript Just Surpise系列 第 24

正規表達式(Regular Expression)

  • 分享至 

  • xImage
  •  

正規表達式(Regular Expression),有時候會稱呼正則表達式,通常縮寫會寫成 regexregexp

正規表達式的概念並不僅限於 JS,而是適用於所有的文字相關的內容,是個我覺得即使不寫程式的人也要學的超好用文字匹配方式。
儘管不同環境會有一些進階語法的差異,但基本語法大都一樣。

許多進階一點的文字編輯器包含 Sublime,Notepad++ 等等在做搜尋的時候都有「使用正規表達式語法」之類的選項可以選,讓你透過正規表達式進行搜尋取代

先說明正規表達式是在做什麼。

Ticket001
Ticket002

假設想要在一大篇文章找出上面的幾個字串,如果有點概念的人可能會想到模糊搜尋(Fuzzy Search),即使用具有任意字元(Wildcard)、佔位符的搜尋語法,假設以 % 代表任意字元,那我們可能會搜尋 Ticket00%,這樣就可以找到上面兩個字串。
模糊搜尋可以說是使用門檻比較低,但可能不夠精確,正規表達式做的就是更精確的文字內容匹配。

Ticket#001
Ticket234

像這兩個要用模糊搜尋來抓出來就相對不容易,以 regex 的角度,可以寫做 ^Ticket#?\d+$
我們可以口語地解釋這段表達式:字串起始為 Ticket,後面出現一次或不出現 #,再接上一個或多個數字,結束字串。
這就是正規表達式能做到的事情。

其他更多正規表達式能使用的字元建議參照 Wiki,如果有不懂的也可以問 ChatGPT 並讓他附上語句說明(像上面這樣口語化解釋),ChatGPT 在樣本足夠和狀況描述清楚的情況下,寫出來的正規表達式通常都蠻精準的(只是有時候會寫的比較侷限,所以還是要看得懂才能自己改)。

JS 中的正規表達式

這才是這篇的主題,畢竟是 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 中正規表達式物件支持的物件方法有幾種:

  1. Regex.test(string)
    如果比較對象是否符合正規表達式,如果符合就會回傳 true,否則回傳 false
    const regex1 = new RegExp('foo');
    const str1 = 'table football, foosball';
    
    regex1.test(str1);//true
    
  2. Regex.exec(string)
    返回第一次匹配結果的陣列,如果沒有匹配到東西,會回傳 null
    通常會跟我們待會提到的 Flag 一併使用。

其餘還有一些基於 String 方式提供相容性的如 [Symbol.match][Symbol.matchAll][Symbol.replace]``[Symbol.search]``[Symbol.split]
使用上都和 String 一樣,只是允許了尋找模式、取代模式的字串變為正規表達式寫法,提供更大的靈活性。

Flags

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。

  1. 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
    
  2. 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
    
  3. 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        
    
  4. 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
    
  5. y(ES 6)
    sticky 黏著模式,這個模式下, 匹配會從指定的 lastIndex 開始,從該位置的第一個字開始就必須開始匹配,否則視為匹配失敗,對 lastIndex 做對應操作。
    如果同時掛有 gy 標示,則 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
    
  6. 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!


上一篇
反射(Reflect)
下一篇
日期物件(Date)
系列文
Don't make JavaScript Just Surpise31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言