# 正規表達式
`Regular Expression`
從入門到入土
---
## 放在前面
Regex
- 適用於:
所有需要字串**驗證、比對、搜尋、擷取**等場景
- 不適用於:
長文匹配(效能問題)
不支援環境
----
工具
- [本機JS調試(F12 Dev Tool)](about:blank)
- [線上測試Regex101](https://regex101.com/)
- [規則一覽Wiki](https://zh.wikipedia.org/zh-tw/正規表示式#PCRE表达式全集)
---
## 引入
各位是否遇過需要搜尋**特定字串**的情況?
假定要從Youtube網址中擷取出影片ID(v=xxxxxxxx)
在輸出`3HDP4xp8Oj4`的前提下,各位會如何做呢?
```=
https://www.youtube.com/watch?v=3HDP4xp8Oj4&feature=youtu.be
```
----
使用[String.split](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/split)?
使用[URLSearchParams](https://developer.mozilla.org/docs/Web/API/URLSearchParams)?
或者...
使用[正規表達式](https://google.com/search?q=regex)!?
----
[**測試網址**](https://jsbench.me/dxlegluvt7)

---
## 規則
- \
- ?、+、\*、{}
- ()、|
- []
- ^、$、\b
- (?=)
- flag
---
### 字元匹配
在Regex內的一般文字將直接配對
```javascript=
/abc/ // 匹配abc
"123abc456".match(/abc/)
// ['abc', index: 3, input: '123abc456', groups: undefined]
```
**「.」** 可以匹配`\r\n`(換行字元)以外任何字元
```javascript=
/tr.../ // tr以及任意三個字元
"trick or treat".match(/tr.../g)
// ['trick', 'treat']
```
----
### 集合
`[]`中括號內的字元,
會以「或」的形式當成**一個普通字元**配對
```javascript=
/[早午晚]安/ // 早安 or 午安 or 晚安
"早安 午安 晚安 平安".match(/[早午晚]安/g)
// ['早安', '午安', '晚安']
```
`[]`內可以使用`「-」`表示連續的「或」
```javascript=
/[0-9A-Za-z]/ // 數字+英文字母大小寫
"59分 87分 8a分".match(/[6-9][0-9]分/g)
// ['87分']
```
----
#### 反集合
在`[]`的開頭加入跳脫字元`「^」`
代表**除了涵蓋內**的字元都可以配對
同樣可以使用`「-」`表示連續排除
```javascript=
/[^abc123ABC]/
// 排除小寫a-z與空格
"Apple book Cat Docker".match(/[^a-z ]../g)
// ['App', 'Cat', 'Doc']
```
---
### 量詞
`{}`量詞均**代表前面的表達式**重複次數
- `{x}` **剛好**出現x次
- `{x,}`**至少出現x次**,**至多無限**次
- `{x,y}`至少出現x次,且**至多只能**出現y次,
同時x<=y (否則報錯)
- ⚠特別注意,`xy`只能是數字,
並且`,`間不能有空格
想想看如果放入非數字,或者加空格會發生什麼?
[點我看答案](https://regex101.com/r/SWlI6H/1)
----
#### 量詞-縮寫
- `?`代表前面的表達式**可有可無** (0~1次) = `{0,1}`
- `+`代表前面的表達式**至少一次** (1~inf次) = `{1,}`
- `*`代表前面的表達式**隨意出現** (0~inf次) = `{0,}`
```javascript=
/https?:\/{2}example.com/ /*匹配example.com,且https或http皆可*/
/[0-9]+.[0-9]+/ /*匹配浮點數(有潛在問題)*/
/.*/ /*隨意匹配*/
```
想一下第二個範例的浮點數潛在什麼問題?
[點我看答案](https://regex101.com/r/EqiKZ1/1)
----
#### 非貪心量化
詳細介紹 [Regular expression: 貪婪、非貪婪](https://gist.github.com/yosiaken/dbe1e4f6a7f730ce5482981d9f251231)
簡單來講,在量詞後面加上`?`會盡可能減少配對
```javascript=
re = /阿.+是蟑螂!*/
re2 = /阿.+?是蟑螂!*/
str = "阿啊啊啊是蟑螂啊啊啊幹幹幹幹幹幹是蟑螂!!!!ldsjghiwjp"
/* re <------------------------------------>
re2 <---------->
貪婪與非貪婪的配對數量差距
*/
```
[貪婪效能測試](https://jsbench.me/mslelk3z68)
---
### 特殊字元「\」
反斜線`\`可以將字元轉譯,
包含轉譯成特殊字元(`bBdDfnrsStuvwWx`),
以及轉譯回普通字元(`^$()*+?.[\{|`)
- `\f`匹配分頁符號(form-feed)
- `\n`匹配換行(line-feed)
- `\r`匹配回車(return)
- `\t`匹配製表符(horizontal tab)
- `\v`匹配垂直製表符(vertical tab)
- `\x--`以16進位的ASCII code匹配 (00~ff)
- `\u----`以10進位unicode匹配 (0000~9999)
----
#### 特殊字元-縮寫
| 符號 | 等效縮寫 | 匹配內容 |
| ----- | ---------------- | ----------------- |
| `\d` | `[0-9]` | 數字(*註1) |
| `\D` | `[^0-9]` | 非數字 |
| `\s` | `[\r\n\t\f\v ]` | 空格、換行等(*註2) |
| `\S` | `[^\r\n\t\f\v ]` | 非空格、換行等 |
| `\w` | `[A-Za-z0-9_]` | 單字字元(*註3) |
| `\W` | `[^A-Za-z0-9_]` | 非單字字元 |
----
以下狀況於Unicode的Regex出現
- 註1: `\d\D` 會符合/排除**全形**數字字元
- 註2: `\s\S` 會符合/排除**全形、特殊**空格
- 註3: `\w\W` 會符合/排除**各國文字**
---
### 群組
`()`小括號有三個功能
1. 提高匹配的優先順序
2. 把一段表達式包裹起來(通常用於量化)
3. 將括號內匹配到的擷取為群組輸出
```javascript=
"abc".match(/([a-z]+)/)[1] // abc
"abc".match(/[a-z]+/)[1] // undefined
```
----
#### 或
`|`將表達式分隔成左右兩段,以「或」的形式匹配
如果未放置在括號內,則作用在全域
```javascript=
"fool".match(/food|l/) // l
"fool".match(/foo(d|l)/) // fool
```
----
#### 命名/非捕捉 群組
- `(?<name>)`在括號開頭加上`?<>`,
同時在`<>`內輸入文字即可為群組命名
- `(?:)`如果不需要括號的捕捉功能,
可在括號開頭加上`?:`
```javascript=
"25歲".match(/(?<age>\d+)/).groups?.age // 25
"25歲".match(/(\d+)/).groups?.age // undefined
"abc".match(/([a-z]+)/)[1] // abc
"abc".match(/(?:[a-z]+)/)[1] // undefined
```
----
#### 向後參考
- `()\num`當群組捕捉到文字時,
可在其後方使用向後參考匹配相同的文字
`\num`意思為第num個被捕捉到的文字
```javascript=
"一心一意".match(/(.).\1./) // 一心一意
"一石二鳥".match(/(.).\1./) // null
```
---
### 定位匹配
- `^`匹配至**字串開頭**
- `$`匹配至**字串結尾**
- `\b`匹配至**單字邊界**
- `\B`匹配**非單字邊界**
```javascript=
"123".match(/^\d+$/) // 123
"a123".match(/^\d+$/) // null
(/er\b/).test("never") // true
(/er\b/).test("version") // false
```
----
#### 向前/後看
「在一個字串中找出連續數字 6~8 個」
"12345 XD Hi12345678ab666666cd987654321"
只能輸出:12345678 和 666666
思考一下如何解決?
[點我看解答](https://regex101.com/r/55R5X8/1)
----
- `X`與`Y`分別為一段表達式,
在`表達式X`的前/後加入`(?=)`或`(?!)`,
可以指定`表達式X`前/後方須出現的`表達式Y`。
- ⚠如果`Y`放在`X`前方,
則`?`的後方需要多一個小於(`<`)符號
----
ahead與behind可以合併使用,
且**不計入匹配範圍**
- look ahead
- Positive: X(?=Y)
- Negative: X(?!Y)
- look behind
- Positive: (?<=Y)X
- Negative: (?<!Y)X
[RegExp 應用: lookahead , lookbehind](http://darkk6.blogspot.com/2017/03/regexp-lookahead-lookbehind.html)
---
### 優先級
| 優先權 | 符號 |
| ----- | -------------------------- |
| 最高 | `\` |
| 高 | `()`、`(?:)`、`(?=)`、`[]` |
| 中 | 量詞 `*`、`+`、`?`、`{n}` |
| 低 | `^`、`$`、中介字元 |
| 次低 | 相鄰字元 |
| 最低 | `\|` |
---
### Flag
flag通常放置於雙斜線後方
```javascript=
/[\w\W]*/gmiu
```
| 符號 | 全稱 | 含義 |
| ----- | ----------- | ------------------- |
| `g` | Global | 匹配所有可能 |
| `m` | Multi line | `^`、`$`可以匹配`\r\n` |
| `i` | Insensitive | 大小寫不敏感 |
| `u` | Unicode | 以unicode進行匹配 |
---
## 練習
[嘗試做做看IP驗證吧](https://regex101.com/r/jWbXsj/1)
---
## 結語
雖然Regex可以做的事很多,
但通常善用第三方lib會是更好的選擇。
例如時間驗證等等
----
## 參考
- [規則一覽Wiki](https://zh.wikipedia.org/zh-tw/正規表示式#PCRE表达式全集)
- [RegExp 應用: lookahead , lookbehind](http://darkk6.blogspot.com/2017/03/regexp-lookahead-lookbehind.html)
- [Regular expression: 貪婪、非貪婪](https://gist.github.com/yosiaken/dbe1e4f6a7f730ce5482981d9f251231)
- [淺談 regex 及其應用](https://marco79423.net/articles/淺談-regex-及其應用)
- [[JS] 正則表達式(Regular Expression, regex)](https://pjchender.dev/javascript/js-regex/)
---
# End