Sammie's Blog

Regex | Regular Expression 不用就忘記

剛開始收到主題的時候想說regex以前老師有講過一點點,但是查了資料才發現根本就忘得差不多了XD

在這段期間也幾乎都是google一下大家常用的規則,然後就複製貼上再實際測試哪個最符合想要的,沒在管內容長怎樣

畢竟長/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+.[a-zA-Z0-9-.]+$/的字串在程式寫正順的時候,沒有人有那個美國時間慢慢查慢慢理解啦!!

但是就算有時間檢查這一大堆字串的時候,好像也會有種有看沒有懂的感覺,或是要每看完一次隔幾天就又忘了

但是其實,regex是學一次可以用一輩子的神奇寶貝欸XD 所以,話不多說,開始吧!!

介紹


常見使用時機

  1. 比對找出特定的資料是否存在
  2. 判定輸入資料的格式是否符合規定
  3. 由字串中擷取特定的欄位,如分群比對、解析網頁內容
  4. 內容更換

工具

  1. 網頁開發工具,例如在Chrome的開發人員模式下按 (Windows)Ctrl+Shift+F (mac)Alt+Command+I
  2. IDE當中,例如Visual Studio Code的搜尋功能
  3. 文件檔案當中,例如Google文件的搜尋功能

Playground

  1. Regex101 https://regex101.com/
  2. RegEx Testing https://www.regextester.com/
  3. Debuggex: Online visual regex tester. JavaScript, Python, and PCRE. https://www.debuggex.com/

Library

Regular Expression Library http://regexlib.com/Default.aspx


語法

1. Character Classes(字元類別)

a:英文字母 a 這個字元,大小寫有區分
0:數字 0 這個字元
.:任意字元,但是不能是換行字串的開頭
-:字元集合,用來指定字源的區間,必須包在中括號裡面

