PJCHENder 未整理筆記

[JS] 正則表達式(Regular Expression, regex)

2017-09-26

[JS] 正則表達式(Regular Expression, regex)

@(Javascript)[JavaScript, regex]

1
2
3
4
'str'.match(/[0-9]+/)          // 1 次以上的數字,等同於 "\d"
'str'.match(/[A-Za-z]+/) // 1 次以上的英文字
'str'.match(/[A-Za-z0-9_]+/) // 1 次以上的英數字含底線,等同於 "\w"
'str'.match(/.+/) // 1 次以上的任意字元
1
2
3
4
5
6
* 表示前一個字元可以是 0 個或多個,例如 /ab*c/,因此 ac, abc, abbbbc 都符合規則。
+ 表示前一個字元可以是 1 個或多個,例如 /a+b/ ,ab, aaaaab 都符合規則。
? 表示前一個字元可以是 0 個或 1 個
^ 匹配輸入的開頭,例如 /^a/ , a dog 會符合,但 cats 中的 a 不會。
$ 匹配輸入的結尾,例如 /t$/,eat 會符合,但 eaten 中的 t 不會。
. 用來表示任意字元

建立正規式

正則表達式的規則稱作 pattern。在 JavaScript 中可以透過 Regular expression literals 的方式或建構式的方式來建立 regular expressions pattern:

Regular expression literals

1
2
3
4
5
/**
* Regular expression literals: script 載入時即編譯
* 當 pattern 不會改變時,使用此方式定義 pattern 效能較好。
**/
var re = /ab+c/;

Function Constructor

1
2
3
4
5
6
7
/**
* Function constructor for RegExp object: 程式執行過程才會被編譯
* 效能較差,適合用在 regular expression pattern 可能會改變時使用
**/

var re = new RegExp('ab+c');
var myRe = new RegExp('d(b+)d', 'g');

Regular expression literals 效能較好,適合 pattern 不會改變的情況;Function Constructor 效能較差,適合用在 pattern 可能動態改變的情況。

使用正規式

在 JavaScript 中可以使用正規式的函式包含

  • RegExp.prototype.test():搜尋字串中是否有符合的部分,回傳 true/false
  • RegExp.prototype.exec():以陣列回傳字串中匹配到的部分,否則回傳 null
  • String.prototype.match():以陣列回傳字串中匹配到的部分,否則回傳 null
  • String.prototype.replace():尋找字串中匹配的部分,並取代之。
  • String.prototype.search():尋找字串中是否有符合的部分,有的話回傳 index,否則回傳 -1
  • String.prototype.split():在字串根據匹配到的項目拆成陣列。

簡單來說,當你想要看字串是否包含某 pattern 時,使用 testsearch;想要更多的資訊(花較多耗效能),則使用 execmatch

String.prototype.replace():取代內容

使用 String.prototype.replace(regex|substr, newSubstr) 來置換內容,這個方法會回傳置換後的新字串,不會改變原本的字串:

1
2
3
4
5
6
// 只接把 regex 寫在裡面
newString = <String>.replace(/<p>/g, '<div class="paragraph">')

// 先建立 regex
let regex = new RegExp(wordToBeReplaced, 'gi')
let newString = <String>.replace(regex, 'wordToReplace')

/.../g: global 的意思,找到之後會繼續往後配對
/.../i: case insensitive 的意思

1
2
3
4
5
6
7
/* 找到第一個就不往後找 */
'banana'.replace(/na/, 'NA') // 'baNAna'

/* 把 n 後面和後面的字元 */
'banana'.replace(/n./, 'NA') // 'baNAna'
'banana'.replace(/na/g, 'NA') // 'baNANA'
'banana'.replace(/na/gi, 'NA') // 'baNANA'

搭配括號和 $n

1
2
3
4
var re = /(\w+)\s(\w+)/;
var str = 'John Smith';
var newStr = str.replace(re, '$2, $1');
console.log(newStr);

