正規表達式(Regular Expression,簡稱 regex 或 re)是一種強大的文字模式匹配工具。在 Hook 系統中,主要用於解析 Markdown 連結和驗證輸入格式。

基本用法

re.search() - 搜尋匹配

1import re
2
3text = "Hello, Python 3.11!"
4
5# 搜尋數字
6match = re.search(r'\d+\.\d+', text)
7if match:
8    print(match.group())  # "3.11"

re.match() - 從開頭匹配

1import re
2
3text = "Python is great"
4
5# 只從字串開頭匹配
6match = re.match(r'Python', text)  # 成功
7match = re.match(r'great', text)   # None(不是從開頭開始)

re.findall() - 找出所有匹配

1import re
2
3text = "Call 123-456-7890 or 098-765-4321"
4
5# 找出所有電話號碼
6phones = re.findall(r'\d{3}-\d{3}-\d{4}', text)
7# ['123-456-7890', '098-765-4321']

re.sub() - 替換

1import re
2
3text = "Hello   World"
4
5# 將多個空格替換為單個
6result = re.sub(r'\s+', ' ', text)
7# "Hello World"

正規表達式語法

基本字元

模式說明範例
.任意字元(除換行)a.c 匹配 “abc”, “a1c”
\d數字 [0-9]\d+ 匹配 “123”
\w單字字元 [a-zA-Z0-9_]\w+ 匹配 “hello”
\s空白字元\s+ 匹配空格、tab
^字串開頭^Hello
$字串結尾World$

數量詞

模式說明範例
*0 或多次a* 匹配 “”, “a”, “aaa”
+1 或多次a+ 匹配 “a”, “aaa”
?0 或 1 次a? 匹配 “”, “a”
{n}恰好 n 次a{3} 匹配 “aaa”
{n,m}n 到 m 次a{2,4} 匹配 “aa”, “aaa”, “aaaa”

群組與擷取

1import re
2
3text = "[Link Text](https://example.com)"
4
5# 使用群組擷取
6match = re.search(r'\[([^\]]+)\]\(([^)]+)\)', text)
7if match:
8    link_text = match.group(1)  # "Link Text"
9    link_url = match.group(2)   # "https://example.com"

實際範例:Markdown 連結檢查

來自 .claude/lib/markdown_link_checker.py

 1import re
 2
 3class MarkdownLinkChecker:
 4    # Markdown 連結正則表達式
 5    # 匹配 [text](/python/03-stdlib/regex/target) 格式,排除圖片 ![alt](/python/03-stdlib/regex/src)
 6    INLINE_LINK_PATTERN = re.compile(
 7        r'(?<!!)\[([^\]]+)\]\(([^)]+)\)'
 8    )
 9
10    # 引用式連結定義 [ref]: target
11    REFERENCE_DEF_PATTERN = re.compile(
12        r'^\s*\[([^\]]+)\]:\s*(.+)$',
13        re.MULTILINE
14    )
15
16    # 引用式連結使用 [text][ref]
17    REFERENCE_USE_PATTERN = re.compile(
18        r'\[([^\]]+)\]\[([^\]]+)\]'
19    )

模式解析

r'(?<!!)\[([^\]]+)\]\(([^)]+)\)' 解析:

部分說明
(?<!!)負向前瞻,確保前面不是 !(排除圖片)
\[匹配字面 [
([^\]]+)群組 1:擷取連結文字(一個或多個非 ] 字元)
\]匹配字面 ]
\(匹配字面 (
([^)]+)群組 2:擷取連結目標(一個或多個非 ) 字元)
\)匹配字面 )

使用範例

 1def parse_markdown_links(self, content: str) -> list[dict]:
 2    """解析 Markdown 內容中的所有連結"""
 3    links = []
 4    lines = content.split('\n')
 5
 6    for line_num, line in enumerate(lines, start=1):
 7        # 行內連結 [text](/python/03-stdlib/regex/target)
 8        for match in self.INLINE_LINK_PATTERN.finditer(line):
 9            links.append({
10                "text": match.group(1),
11                "target": match.group(2),
12                "line": line_num
13            })
14
15    return links

