Python 資料結構 – 字串(str)
正規表示式(re 模組)
簡介
在日常的文字處理、檔案分析、網頁爬蟲或資料清理工作中,字串 是最常出現的資料型別。而單純使用字串的切割、搜尋或比對方法往往只能解決簡單的需求,當面對 複雜的模式(例如驗證電子郵件、抽取日期、過濾不合法字元)時,手寫大量的 if…else 會讓程式既冗長又難以維護。
正規表示式(Regular Expression,簡稱 regex) 正是為了這類需求而設計的強大工具。Python 內建的 re 模組提供了一套完整且表意清晰的語法,讓開發者能以「模式」的方式描述字串結構,並以極高的效率完成搜尋、取代、分割等操作。掌握 re,不只可以大幅縮短程式碼,還能提升程式的可讀性與可維護性,對於 初學者 與 中級開發者 都是必備技能。
核心概念
1. 基本函式與使用流程
re 模組最常用的函式有四個:
| 函式 | 功能說明 |
|---|---|
re.search(pattern, string, flags=0) |
在 整個字串 中搜尋第一個符合 pattern 的位置,回傳 Match 物件或 None。 |
re.match(pattern, string, flags=0) |
只在 字串開頭 嘗試比對,若成功回傳 Match 物件,否則 None。 |
re.findall(pattern, string, flags=0) |
找出 所有 不重疊的匹配,回傳字串列表。 |
re.sub(pattern, repl, string, count=0, flags=0) |
將符合 pattern 的部分 取代 為 repl,count 控制替換次數。 |
使用步驟:
- 編寫正規表示式(
pattern)。- 呼叫對應的
re函式,傳入目標字串。- 處理返回的
Match物件(如取出子群組)或結果列表。
2. 正規表示式的語法要點
2.1 文字與特殊字符
| 符號 | 說明 |
|---|---|
. |
任意單一字元(除換行) |
\d |
任意數字,等價於 [0-9] |
\D |
非數字,等價於 [^0-9] |
\w |
單字字符(字母、數字、底線),等價於 [A-Za-z0-9_] |
\W |
非單字字符,等價於 [^A-Za-z0-9_] |
\s |
任意空白字符(空格、Tab、換行) |
\S |
非空白字符 |
^ |
字串開頭(或在 [] 中表示「非」) |
$ |
字串結尾 |
[] |
字元集合,如 [abc]、[a-z] |
| ` | ` |
() |
捕獲群組,用於提取子字串或設定優先順序 |
?、*、+、{m,n} |
量詞,分別代表「0 或 1 次」「0 次或多次」「1 次或多次」與「最少 m 次、最多 n 次」 |
2.2 貪婪與非貪婪
預設量詞是 貪婪(greedy),會盡可能匹配最多的字元。若想讓它 非貪婪(lazy),在量詞後加上 ?:
re.search(r'<.*>', '<div>test</div>') # 會一次匹配整個 "<div>test</div>"
re.search(r'<.*?>', '<div>test</div>') # 非貪婪,分別匹配 "<div>" 和 "</div>"
3. 常用旗標(flags)
| 旗標 | 說明 |
|---|---|
re.IGNORECASE(或 re.I) |
忽略大小寫 |
re.MULTILINE(或 re.M) |
讓 ^、$ 匹配每一行的開頭與結尾 |
re.DOTALL(或 re.S) |
讓 . 能匹配換行符 |
re.VERBOSE(或 re.X) |
允許在正規表示式中加入空白與註解,提高可讀性 |
程式碼範例
以下範例均使用 Python 3,搭配詳細註解說明每一步的意圖與結果。
範例 1:驗證電子郵件格式
import re
# 正規表示式說明:
# ^ : 開頭
# [\w\.-]+ : 使用者名稱,可包含字母、數字、底線、點或破折號,一次或多次
# @ : 必須有 @ 符號
# [\w\.-]+ : 網域名稱,同上
# \.[a-zA-Z]{2,}$ : 最後的頂級域名,至少兩個字母
email_pattern = r'^[\w\.-]+@[\w\.-]+\.[a-zA-Z]{2,}$'
def is_valid_email(email: str) -> bool:
"""回傳 email 是否符合基本格式"""
return re.match(email_pattern, email) is not None
# 測試
print(is_valid_email('alice@example.com')) # True
print(is_valid_email('bob@sub.domain.co')) # True
print(is_valid_email('invalid@@test.com')) # False
重點:使用
re.match僅檢查字串開頭,配合^與$確保整個字串都符合規則。
範例 2:抽取所有日期(YYYY/MM/DD 或 YYYY-MM-DD)
import re
text = """
今天是 2024/03/15,明天是 2024-03-16。
上個月的資料為 2024/02/28,請自行核對。
"""
date_pattern = r'(\d{4})[-/](\d{2})[-/](\d{2})'
# findall 會回傳每個捕獲群組的 tuple
dates = re.findall(date_pattern, text)
# 轉換成易讀的字串格式
formatted = [f'{y}/{m}/{d}' for y, m, d in dates]
print(formatted) # ['2024/03/15', '2024/03/16', '2024/02/28']
技巧:利用 捕獲群組
()把年、月、日分別抽出,之後可自行組合或轉換為datetime物件。
範例 3:使用 re.sub 清理文字中的多餘空白
import re
raw = "Python 是 一種 高階 程式語言。\n\n 它 支援 多種 風格。"
# 1. 把連續的空白(包括換行)壓縮成單一空格
compressed = re.sub(r'\s+', ' ', raw).strip()
print(compressed)
# => "Python 是 一種 高階 程式語言。 它 支援 多種 風格。"
# 2. 若只想保留單行換行,可先把多餘的空白換成空字串
single_line = re.sub(r'[ \t]+', '', raw) # 移除空格與 Tab
single_line = re.sub(r'\n{2,}', '\n', single_line) # 把連續換行縮成一個
print(single_line)
說明:
\\s+匹配任意連續的空白字元(空格、Tab、換行),strip()去除首尾多餘空格。
範例 4:使用 re.VERBOSE 撰寫可讀性高的正規表示式
import re
phone_pattern = re.compile(r'''
^ # 開頭
(?:\+?886|0) # 台灣國碼 (+886) 或本地開頭 0
[-\s]? # 可有 - 或空白
(?:9\d{2}|[2-8]\d{1,2}) # 手機 9xx 或市話區碼
[-\s]? # 可有 - 或空白
\d{3}[-\s]?\d{3}$ # 後 6 位數字,允許 - 或空白分隔
''', re.VERBOSE)
def normalize_phone(num: str) -> str:
"""將電話號碼正規化為 09xx-xxx-xxx 格式"""
m = phone_pattern.match(num)
if not m:
raise ValueError('Invalid phone number')
# 只保留數字,重新組合
digits = re.sub(r'\D', '', num)
return f'{digits[:4]}-{digits[4:7]}-{digits[7:]}'
print(normalize_phone('+886 912-345-678')) # 0912-345-678
print(normalize_phone('02-1234-567')) # 0212-345-67 (示例,實務請自行調整)
重點:
re.VERBOSE讓我們在正規表示式內加入註解與換行,極大提升可讀性,特別適合 複雜模式。
範例 5:利用 re.finditer 取得匹配位置與內容
import re
log = "2024-03-15 10:23:45 ERROR 系統錯誤\n2024-03-15 10:24:01 INFO 任務完成"
timestamp_pattern = r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}'
for m in re.finditer(timestamp_pattern, log):
print(f'找到時間: {m.group()} (位置 {m.start()}~{m.end()})')
應用:
finditer返回 迭代器,每個元素都是Match物件,適合需要同時取得 匹配文字 與 其在原字串中的索引 時使用。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 建議的解決方式 |
|---|---|---|
忘記加 r 前綴 |
正規表示式中常出現反斜線(\),若未使用 raw string,Python 會先把 \ 當作跳脫字元,導致錯誤。 |
永遠 用 r'...' 包住模式,例如 r'\d+'。 |
| 貪婪匹配導致過度捕獲 | .* 會一次吞掉所有可能的字元,常造成「最後一個」才匹配成功。 |
使用 非貪婪 量詞 .*?,或限制長度 {m,n}。 |
忘記使用 ^、$ |
search 只要在字串中找到任意位置就回傳,可能匹配到不想要的子字串。 |
若要驗證全字串,在模式兩端加上 ^ 與 $,或改用 fullmatch(Python 3.4+)。 |
| 過度使用捕獲群組 | 每個 () 都會產生一個子群組,若不需要取出,可不必要地佔用記憶體。 |
使用 非捕獲群組 (?:…) 只做分組而不產生子群組。 |
| 忽略 Unicode 需求 | 預設 \w 僅匹配 ASCII,對中文、日文等多語言文字會失效。 |
加上 re.UNICODE(Python 3 預設)或使用 Unicode 類別 \p{L}(需 regex 第三方套件)。 |
| 在大量文字上重複編譯 | 每次呼叫 re.search 會重新編譯模式,效能不佳。 |
使用 預編譯:pattern = re.compile(r'...'),之後重複使用 pattern.search(...)。 |
最佳實踐:
- 先寫測試:用簡短的字串驗證模式是否正確,再擴展。
- 保持可讀性:對於超過 30 個字元的模式,建議使用
re.VERBOSE並加入註解。 - 適度抽象:把常用的模式抽成函式或常數,避免硬編碼。
- 考慮效能:在大檔案上使用
finditer逐行處理,或先做簡單的字串搜尋再套用正規表示式。
實際應用場景
| 場景 | 需求 | 正規表示式的角色 |
|---|---|---|
| 表單驗證 | 檢查使用者輸入的電話、身分證、郵箱等格式 | re.fullmatch 配合適當的量詞與邊界 |
| 日誌分析 | 從大量的伺服器 log 中抽取時間戳、錯誤代碼、IP 位址 | re.finditer 搭配命名群組 ((?P<name>…)) |
| 網頁爬蟲 | 從 HTML 中抓取標題、連結、價格等資訊 | re.search 與非貪婪量詞避免跨標籤匹配 |
| 資料清理 | 移除 CSV 檔中的多餘空白、特殊字元或格式化日期 | re.sub 進行一次性批次取代 |
| 自然語言處理前處理 | 把文字中的 URL、emoji、標點符號標準化 | re.sub + re.compile 產生高效的前置處理管線 |
案例:假設你在開發一個 客服系統,需要自動摘錄使用者訊息中的「訂單編號」(
ORD-20240315-1234) 以便快速查詢。只要寫一個簡短的正規表示式r'ORD-\d{8}-\d{4}',配合re.search即可即時抓取,省去繁瑣的字串切割程式碼。
總結
正規表示式是 字串處理的萬能鑰匙,Python 的 re 模組提供了完整且易於使用的 API,讓開發者可以用簡潔的模式描述複雜的文字規則。掌握以下幾點,即可在日常開發中發揮最大效益:
- 使用 raw string (
r'...') 防止跳脫字元錯誤。 - 適當加上邊界 (
^、$) 或使用fullmatch,確保匹配整個字串。 - 善用捕獲與非捕獲群組,只保留必要的子字串。
- 預編譯常用模式,提升效能。
- 利用
re.VERBOSE撰寫可讀性高的長模式。
透過本文的概念說明與實作範例,你應該已經能在 驗證、抽取、清理、分析 四大常見任務中,熟練運用 re 模組解決問題。未來不管是簡單的表單驗證,還是大型日誌分析,只要把需求抽象成「模式」——正規表示式就會成為你最可靠的好幫手。祝你在 Python 的文字世界裡玩得開心、寫得順手!