搭配 replacer function

1
2
3
4
5
6
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 - #$*%

String.prototype.match():尋找並取出內容

String.prototype.match( ) @ MDN

使用 String.prototype.match(regexp) 這個方法來判斷給的字串當中是否有符合該 regexp pattern 的內容,有的話以陣列回傳,沒有的話回傳 null

如果這個 regexp 的 pattern 包含 g 標籤,那麼 str.match() 回傳的結果和 RegExp.exec() 是一樣的,在回傳的陣列中會包含:

  • input 屬性:原本被解析的字串
  • index 屬性:第一個被找的字串的 index 值
  • 所有配對的結果
1
2
3
4
/* 不包含 g 的話,結果和 RegExp.exec() 一樣 */

let matchedResult = 'An apple a day, keeps apple away.'.match(/(a.)(p.)e/);
// [ 'apple', 'ap', 'pl', index: 3, input: 'An apple a day, keeps apple away.' ]

如果 pattern 中包含 g 的話,那麼回傳的陣列中會直接是整個被 matched 到的字:

1
2
3
4
/* 包含 g 的話,會直接回傳配對到的結果 */

let matchedResult = 'An apple a day, keeps apple away.'.match(/(a.)(p.)e/g);
// [ 'apple', 'apple' ]

若給入一個非 regexp 的物件,則會自動透過 new RegExp(obj) 轉換;若沒有代入任何參數的話,則會得到帶有空字串的陣列([""])。

使用範例

每個 () 會變成一個 $

1
2
3
4
5
6
7
let matchedResult = 'An apple a day'.match(/(a.)(p.)e/);
RegExp.$1; // ap
RegExp.$2; // pl

'banana'.match(/(.)(a)/g) // [ 'ba', 'na', 'na' ]
// $1 = ['b', 'n', 'n']
// $2 = ['a', 'a', 'a']
1
2
3
4
5
6
// "/" 是特殊字元要用反斜線
'2017/05/16'.match(/(.*)\/(.*)\/(.*)/)
// ['2017/05/16', '2017', '05', '16']

'2017/05/16'.match(/.*\/.*\/.*/)
// [ '2017/05/16' ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 擷取網址中的內容
**/

let url = 'https://www.ptt.cc/bbs/CodeJob/M.1513840968.A.F93.html'
let timestamp = url.match(/\/M\.(.+)\.A/)

console.log(timestamp[1]) // 1513840968

// result of timestamp
[
'/M.1513840968.A', // 該正規式會匹配到的內容
'1513840968', // 透過 match () 選取到的內容
index: 30, // 從哪個 index 開始批配到
input: 'https://www.ptt.cc/bbs/CodeJob/M.1513840968.A.F93.html' // 輸入的內容
]

搭配 filter 篩選結果

搭配 Array.prototype.filter 我們就可以根據使用者輸入的內容(wordToMatch)來從 cities 中篩選資料:

1
2
3
4
5
6
7
8
9
10
function findMatch (wordToMatch, cities) {
return cities.filter(place => {
/**
* g: global search
* i: case insensitive search
**/
let regex = new RegExp(wordToMatch, 'gi')
return place.city.match(regex) || place.state.match(regex)
})
}

[JS30] Day06: AJAX Type Ahead

String.prototype.search():檢驗字串是否包含

1
2
3
4
5
var str = "hey JudE";
var re = /[A-Z]/g;
var re2 = /[.]/g;
console.log(str.search(re)); // returns 4, which is the index of the first capital letter "J"
console.log(str.search(re2)); // returns -1 cannot find '.' dot punctuation

RegExp.prototype.test():檢驗字串是否包含

1
2
// 判斷是不是數值
/^[0-9]+$/.test(<testString>) // return True/False

RegExp.prototype.exec():尋找並取出內容

