# 正規表示式
[](https://hackmd.io/EWQ1ZAAhStOSWzieyx_TLQ)

## 1. 正規表示式簡介 Introduction to Regular Expressions
正規表示式(Regular Expression,常簡寫為 regex、regexp 或 RE)是一種用來描述字串模式的語法規則。它可以用一組約定的符號當作樣板,比對想要的字串格式,例如找出符合電子郵件或手機號碼格式的資料。
許多程式語言和文字編輯器都支援正規表示式,它是一種強大且通用的文字模式比對工具。
:::info
RE的線上比對網站:https://regex101.com/
這個網站能幫助你即時測試和理解正規表示式的比對結果。
:::
## 2. 正規表示式基本語法 Basic Regex Syntax

### 2.1 字元類別與特殊符號 Character Classes & Metacharacters
- `.` 代表除了換行符號外的任意單一字元
- `\d` 代表單個數字字元,相當於 [0-9]。
- `\w` 代表單個單字字元(a-z、A-Z、0-9、下劃線 _)。
- `\s` 代表單個空白字元(空格、Tab 等)。
- `\D`、`\W`、`\S` 分別是 `\d`、`\w`、`\s` 的反向(否定),例如 \D 比對任何非數字字元。
**範例:**
- `.at` 可比對 "cat"、"hat"、"rat" 等。
- `\d\d\d `可比對三位數字。
- `\w\w\w` 可比對三個單字字元。
#### 字元集合 Character Sets []
- `[]` 字元集合:比對中括號內任一個字元。例如 [abc]ar 可比對 "aar"、"bar"、"car"。
- `[0-9]`、`[a-z]`:中括號內的連字號代表範圍,可比對 0-9 或 a-z(區分大小寫)。
- `[^A-Z]`:若 ^ 出現在中括號內,表示否定,即排除 A-Z,其餘皆可。
**範例:**
- `[cab]ar` 可比對 "car"、"bar"、"aar"。
- `[^0-9]` 可比對任何非數字字元。
- `^c` 可比對開頭是 'c' 的字串。
### 2.2 數量符號 (Quantifiers)
數量符號可用來設定緊鄰其前方的模式出現的次數:
- `?`:前面的字元是可選的,可出現 0 次或 1 次。
- `*`:前面的字元可出現任意次(含 0 次)。
- `+`:前面的字元至少出現一次。
- `{n}`:前面的字元重複 n 次。
- `{m,n}`:前面的字元至少重複 m 次,最多 n 次。
**範例:**
- `colou?r`:可比對 "color" 和 "colour"。
- `\d{3,}`:可比對 3 個或更多數字。
- `\d*`:可比對任意數目的數字。
- `a+b`:可比對 "ab"、"aaab"、"aaaaaab" 等。
- `\w{3}`:可比對連續 3 個字元。
- `\d{2,4}`:可比對 2 到 4 個數字。
#### 貪婪 vs. 非貪婪匹配 (Greedy vs. Non-Greedy)
預設情況下,`*` 和 `+` 都是「貪婪的」,它們會盡可能匹配**最長**的字串。如果在它們後面加上一個 `?` (例如 `*?` 或 `+?`),它們就會變成「非貪婪」模式,轉而匹配**最短**的字串。
**範例:**
- This.*test (貪婪):在 "This is a test. This is only a test." 中,會比對整個字串,因為 .* 會盡可能多地匹配字元。
- 匹配結果:['This is a test. This is only a test']
- This.*?test (非貪婪):在同樣的字串中,會分別比對到 "This is a test" 與 "This is only a test",因為 .*? 在找到第一個 "test" 後就會停止,然後繼續向後尋找下一個匹配。
- 匹配結果:['This is a test', 'This is only a test']

### 2.3 開頭與結尾符號 Anchors & Boundaries
- `^` 若出現在模式開頭,表示必須從字串的**第一個位置開始比對**。
- `$` 若出現在模式結尾,表示直到字串的**最後一個位置才結束比對**。
- `\b` 單詞邊界 (Word Boundary):\b 會比對一個「單字字元」(\w) 和「非單字字元」(\W) 之間的位置。它能確保你比對到的是一個獨立的單詞。
**範例:**
- `^Hello`:可比對開頭是 "Hello" 的字串。
- `world$`:可比對結尾是 "world" 的字串。
- `^Hello world$`:完全比對 "Hello world" 字串,前後沒有空白或其他字元。
- `\bcat\b`:只比對獨立的單詞 "cat",而不會比對到 "caterpillar" 或 "tomcat"。
### 2.4 分組與選擇 Grouping & Alternation
* |:或 (OR)。用來比對兩個模式的任一個。
* ():捕獲分組 (Capturing group)。將小括號內的模式視為一個整體,並將比對到的內容「捕獲」起來,便於後續提取。
* (?:...):非捕獲分組 (Non-capturing Group)。功能與 () 類似,可以將一部分模式群組起來,但不會將比對到的內容儲存為一個可提取的分組。
**範例:**
* c(at|ow) 可比對 "cat" 或 "cow"。
* (\d{4})-(\d{6}) 針對電話號碼 0912-345678,可比對整個 "0936-279195",並透過分組提取 "0936"(第一組)和 "279195"(第二組)。
* \b(?:\d{1,3}\.){3}\d{1,3}\b (比對 IP 位址)。此處 (?:...) 僅用於將「數字+點」的組合重複 3 次,而不會單獨捕獲 192. 或 168. 等部分。
:::success
**() 與 [] 的區別**
- `[]` 字元集合:比對集合內的任一字元。例如 [abc] 只會比對 'a' 或 'b' 或 'c' 這單一字元。
- `()` 分組:將一個模式視為一個整體。例如 (abc) 會比對 "abc" 這個連續的字串,並將其捕獲為一個分組。
:::
### 2.5 預查斷言 Lookaround Assertions (進階)
預查斷言是一種「零寬度斷言」,它會檢查目前位置的左邊或右邊是否符合某個模式,但不會實際消耗任何字元,只會檢查條件是否滿足。
* (?=...):正向預查 (Positive Lookahead)。檢查目前位置的右邊是否符合某個模式。
* (?!...):負向預查 (Negative Lookahead)。檢查目前位置的右邊是否不符合某個模式。
* (?<=...):正向後查 (Positive Lookbehind)。檢查目前位置的左邊是否符合某個模式。(此處略過,作為進階主題)
* (?<!...):負向後查 (Negative Lookbehind)。檢查目前位置的左邊是否不符合某個模式。(此處略過,作為進階主題)
**範例:**
- 正向預查:驗證密碼強度
`^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$` (驗證密碼)。它包含多個條件:
1. `(?=.*[a-z])`:從字串開頭向後看,必須存在一個小寫字母。
1. `(?=.*[A-Z])`:從字串開頭向後看,必須存在一個大寫字母。
1. `(?=.*\d)`:從字串開頭向後看,必須存在一個數字。
1. `.{8,}`:如果上面三個條件都滿足,則實際比對並消耗至少 8 個任意字元。
- 負向預查:找出不是 .txt 結尾的檔案名稱
`\b\w+\.(?!txt$)\w+\b`
這個模式會比對一個單詞,後面跟著一個點 .,但這個點後面不能是 txt 且為字串結尾。
例如:在 "file.doc, report.pdf, notes.txt" 中,會比對 "file.doc" 和 "report.pdf",但不會比對 "notes.txt"。
## 3. Python 中的正規表示式應用 Regex in Python
Python 使用內建的 re 模組來實作正規表示式。
在 Python 中,建議使用 r'...'(raw string)來定義 RE 樣板,可避免反斜線 \ 被視為跳脫字元。
### 3.1 尋找單一匹配
RE 以下的方法可用來比對一次字串
- search():掃描整個字串,返回第一個成功的匹配。
- match():只從字串的開頭進行比對,若開頭不符則失敗。
- fullmatch():比對整個字串是否與模式完全相同。
若比對成功,它們會回傳一個 Match 物件(下一節探討);若失敗,則回傳 None。由於匹配失敗會回傳 None,因此在使用 Match 物件前,需先檢查是否存在:
`if match` 或 `if match is not None`
**範例:search、match、fullmatch 的區別**
```python=
import re
# 範例字串
text = "哈囉,世界!這是一個範例文字。"
# 使用 re.match() 來比對以 "哈囉" 開頭的字串
match_obj = re.match(r'哈囉', text)
if match_obj:
print("re.match() 比對成功:", match_obj.group())
print(f"開始位置: {match_obj.start()}, 結束位置: {match_obj.end()}")
else:
print("re.match() 未找到比對")
# 使用 re.search() 來搜尋包含 "世界" 的字串
search_obj = re.search(r'世界', text)
if search_obj:
print("re.search() 比對成功:", search_obj.group())
print(f"開始位置: {search_obj.start()}, 結束位置: {search_obj.end()}")
else:
print("re.search() 未找到比對")
# 使用 re.fullmatch() 來比對整個字串
fullmatch_obj = re.fullmatch(r'哈囉,世界!這是一個範例文字。', text)
if fullmatch_obj:
print("re.fullmatch() 比對成功:", fullmatch_obj.group())
print(f"開始位置: {fullmatch_obj.start()}, 結束位置: {fullmatch_obj.end()}")
else:
print("re.fullmatch() 未找到比對")
```
輸出:
```
re.match() 比對成功: 哈囉
開始位置: 0, 結束位置: 2
re.search() 比對成功: 世界
開始位置: 3, 結束位置: 5
re.fullmatch() 比對成功: 哈囉,世界!這是一個範例文字。
開始位置: 0, 結束位置: 15
```
:::info
**re.match()與re.search()的差別**
re.match只有匹配字串的開頭,如果字符串開頭就不符合正則表達式,則匹配失敗,函式回傳 None;而re.search()則是整個字串都會做匹配,只要找到一個匹配就表示成功,整個字串都沒有匹配才會回傳 None。
:::
### 3.2 Match 物件與分組提取 Match Object & Group Extraction
上一節介紹的三個方法若比對成功會回傳一個 Match 物件,而 Match 物件還可以透過下列屬性獲得更多資訊:
- match.group(0) 或 match.group():獲得完整的比對內容。
- match.group(1), match.group(2)...:獲得第一個、第二個... () 分組的內容。
- match.groups():以元組 (tuple) 形式返回所有分組的內容。
- match.start() / match.end():返回匹配子串的起始/結束索引。
**範例:提取 Email 的使用者名稱與域名**
```python=
import re
text = "聯絡信箱是 username@example.com,請盡快回覆。"
# 我們使用兩個 () 來分別捕獲 @ 前後的部分
pattern = r'([a-zA-Z0-9_.+-]+)@([a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+)'
result = re.search(pattern, text)
if result:
print(f"完整匹配 (group): {result.group()}")
print(f"完整匹配 (group(0)): {result.group(0)}")
print("-" * 20)
print(f"第一個分組 (group(1)): {result.group(1)}") # 使用者名稱
print(f"第二個分組 (group(2)): {result.group(2)}") # 域名
print("-" * 20)
print(f"所有分組 (groups): {result.groups()}")
print(f"匹配範圍: 從 {result.start()} 到 {result.end()}")
```
輸出:
```
完整匹配 (group): username@example.com
完整匹配 (group(0)): username@example.com
--------------------
第一個分組 (group(1)): username
第二個分組 (group(2)): example.com
--------------------
所有分組 (groups): ('username', 'example.com')
匹配範圍: 從 6 到 26
```
:::success
**進階技巧:命名分組 (Named Groups)**
當分組很多時,用數字 (group(1), group(2)) 可能會造成混亂。此時可以使用 (?P<name>...) 語法為分組命名,之後便可透過名稱來提取,增加程式碼的可讀性。
```python=
import re
text = "我的電話是 0912-345678"
pattern = r'(?P<area_code>\d{4})-(?P<number>\d{6})'
match = re.search(pattern, text)
if match:
# 透過名稱提取分組
print(f"區碼: {match.group('area_code')}")
print(f"號碼: {match.group('number')}")
```
輸出:
```
區碼: 0912
號碼: 345678
```
:::
### 3.3 尋找所有匹配 Finding All Matches
`re.findall()` 可以找到字串中**所有**非重疊的匹配,並以列表 (list) 形式返回。
**範例 1:找出所有 Email**
```python=
import re
text = "My emails are test@test.com and hello@world.org"
match = re.findall(r'[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+', text)
print(match)
for email in match:
print(email)
```
輸出:
```
['test@test.com', 'hello@world.org']
test@test.com
hello@world.org
```
**範例 2:貪婪 vs. 非貪婪的應用**
RE 本質上是貪婪的(找出最長比對),若要找出所有可能的最短比對,就要使用非貪婪匹配(加 ?)。
```python=
import re
# 範例 1:文字段落
text1 = "This is a test. This is only a test."
print(re.findall(r'This.*test', text1)) # 貪婪匹配:['This is a test. This is only a test']
print(re.findall(r'This.*?test', text1)) # 非貪婪匹配:['This is a test', 'This is only a test']
# 範例 2:HTML 標籤
html = "<p>First paragraph.</p><div>Some content.</div><p>Second paragraph.</p>"
# 貪婪模式:.* 會從第一個 <p> 一直匹配到最後一個 </p>
greedy_matches = re.findall(r'<p>.*</p>', html)
print(f"貪婪匹配: {greedy_matches}") # 貪婪匹配:['<p>First paragraph</p><p>Second paragraph</p>']
# 非貪婪模式:.*? 只會匹配到最近的那個 </p>
non_greedy_matches = re.findall(r'<p>.*?</p>', html)
print(f"非貪婪匹配: {non_greedy_matches}") # 非貪婪匹配:['<p>First paragraph</p>', '<p>Second paragraph</p>']
```
:::success
**使用 re.finditer() 處理大型檔案**
`re.finditer(pattern, string)` 與 `findall` 類似,但返回一個迭代器 (iterator),其元素為 Match 物件。在處理大型檔案時,這種方式更節省記憶體。
:::
### 3.4 字串替換與分割 Substitution & Splitting
#### 取代符合的字串
使用 re.sub(pattern, repl, string) 可以用新字串 repl 替換所有比對到的模式 pattern。
**範例:**
```python=
import re
text = "My phone is 0936-279195, your phone is 0988-123456"
new_text = re.sub(r'\d{4}-\d{6}', '******', text)
print(new_text)
```
輸出:
```
My phone is ******, your phone is ******
```
**範例:搭配分組進行字串替換:**
```python=
import re
text = "My phone is 0936-279195, your phone is 0988-123456"
pattern = r"(\d{4})-(\d{6})"
replacement = r"\1-####"
result = re.sub(pattern, replacement, text)
print(result)
```
**範例:repl 也可以是一個回呼函數,用來對每個比對到的地方做進一步處理:**
```python=
import re
text = "Their phone numbers are 0936-279195 and 0988-123456"
def replace_phone(match):
phone = match.group()
return phone[:4] + "-******"
new_text = re.sub(r'\d{4}-\d{6}', replace_phone, text)
print(new_text)
```
輸出:
```
Their phone numbers are 0936-****** and 0988-******
```
#### 分割字串
re.split(pattern, string):依據比對到的模式 pattern 分割字串。
**範例:以空白分割字串**
```python
import re
text = "apple banana\tcherry"
print(re.split(r'\s+', text)) # ['apple', 'banana', 'cherry']
```
### 3.5 修改比對行為:旗標 (Flags)
在進行正規表示式比對時,有時我們需要微調比對的規則,例如「忽略大小寫」或「讓 `.` 也能比對換行符號」。這時就可以使用 `flags` 參數。
`flags` 可以在兩種地方使用:
1. 在 `re.compile()` 中設定,建立一個帶有特定比對規則的樣板物件。
2. 直接在 `re.search()`, `re.findall()`, `re.sub()` 等函式中當作參數傳入,僅對該次操作生效。

#### 常用旗標 (Flags)
以下介紹幾個最常用的旗標:
**1. `re.IGNORECASE` (或 `re.I`):忽略大小寫**
讓樣板在比對時不區分英文字母的大小寫。
```python=
import re
text = "Hello, world. HELLO, PYTHON."
# 不使用 flag,只會找到第一個 "Hello"
print(re.findall(r'hello', text)) # []
# 使用 re.IGNORECASE,大小寫的 "hello" 都會被找到
print(re.findall(r'hello', text, flags=re.IGNORECASE)) # ['Hello', 'HELLO']
```
**2. `re.MULTILINE` (或 `re.M`):多行模式**
預設情況下,`^` 和 `$` 只會比對整個字串的開頭和結尾。啟用多行模式後,`^` 和 `$` 也能比對每一行的開頭和結尾。
```python=
import re
text = """first line
second line
third line"""
# 不使用 flag,^ 只比對整個字串的開頭
print(re.findall(r'^\w+', text)) # ['first']
# 使用 re.MULTILINE,^ 會比對每一行的開頭
print(re.findall(r'^\w+', text, flags=re.MULTILINE)) # ['first', 'second', 'third']
```
**3. `re.DOTALL` (或 `re.S`):點 (.) 匹配所有字元**
預設情況下,`.` (點) 會比對除了換行符號 `\n` 以外的任何字元。啟用此旗標後,`.` 就能比對包含換行符號在內的**所有**字元。
```python=
import re
text = "hello\nworld"
# 不使用 flag,. 遇到換行符號 \n 就會停止
match1 = re.search(r'hello.world', text)
print(match1) # None
# 使用 re.DOTALL,. 可以成功比對換行符號
match2 = re.search(r'hello.world', text, flags=re.DOTALL)
print(match2.group()) # 'hello\nworld'
```
#### 組合使用多個旗標
若要同時使用多個旗標,可以用 `|` (位元 OR 運算子) 將它們連接起來。
```python=
import re
text = "Start of line 1\nEND OF LINE 2"
# 同時忽略大小寫 (I) 並啟用多行模式 (M)
pattern = re.compile(r'^start.*end$', flags=re.I | re.M | re.S)
match = pattern.search(text)
if match:
print("成功匹配多行且忽略大小寫的內容:")
print(match.group())
```
輸出:
```
成功匹配多行且忽略大小寫的內容:
Start of line 1
END OF LINE 2
```
| 旗標 (Flag) | 縮寫 | 說明 |
| ------------------- | ------ | ------------------------------------------------------------ |
| `re.IGNORECASE` | `re.I` | 進行忽略大小寫的比對。 |
| `re.MULTILINE` | `re.M` | 使 `^` 和 `$` 能作用於每一行的開頭和結尾。 |
| `re.DOTALL` | `re.S` | 使 `.` 特殊字元可以比對任何字元,包含換行符號 `\n`。 |
| `re.VERBOSE` | `re.X` | 允許在樣板中加入空白和註解,讓複雜的 RE 更易讀(進階用法)。 |
### 3.6 預編譯模式 Compiling Patterns
若一個正規表示式需要被重複使用,建議先用 re.compile() 將其編譯成一個【樣板物件】,可以提升執行效率。編譯後的物件擁有 search(), findall() 等所有方法。
```python=
import re
pattern = re.compile(r'\d{4}-\d{6}')
# 使用樣板物件的方法
match = pattern.search("Call me at 0936-279195")
print(match.group())
matches = pattern.findall("test 0936-123456 and 0977-456789")
print(matches)
```
輸出:
```
0936-279195
['0936-123456', '0977-456789']
```
`re.compile()` 的第二個參數可以傳入 `flags` 來修改比對行為,詳細用法請參考上一節 **3.5 修改比對行為:旗標 (Flags)**。
範例:`pattern = re.compile(r'hello', flags=re.IGNORECASE)`
### 3.7 RE 使用時機
* 避免過於複雜的正規表示式。
* 如果你只需要尋找一個匹配項的話,可以使用 re.search() 方法。
* 如果你需要尋找多個匹配項,可以使用 re.findall() 方法。
* 如果你只需要尋找給定的文字串是否以某一個模式開頭的話,可以使用 re.match() 方法。
* 如果需要替換字串中符合模式的部分,可以使用 re.sub() 方法。
* 如果你需要對一個或多個文字串進行多次匹配的話,可以先使用 re.compile() 將正規表達式模式編譯成正規表達式物件,然後再使用各種方法進行匹配操作。
:::success
若模式太複雜,可能導致 RE 的性能低下,例如:ReDoS(RE 拒絕服務)風險,避免易被惡意輸入容易卡住的模式。
:::
## 4. 實務應用範例 Practical Applications
### 4.1 格式驗證 Pattern Validation
**範例:驗證電子郵件**
```python
import re
email = "test@example.com"
pattern = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
print(re.match(pattern, email)) # <re.Match object; span=(0, 17), match='test@example.com'>
# 測試無效 email
invalid_email = "test@.com"
print(re.match(pattern, invalid_email)) # None
```
**範例:驗證 URL**
```python
import re
url_pattern = re.compile(r'^(https?://)?([\da-z\.-]+)\.([a-z\.]{2,6})([/\w \.-]*)*/?$')
print(url_pattern.match("https://www.example.com/path")) # <re.Match object; ...>
print(url_pattern.match("ftp://example.com")) # None
```
**範例:驗證密碼強度**
```python
import re
# 使用了 ^, $, 與 (?=...) 正向預查
pw_pattern = re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{8,}$')
print(pw_pattern.match("Passw0rd123")) # <re.Match object; ...>
print(pw_pattern.match("password123")) # None (缺少大寫)
print(pw_pattern.match("Password")) # None (缺少數字)
print(pw_pattern.match("Pass123")) # None (長度不足)
```
### 4.2 資料擷取 Text Extraction
**範例:提取 IP 地址**
```python=
import re
text = "Server IP: 192.168.1.1, Client IP: 10.0.0.255, Invalid: 999.999.999.999"
# 使用了 \b 單詞邊界與 (?:...) 非捕獲分組
ip_pattern = re.compile(r'\b(?:\d{1,3}\.){3}\d{1,3}\b')
ips = ip_pattern.findall(text)
print(ips) # ['192.168.1.1', '10.0.0.255']
```
**解析 HTML 標籤(簡單情況)**
```python=
import re
html_text = """
<div class="member_name">
<a href="/profile/john">John Doe</a>
</div>
<div class="member_info_title">職稱</div>
<div class="member_info_content">工程師</div>
<div class="member_contact">
<a href="mailto:john@example.com">聯絡</a>
</div>
"""
# 提取姓名
# 使用 [^"]+ 來非貪婪匹配「除了雙引號以外」的任何字元(href 的屬性值)
name_pattern = re.compile(r'<div class="member_name">\s*<a href="[^"]+">([^<]+)</a>')
names = name_pattern.findall(html_text)
# 提取職稱
title_pattern = re.compile(
r'<div class="member_info_title">.*?職稱</div>\s*<div class="member_info_content">([^<]+)</div>'
)
titles = title_pattern.findall(html_text)
# 提取 Email
email_pattern = re.compile(r'mailto:([^"]+)')
emails = email_pattern.findall(html_text)
print("Names:", names)
print("Titles:", titles)
print("Emails:", emails)
```
輸出:
```
Names: ['John Doe']
Titles: ['工程師']
Emails: ['john@example.com']
```
### 4.3 資料清理 Data Cleaning
**範例:清理多餘空白**
將文字中的連續空白替換為單一空格,並去除首尾空白,常用於資料清洗。
```python=
import re
messy_text = " This is a test . "
clean_text = re.sub(r'\s+', ' ', messy_text).strip()
print(clean_text) # 'This is a test .'
```
### 4.4 日期與網址解析 Date & URL Parsing
**範例:解析日期(YYYY-MM-DD 格式)**
從文字中提取標準日期,適用於報告或資料處理。
```python=
import re
date_text = "Meeting on 2023-10-14 and 2024-01-01, not 2023-13-01"
date_pattern = r'\d{4}-\d{2}-\d{2}'
dates = re.findall(date_pattern, date_text)
print(dates) # ['2023-10-14', '2024-01-01'] (此處忽略了無效日期,因為 RE 只比對格式,不驗證邏輯)
```
:::success
關於使用 RE 解析 HTML 的提醒
雖然正規表示式可以用來解析簡單的 HTML,但它非常脆弱。HTML 結構的微小變化,例如多一個空格、換行、屬性順序調換,或出現巢狀標籤,都可能導致 RE 比對失敗。
因此,強烈不建議在正式專案中使用 RE 解析複雜的 HTML。請務必使用專業的 HTML 解析套件,例如:
- BeautifulSoup:語法友善,易於上手,適合初學者與快速開發。
- lxml:基於 C 語言,效能極高,支援 XPath 和 CSS 選擇器,適合處理大型文件。
:::
## 5. 下課練習
請在 https://regex101.com/ 練習以下題目:
1. 試用 RE 驗證台灣手機號碼格式(例如: 0912-345678),並提取後 4 碼。
2. 試用 RE 提取字串中的 URL(例如: https://www.google.com.tw, http://tw.yahoo.com)。
3. 試用 RE 提取 IP:Port(例如: 140.128.73.11:8080, 120.108.9.37:4123)。
