本文 AI 產出,尚未審核

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 的部分 取代replcount 控制替換次數。

使用步驟

  1. 編寫正規表示式pattern)。
  2. 呼叫對應的 re 函式,傳入目標字串。
  3. 處理返回的 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(...)

最佳實踐

  1. 先寫測試:用簡短的字串驗證模式是否正確,再擴展。
  2. 保持可讀性:對於超過 30 個字元的模式,建議使用 re.VERBOSE 並加入註解。
  3. 適度抽象:把常用的模式抽成函式或常數,避免硬編碼。
  4. 考慮效能:在大檔案上使用 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 的文字世界裡玩得開心、寫得順手!