編譯正規表達式

對於重複使用的模式,預先編譯可提升效能:

1import re
2
3# 編譯模式
4pattern = re.compile(r'\d+')
5
6# 重複使用
7pattern.search(text1)
8pattern.findall(text2)
9pattern.sub('X', text3)

實際應用:Hook 驗證

來自 .claude/lib/hook_validator.py

 1class HookValidator:
 2    # 共用模組導入模式
 3    HOOK_IO_PATTERNS = [
 4        r"from\s+hook_io\s+import",
 5        r"from\s+lib\.hook_io\s+import",
 6    ]
 7
 8    # 命名規範模式
 9    VALID_NAME_PATTERNS = [
10        r"^[a-z0-9](/python/03-stdlib/regex/[a-z0-9\-_]*[a-z0-9])?\.py$",
11    ]
12
13    def _has_import(self, content: str, patterns: list[str]) -> bool:
14        """檢查是否有符合任一模式的導入"""
15        return any(
16            re.search(pattern, content)
17            for pattern in patterns
18        )
19
20    def check_naming_convention(self, hook_path: Path) -> list:
21        """檢查命名規範"""
22        filename = hook_path.name
23
24        valid_name = any(
25            re.match(pattern, filename)
26            for pattern in self.VALID_NAME_PATTERNS
27        )
28        # ...

常用旗標

re.IGNORECASE(忽略大小寫)

1import re
2
3re.search(r'hello', 'Hello World', re.IGNORECASE)  # 匹配

re.MULTILINE(多行模式)

1import re
2
3text = """line 1
4line 2
5line 3"""
6
7# 每行開頭的 "line"
8matches = re.findall(r'^line', text, re.MULTILINE)
9# ['line', 'line', 'line']

re.DOTALL(點號匹配換行)

1import re
2
3text = "start\nmiddle\nend"
4
5# 無 DOTALL:. 不匹配換行
6re.search(r'start.*end', text)  # None
7
8# 有 DOTALL:. 匹配換行
9re.search(r'start.*end', text, re.DOTALL)  # 匹配

外部連結判斷

 1class MarkdownLinkChecker:
 2    EXTERNAL_PATTERNS = [
 3        r'^https?://',
 4        r'^mailto:',
 5        r'^tel:',
 6        r'^ftp://',
 7    ]
 8
 9    def _is_external_link(self, target: str) -> bool:
10        """檢查是否為外部連結"""
11        return any(
12            re.match(pattern, target)
13            for pattern in self.EXTERNAL_PATTERNS
14        )

最佳實踐

1. 使用原始字串

1# 好:使用 r'' 避免跳脫問題
2pattern = r'\d+\.\d+'
3
4# 不好:需要雙重跳脫
5pattern = '\\d+\\.\\d+'

2. 預編譯重複使用的模式

1# 好:編譯後重複使用
2pattern = re.compile(r'\d+')
3for text in texts:
4    pattern.findall(text)
5
6# 不好:每次都重新編譯
7for text in texts:
8    re.findall(r'\d+', text)

3. 使用命名群組

1# 有命名群組:更易讀
2pattern = re.compile(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})')
3match = pattern.search("2024-01-20")
4if match:
5    print(match.group('year'))   # "2024"
6    print(match.group('month'))  # "01"

思考題

  1. re.search()re.match() 有什麼區別?
  2. 為什麼 Markdown 連結模式使用 (?<!!) 負向前瞻?
  3. re.compile() 的好處是什麼?

實作練習

  1. 寫一個正規表達式,驗證電子郵件格式
  2. 從 Python 原始碼中擷取所有函式定義(def function_name(
  3. 實作一個函式,將 Markdown 標題(# Title)轉換為 HTML(<h1>Title</h1>

上一章:subprocess - 執行外部命令 下一章:logging - 日誌系統