1
2
3
4
5
6
7
8
9
10
let regexp = /d(b+)d/g;
let matchedResult = regexp.exec('cdbbdbsbz');
// [ 'dbbd', 'bb', index: 1, input: 'cdbbdbsbz' ]

// 透過 while 取出所有配對到的結果
while ((arr = regexp.exec('table football, foosball')) !== null) {
console.log(`Found ${arr[0]}. Next starts at ${regexp.lastIndex}.`);
// expected output: "Found foo. Next starts at 9."
// expected output: "Found foo. Next starts at 19."
}

使用 RegExp.$1 來取得配對到的值

這是非標準的使用方式,請勿在正式環境使用

1
2
3
4
5
var re = /(\w+)\s(\w+)/;
var str = 'John Smith';
str.replace(re, '$2, $1'); // "Smith, John"
RegExp.$1; // "John"
RegExp.$2; // "Smith"

RegExp.$1-$9 @ MDN

群組與命名群組(Group and Named Capture Group)

透過 () 可以把配對到的內容分成不同組別放到陣列中:

1
2
3
4
5
6
7
8
9
10
const regexp = /(\w+)\.jpg/;
const matched = regexp.exec('File name: cat.jpg');
// [
// 'cat.jpg',
// 'cat',
// index: 11,
// input: 'File name: cat.jpg',
// groups: undefined
// ]
const fileName = matched[1]; // car

配對到的內容會被放在陣列當中,因此可以透過解構賦值(destructuring assignment)的方式,將想要的內容取出:

1
const [ match, year, month, day ] = regexpWithGroup.exec(str);

命名群組(named group)

:exclamation: 命名群組(named group)的這個功能屬於 ES2018 須留意相容性。

在 ES2018 中則可以使用 (?<name>…) 為組別命名,所有命名的組別會被放在名為 groups 物件內:

1
2
3
4
5
6
7
8
// 使用 (?<name>) 來為組別命名
const regexp = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = regexp.exec('2020-03-04');

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

如果命名群組中的內容沒有被匹配到的話,該群組 groups 的屬性仍會存在,只是會得到 undefined 的值。

搭配 replace 使用

replace 後面可以接 function,在這個 function 則可以直接取得配對到的內容和分組的結果:

1
2
3
4
5
6
7
const str = 'War & Peace';

const result = str.replace(/(?<War>War) & (?<Peace>Peace)/, function(match, group1, group2, offset, string) {
return group2 + ' & ' + group1;
});

console.log(result); // → Peace & War

Sample Code

1
<iframe height="400px" width="100%" src="https://repl.it/@PJCHENder/Regex-in-ES2018?lite=true" scrolling="no" frameborder="no" allowtransparency="true" allowfullscreen="true" sandbox="allow-forms allow-pointer-lock allow-popups allow-same-origin allow-scripts allow-modals"></iframe>

特殊字元 (character)

Regular Express Reference @ MDN

標籤(flag)

1
2
3
regex = /hello/; // 區分大小寫,匹配 "hello", "hello123", "123hello123", "123hello",但不匹配 "hell0", "Hello"
regex = /hello/i; // 不區分大小寫 "hello", "HelLo", "123HelLO"
regex = /hello/g; // 全域搜尋

dotAll Flag, /s

ES 2019 新增 /s 的標籤,過去 . 可以用來匹配除了換行符號以外(\n, \r)的所有字元:

1
2
3
// 過去 . 可以匹配到除了「換行符號」以外的所有字元
console.log(/./.test('\n')); // → false
console.log(/./.test('\r')); // → false

過去雖然可以使用 [\w\W] 來匹配到換行符號,但這不是最好的做法:

1
2
console.log(/[\w\W]/.test('\n'));    // → true
console.log(/[\w\W]/.test('\r')); // → true

在 ES 2019 中,只要最後有標記 /s 的標籤,如此 . 將也能夠匹配到換行符號:

1
2
console.log(/./s.test('\n'));    // → true
console.log(/./s.test('\r')); // → true