[]:括號內的任何字元
[^]:不在括號內的任何字元
RegExp 說明 範例
/a/ 只含有字母 a 的字串 “a”,”aaa”
/./ 含有任意字元的字串 Hello World!
/.a/ a之前有任意字元的2碼字串,
但不可為換行的第一個字
An apple a day keeps the doctor away.
/[aeiou]/ 字串中的小寫母音 Buy ice cream in IKEA.
/[^aeiouAEIOU]/ 字串中不是母音的字元 Monkey likes banana.
/[0-9]/ 字串中的數字字元 1 file have changed at 2020-02-**18.
/[a-z]/ 字串中小寫字母的字元 1 file have changed at 2020-02-18.
/[^a-zA-Z]/ 不含英文字母的字串 “2266”,”9487”
1.1 特殊字元
\.:表示 . 這個字元,但是不能寫成 /./ 會被判斷成任意字元
\+:表示 + 這個字元,類似的還有 \? \*
\(:表示 ( 這個字元,類似的還有 \) \[ \]
\\:表示 \ 這個字元

\d:任何數字的字元,等同 [0-9]
\D:任何非數字的字元,等同 [^0-9]
\w:任何數字字母底線,等同 [A-Za-z0-9_]
\W:任何非數字字母底線,等同 [^A-Za-z0-9_]
\s:任何空白字元(空白/換行/tab...),等同 [ \f\n\r\t\v]
\S:任何非空白字元(空白/換行/tab...),等同 [^ \f\n\r\t\v]

\f:form feed 換頁
\n:line feed 換行
\r:carriage return 回列首
\t:tab 制表符
\v:vertical tab 垂直制表符
※ Windows text files use "\r\n" to terminate lines, while Unix test files use "\n"
RegExp 說明 範例
/https:\/\// 超文本傳輸安全協定 https://www.google.com
/\w\w\w\w@gmail\.com/ 帳號為 4 碼的 Gmail 信箱 “test@gmail.com”,
“oops@gmail.com”
/[0-9\+\-\*\/]/ 四則運算算式 “1+2*3”
/[A-Z][1-2]\d\d\d\d\d\d\d\d/ 簡易驗證身分證字號 “F123456789”


2. Repetition(重複次數)

*:出現 0 次以上,等同於 {0,}
?:出現 0 次或 1 次,可有可無的意思,等同於 {0,1}
+:出現 1 次以上,等同於 {1,}
  
{n}:出現 n 次
{n,}:出現 n 次以上
{,n}:出現 n 次以下
{n,k}:出現 n 次到 k 次
RegExp 說明 範例
/[^aeiou]*/ 不包含小寫母音的字串 “COcOnUt”,”grApE”
/^[A-Za-z][1-2]\d{8}$/ 簡易驗證身分證字號 “A123456789”
/^09\d{2}-?\d{3}-?\d{3}$/ 手機號碼 “0912345678”,
“0912-345-678”
/^0\d{1,3}-?\d{5,8}/ 台灣市內電話 “04-22203585”,
“037-322150”,
“0836-89146”

題外話一下,馬祖的區碼是0836,我以為苗栗037已經很多位數了...


3. Flag(旗標)

g:global search,比對字串中所有符合的配對,否則只找一個
i:case-insensitive search,規則不區分大小寫
m:multi-line search,多行比對,^比對每行開頭,$比對每行結尾
RegExp 說明 範例
/xyz[0-9]/ 第一組xyz開頭後接數字 xyz123xyz456
/xyz[0-9]/g 全部xyz開頭後接數字 xyz123xyz456
/xyz[0-9]/ig 全部xyz開頭後接數字且不區分大小寫 xyz123XYZ456


4. Anchoring Metacharacters(錨點元字符):設定規則邊界

^:字串開頭
$:字串結尾

\b:word boundary 比對字串邊界非一般字元
\B:non-word boundary 比對字串邊界為非一般字元

※ 注意 \b 和 [\b] 是不一樣的,[\b] 是用來比對 backspace

多數基本資料 input 的結果都會有 ^ 和 $,例如身分證、生日、電話等等

RegExp 說明 範例
/^https?:\/\// 超文本傳輸安全協定 https://www.google.com
http://domain.ca
/\.com$/ .com結尾的字串 https://tw.yahoo.com
https://www.kkday.com
/\bgo\b/ 邊界不是一般字元或底線的詞 go go_goyago.go.go-go-go.go
/\Bgo\B/ 邊界是一般字元或底線的詞 go go_goyago.go.go-go-go.go
4.1 lookahead(往前看/右合樣)、lookbehind(往後看/左合樣)
(?=):Positive lookahead 代表「往前看是」的意思,後面接上字元
(?!):Negative lookahead 代表「往前看不是」的意思,後面接上字元

※ 以下不支援 IE, Edge, Firefox, Safari,屬於 ES2018 的功能
(?<=):Positive lookbehind 代表「往後看是」的意思,前面接上字元
(?<!):Negative lookbehind 代表「往後看不是」的意思,前面接上字元
RegExp 說明 範例
/\d(?=[AB])/ 找有A或B接在後面的「數字」,等同於數字後面有A或B 123A45B67AB8A0B
/\d(?![AB])/ 找沒有A或B接在後面的「數字」,等同於數字後面沒有A或B 123A45B67AC8E0B
/(?<=[OP])./ 找有O或P接在前面的「任何字元」,等同於字元前面有O或P AOEPSOFPOPVFOP
/(?<![OP])./ 找沒有O或P接在前面的「任何字元」,等同於字元前面沒有O或P AOEPSOFPOPVFOP


白話文這樣說

/Peter(?=Mary)/
Peter後面要接上Mary,這樣Peter才有意義 => Mary是Peter存在的意義
/Peter(?!Jessica)/
Peter後面一定不能接Jessica,這樣Peter才有意義 => Peter跟Jessica在一起變成沒意義的空殼

/(?<=George)Michelle/
Michelle前面要有George,這樣Michelle才有意義 => George是Michelle存在的意義
/(?<!Louis)Michelle/
Michelle前面一定不能接Louis,這樣Michelle才有意義 => Michelle跟Louis再一起變成沒意義的空殼


5. Alternation(交替)、Grouping(群組)

|:代表「或」的意思
():代表「群組」的意思

※ 以下不支援 IE, Edge, Firefox
?<GROUPNAME>:代表「群組命名」的意思`,屬於 ES2018 的功能
RegExp 說明 範例
/cat|dog/ 貓或狗 “cat”,”dog”
/I like cat|dog/ 我喜歡貓或狗 “I like cat”,”dog”
/I like (cat|dog)/ 我喜歡貓或我喜歡狗 “I love cat”,”I love dog”
/Pet(Hamster)?/ 寵物鼠 “Pet”,”PetHamster”
/(?<pet>cat|dog|hamster)/ 寵物包含貓、狗、老鼠 “cat”,”dog”,”hamster”
/^([12]\d{3}-(0[1-9]|1[0-2])-
(0[1-9]|[12]\d|3[01]))$/
西元生日格式 “1978-12-26”

GROUPNAME 可以搭配 match() 方法做使用,等一下有範例


6. Greedy(窮盡)、Lazy(最少)

※ Greedy 盡可能多地從每次出現的特定模式進行比對,可以說是匹配優先
*:greedy 窮盡比對零或多次,類似的還有 + ? {n} {n,} {n,k}

※ Non-greedy(Lazy) 比對的項目愈少愈好,可以說是忽略優先
*?:non-greedy(lazy) 非窮盡比對零或多次 +? ?? {n}? {n,}? {n,k}?
RegExp 說明 範例
/A.*Z/ A開頭Z結尾的最大集合字串 eeeAiiZuuuuAoooZeeee
/A.*?Z/ A開頭Z結尾的許多小字串 eeeAiiZuuuuAoooZeeee

和狀態機的 backtrack 這個關鍵字有相關,有興趣可以查看看!!


在程式中使用

Html 5

<form>
  <input type="text" name="mobile" id="mobile" pattern="^09\d{2}-\d{3}-\d{3}" />
</form>

JavaScript

方法 說明
exec 以陣列回傳字串中匹配到的部分,無匹配則回傳null
test 搜尋字串中是否有符合的部分,回傳true/false
match 以陣列回傳字串中匹配到的部分,無匹配則回傳null
search 尋找字串中是否有符合的部分,有會回傳index,無則回傳-1
replace 尋找字串中匹配的部,第二個參數為取代之新字串
split 在字串根據匹配到的項目拆成陣列
/* 基本 function 介紹 */

var pattern = /[A-Z]\d{9}/; // 效能較好,適合 pattern 不會改變的情況
// var pattern = new RegExp('[A-Z]\d{9}'); // 效能較差,適合用在 pattern 可能動態改變的情況
var id = 'F123456789 is user identification of John';

console.log(pattern.exec(id)); // ["F123456789"]
console.log(pattern.test(id)); // true
console.log(id.match(pattern)); // ["F123456789"]
console.log(id.search(pattern)); // 0
console.log(id.replace(pattern, '5487')); // "5487 is user identification of John"
console.log(id.split(pattern)); // ["", " is user identification of John"]

單純查看字串是否包含某 pattern,建議使用 test 及 search,效能較好

/* 範例一 */
var re = /(\w+)\s(\w+)/;
var str = 'John Smith';
var newStr = str.replace(re, '$2, $1');
console.log(newStr); // "Smith John"
// 每一個 regex 配對到的 "內容" 都會變成一個 $數字

function replacer(match, p1, p2, p3, offset, string) {
  // p1 is non-digits, p2 digits, and p3 non-alphanumerics
  return [p1, p2, p3].join(' - ');
}
var newString = 'abc12345#$*%'.replace(/([^\d]*)(\d*)([^\w]*)/, replacer);
console.log(newString);  // abc - 12345 - #$*%
/* 範例二 */
var matched = 'An apple a day keeps the doctor away.'.match(/(a.)(p.)e/);
var globalMatched = 'An apple a day keeps the doctor away.'.match(/(a.)(p.)e/g);

console.log(RegExp.$1); // "ap" 
console.log(RegExp.$2); // "pl"
console.log(matched[1]); // "ap" 
console.log(matched[2]); // "pl"

// [0]配對到的、[1]從配對到的裡找第一組、[2]從配對到的裡找第二組
console.log(matched); // ["apple", "ap", "pl"] 
console.log(globalMatched); // ["apple"]

RegExp $n 非標準的使用方式,請勿在正式環境使用

/* 範例三 */
var regexp = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
var match = regexp.exec('2020-19');

console.log(match.groups);          // {year: "2020", month: "02", day: "19"}
console.log(match.groups.year);     // 2020
console.log(match.groups.month);    // 02
console.log(match.groups.day);      // 19

練習題


補充

狀態機表現

字母、數字

/gmail/ 作為規則為例,其實是從 g-m-a-i-l 找到前一個字母後繼續向右找
Imgur

集合

/h[aeiou]ng/ 作為規則,則是嘗試 a|e|i|o|u 由上往下找
Imgur

重複次數:? /* /+ /{}
Alternation(交替)

/cat|dog/ 作為規則
cat|dog

Greedy(窮盡), Lazy(最少)

學習資源