# 【Python 筆記】正規運算式(Regular Expression) [TOC] 感謝您點進本篇文章,本篇文章為我 LukeTseng 個人首篇 Python 筆記系列文章,主要記錄我個人學習軌跡,所有內容皆用於個人學習用途,斟酌參考。若文章有任意處有誤,煩請各位指點,謝謝。 ## 簡介 這東西的名字很多,有什麼正規表達式、正規表示法、規則運算式、常規表示法等等,我個人比較習慣叫他是正規運算式,總之他的名字就是 Regular Expression(簡稱 RE、regex)就對了,在這之後我都叫他是 regex 縮寫。 regex 是一套用來描述和比對字串樣式規則的「語法」或「表示法」,通常被嵌入在各大程式語言當中,例如我們最愛用的 Python 就是其一。regex 在處理和解析字串方面十分強大,因此也是我們要學習的對象。 你可能想說在 notepad 裡面就可以用搜尋、取代這些功能了,為什麼還需要 regex?因為 regex 他不只是只有搜尋、取代這些功能而已,他還有篩選的功能,例如可以篩出特定字元、字串,以此來找到電子郵件格式及電話號碼。 ## 在 Python 使用 regex ```python import re ``` Python 本身就有內建 regex,因此只要直接引入就好了。 ## 編譯 regex 在做 regex 之前,都需要做編譯的動作,因此以下就是我們 regex 最基本的語法: ```python= import re p = re.compile('ab*') ``` :::info 建議在 regex 字串前加上 r 讓字串轉成原始字串,這樣反斜線 `\` 就不會當成跳脫字元處理。 ::: 而上面 `'ab*'` 的意思就是「**必須要有一個字母 a**」以及「**0 個或 1 個以上的的字母 b**」。 ## 字元類別(Character Classes) 字元類別允許匹配指定集合中的任一個字元,用中括號 `[]` 圍起來。 常見的有以下幾種: - `[abc]`:匹配 a、b 或 c。 - `[a-z]`:匹配任何小寫字母。 - `[A-Z]`:匹配任何大寫字母。 - `[0-9]`:匹配任何數字。 - `[^abc]`:匹配除了 a、b、c 以外的任何字元(否定)。 ## 特殊字元序列 * `\d`:匹配任何數字,等同於 `[0-9]`。 * `\D`:匹配任何非數字字元。 * `\w`:匹配任何字母、數字或底線,等同於 `[a-zA-Z0-9_]`。 * `\W`:匹配任何非字母數字字元。 * `\s`:匹配任何空白字元(空格、tab、換行等)。 * `\S`:匹配任何非空白字元。 ## 量詞(Quantifiers) 用來指定某個模式應出現的次數: * `*`:匹配前面的字元 0 次或多次。 * `+`:匹配前面的字元 1 次或多次。 * `?`:匹配前面的字元 0 次或 1 次。 * `{n}`:精確匹配 n 次。 * `{n,}`:至少匹配 n 次。 * `{n,m}`:匹配 n 到 m 次。 ## 錨點(Anchors) 用來指定匹配在字串中的位置: * `^`:匹配字串的開頭。 * `$`:匹配字串的結尾。 * `\b`:匹配單詞邊界。 * `\B`:匹配非單詞邊界。 ## 常用方法 | 方法 | 語法 | 說明 | 回傳值 | | :-- | :-- | :-- | :-- | | `re.compile()` | `re.compile(pattern, flags=0)` | 將正規表達式編譯成模式物件,可重複使用以提升效能 | Pattern 物件 | | `re.match()` | `re.match(pattern, string, flags=0)` | 從字串**開頭**開始匹配,只檢查開頭是否符合模式 | Match 物件或 None | | `re.search()` | `re.search(pattern, string, flags=0)` | 掃描整個字串,尋找**第一個**符合模式的位置 | Match 物件或 None | | `re.findall()` | `re.findall(pattern, string, flags=0)` | 找出**所有**不重疊的匹配結果 | 包含所有匹配字串的列表 | | `re.finditer()` | `re.finditer(pattern, string, flags=0)` | 找出所有匹配結果,以迭代器形式回傳 | Match 物件的迭代器 | | `re.sub()` | `re.sub(pattern, repl, string, count=0, flags=0)` | 將所有符合模式的部分替換成指定字串,count 可限制替換次數 | 替換後的新字串 | | `re.subn()` | `re.subn(pattern, repl, string, count=0, flags=0)` | 與 `re.sub()` 類似,但會回傳替換次數 | (新字串, 替換次數) 的元組 | | `re.split()` | `re.split(pattern, string, maxsplit=0, flags=0)` | 根據符合的模式分割字串,maxsplit 可限制分割次數 | 分割後的字串列表 | | `re.fullmatch()` | `re.fullmatch(pattern, string, flags=0)` | 檢查**整個字串**是否完全符合模式 | Match 物件或 None | ### 1. `re.match()` 範例 `re.match()` 為從字串開頭開始匹配模式(pattern)的方法。如果開頭符合模式則回傳 Match 物件,否則回傳 None。 以下程式碼中的 pattern 的 `^` 符號是一個錨點(anchor),表示匹配字串的開頭位置。這個符號確保 pattern 必須從字串的最開始就符合,而不是在字串中間找到符合的部分。 `^` 放在 regex 的開頭,表示 `text` 字串一開始就要是 `Hello` 否則不匹配。 :::info 在當 `re.match()` 或 `re.search()` 成功匹配時,會回傳 `match` 物件,這物件會提供三個方法來取得匹配資訊: 1. `match.group()`:以字串形式回傳匹配到的內容。 2. `match.start()`:回傳匹配內容在字串中的起始位置(索引值)。 3. `match.end()`:回傳匹配內容的結束位置(最後一個字元的索引 + 1)。 ::: ```python= import re text = "Hello, world!" pattern = r'^Hello' # 檢查字串是否以 "Hello" 開頭 match = re.match(pattern, text) if match: print("字串以 'Hello' 開頭") print("匹配內容:", match.group()) # Hello else: print("不匹配") ``` Output: ``` 字串以 'Hello' 開頭 匹配內容: Hello ``` ### 2. `re.search()` 範例 ```python= import re text = 'Hello World!, this is my first program for Python!' m = re.search(r'Python', text) if m: print('找到:', m.group()) # 找到: Python print('起始位置:', m.start()) # 起始位置: 43 print('結束位置:', m.end()) # 結束位置: 49 else: print('未找到') ``` Output: ``` 找到: Python 起始位置: 43 結束位置: 49 ``` ### 3. `re.findall()` 範例 `\d` 表示任何一個數字字元,同 `[0-9]` 就是 0 到 9 的意思。 而 `\d` 再加上一個 `+`,表示前面的 pattern 至少要出現 1 次以上。 `\d+` 整句話翻譯過來的話就是 0 到 9 的數字至少要出現 1 次以上。 `re.findall()` 方法會回傳一個匹配到的字串列表。 ```python= import re string = "My phone number is 0987654321, and my friend's phone number is 0912345678" regex = r'\d+' matches = re.findall(regex, string) print(matches) # ['0987654321', '0912345678'] ``` Output: ``` ['0987654321', '0912345678'] ``` ### 4. `re.sub()` 範例 sub 是取自 substitute 的縮寫,表示把字串交換,如下範例所示,將 Python 替換成 C++ 字串。 ```python= import re text = "I like Python because Python is so powered." result = re.sub(r'Python', 'C++', text) print(result) # I like C++ because C++ is so powered. ``` Output: ``` I like C++ because C++ is so powered. ``` 那 `re.sub()` 還有他的親姊妹叫做 `re.subn()`,兩者用法完全一樣,只差在 `re.subn()` 回傳一個元組,前面是替換完成的字串,後面是替換的次數,沿用上述範例: ```python= import re text = "I like Python because Python is so powered." result = re.subn(r'Python', 'C++', text) print(result) # ('I like C++ because C++ is so powered.', 2) ``` Output: ``` ('I like C++ because C++ is so powered.', 2) ``` ### 5. `re.split()` 範例 `re.split()` 根據匹配到的 pattern 來分割字串。 以下範例展示了將 `,;:` 的 pattern 分割字串,達到如內建方法 `.split()` 一樣的事情。但差別在於內建的 `.split()` 只能找到一種 pattern 而已。 ```python= import re text = "apple,banana;orange:grape" result = re.split(r'[,;:]', text) print(result) # ['apple', 'banana', 'orange', 'grape'] ``` Output: ``` ['apple', 'banana', 'orange', 'grape'] ``` ## 字元類別、特殊字元序列、量詞、錨點的一些例子 前面說明過了一些使用的方法,那麼就可以來講一些特殊的例子來玩玩看了。 `[^0-9]` 除了 0 到 9 以外的字元: ```python= import re print(re.findall(r'[^0-9]', "abc123")) # ['a', 'b', 'c'] ``` Output: ``` ['a', 'b', 'c'] ``` `\w+` 匹配任何字母、數字或底線: ```python= import re print(re.findall(r'\w+', "他說了 *** 在某種語言中")) # ['他說了', '在某種語言中'] ``` Output: ``` ['他說了', '在某種語言中'] ``` `*`、`?`、`{n, m}` 範例: ```python= import re # * 匹配 0 個或多個 'b' print(re.findall(r'ab*', "a ab abb abbb")) # ['a', 'ab', 'abb', 'abbb'] # ? 匹配前面的字元 0 次或 1 次 print(re.findall(r'colou?r', "color and colour")) # ['color', 'colour'] # {n,m} 匹配 2 到 4 個數字 print(re.findall(r'\d{2,4}', "1 22 333 4444 55555")) # ['22', '333', '4444', '5555'] ``` Output: ``` ['a', 'ab', 'abb', 'abbb'] ['color', 'colour'] ['22', '333', '4444', '5555'] ``` `$`、`\b` 範例: ```python= import re # $ 匹配 Hello World 字串結尾 print(re.search(r'World$', "Hello World")) # <re.Match object; span=(6, 11), match='World'> print(re.search(r'Hello$', "Hello World")) # None # \b 匹配單詞邊界 text = "cat cats caterpillar" print(re.findall(r'\bcat\b', text)) # ['cat'] print(re.findall(r'cat', text)) # ['cat', 'cat', 'cat'] ``` ## 一些小應用 ### 1. 電子郵件驗證 ```python= import re def validate_email(email): pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' if re.match(pattern, email): return True return False print(validate_email("user@example.com")) # True print(validate_email("invalid.email")) # False print(validate_email("user@domain.co.uk")) # True ``` ### 2. 電話號碼擷取 ```python= import re text = """ Contact Information: Office phone number: 02-1234-5678 phone number: 0912-345-678 home phone number: (02)8765-4321 """ # 台灣電話號碼格式 patterns = [ r'\d{2}-\d{4}-\d{4}', # 02-1234-5678 r'\d{4}-\d{3}-\d{3}', # 0912-345-678 r'\(\d{2}\)\d{4}-\d{4}' # (02)8765-4321 ] for pattern in patterns: matches = re.findall(pattern, text) print(f"找到: {matches}") ``` Output: ``` 找到: ['02-1234-5678'] 找到: ['0912-345-678'] 找到: ['(02)8765-4321'] ``` ### 3. 資料擷取 ```python= import re # 從網頁內容擷取所有連結 html_content = """ <a href="https://example.com">範例</a> <a href="https://test.com">測試</a> <a href="/local/path">本地連結</a> """ urls = re.findall(r'href=["\']([^"\']+)["\']', html_content) print(urls) # ['https://example.com', 'https://test.com', '/local/path'] # 擷取價格資訊 text = "商品 A: $1,299 元,商品 B: $599 元,商品 C: $2,999 元" prices = re.findall(r'\$[\d,]+', text) print(prices) # ['$1,299', '$599', '$2,999'] ``` Output: ``` ['https://example.com', 'https://test.com', '/local/path'] ['$1,299', '$599', '$2,999'] ``` ### 4. 密碼強度驗證 ```python= import re def check_password_strength(password): # 至少 8 個字元 if len(password) < 8: return False, "密碼長度至少需要 8 個字元" # 至少包含一個大寫字母 if not re.search(r'[A-Z]', password): return False, "至少需要一個大寫字母" # 至少包含一個小寫字母 if not re.search(r'[a-z]', password): return False, "至少需要一個小寫字母" # 至少包含一個數字 if not re.search(r'\d', password): return False, "至少需要一個數字" # 至少包含一個特殊字元 if not re.search(r'[!@#$%^&*(),.?":{}|<>]', password): return False, "至少需要一個特殊字元" return True, "密碼強度良好" print(check_password_strength("weak")) # False, 不符合要求 print(check_password_strength("Strong123!")) # True, 密碼強度良好 ``` Output: ``` (False, '密碼長度至少需要 8 個字元') (True, '密碼強度良好') ``` ## Regex 線上除錯工具 網路上隨便找就有了,這邊推薦兩個網站: - regex101.com - regexr.com 輸入自己的 Regex 就可以在他的測試文件裡面知道哪些資料是被篩選的。 當在設計複雜的 Regex 的時候,就可以用這個試試看。 ## 總結 基礎語法架構: 1. 字元類別:`[abc]`、`[a-z]`、`[0-9]` 等用於匹配指定集合中的字元。 2. 特殊字元序列:`\d`(數字)、`\w`(字母數字底線)、`\s`(空白字元)。 3. 量詞:`*`(0次或多次)、`+`(1次或多次)、`?`(0次或1次)、`{n,m}` 控制出現次數。 4. 錨點:`^`(字串開頭)、`$`(字串結尾)、`\b`(單詞邊界)等定位符號。 常用方法表: | 方法 | 語法 | 說明 | 回傳值 | | :-- | :-- | :-- | :-- | | `re.compile()` | `re.compile(pattern, flags=0)` | 將正規表達式編譯成模式物件,可重複使用以提升效能 | Pattern 物件 | | `re.match()` | `re.match(pattern, string, flags=0)` | 從字串**開頭**開始匹配,只檢查開頭是否符合模式 | Match 物件或 None | | `re.search()` | `re.search(pattern, string, flags=0)` | 掃描整個字串,尋找**第一個**符合模式的位置 | Match 物件或 None | | `re.findall()` | `re.findall(pattern, string, flags=0)` | 找出**所有**不重疊的匹配結果 | 包含所有匹配字串的列表 | | `re.finditer()` | `re.finditer(pattern, string, flags=0)` | 找出所有匹配結果,以迭代器形式回傳 | Match 物件的迭代器 | | `re.sub()` | `re.sub(pattern, repl, string, count=0, flags=0)` | 將所有符合模式的部分替換成指定字串,count 可限制替換次數 | 替換後的新字串 | | `re.subn()` | `re.subn(pattern, repl, string, count=0, flags=0)` | 與 `re.sub()` 類似,但會回傳替換次數 | (新字串, 替換次數) 的元組 | | `re.split()` | `re.split(pattern, string, maxsplit=0, flags=0)` | 根據符合的模式分割字串,maxsplit 可限制分割次數 | 分割後的字串列表 | | `re.fullmatch()` | `re.fullmatch(pattern, string, flags=0)` | 檢查**整個字串**是否完全符合模式 | Match 物件或 None | ## 參考資料 [Regular Expression HOWTO — Python 3.14.0 documentation](https://docs.python.org/3/howto/regex.html) [Python RegEx - GeeksforGeeks](https://www.geeksforgeeks.org/python/regular-expression-python-examples/) [Python 規則運算式  |  Python Education  |  Google for Developers](https://developers.google.com/edu/python/regular-expressions?hl=zh-tw) [Python RegEx](https://www.w3schools.com/python/python_regex.asp) [Mastering Python RegEx: A Deep Dive into Pattern Matching - StrataScratch](https://www.stratascratch.com/blog/mastering-python-regex-a-deep-dive-into-pattern-matching/) [使用正規表達式 re - Python 教學 | STEAM 教育學習網](https://steam.oxxostudio.tw/category/python/library/re.html)