Learning Objectives
- 正規表達式(regular expression)語法:特殊字元、集合、重複次數、群組
- 使用 re 模組:compile()、search()、match()、findall() 與 group()
- 貪婪與非貪婪比對
- re 模組的編譯標誌(flag):re.I、re.M、re.S
為什麼需要正規表達式
要找出文字裡「所有電話號碼」「所有 email」「以大寫字母開頭的句子」,用 in 或 find() 只能找固定的字。
正規表達式(regular expression,簡稱 regex)用一串特殊字元描述「長什麼樣子的字串」,
例如 \d{4}-\d{2}-\d{2} 代表「4 個數字、橫線、2 個數字、橫線、2 個數字」這種日期格式。
正規表達式源自 UNIX 環境,現在幾乎所有程式語言與文字編輯器都支援。Python 內建的 re 模組提供 Perl 風格的語法;
樣式寫錯時會引發 re.error 例外。
使用步驟
import rere.compile(r'樣式')建立 Regex 物件,樣式記得用 raw string(r'...')- 呼叫
search()在字串裡找第一個符合的地方,傳回 Match 物件;找不到傳回None(match()只比對開頭) - 呼叫 Match 物件的
group()取得實際符合的字串
為什麼要用 raw string? 一般字串裡 \n 是換行、\t 是 Tab;正規表達式大量使用反斜線(\d、\.),
寫成 r'\d' 反斜線就會原封不動交給 re,不會先被字串轉義吃掉。
>>> len('\n'), len(r'\n')
(1, 2)
>>> print(r'C:\new\table')
C:\new\table
compile → search/match → group
search() 掃描整個字串找第一個符合的位置;match() 只看字串開頭符不符合。兩者找不到時都傳回 None,所以使用前常先用 if 檢查。
特殊字元速查
| 字元 | 意思 | 例子 |
|---|---|---|
. |
換行以外的任意一個字元 | b.t 符合 bat、bet、but |
^ |
字串開頭 | ^up 符合 up、upper 開頭的 up |
$ |
字串結尾 | ear$ 符合 clear、top gear 結尾的 ear |
* |
前一個元素 0 次以上 | ab* 符合 a、ab、abb |
+ |
前一個元素 1 次以上 | ab+ 符合 ab、abb |
? |
前一個元素 0 或 1 次 | ab? 符合 a、ab |
{m}、{m,n} |
剛好 m 次;m 到 n 次({,n} 最多 n 次、{m,} 至少 m 次) |
a{3,5} 符合 3~5 個 a |
[ ] |
集合中的任一字元;- 表示範圍;[^...] 不在集合中 |
[a-zA-Z]、[0-9]、[^aeiou] |
( ) |
群組 | (ab)+ 符合 ab、abab |
\ |
跳脫特殊字元,或表示特殊序列 | \. 比對句點本身;\d 數字、\w 文字字元、\s 空白 |
另外,| 代表「或」,例如 cat|dog 符合 cat 或 dog。
另外還有「前後看」:prev(?=next)(後面接著 next 的 prev)、prev(?!next)、(?<=prev)next、(?<!prev)next。
特殊字元逐一試
re.findall(樣式, 字串) 傳回所有符合的字串組成的串列。迴圈每一圈測試一組(樣式, 文字),比對輸出與右邊的 pat、text。也可以在下方改樣式再跑一次。
貪婪與非貪婪
*、+、{m,n} 預設是貪婪的:在符合的前提下盡可能多吃字元。後面加上 ? 變成非貪婪:盡可能少吃。比較 <.*> 和 <.*?> 在 '<a> b <c>' 上的結果。
想一想:樣式比對到什麼
下面的程式會印出什麼?
import re
print(re.findall(r"\d+", "a1b22c333"))
下面的程式會印出什麼?
import re
print(re.search(r"<.+>", "<a> b <c>").group())
下面的程式會印出什麼?
import re
print(re.findall(r"^[A-Z]\w*", "Apple banana Cherry"))
群組:抓出需要的部分
樣式裡的小括號 ( ) 形成群組。group(0) 或 group() 是整個符合的字串,group(1)、group(2)…依左括號出現的順序編號;groups() 傳回全部群組的元組。
編譯標誌:re.I、re.M、re.S
標誌(flag)放在 re.compile(樣式, 標誌) 或 re.findall(樣式, 字串, 標誌) 的最後,改變比對方式。text 有三列,注意 \n 對 ^ 與 . 的影響。
常用標誌與方法
| 標誌 | 說明 |
|---|---|
re.I(re.IGNORECASE) |
忽略大小寫 |
re.M(re.MULTILINE) |
多列模式,^、$ 以每一列為單位 |
re.S(re.DOTALL) |
讓 . 也比對換行字元 |
re.A(re.ASCII) |
\w、\d、\s 等只比對 ASCII 字元(預設是 Unicode,所以 \w 也比對中文字) |
re.X(re.VERBOSE) |
樣式可以分段、加註解,比較好讀 |
多個標誌用 | 合併,例如 re.I | re.M。
| 方法 | 傳回 |
|---|---|
search(string) |
掃描整個字串,第一個符合的 Match 物件;沒有就 None |
match(string) |
只比對開頭;不符合就 None |
fullmatch(string) |
整個字串都符合才傳回 Match 物件(適合檢查格式) |
findall(string) |
所有符合字串組成的串列 |
prog = re.compile(樣式) 再 prog.search(s),等同 re.search(樣式, s);同一個樣式要用很多次時,先 compile 比較有效率。
Lab:在檔案裡逐列搜尋
這是 Lab 的 regex.py,改成直接讀取工作目錄裡的 regex_data.txt(內容在「檔案」分頁)。目前樣式是 [Tt]he;請把第 4 行依序換成 r'\d'、r'\d{3}'、r'^[A-Z]'、r'^[^A-Z]'、r'b.t'、r'\.$' 再跑,觀察印出哪些列。
想一想:re 的方法
下面的程式會印出什麼?
import re
print(re.match(r"\d+", "abc123"))
下面的程式會印出什麼?
import re
mo = re.search(r"(\w+)@(\w+)", "id: amy@ncu")
print(mo.group(2))
下面的程式會印出什麼?
import re
print(len(re.findall(r"the", "The the THE", re.I)))
實作:抓出 email 的帳號與網域
用 re.findall() 搭配兩個群組,找出文字裡所有 email,每個印一行「帳號 網域」。預期輸出:amy ncu.edu.tw 和 bob gmail.com。
重點精華回顧
- 正規表達式用特殊字元描述字串的樣子;Python 用內建的
re模組,樣式一律寫成 raw stringr'...'。 - 步驟:
re.compile()建立 Regex 物件 →search()/match()得到 Match 物件(找不到是None)→group()取得內容。 - 特殊字元:
.任意字元、^開頭、$結尾、*+?{m,n}重複次數、[ ]集合、|或、( )群組、\跳脫。 *、+、{m,n}預設貪婪(盡可能多),加上?變非貪婪(盡可能少)。findall()傳回所有符合的字串;樣式有群組時傳回群組組成的元組。- 標誌:
re.I忽略大小寫、re.M多列、re.S讓.比對換行、re.X可加註解。