普通字元 //

1
2
regex = /a/
regex = /is/

反斜線 \

1
2
3
4
5
6
/* 在「非」特殊字元前面使用反斜線時,表示要把反斜線後的字當作是特殊字元 */
regex = /\b/ // b 原本不是特殊字元,這個 b 要當成特殊字元

/* 在特殊字元前面使用反斜線時,表示要把反斜線後的字當作是「非」特殊字元 */
regex = /if\(true/ // ( 原本是特殊字元,但這裡要當成非特殊字元
regex = /1\+2=3/ // + 原本是特殊字元,但這裡要當成非特殊字元

任意一個字元 .

可以用來匹配除了換行符號(\n)以外的所有字元:

1
2
3
regex = /a.man/    // a*man 都會 match,例如 "acman", "awman", 但 "a\nman" 無法匹配。

regex = /.a/ // 任何一個字元後加上 a

多個字元 []

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 小寫 a 或大寫 A
regex = /[aA]/

// 匹配所有不是 a 或 A 的字
regex = /[^aA]/

// a, e, i, o, u 都會 match
regex = /[aeiou]/

// 英文字母
regex = /[a-z]/; // 所有小寫的字母,從小寫 a 到小寫 z
regex = /[A-Z]/; // 所有大寫的字母,從大寫 A 到大寫 Z
regex = /[a-zA-Z]/; // 所有英文字母

// 數字 5 ~ 8
regex = /[5-8]/

括號():套用到所有

1
2
3
/^a |^the |^an/      // 套用到裡面所有的

/^(a |the |an )/ // 等同於

不是(除了) ^

1
2
3
4
5
/* 不是 a 都會 match */
/[^a]/

/* 不是數字都會 match */
/[^0-9]/

多個字元縮寫 \d, \w, \s, \b, \D, \W, \S

keywords: \d, \w, \s, \b, \D, \W, \S

\d: digit,[0-9]
\w: word,包含英文大小寫、數字、底線,[A-Za-z0-9_]
\s: space,包含 space, tab, form feed, line feed,[\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]

\D: 不是 digit,等同於 [^\d]
\W: 不是 word [^\w]
\S: 不是 space [^\s]

1
2
3
4
5
6
7
8
9
/* 所有 word + e */
/\we/

/* 連續兩個任意的數值 */
/\d\d/

/* 句子中結尾為 s 的單字 */
/s\b/
/\b[a-z]/g // 句子中各個單字的第一個字母

其他特殊字元

\t:tab
\b:word boundary,用來比對單字和單字間的空白,/s\b/ 則會比對句子中最一個字母是 s 的單字

Word boundary \b, \B

透過 \b 可以配對 word boundaryword boundary 指的是一個字元的前後沒有其他任何字元

要注意 \b[\b] 是不一樣的,[\b] 是用來配對 backspace。

1
2
3
// is 這個單字才會被選到,Th`is` 的 is 不會
let matchedResult = 'This is an apple.'.match(/\bis\b/);
// [ 'is', index: 5, input: 'This is an apple.' ]

相反地,\B 則是 non-word boundary,包含:

  • Before the first character of the string, if the first character is not a word character.
  • After the last character of the string, if the last character is not a word character.
  • Between two word characters
  • Between two non-word characters
  • The empty string
1
2
3
4
// 使用 \B 會配對到 This 中的 is

let matchedResult = 'This is an apple.'.match(/\Bis/);
// [ 'is', index: 2, input: 'This is an apple.' ]

出現次數 * + ? {} {, }

keywords: *, +, ?, {次數}, {最少次數, 最多次數}

*: 任意次數,等同於 {0,}
+: 至少一次(後面要跟著),等同於 {1,}
?: 零或一次(有或沒有),等同於 {0,1}
{次數}
{最少次數, 最多次數}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
regex = /abc/ // 找到符合 "abc"

regex = /ab*c/ // *表示前一個單字可以是 0 個或多個,因此 ac, abc, abbbbc 都符合規則

regex = /n?a/ // n 可有可無

regex = /a{2}/ // a 要 2 次,所以會是 a

regex = /a{2,4}/ // a 介於 2 次到 4 次之間

regex = /a{2,}/ // 2 次以上的 a 都可以,大括號後面不要有空格

regex = /(hello){4}/ // 4 次的 hello,hellohellohellohello

regex = /\d{3}/ // 3 次的數字

開頭與結尾

keywords: ^

^ 開頭
$ 結尾

1
2
3
4
5
6
7
8
9
10
11
12
/* 以 A 開頭的字才會匹配到 */
/^A/gm.test('Abc') // true
/^A/gm.test('bac') // false

/* 開頭有 He */
/^He/

/* 結尾有 llo */
/llo$/

/* 開頭 He 結尾 llo 中間任意字元可以有任意次數 */
/^He.*llo$/

|

1
2
3
4
5
// and 或 android,match 到 `and`roid 就不 match `android`
/and|android/

// match 到 android 還是會 match and
/android|and/

LookAround Assertions

keywords: x(?=y), x(?!y)
  • Lookahead assertions: x(?=y), x(?!y)
  • Lookbehind assertions: (?<=y)x, (?<!y)x

Look Ahead

1
2
3
4
5
6
7
8
9
10
11
12
// foo(?=bar),foo 後面要跟著 bar 才會配對到 foo
const regexp = /foo(?=bar)/;
regexp.exec('foo') // null
regexp.exec('bar') // null
regexp.exec('foobar') // [ 'foo', index: 0, input: 'foobar', groups: undefined ]

// foo(?!bar),foo 後面不能跟著 bar,如此才會配對到 foo
const regexp = /foo(?!bar)/;
regexp.exec('foo') // [ 'foo', index: 0, input: 'foo', groups: undefined ]
regexp.exec('foo123') // [ 'foo', index: 0, input: 'foo123', groups: undefined ]
regexp.exec('bar') // null
regexp.exec('foobar') // null

Look Behind

1
2
3
4
5
6
7
8
9
10
11
12
// (?<=foo)bar,當 bar 前面有 foo 時才會配對到 bar
const regexp = /(?<=foo)bar/;
regexp.exec('foo') // null
regexp.exec('bar') // null
regexp.exec('foobar') // [ 'bar', index: 3, input: 'foobar', groups: undefined ]

// (?<!foo)bar,當 bar 前面沒有 foo 時才會配對到 bar
const regexp = /(?<!foo)bar/;
regexp.exec('foo') // null
regexp.exec('bar') // [ 'bar', index: 0, input: 'bar', groups: undefined ]
regexp.exec('123bar') // [ 'bar', index: 3, input: '123bar', groups: undefined ]
regexp.exec('foobar') // null

:exclamation: Lookbehind assertions 屬於 ES2018 的語法,須注意相容性。

常用例子

西元生日

1
/^[1-9]\d{3}-\d{2}-\d{2}$/

開頭只能 1-9 ^[1-9]
連續三個數字 \d{3}
連續兩個數字 \d{2}

身份證字號

1
/^[A-Z]\d{9}$/

第一個是大寫英文字,後面連續 9 個數字

GMAIL

1
/^\w+@gmail\.com$/

+ 不能什麼都不填
\. 因為 . 是特殊字元

網址(URL)

1
2
3
4
5
/* 需要 http(s) protocol */
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/

/* 不需要 http(s) protocol */
/[-a-zA-Z0-9@:%._\+~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:%_\+.~#?&//=]*)/

What is a good regular expression to match a url @ StackOverflow

HTML 標籤

Match HTML Tag

1
2
3
// 關鍵在於 [^>]*,匹配除了 > 以外的其他內容

/<\s*a[^>]*>(.*?)<\s*/\s*a>/g

筆記來源

掃描二維條碼,分享此文章