2022年5月31日 星期二

PyTorch 線上課程筆記 : 機器學習與深度學習 (二)

今天繼續聽 PyTorch 線上課程第二課, 本系列之前的筆記參考 : 


今日筆記如下 :
    1. 學 AI 必上的課程 : 
      (1). 史丹佛大學 CS231n 課程
      (2). 台大林軒田教授機器學習基石&技法
      (3). 台大李宏毅教授機器學習課程
    2. 一般程式利用邏輯條件來判斷也是人工智慧之一 (例如判斷質數), 稱為 rule-based AI , 但這些規則以外的問題完全無法解決. 
    3. 學習就是獲得解答的能力, 學習的重點是 how to learn 而非 what to learn. 
    4. 機器學習是希望機器透過資料去找出邏輯 (函數關係), 找出接近完美函數的函數. 機器學習是利用資料與演算法, 從假設集函數 (set of function) 中找出一個與完美函數無限接近的最佳函數.
    5. 遷移學習 (transfer learning) 是將某領域 (可以完全不同) 訓練得到的模型拿來做預測 (經過一些 fine-tunning), 此作法可以節省大量訓練時間. 
    6. Deep learning -> Structure learning (最難的)  
    7. 吳恩達 : 目前 99% 的 AI 都是 Deep Learning.
    8. Alpha Go 作者 David Silver : AI=DL+RL
    9. DL 的基礎知識 : 
      (1). 神經元 (neuron) : 已脫離生物學, 偏向統計學與信號學
      (2). 權重 (weight) 與偏移 (bias) : 就是函數的所有變數
      (3). 損失函數 (loss function) : 達成最佳化的技術
      (4). 線性迴歸 (linear regression) : 直線的分布情況 (做分類)
      (5). 邏輯迴歸 (logistic regression) : 不是直線分布
      (6). 啟動函數 (activation function) : 代表神經元的特色
      (7). 神經網路 (neural network) 
      (8). 層 (layer), 輪 (epoch, 同樣跑幾回), 批次 (batch, 資料分拆)
      (9). 訓練 (train), 驗證 (validation), 測試 (test)
      (10). 梯度下降 (SGD) : 損失函數對權重的偏微分
      (11). 學習率 (learning rate) : 控制梯度下降的速度
      (12). 連鎖率 (chain rule) : 反向傳遞的計算基礎
      (13). 反向傳遞 (back propagation)
      (14). MSE (Mean Square Error)
      (15). 交叉熵 (cross entropy) : 分類用的損失函數
      (16). Softmax
      (17). Normalization, Regularization
    10. 關於 PyTorch :
      (1). 高速且彈性最大的 DL 框架
      (2). PyTorch 的張量提供與 Numpy 陣列完全相同之運算
      (3). PyTorch 的張量可在 GPU 執行, 但 Numpy 陣列不行
      (4). 支援自動微分功能
      (5). 支援所有張量流 (flow) 運算
      (6). 提供類神經模組 nn
      (7). 內建電腦視覺處理
      PyTorch 中的張量物件 torch.Tensor.
    11. 何謂 Tensor :
      0 維 Tensor=純量
      1 維 Tensor=向量
      2 維 Tensor=矩陣
      3 維以上 Tensor=張量
      張量在神經網路中流動時維度會改變 -> TensorFlow

買聲寶微波爐 RE-C020PR

昨天開車去好市多買貓砂, 然後順便去民族路的全國電子買微波爐, 因為三年前買的美的微波爐壞了, 想說摩托車載不了, 今天開車剛剛好, 哪知目前缺貨 (可見上海封城影響蠻大的), 但楠梓店有貨可調, 因逢端午節車子調度問題, 可能要下周才會到貨, 反正也不急用就付錢買了, 型號為馬卡龍可愛造型 (平台式) RE-C020PR, 價格為 $2789 元. 




店員說因沒辦法立即供貨, 所以到時貨到來取貨時會贈送廚刀三件組以示歉意, 哇, 真的足感心. 

2022年5月30日 星期一

2022 年第 22 周記事

不知不覺已到五月底, 眼看端午節下周即將來臨, 要準備祭祖所需物品了. 甜粽阿運伯母已一起訂 6 顆, 鹹粽岳母會包, 趕不上的話退伍軍人那裏也可以訂, 主要是三牲與水果糖果等物而已. 

菁菁 5/17 日快篩陽, 我從 5/18 日開始自主防疫在家上班, 上周解除隔離後覺得還是繼續居家直到疫情平復為止好了, 現在疫情還在高原期, 盡量減少與人群接觸, 以前百般抗拒在家上班, 現在反而覺得不用趕打卡也不錯. 

本周最令我震驚的是鄉下養了近兩年的小雖貓突然死亡, 她的兒子五月初才疑似吃到老鼠藥中毒身亡, 結果月底母貓自己也不明原因猝死 (無外傷, 非中毒), 留下還在吃奶的三隻小貓仔. 爸說為此他這幾天心情不佳, 因為小雖貓每天都會陪他在曬穀場騎單車, 時間到若沒出去還會在門口喵喵叫催促. 有時我夜讀她還會跳到窗櫺上喵喵叫打招呼. 爸將母子倆葬在菜園東側田埂旁的芭樂樹附近, 我周末回去時給它們念了往生咒迴向. 




小雖貓大約是 2020 年 7 月或 8 月出生, 所以也才不到兩歲, 卻已生過四胎, 前兩胎都生 3~4 隻沒養活, 菁菁說鄉下的浪浪沒辦法像家貓那樣活 10 年那麼長. 

為了調查小雖貓猝死原因, 周六晚上我調看監視器影像, 發現四支攝像頭的紅外線僅一支較清楚, 於是找出幾年前小舅房子蓋好要裝監視器時婷婷代購的海康監視器組 (DS7200), 發現紙箱居然遭到白蟻入侵, 還好內部元件都 OK, 稍事清理後打算叫阿旺下周來幫我更換攝像頭 (本想自己來, 但接頭不同還要去長明街問問, 這要花不少時間, 時間最寶貴, 還是花點錢算了).  




去年六月底幫爸買的小米 POCO M3 手機兩周前突然無法開機, 我拿去裕誠路小米專賣店欲送修, 店員說也可以自己送去建國路 224 號的聯強更快, 可能當日或次日就修好了. 果然第二天就收到完修簡訊可取貨了, 店員說是內部元件故障整個機板換新. 拿回手機後去還書路過全國電子進去看微波爐, 比較之後覺得聲寶的機械旋鈕平台式馬卡龍型號 RE-C020PR 這款不錯, 平台式沒有轉盤好清理, 且方形餐具可直接放進去不怕旋轉時卡住 :


定價 3490, 目前全國賣 2700, 跟拍賣網站價格一樣. 我 2019 年在公司福利社買的中國品牌美的微波爐幾個月前使用時就出現閃光與吱吱聲, 最近終於掛點了, 不再閃光與吱吱叫, 但機器會動不會熱, 狀況跟下面這篇描述的差不多 :


才用三年就 GG, 我以前買的微波爐用到現在都十幾年ㄟ! 詢問台北客服得知高雄維修站在鳥松昌文街1號 (07-3705686), 但水某說過保還要修嗎? 想想搞不好維修費要 1000 元不值得. 我看以後不買中國品牌了 (除了小米).

周日整理鄉下冰箱, 將冷凍室以前母親做的破布子清出約十袋左右, 她仙遊之後一兩年我還有拿出來煎過幾次, 帶便當非常下飯, 但之後就一直塞在那裏. 小舅媽說放太久了不要再吃, 但我一直沒時間清. 最近冰箱上層快被波羅蜜塞爆, 所以不得不清. 但我還是留下一包繼續放上層, 紀念用. 




周日下午爬山回來經過圍牆外, 發現路旁的波蘿蜜應該過兩周可採, 我跟爸說底下好採的我們自己採送農會, 那些較高的讓來詢問的人採, 他們有工具比較厲害 : 




最近兩周都在專心複習 Python 正規表示法並整理筆記, 推遲了快兩年終於把這個項目大致完成了, 只剩進階篇還沒寫完. 最近水某在看 Netflix 的韓劇 "我的出走日記" 時我也看了一些, 我覺得還蠻好看的, 但現在沒空看, 先記下來. 

記憶卡處於禁止寫入的狀態問題

周末為了探究小雖貓瘁死原因調閱監視器影像時發現車庫米家監視器的 MicroSD 卡停在 5/26 日且提示須格式化, 執行線上格式化失敗, 只好拿梯子取下卡片用筆電格式化, 但 Win 10 檔案總管竟然無法進行格式化, 改用 SDFormatter 則出現 "記憶卡處於禁止寫入的狀態" : 




確認轉卡並未設在 Lock 位置, 為何會禁止寫入? 我找到下面這篇文章 :


解決辦法是用橡皮擦把 MicroSD 卡接點擦一擦, 也可以用鉛筆筆芯把接點塗一塗, 我都是過了, 很可惜對這張卡無效. 由於手邊沒有空白卡片, 只好將以前華為榮耀3C 手機用的威剛 16GB 卡備份後格式化拿來先用, 因為小雖貓留下的三隻小貓這幾天賴在外面鐵架上, 晚上要抓他們回來有點困難, 所以需要車庫這支監視器錄影. 

2022年5月28日 星期六

2022年5月26日 星期四

Python 學習筆記 : 正規表示法 (實例篇)

對正規表示法的原理與用法有了基本了解後, 應該透過更多常用實例來測試看看是否真正理解其用法. 本系列前面的文章參考 :


本篇參考書籍 :

1. 比對 0~255 之間的整數數字 :

比對一個整數是否在 0~255 很常用, 例如網路位址或 RGB 色碼等都以一個 byte 為一個單位, 故其值為 0~255 的整數, 其正規式如下 : 

pattern=r'''^([0-9]|                    # 一位數 :  0~9 不限制
                    [0-9]{2}|              # 兩位數 :  0~9 不限制
                    1[0-9][0-9]|          # 三位數且百位是 1 : 十位與個位 0~9 不限制
                    2[0-4][0-9]|          # 三位數且百位是 2 十位是 0~4 : 個位 0~9 不限制
                    25[0-5])$'''           # 三位數且百位是 2 十位是 5 : 個位 0~5 

注意, 因為正規式為五個選項構成, 所以必須用小括號納為一個群組, 否則前面位數較少者會先匹配, 例如 '255' 會在第一個選項 [0-9] 只匹配 2 就停止. 

例如 : 

>>> import re    
>>> pattern=r'^([0-9]|[0-9]{2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$'     
>>> re.match(pattern, '9')   
<re.Match object; span=(0, 1), match='9'>
>>> re.match(pattern, '99')    
<re.Match object; span=(0, 2), match='99'>
>>> re.match(pattern, '199')    
<re.Match object; span=(0, 3), match='199'>
>>> re.match(pattern, '249')   
<re.Match object; span=(0, 3), match='249'>
>>> re.match(pattern, '255')   
<re.Match object; span=(0, 3), match='255'>

可見均能正確匹配 0~255 之整數. 


2. 比對台灣身分證號碼 :

台灣身分證號碼格式如下 :
  • 第一個字元為大寫英文字母
  • 後面跟著 9 位數字字元
  • 第二個字元 (即第一個數字) 1=為男性; 2=為女性, 
  • 第三個字元起是 8 個 0~9 的數字, 例如 S123456789
正規式如下 : 

pattern=r'''^[A-Z]              # 大寫英文字母
                   [12]                # 第一碼 1=男性, 2=女性
                   [0-9]{8}$'''       # 後八碼是 0~9 的數字

例如 : 

>>> import re
>>> pattern=r'^[A-Z][12][0-9]{8}$'  
>>> re.search(pattern, 'S123456789')                            # 匹配
<re.Match object; span=(0, 10), match='S123456789'>
>>> re.search(pattern, 'S223456789')                            # 匹配
<re.Match object; span=(0, 10), match='S223456789'>
>>> re.findall(pattern, 'S323456789')                            # 第二字元只能 1 或 2
[]
>>> re.findall(pattern, 'a123456789')      # 第一字元須大寫
[]
>>> re.findall(pattern, 'A12345678')       # 碼數不足 9 碼
[]

前兩例目標字串與正規式相符故匹配; 第三例的第二字元為 3 不匹配, 後兩例因第一字元小寫與碼數不足而不匹配.

參考 :



3. 比對台灣手機號碼 :

在前一篇正規式基礎篇中曾以台灣固網電話碼為例說明群組之用途, 行動電話號碼的比對方式也是類似固網, 但此處要比對的是下列三種格式 :
  • 09-33123456
  • 0933-123456
  • 0933123456
台灣行動電話字頭為 09, 接著可能有一個  '-' 字元, 然後是 8 碼數字. 或者 09 後面有兩碼原始業者字頭, 接著可能有一個 '-' 字元, 然後是 6 碼數字, 其正規式如下 :

pattern=r'''(09[-]?\d{8}|              # 09 後面跟著可有可無的 '-', 接著是 8 碼數字
                  09\d{2}[-]?\d{6})'''    # 09 後面跟著 2 碼數字與可有可無的 '-', 接著是 6 碼數字

例如 : 

>>> import re
>>> pattern=r'(09[-]?\d{8}|09\d{2}[-]?\d{6})'        
>>> re.findall(pattern, '09-33123456')     # 匹配 
['09-33123456']
>>> re.findall(pattern, '0933-123456')     # 匹配
['0933-123456']
>>> re.findall(pattern, '0933123456')      # 匹配
['0933123456']
>>> re.findall(pattern, '0833123456')      # 不匹配 : 不是 09 開頭
[]
>>> re.findall(pattern, '093312345')        # 不匹配 : 碼數不足
[]
>>> re.findall(pattern, '0933-12345')       # 不匹配 : 碼數不足
[]

可見符合上列三種寫法的號碼均匹配. 


4. 比對電子郵件信箱 :

電子郵件信箱以 @ (英文念 at) 為界, 前面是使用者名稱 (稱為 local name); 後面是郵件主機網址 (稱為 domain name). 在 RFC 規範中使用者名稱最長 64 字元, 可以使用小數點但不可在開頭或連續, 整個 e-mail 總長最多 255 字元 ... 規則非常複雜, 但此處使用如下的簡化正規式 :

pattern=r'[\w.]+@[\w.]+'   

其中字元集裡 \w 表示英文字母, 數字或底線, 小數點不須跳脫, + 表示這種字元會出現 1 次以上, 例如 : 

>>> import re
>>> pattern=r'[\w.]+@[\w.]+'     
<re.Match object; span=(0, 16), match='abc123@gmail.com'>
>>> re.search(pattern, 'abc.tw@gmail.com')      
<re.Match object; span=(0, 16), match='abc.tw@gmail.com'>
>>> re.findall(pattern, 'To:abc.tw@gmail.com;abc123@gmail.com')       
['abc.tw@gmail.com', 'abc123@gmail.com']  

在 "Python 自動化的樂趣" 這本書裡, 作者使用下列正規式來比對 e-mail :

pattern=r'''[a-zA-Z0-9._%+-]+          # 使用者名稱
                 @ 
                 [a-zA-Z0-9.-]+                 # 網域名稱
                 (\.[a-zA-Z]{2,4})'''           # .com/.org, ...

例如 : 

>>> import re
>>> pattern=r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+(\.[a-zA-Z]{2,4})'   
>>> re.match(pattern, 'abc.tw@gmail.com')    
<re.Match object; span=(0, 16), match='abc.tw@gmail.com'>   
>>> re.search(pattern, 'abc.tw@gmail.com')    
<re.Match object; span=(0, 16), match='abc.tw@gmail.com'>
>>> re.search(pattern, 'abc.tw@yahoo.com.tw')    
<re.Match object; span=(0, 19), match='abc.tw@yahoo.com.tw'>
>>> re.findall(pattern, 'abc123@gmail.com')     
['.com']  
>>> re.findall(pattern, 'To:abc.tw@gmail.com;abc123@gmail.com')   
['.com', '.com']

可見 re.match() 與 re.search() 都可匹配 e-mail, 但 re.findall() 卻只匹配最後的 (\.[a-zA-Z]{2,4}).  

在 "處理大數據的必備美工刀 " 這本書附錄 B 有介紹符合 RFC 規範的 e-mail 正規式 :

pattern=r'''(?!.)(?![\w.]*?\.\.)[\w.]{1,64}@    # 環視 (?!.) 禁小數點開頭, 後禁連續小數點 
                 (?=[-a-zA-Z0-9.]{0,255}(?![-a-zA-Z0-9.]))   # 總長不超過 255 字元
                 ((?!-)[-a-zA-Z0-9]{1,63}\.)*   # 重複的 '欄位.' 結構
                 ((?!-)[-a-zA-Z0-9]){1,63}'''   # 必要的最後欄位

這裡用到了正規表示法進階功能的環視 (look around) 與斷言 (assertion), 真的很複雜. 但我實際測試卻 NG, 檢查了幾遍確認並未打錯啊, 奇怪 : 

>>> import re
>>> pattern=r'(?!.)(?![\w.]*?\.\.)[\w.]{1,64}@(?=[-a-zA-Z0-9.]{0,255}(?![-a-zA-Z0-9.]))((?!-)[-a-zA-Z0-9]{1,63}\.)*((?!-)[-a-zA-Z0-9]){1,63}'     
>>> re.search(pattern, 'abc123@gmail.com')       
>>> re.search(pattern, 'abc.tw@g-mail.com.tw')    
>>> re.findall(pattern, 'To:abc.tw@gmail.com;abc123@gmail.com')     
[]

有空再回頭研究哪裡有問題.


5. 比對網址 (URL) :

URL 的格式如下 :

[協定]://[主機位址]:[埠號]/[檔案路徑][檔名]?[查詢]#[ID]

其中協定有 http, https, 與 ftp 等, 所以較簡單的 URL 正規式可以這麼寫 :

pattern=r'''(https?|ftp)://             # 協定部分, ? 表示前面 s 可有可無
                 [\w.]+'''                      # 可以是英文字母, 數字, 底線, 小數點

例如 :

>>> import re
>>> pattern=r'(https?|ftp)://[\w.]+'     
>>> re.search(pattern, 'http://google.com.tw')    
<re.Match object; span=(0, 20), match='http://google.com.tw'>     
>>> re.search(pattern, 'https://google.com.tw')      
<re.Match object; span=(0, 21), match='https://google.com.tw'>
>>> re.search(pattern, 'ftp://abc.com.tw')     
<re.Match object; span=(0, 16), match='ftp://abc.com.tw'>

但是這個簡單的正規式只有比對到主機而已, 後面的埠號, 檔名與查詢參數均未納入. 

在 "處理大數據的必備美工刀 " 這本書附錄 B 有介紹一個較精細的 URL 正規式 (雖然也還不是嚴格的 RFC 格式) :

pattern=r"""(https?|ftp)://             # 協定部分, ? 表示前面 s 可有可無
                 [^?:]+                          # 主機名稱
                 (:[0-9]{1,5})?             # 埠號 (可有可無)
                 (/?|(/[^/]+)*                 # 檔案路徑與檔名
                 (\?[^\s"']+)?)"""           # 查詢參數 (可有可無), ? 要跳脫

此處第三列埠號其實只到 65535 而已, 但為了簡化用 [0-9]{1,5} 使得最高 99999 埠也會匹配. 

例如 :

>>> import re
>>> pattern=r"""(https?|ftp)://[^?:]+(:[0-9]{1,5})?(/?|(/[^/]+)*(\?[^\s"']+)?)"""    
>>> re.search(pattern, 'https://google.com.tw')   
<re.Match object; span=(0, 21), match='https://google.com.tw'>
>>> re.search(pattern, 'http://google.com.tw')   
<re.Match object; span=(0, 20), match='http://google.com.tw'>
>>> re.search(pattern, 'ftp://abc.com.tw')    
<re.Match object; span=(0, 16), match='ftp://abc.com.tw'>
>>> re.search(pattern, 'https://google.com.tw:80')   
<re.Match object; span=(0, 24), match='https://google.com.tw:80'>
>>> re.search(pattern, 'https://abc.com.tw:5000/get_stocks?id=2330')   
<re.Match object; span=(0, 24), match='https://abc.com.tw:5000/'> 

此正規式可以比對到埠號都沒問題, 但後面的檔案名稱與查詢參數都沒比對出來, 我仔細研究後發現這是因為書裡的正規式將埠號後面的 /? 放在管線 | 的前面所致, 因為它會先貝匹配然後比對就停止了. 下面是將 /? 放到管線 | 後面的結果 : 

>>> import re
>>> pattern=r"""(https?|ftp)://[^?:]+(:[0-9]{1,5})?((/[^/]+)*(\?[^\s"']+)?|/?)"""    
>>> re.search(pattern, 'https://abc.com.tw:5000/stock/get_stocks?id=2330')  
<re.Match object; span=(0, 48), match='https://abc.com.tw:5000/stock/get_stocks?id=2330'>
>>> re.search(pattern, 'https://abc.com.tw:5000/get_stocks?id=2330')      
<re.Match object; span=(0, 42), match='https://abc.com.tw:5000/get_stocks?id=2330'>
>>> re.search(pattern, 'https://abc.com.tw:5000/?id=2330')     
<re.Match object; span=(0, 32), match='https://abc.com.tw:5000/?id=2330'>    
>>> re.search(pattern, 'https://abc.com.tw:5000/')    
<re.Match object; span=(0, 23), match='https://abc.com.tw:5000'> 

這樣檔案路徑與參數就都會匹配了, 但是最後一個例子結尾的 '/' 不見了 (???).

另外, 在 "精通正規表達式" 這本書的第五章 HTTP 範例中使用的 URL 正規式相對簡單容易理解, 我稍做修改其中協定部分, 擴充為可比對 http/https/ftp 三種協定, 正規式如下 : 

pattern=r'''(https?|ftp)://                     # 三種協定
                 ([^/:]+)                              # 主機
                 (:(\d+))?                            # 埠號 (可有可無)
                 (/.*)?'''                               # 路徑檔案與查詢參數等

例如 :

>>> import re
>>> pattern=r'(https?|ftp)://([^/:]+)(:(\d+))?(/.*)?'   
>>> re.search(pattern, 'https://abc.com.tw:5000/stock/get_stocks?id=2330')   
<re.Match object; span=(0, 48), match='https://abc.com.tw:5000/stock/get_stocks?id=2330'>
>>> re.search(pattern, 'https://abc.com.tw:5000/get_stocks?id=2330')   
<re.Match object; span=(0, 42), match='https://abc.com.tw:5000/get_stocks?id=2330'>
>>> re.search(pattern, 'https://abc.com.tw:5000/?id=2330')   
<re.Match object; span=(0, 32), match='https://abc.com.tw:5000/?id=2330'>
>>> re.search(pattern, 'https://abc.com.tw:5000/')   
<re.Match object; span=(0, 24), match='https://abc.com.tw:5000/'>

可見此正規式簡單又好用. 


6. 比對網頁中的超連結 (href) :

網頁中的超連結放在 a 標籤的 href 屬性中, 例如 :

<a href="http://www.google.com.tw">Google</a>
<a href='http://www.google.com.tw'>Google</a>
<a href=http://www.google.com.tw>Google</a>

可見 href 屬性的值可以用單引號或雙引號括起來, 也可以不需要引號. 從網頁 HTML 原始碼中擷取超連結網址的正規式如下 :

pattern=r'''href=               
                 [\'"]?                # 可有可無的引號 (第一個是單引須跳脫)
                 ([^\'" >]+)        # URL (一個以上非引號與 > 字元)
                 [\'"]?'''              # 可有可無的引號 (第一個是單引須跳脫)

其中 URL 部分用小括號做成分組, 所以呼叫 group(1) 會傳回不包含 href 的 URL 本身. 這是我從下列這篇論壇討論文章中拿來修改的 :


注意, 由於長原始字串使用三個單引號, 所以字元集裡面的單引號須跳脫 (雙引號不用). 字元集裡面除了 '-' (表示放中間時) 與 ']' (任何處) 須跳脫外, 其餘特殊符號都不須跳脫 (跳也沒關係), 但引號就要看正規式字串外圍是用甚麼而定, 用單引號就要跳單引號, 用雙引號就要跳雙引號, 否則正規式會被截斷, 可能會出現錯誤. 例如 : 

>>> import re    
>>> html='''  
<a href="http://www.google.com" target="_blank">Google</a> 
<a href='http://tw.yahoo.com' >Yahoo Taiwan</a> 
<a href=http://twitter.com >Yahoo Taiwan</a>''' 
>>> match=re.search(r'href=[\'"]?([^\'" >]+)[\'"]?', html)   
>>> match   
<re.Match object; span=(4, 32), match='href="http://www.google.com"'>    
>>> match.group()    
'href="http://www.google.com"'
>>> match.group(1)   
'http://www.google.com'   

可見呼叫 Match 物件的 group() 或 group(0) 會傳回整個匹配字串, 呼叫 group(1) 則傳回第一個分組的匹配字串, 也就是 URL 的部分; 但呼叫 re.findall() 則是將各分組匹配字串放在串列中傳回, 例如 : 

>>> re.findall(r'href=[\'"]?([^\'" >]+)[\'"]?', html)     
['http://www.google.com', 'http://tw.yahoo.com', 'http://twitter.com']    

但這個簡單的正規式沒有處理標籤 a 後面與 href 左右可能有超過 1 個空格的情形, 對於不標準的網頁可能會無法匹配全部超連結內的 URL, 例如 : 

>>> html='''   
<a href="http://www.google.com" target="_blank">Google</a>   
<a href = 'http://tw.yahoo.com' >Yahoo Taiwan</a>   
<a href= http://twitter.com >Yahoo Taiwan</a>'''  
>>> re.findall(r'<a[^>]+href=["\']?(.*)["\']?', html)    
['http://www.google.com" target="_blank">Google</a>', ' http://twitter.com >Yahoo Taiwan</a>']

此例中的網頁原始碼與上面不同, 刻意在第二個與第三個 a 標籤的 href 周圍留了空格, 可見此正規式無法抓出全部 URL. 

在 "處理大數據的必備美工刀 " 這本書附錄 B-19 頁介紹了一個較精細的超連結 URL 正規式, 它就有對 href 周邊空格進行處理, 但原式有點複雜, 我先將其簡化修改如下 :

pattern=r'''<a\s+                            # 超連結標籤 (後有一個以上空格)
                href\s*=\s*                    # href 屬性 (等號前後可能有 0 個以上空格)
                ["\']?([^"\'\s]+)["\']?'''    # 將 URL 分組 (前後可能有引號)
             
此正規式以 \s 來代表空格 (事實上還包括 Tab 等字元), URL 部分以分組 ([^"\'\s]+) 捕捉, 例如 : 

>>> html='''   
<a href="http://www.google.com" target="_blank">Google</a>   
<a href = 'http://tw.yahoo.com' >Yahoo Taiwan</a>   
<a hrefhttp://twitter.com >Yahoo Taiwan</a>'''  
>>> re.search(r'<a\s+href\s*=\s*["\']?([^"\'\s]+)["\']?', html)    
<re.Match object; span=(1, 32), match='<a href="http://www.google.com"'>
>>> re.findall(r'<a\s+href\s*=\s*["\']?([^"\'\s]+)["\']?', html)    
['http://www.google.com', 'http://tw.yahoo.com', 'http://twitter.com']                 

嗯, 這個正規式比較優. 


7. 比對網頁中的圖片網址 (src) :

網頁中的圖片以 img 標籤呈現 (沒有結束標籤), 圖片的網址放在 src 屬性中, 例如 : 

<img src="http://abc.com.tw/cat.jpg">
<img src = dog.jpg width=300 height=200>
<img   src = 'bird.jpg' alt='bird'>

擷取圖片網址的正規式與上面擷取網址的類似, 只要將 a 標籤改成 img 標籤, 將 href 屬性改成 src 屬性即可 : 

pattern=r'''<img\s+                        # 圖片標籤 (後有一個以上空格)
                src\s*=\s*                      # src 屬性 (等號前後可能有 0 個以上空格)
                ["\']?([^"\'\s]+)["\']?'''    # 將 URL 分組 (前後可能有引號)

例如 :

>>> import re    
>>> html='''   
<img src="http://abc.com.tw/cat.jpg">   
<img src = /images/dog.jpg width=300 height=200>   
<img   src = 'bird.jpg' alt='bird'>'''   
>>> re.findall(r'<img\s+src\s*=\s*["\']?([^"\'\s]+)["\']?', html)     
['http://abc.com.tw/cat.jpg', '/images/dog.jpg', 'bird.jpg']     

但是有些網頁的 src 不見得是緊接在 img 後面, 可能穿插其它屬性, 這樣上面的簡化正規式就會比對破功了, 例如 : 

>>> html='''  
<img src="http://abc.com.tw/cat.jpg">   
<img src = /images/dog.jpg width=300 height=200>   
<img border='1'  src = 'bird.jpg'>   
<img alt=deer  src = 'deer.jpg'>'''   
>>> re.findall(r'<img\s+src\s*=\s*["\']?([^"\'\s]+)["\']?', html)       
['http://abc.com.tw/cat.jpg', '/images/dog.jpg']       # 後面兩張圖片之 URL 沒抓到

此例第三, 四張圖片的 src 前面分別有 border 與 src 參數, 導致這兩張圖片不匹配, 解決辦法就是在正規式的 src 前面添加可能會出現的東西, 想法很簡單,  src 前面會出現的是某個屬性的值, 它可能用單引號或雙引號括起來, 但也可能沒有, 就像此例中的 deer 一樣, 因此只要匹配這三種情況即可, 修改後的正規式如下 : 

pattern=r'''<img                             # 圖片標籤 (後有一個以上空格)
                 [^>]*                            # 任何不是結束標籤的字元
                \s+src\s*=\s*                 # src 屬性 (等號前後可能有 0 個以上空格)
                ["\']?([^"\'\s]+)["\']?'''     # 將 URL 分組 (前後可能有引號)

此處我在 img 後面添加上面用過的 [^>]* 來表示 src 之前的任何可能的屬性設定. 注意, 長字串時字元集面的引號都不須要跳脫 (但跳脫也無妨), 但若寫成單列的短字串時, 就要看正規式字串整個外面是用單引還是雙引, 用哪個就跳哪個, 否則字串會被提早截斷而錯誤, 例如 : 

>>> html='''  
<img src="http://abc.com.tw/cat.jpg">   
<img src = /images/dog.jpg width=300 height=200>   
<img border='1'  src = 'bird.jpg'>   
<img alt=deer  src = 'deer.jpg'>'''   
>>> re.findall(r'<img[^>]*\s+src\s*=\s*["\']?([^"\'\s]+)["\']?', html)     
['http://abc.com.tw/cat.jpg', '/images/dog.jpg', 'bird.jpg', 'deer.jpg']    

可見四個圖片 URL 都能順利捕捉了. 

所以上面的網頁超連結也可以用 [^>]* 來處理 href 前面有其它屬性的問題 :

>>> html='''    
<a  target="_blank"   href="http://www.google.com">Google</a>     
<a target="_blank" href = 'http://tw.yahoo.com' >Yahoo Taiwan</a>      
<a href= http://twitter.com >Yahoo Taiwan</a>'''    
>>> re.findall(r'<a[^>]*\s+href\s*=\s*["\']?([^"\'\s]+)["\']?', html)    
['http://www.google.com', 'http://tw.yahoo.com', 'http://twitter.com']

此例刻意將 target="_blank" 放在 href 前面仍能正確匹配三個 URL. 

茲將以上所測試隻常用正規式表列如下 :


 常見的比對任務 正規式
 比對 0~255 的整數 ([0-9]|[0-9]{2}|1[0-9][0-9]|2[0-4][0-9]|25[0-5])
 比對台灣身分證號碼 [A-Z][12][0-9]{8}
 比對台灣手機號碼 (09[-]?\d{8}|09\d{2}[-]?\d{6})
 比對電子郵件信箱 [\w.]+@[\w.]+ 
 比對網址 (URL) (https?|ftp)://([^/:]+)(:(\d+))?(/.*)?
 比對網頁中的超連結 <a[^>]*\s+href\s*=\s*["\']?([^"\'\s]+)["\']?
 比對網頁中的圖片網址 <img[^>]*\s+src\s*=\s*["\']?([^"\'\s]+)["\']?


參考 : 


2022年5月25日 星期三

抖音一姐唐藝

最近在臉書流行音樂網社群看到唐藝的演唱, 原先只覺得就是眾多翻唱口水歌的網紅之一而已, 但看了她幾次的表演後覺得很有特色, 主要是舞蹈動感十足, 演唱方式熱情很有煽動力, 尤其是電音類型的快節奏歌曲. 搜尋 Google 才知道原來她在抖音有超過 900 萬粉絲, 在中國有抖音一姐稱號哩! 

唐藝原名曹世晶, 湖南益陽人, 曾經參加中國夢之聲選秀節目擠進 20 強. 2018 年起在長沙國金街九號口開始做戶外演唱事業, 街頭就是她的直播間, 據說很多去長沙玩的人會特地去現場看她的表演, 把戶外直播間變成了吸引遊客的地方觀光資源. 

下面是 Youtube 上她演唱的幾首很有動感的流行歌曲 : 


















看了她的表演感覺全身都動起來了. 雖然有人批評她歌藝並不出色, 但如同她自己在台上說的, "歌唱得好不好沒關係, 開心最重要". 但也是因為歌唱得好才會吸引這麼多粉絲吧?

高科大還書 1 本 : 一本書精通Python : 爬蟲遊戲AI完全制霸

昨天詢問母校圖書館入館防疫規定, 得知現在疫苗打三劑即可換證入館, 傍晚下班後便去還這本被預約的書, 順便掃描一下架上有無好書, 呵呵 : 


此書為中國人所寫, 書中範例非常豐富, 前幾章是 Python 基礎 (含 tkinter), 爬蟲部分都是以中國簡體網站與工具為例, 例如使用百度 API 或微信機器人等. 遊戲包含 Pygame, 拼圖遊戲, 象棋, 五子棋等. 後面三章則是機器學習部分 : 用單純貝氏做文字分類, CNN 手寫數字識別, 以及爬取豆瓣影評製做詞雲. 

2022年5月22日 星期日

2022 年第 21 周記事

因為菁菁去接睫毛老師那裡上課染疫確診, 周三起我便開始了 0+7 自主防疫的在家上班生活, 下周二解除後我還是繼續在家上班, 以前不太喜歡這種方式, 總覺得要進辦公室才有上班的 fu, 而且每周要交工作日誌也很煩, 但現在覺得還不錯ㄟ, 不用六點早起準備早餐趕刷卡, 可以睡到七點半, 只要八點半開電腦連線工作即可, 我發現工作效率比在公司還高. 

周六傍晚再次快篩陰, 所以就回去鄉下, 主要是幫爸煮一周的便當, 還書, 以及採魚腥草回來. 小雖貓生的三個小貓又大一些了, 但仍在前門屋簷兜轉, 還不敢到曬穀場, 有時偷偷看它們打鬧也非常有趣. 我每隻抓起來檢查, 初步看應該都是公貓. 




今天上市集後去種子行買了 20 株空心菜來補滿菜圃, 還買了三株絲瓜苗, 以及一株朝天椒, 都種在盆栽裡, 此處做個記錄以便計算收成時間 : 







氣象說下周又要下雨了, 趁著今天太陽還炙烈, 把沾到白蟻土丘的薄床墊抬到曬穀場, 拉開花布的拉鍊發現原來裡面是發泡塑膠與一片人工紗, 清洗完泥垢後拿到頂樓曬, 幸好太陽賞臉曬整天, 下午就乾了. 

已經很久沒追劇了, 五月上旬跟水某看 Netflix 中國武俠愛情劇 "且試天下", 本周終於來到完結篇, 此劇劇情其實很老梗, 但武俠劇現在大概只有中國有在拍, 而且男女主角 (楊洋趙露思) 一個俊一個美, 也足以吸引我繼續看下去. 結局與我預想有點出入, 但這麼安排還可以啦, 如果兩人真成帝后那更老土. 

前陣子阿泉伯分享一包鹿肉 (筋骨肉) 給爸, 但我不會料理鹿肉啊! 因為佔了冰箱冷凍一大塊地方, 本周解凍取 1/3 按照下面這篇來做紅燒鹿肉 :


但我沒這麼多時間來燜一小時, 所以拌炒後就起鍋, 發現太韌咬不斷, 我又倒入壓力鍋再煮一遍, 咬得動了, 但還不滿意, 我喜歡軟爛. 下周剩下的部分我想直接進壓力鍋煮兩次看看. 

花了三周時間複習測試, 本周終於整理完 Python 正規表示法筆記, 好累! 正規式久沒用很快就會忘記, 每次要用都要重看好幾本書, 希望有了筆記以後能縮短恢復功力的時間. 


2022-05-27 補充 :

早上爸打電話來說小雖貓早晨被發現躺在蓮霧樹下的乾葉堆上死了, 令我非常震驚, 周日我還撫摸它哩, 它真是一隻好貓, 可說是爸的寵物, 每天都會陪他騎腳踏車, 爸說想到小雖貓都鼻酸了. 很奇怪, 無外傷, 口中亦無泡泡, 應該不是被野狗襲擊, 也非中毒, 爸半夜起床上廁所還餵它吃東西, 但早晨就暴斃, 它還在哺乳中應該不會亂跑吃到老鼠藥, 實在是令人匪夷所思. 周末回去再調監視器來調查看看. 

市圖還書 3 本 (反爬蟲, TensorFlow 2, 機率統計)

 本周市圖還書 3 本 : 
No.1 是主要是講反爬蟲技術, 現在還用不到 (以後可能也用不到, 我通常是爬人家), No.2 閱讀中將回借, No.3 有人預約先還. 

2022年5月21日 星期六

Python 學習筆記 : 正規表示法 (基礎篇)

正規表示法易學難精, 是一種學 N 遍會忘記 N+1 遍的東西 (因為寫正規式比較像是藝術, 離科學似乎較遠), 我從 Javascript 到 PHP 都學過, Python 的正規式也看過好幾遍, 但每次要用時都還是要重新複習, 真的很傷腦筋. 每次複習完都想把筆記整理起來以便縮短複習時間, 但都因忙它事而中輟. 這回因為撰寫網路爬蟲程式與學習自然語言處理的需要, 終於把這篇寫完了. 

本篇測試所參考的書籍如下 :      
其中第一本 "精通正規表達式" 詳細介紹了正規式的內部運作原理, 可說是徹底了解正規表示法機制的好書, 但此書出版較早並未納入 Python, 而是聚焦於 Perl, Java, PHP 與 .Net 等語言的正規式語法. 第二本 "處理大數據的美工刀" 我認為是中文書裡寫得最好的, 但可惜全書範例大部分使用 Python 2.7 版. 第三本 "增壓的 Python" 則是翻譯書, 其中正規式部分也做了很好的鋪陳. 


一. 關於正規表達式 :    

正規表示法 (又譯正則表示法, 正規表達式) 是字串處理的瑞士刀, 它使用詮釋字元 (metacharacter) 組成的範本字串來描述符合特定規則的文字, 被廣泛地應用於網路爬蟲, 系統維護, 自動化測試, 以及自然語言處理等大量文本操作場域, 主要用來檢索 (search), 擷取 (extract), 驗證 (validate) 或替換 (replace) 符合特定樣式 (pattern) 的字串. 

使用正規表示法可簡潔地完成大量文本資料的過濾與比對工作, 具有大幅降低程式碼量以及提高可重用性與執行效能等好處. 正規式雖然不像物件導向技術那樣可登上軟體工程的殿堂, 但所有經常處理文本資料的程式員都知道它的價值在哪, 因為大部分的軟體都與文本處理或分析 (text processing or analytics) 有關. 

為何需要用到正規表示法? 這是因為使用程式語言的語法基本上只能做精確比對, 具體而言就是使用關係運算子 == 去比對, 所以 if str == 'abc' 要成真只有 str 之值為 'abc' 才行 (匹配). 如果要做複雜的比對必須使用巢狀的 if else 去判斷. 正規表示法則是內嵌於程式語言中專門用來描述字元排列關係的小型語法, 它不僅能做精確比對, 也可以極有彈性地做模糊比對. 

正規表達式起源於數學中的形式語言 (formal language) 理論, 最早可追溯到神經生理學家 Warren McCullochWalter Pitts 於 1943 年所寫的神經網路論文; 不過 "Regular expression" 一詞卻要到 1956 年數學家 Stephen Kleene 提出有限自動機 (Finite Automata) 的論文時才第一次出現, 這個有限自動機就是正規表式法的數學理論模型. 

1960 年代 Unix 設計者之一的 Kenneth Thompson 根據有限自動機模型設計了正規表達式, 並將其導入 Unix 的文字編輯器 qed, ed, 以及 grep 之中, 由於 Unix 的應用程式大都遵循 POSIX 介面規範, 所以後來 IEEE 在制定正規式的語法標準時, 這個 UNIX 版本的正規表示法就被稱為 POSIX 流派 (flavor). 

 Perl 語言的設計者 Larry Wall 在 1986 年發布的 Perl 語言函式庫中實作了正規式引擎, 其正規式句法被稱為 PCRE (Perl Compatible Regular Expression) 流派, 後來許多程式語言 (例如 Java, R, Python, PHP, Julia, Ruby 等) 也採用了 PCRE 字元集, 使得 PCRE 漸漸成為主流, 而正規式也成為現代主流程式語言必備的基礎模組.

Python 最早期的 re 模組基本上屬於 PCRE 流派, 後來加入的 regex 模組則同時兼具 PCRE 與 POSIX 流派之功能. POSIX 風格的正規表達式版本基本上與 PCRE 相容, 但增加了一些額外的功能而且完全支援 Unicode. 有些語言例如 PHP, R, 與 Python 等同時支援 PCRE 與 POSIX 風格的正規表達式引擎 , 參考 :

Comparison of regular expression engines

但各程式語言之正規表達式語法存在一些差異, 參考 :

Comparing regular expressions in Perl, Python, and Emacs

正規式引擎處理字串的方式是先依照正規式產生有限狀態機 (Python 採用的是非確定性有限狀態機, nondeterministic finite sate machine, NFA), 它是一種有序的資料結構 (是一種物件), 狀態會在依序讀取與比對字串過程中移轉, 簡單來講, 如果比對途中發現不符就會跳出狀態機, 表示比對失敗 (又稱為不匹配); 否則就會走完整個字串, 停留在最後一個狀態, 表示比對成功 (又稱為匹配). 不過, 實際上, 狀態機的運作還包含回溯 (backtracking) 與貪婪 (greedy) 等較複雜的機制.

使用正規式進行樣式比對分為兩個時期, 一為編譯時期, 正規式首先會被編譯成有限狀態機 (在 Python 中即 Pattern 物件), 正規式必須先經過編譯才能拿來進行比對, 搜尋, 與取代等操作. 第二個時期為執行時期, 正規式的求值器 (evaluator) 會從目標字串逐一讀取字元, 依據有限狀態機來比對是否有匹配項目, 支援正規表示法的程式語言會提供相關函式或物件方法來處理編譯與執行正規式之作業. 
 
狀態機可用有向的圖來表示, 例如正規式 ca*t 的有限自動機如下圖所示 : 




開始執行比對時狀態機是在狀態 1, 它會從目標字串逐一讀取字元, 若讀到字元 c 就進入狀態 2, 在狀態 2 若讀取到字元 a 就繼續停留在此狀態, 讀到 a 以外字元就是比對失敗 (不匹配). 在狀態 2 若讀取到字元 t 則進入狀態 3 比對成功 (匹配), 若讀到 t 以外字元就算比對失敗 (不匹配) 並停止比對.


二. 正規式的語法 :      

正規表達式 (簡稱正規式) 是由字面字元 (literal, 又稱文字常量, 即英文字母, 數字字元, 底線等) 與一些特殊字元 (稱為描述字元詮釋字元 metacharacters, 主要是標點符號) 組成的樣式描述字串, 用來精簡地描述特定的字串樣式 (pattern). 如果以自然語言來比喻, 字面字元相當於單字, 而詮釋字元則是文法, 正規表示法的強大表達能力來自於其詮釋字元. 

在 Python 中, 正規式其實就是一般的, 只不過為了避免裡面的字元被轉義, 通常會在字串前面加 r 或 R 以原始字串 (raw string) 來表示, 例如 :

pattern=r'abc[1-3]+'                  # 使用單引號
pattern=r"abc[1-3]+"                # 使用雙引號
pattern=r'''abc[1-3]+'''               # 使用單引號長字串
pattern=r"""abc[1-3]+"""         # 使用雙引號長字串

此正規式中含有字面值 'abc' , 後面跟著的都是特殊字元, 其中 [1-3] 是字元集, + 是代表 1 次以上的量詞, 表示可出現 1 個以上的 1~3 的數字, 例如 'abc1', 'abc2', 'abc3', 'abc12', 'abc123', .... 等字串都會匹配此正規式. 注意, 正規式裡的字面值必須精確匹配, 例如正規式 r'cat' 可以匹配 'category' 但不能匹配 'car'. 

前面冠 r 的原始字串並非一種新的字串型態, 它只是通知 Python 直譯器不要進行字元的轉義而已, 特殊字元若不使用原始字串的話會先被直譯器轉義而失去其作用. 若正規式中沒有特殊字元, 其實有無加 r 並無差別, 但若含有特殊字元就有差別了, 例如 : 

>>> r'abc'=='abc'               # 未含特殊字元時兩者相同
True   
>>> r'abc\n'=='abc\n'        # 含有特殊字元時兩者不同 (後者 \n 會被轉義)
False    

因此最好養成在正規式前面冠 r 的習慣, 這樣比較保險.

正規式中的特殊字元是用來改變附近字元的涵義, 例如 + 並不是要尋找加號, 而是一個量詞 (次數) 修飾語, 用來搭配前面的字元或群組表示其發生的次數為 1 次或更多次. 如果要匹配的是這些特殊字元本身, 則必須在其前面加一個倒斜線來跳脫其特殊用途, 例如要比對加號需使用 \+. 

正規表達式中的特殊字元可分成下列四類 :
  • 描述字元 (metacharacter) :
    用來指定一個特定的字元, 例如任何字元, 任何數字字母 /d, 任何文數字底線 /w 等, 但也包括非字元描述, 例如描述特定位置 (主要是列首 ^ 與列尾 $) 等, 它們都一次只匹配一個字元, 但可搭配量詞來匹配一個以上的字元. 
  • 字元集 (character set) : 
    與描述字元一樣用來指定一個特定的字元, 但以 [] 列舉集合元素的方式來指定. 
  • 群組或分組 (group) :
    使用小括號 () 來將一部份正規式納為一個比對群組, 可將小表達式合併成更大的表達式. 
  • 量詞 (quantifiers) :
    此為運算子, 用來表示單個字元或群組重複之次數. 
詮釋字元的用法摘要如下表 : 

 正規表達式 說明
 .  代表除換列字元 \n 以外的任何字元, 例如 a.c 匹配 abc, a8c 等
 ^  代表一個字串的開頭位置, 例如 ^abc 表示以 abc 開頭的字串
 $ 代表一個字串的結束位置, 例如 abc$ 表示以 abc 結尾的字串
 * 代表前一個表達式 (字元或分組) 可出現 0 次以上
 +  代表前一個表達式 (字元或分組) 可出現 1 次以上
 ?  代表前一個表達式 (字元或分組) 可出現 0 次或 1 次, 或放在 {} 後面設定為非貪婪
 \ 脫逸 (escape), 代表後面的 (特殊) 字元以一般字元 (原義) 處理 
 | 選擇 (or), 代表前一個表達式或後一個表達式二選一, 例如 a|b 比對 a 或 b
 () 群組 (group), 代表括號內的表達式形成一個群組, 例如 (ab)+ 比對 ab, abab 等
 \num 參照前面已經匹配的群組, num 為 1 起始的群組編號, 例如 \1 為群組 1 
 [abc.] 字元集, 代表 a 或 b 或 c 任一個字元
 [a-z] 字元集, 代表 a~z 連續區間內的任一個字元, [a-c] 表示 a, b, c 任一個字元
 [0-9] 字元集, 代表 0~9 連續區間內的任一個字元, [1-3] 表示 1, 2, 3 任一個字元
 [^abc] 字元集, 代表不是 a 或 b 或 c 的任一個字元
 {m} 代表前一個項目 (字元或分組) 必須出現 m 次, 例如 a{2} 比對 aaabc 得 aa
 {m,} 代表前一個項目 (字元或分組) 至少出現 m 次, 例如 a{2,} 比對 aaabc 得 aaa
 {m,n} 代表前一個項目 (字元或分組) 出現 m~n 次, 例如 a{2, 4} 比對 aaaaabc 得 aaaa
 \d 代表數字字元, 等於 [0-9] 或 [0123456789]
 \D 代表非數字字元, 等於 [^0-9] 或 [^0123456789]
 \w 代表數字, 英文字母, 或底線字元, 等於 [0-9a-zA-Z] 之簡寫
 \W 代表數字, 英文字母, 或底線以外的字元, 等於 [^0-9a-zA-Z]
 \s 代表空白, 定位, Tab, 換列, 跳頁字元, 等於 [\r\t\n\f]
 \S 代表空白, 定位, Tab, 換列, 跳頁字元等以外的字元, 等於 [^\r\t\n\f]
 \b 代表詞邊界, 例如 ad\b 匹配 'bad boy' 與 'ad' 但不匹配 'adverb'
 \B 代表不是詞邊界, 例如 ad\B 匹配 'adverb' 但不匹配 'bad boy'
 \A 代表字串的開頭
 \z 代表字串的尾端
 \n 代表換列 (new line) 字元
 \r 代表回車 (carriage return) 字元
 \t 代表水平定位 (tab) 字元
 \f 代表進紙 (feed) 字元
 \v 代表垂直定位 (tab) 字元

用法說明如下 : 
  1. 小括號 () 代表一個群組 (group), 它有兩個用途, 第一個是作為正規式運算子 (例如量詞或選擇等) 的作用單位, 它會大大地影響正規式的解析方式與含義 (跟算術裡括號的作用有一點點類似), 例如 cat+ 樣式中量詞 + 作用的對象是前一個字元 b, 它會匹配 'cat', 'catt', 'cattt', ... 等字串, 但 c(at)+ 樣式中量詞 + 作用的對象是 ab 群組, 它會匹配 'cat', 'catat', 'catatat', ... 等字串. 
  2. 群組 (group) 的第二個用途是參照 (reference), 正規式引擎會替每個群組建立標記 (tag) 以便後續引用或參照, 可以搭配 \num 語法來參照前面已經匹配成功的群組, 例如 \1 參照前面的第一個分組, \2 參照前面的第二個分組等等. 在需要搜尋重複樣式時就必須用到群組的參照功能. 
  3. 小括號 () 內若以 ? 開頭則做為改變比對模式的修飾符 (modifier) 用, 功能與旗標 (flag) 相同 , 但修飾符必須放在正規式的開頭, 例如 r'(?i)[a-z]' 匹配不分大小寫的一個英文字母. 常用的比對模式修飾符如下 :
    (?i) 等於 re.IGNORECASE (不分大小寫模式)
    (?s) 等於 re.DOTALL (單列模式)
    (?m) 等於 re.MULTILINE (多列模式) 
    (?x) 等於 re.VERBOSE (註解模式)
  4. 中括號 [] 代表一個字元集 (character set), 表示匹配這些字元集合中的單一個字元 (支援 Unicode), 故這些字元是 OR (或) 的關係, 例如 [cat] 表示只要字串中含有 c, a, 或 t 中的任一個即匹配成功. 相對來說字面值的正規式則是 AND (且) 的關係, 正規式 abc 表示必須三個字元依序同時出現才能匹配成功. 注意, 字元集中的字元預設是 Unicode 模式, 但可以在正規式最前面加上 (?a) 設定為 ASCII 模式.  
  5. 除了 '-', ']', 以及 '\' 這三個特殊字元在字元集內需要跳脫 (escape) 以表示它們本身外, 其餘的所有字元都不需要跳脫. 在字元集中, 連字號 - 在不同位置用法不同, 如果連字號是在兩個字元中間, 是表示前面字元到後面字元的連續區間, 例如 [3-7] 等同於 [34567], 而 [a-d] 等同於 [abcd]. 如果連字號是在 [] 內的開頭或結尾, 那它就純粹是連字號而已不須跳脫, 但在中間就必須跳脫. 右中括號表示字元集結束, 倒斜線用來跳脫, 故在字元集內它們都必須跳脫.
  6. 字元集內的特殊字元除了 '-' 外還有一個放在開頭表示 not (否定) 意思的 '^' 字元, 用來排除它後面的所列舉的所有字元, 稱為排除型字元集, 例如 r'[^0-9]' 或 r'[^\d]' 表示匹配非數字字元; r'[^aiueo]' 表示匹配非母音字元等. 但 '^' 如果不是在開頭位置就只是普通字元, 沒有否定的意思, 例如 r'[^+-*/]' 表示匹配所有加減乘除以外的字元; 但 r'[+-*/^]' 則是加減乘除與次方字元的任一個. 
  7. 描述字元 /d, /D, /w, /W, /s, /S 可視為是一些常用的字元集之簡寫, 可使正規式更為精簡, 例如 \d 其實等同於字元集 [0-9] 或選擇分組 (0|1|2|3|4|5|6|7|8|9), 它們與字元集一樣預設使用 Unicode 模式, 但可以在前面加上 (?a) 設定為 ASCII 模式. 
  8. 選擇運算子 | 用來在左右兩側的表達式中做 2 選 1 (alternation) 匹配, 它在正規式語法中具有最低的優先順序. 例如 ab|cd 會匹配 'ab' 或 'cd', 但不匹配 'abcd'; 而 a(b|c)d 匹配 'abd' 或 'acd' 但不匹配 'abcd', 這種單一字元的選擇等同於 a[bc]d. 選擇運算子 | 與字元集的差別在字元集是匹配一個字元而已, 而選擇運算子則是匹配較多的字元. 
  9. 特別注意, 量詞 {m,n} 的 m 與 n 中間只能是逗號, 不可含有空格, 否則比對會失敗. 其次, 問號 ? 在正規式中有兩種用途, 一是做為一個次數量詞, 它會修改前面字元或分組發生的次數為 0 次或 1 次; 二是放在 {m,n} 或 {m,} 等範圍量詞後面, 它會修改預設之貪婪模式為非貪婪模式. 
Python 正規式求值器 (evaluator) 預設採取貪婪 (greedy) 的匹配策略, 所謂貪婪是指它會盡可能地達成最長的匹配, 為了達到這個目的會使用目標字串回溯技術 (backtracking). 例如用正規式 r'c.*t' 來比對目標字串 'cat' 字串, 第一個字元 c 匹配成功後, ".*" 表示要比對任何數量的跳行以外字元, 所以 'at' 會匹配成功, 至此已到達目標字串尾端, 求值器要比對剩下的 't' 樣式時已無字元可比對, 似乎比對失敗, 事實上求值器會回溯一個字元讀取到 t, 故最終還是會匹配成功, 例如 :

>>> import re   
>>> if re.match(r'c.*t', 'cat'):      
    print('匹配成功')     
    
匹配成功   

此處匯入 re 模組後呼叫 re.match() 函式並傳入正規式 r'c.*t' 與目標字串 'cat', 比對結果會傳回 None (不匹配, 比對失敗) 或一個 Match 物件 (匹配, 比對成功). 此例顯示正規式的求值器利用回溯使得比對最後匹配成功. re 模組還有許多函式, 如下所述. 


三. 利用 re 模組進行樣式比對, 搜尋, 與取代 : 

Python 從 1.5 版開始就把支援 Perl 風格的正規式模組 re 模組加入了標準函式庫, 此模組事實上是內嵌於 Python 內的一個高度特殊化小型程式語言 (所以學習正規表示法事實上是在學習一種新語言),  re 模組實作了可執行樣式比對, 搜尋與取代的有限狀態機 (Finite Automata), 正規式會先被編譯代表此狀態機的樣式物件, 然後利用此物件的方法或 re 模組的函式對目標字串進行樣式比對, 搜尋與取代等作業. 

教學文件參考 : 



1. 呼叫 re 模組的函式來比對, 搜尋, 或取代 :

首先介紹樣式比對, 搜尋, 與取代的第一種方式, 即直接呼叫 re 模組的函式, 此法虛傳入正規式或樣式物件, 目標字串, 以及備選的旗標等參數. 

使用 re 模組前須先用 import 匯入 :

D:\Python>python   
Python 3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import re   
>>> dir(re)    
['A', 'ASCII', 'DEBUG', 'DOTALL', 'I', 'IGNORECASE', 'L', 'LOCALE', 'M', 'MULTILINE', 'Match', 'Pattern', 'RegexFlag', 'S', 'Scanner', 'T', 'TEMPLATE', 'U', 'UNICODE', 'VERBOSE', 'X', '_MAXCACHE', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', '__version__', '_cache', '_compile', '_compile_repl', '_expand', '_locale', '_pickle', '_special_chars_map', '_subx', 'compile', 'copyreg', 'enum', 'error', 'escape', 'findall', 'finditer', 'fullmatch', 'functools', 'match', 'purge', 'search', 'split', 'sre_compile', 'sre_parse', 'sub', 'subn', 'template']

使用 eval() 函式搭配字串的 startswith() 方法可以將雙底線的內部變數過濾掉,  加上 type() 函式可以顯示每一個成員的類型 : 

>>> import re    
>>> members=dir(re)    
>>> for mbr in members:   
    obj=eval('re.' + mbr)   
    if not mbr.startswith('_'):   
        print(mbr, type(obj))   
        
A <enum 'RegexFlag'>
ASCII <enum 'RegexFlag'>
DEBUG <enum 'RegexFlag'>
DOTALL <enum 'RegexFlag'>
I <enum 'RegexFlag'>
IGNORECASE <enum 'RegexFlag'>
L <enum 'RegexFlag'>
LOCALE <enum 'RegexFlag'>
M <enum 'RegexFlag'>
MULTILINE <enum 'RegexFlag'>
Match <class 'type'>
Pattern <class 'type'>
RegexFlag <class 'enum.EnumMeta'>
S <enum 'RegexFlag'>
Scanner <class 'type'>
T <enum 'RegexFlag'>
TEMPLATE <enum 'RegexFlag'>
U <enum 'RegexFlag'>
UNICODE <enum 'RegexFlag'>
VERBOSE <enum 'RegexFlag'>
X <enum 'RegexFlag'>
compile <class 'function'>
copyreg <class 'module'>
enum <class 'module'>
error <class 'type'>
escape <class 'function'>
findall <class 'function'>
finditer <class 'function'>
fullmatch <class 'function'>
functools <class 'module'>
match <class 'function'>
purge <class 'function'>
search <class 'function'>
split <class 'function'>
sre_compile <class 'module'>
sre_parse <class 'module'>
sub <class 'function'>
subn <class 'function'>
template <class 'function'>

其中大寫者為 re 模組的常數, 主要為用來控制比對模式之旗標 (flag) :

 re 模組常用旗標 說明
 ASCII 使用 ASCII 模式 (簡寫 re.A), 修飾符為 (?a)
 IGNORECASE 不區分大小寫 (簡寫 re.I), 修飾符為 (?i)
 DEBUG 互動模式下會印出除錯訊息 (無簡寫) 
 LOCALE 匹配文數字元與單詞邊界時遵守 LOCALE 區域設定  (簡寫 re.L)
 MULTILINE 多列模式, 可跨越 \n 進行比對 (簡寫 re.M), 修飾符為 (?m)
 DOTALL 單列模式, 比對包含跳行 \n 的所有字元 (簡寫 re.S), 修飾符為 (?s)
 UNICODE 使用 Unicode 模式 (簡寫 re.U), 修飾符為 (?u) 
 VERBOSE 使用註釋模式 (簡寫 re.X), 修飾符為 (?x)
 
大部分旗標都有其簡寫與修飾符 (modifier), 例如 re.IGNORECASE 可以用簡寫 re.I 代替; 而修飾符則是嵌入正規式中的旗標, 它有與旗標相同的功能, 因此使用修飾符就不需要傳入旗標 (重複也無妨), 使用修飾符的好處是正規式可以不修改即能通行於各種程式語言, 而旗標的用法因程式語言而異, 故可移植性 (portability) 沒有修飾符好. 

常用的 re 模組函式用法摘要如下 : 

 re 模組常用函式 說明
 match(pattern, msg) 以 pattern 從頭比對 msg, 匹配傳回 Match 物件, 否則傳回 None
 fullmatch(pattern, msg) 以 pattern 從頭至尾比對 msg 全符合傳回 Match 物件, 否則 None
 search(pattern, msg [, flag=0]) 以 pattern 搜尋整個 msg, 匹配傳回 Match 物件, 否則傳回 None
 findall(pattern, msg [, flag=0]) 在 msg 中搜尋匹配 pattern 樣式的所有子字串, 將結果以 list 傳回
 sub(pattern, rep,msg[,flag=0]) 在 msg 中搜尋匹配 pattern 樣式的所有子字串並以 rep 取代
 split(pattern, msg[, flag=0]) 將 msg 以匹配 pattern 樣式之子字串拆分, 將結果以 list 傳回
 compile(pattern) 編譯正規式 pattern, 傳回 Pattern 物件

注意, 除了 re.compile() 外, 其餘函式中的 pattern 參數可以是正規式字串, 也可以是 re.compile() 傳回的 Pattern 物件. re.compile() 只是單純地編譯正規式字串並傳回代表有限狀態機的 Pattern 樣式物件, 此樣式物件可以被重複使用. 其他函式的 pattern 參數若傳入的是正規式字串時其實會隱性地先呼叫 re.compile() 進行編譯動作 (因為正規式必須先行編譯才能使用) 以取得 Pattern 物件, 然後再執行比對, 搜尋或取代等作業. 

用法說明如下 :
  • re.compile() 會將正規式編譯成 Pattern 物件, 此 Pattern 物件可傳給 re.match(), re.fullmatch, re.search(), re.findall(), re.sub() 等函式當作第一參數, 也可以呼叫與這些函式的同名方法來執行比對, 搜尋, 或替換等操作. 
  • re.match() 是從目標字串的開頭比對指定樣式, 直到不符合的字元出現為止, 若匹配就將該字元與位置 (即索引 0) 存入 Match 物件中傳回, 若不匹配就傳回 None. 而 re.fullmatch() 是整個目標字串從頭到尾都符合樣式才算匹配, 若匹配就將整個目標字串與位置存入 Match 物件中傳回, 若不匹配就傳回 None. 兩者的差別是, re.match() 只要目標字串從開頭就符合樣式就匹配; 而 re.fullmatch() 則是從頭至尾整個都必須符合才匹配, 相當於把 re.match() 的正規式結尾加上 $. 
  • re.search() 是從頭開始搜尋目標字串, 並將第一組匹配的字串與位置存入 Match 物件中傳回, 若不匹配就傳回 None. 而 re.findall() 是從頭搜尋整個目標字串找出全部匹配的子字串, 將其放入串列中傳回, 若不匹配就傳回空串列. 所以兩者的差別是 re.search() 只要找到第一組匹配的子字串就收工了, 但 re.findall() 則繼續搜尋到最後. 匹配時的傳回值型態也不同, re.findall() 不管是否匹配都傳回一個串列; 而 re.search() 匹配時傳回 Match 物件, 不匹配傳回 None.  
  • re.match() 與 re.search() 都是在目標字串 msg 中搜尋特定樣式, 都是傳回 None 或 Match 物件, 兩者差別是 re.match() 必須是從目標字串開頭就符合 (直到不符合之字元出現為止) 才能匹配, 而 re.search() 則在整個字串任何位置第一個符合就匹配成功
  • re.sub() 會從頭搜尋整個字串, 將匹配的子字串以替換字串取代, 傳回替代後的目標字串, 若不匹配則傳回整個目標字串. 
  • re.split() 會從頭搜尋整個字串, 然後以匹配的子字串為分界拆分目標字串成為數個子字串後以串列傳回, 若不匹配則傳回整個目標字串. 
以下用簡單的範例說明這些函式的基本用法, 例如要比對目標字串是否是數字字元開頭, 可呼叫 re.match() 來比對正規式 [0-9] 或 \d, 因為 re.match() 從開頭字元便要符合才會匹配, 例如 :

>>> import re   
>>> pattern=re.compile(r"\d")                    # 比對一個數字字元, 也可以用 r'[0-9]'
>>> pattern   
re.compile('\\d')                                               # 實際的正規式是 '\\d' 跳脫倒斜線
>>> type(pattern)          
<class 're.Pattern'>                                           # 編譯後傳回一個 Pattern 物件

可見編譯後真正的正規式為 '\\d', 編譯時已經自動加上一個倒斜線來跳脫 (escape) 特殊符號 '\' 了. 用原始字串 r'\d' 表示正規式的好處就是不需要去顧慮特殊符號的跳脫問題, 如果不使用原始字串就要自行加上跳脫用的倒斜線, 即 re.compile('\\d'). 

接下來使用這個 Pattern 物件來比對目標字串 : 

>>> match=re.match(pattern, '123abc')      # 從頭比對目標字串 '123abc'
>>> match     
<re.Match object; span=(0, 1), match='1'>      # 匹配第一個字元為數字即結束比對
>>> match=re.match(pattern, 'abc123')      # 從頭比對目標字串 'abc123'
>>> match                                                       # 傳回 None 印出來不會顯示
>>> type(match)                                             # 呼叫 type() 確認傳回 None
<class 'NoneType'>    
>>> if match:                                                   # 根據傳回值判斷 'abc123' 的比對結果  
    print('success')    
else:    
    print('failure')    
    
failure       

可見用正規式 r'\d' 比對目標字串 'abc123' 不匹配, 但比對 '123abc' 則會匹配, 這是因為 re.match() 的功能是從目標字串開頭就必須符合, 故只有以數字開頭的字串才會匹配. 正規式 r'\d' 只是比對一個數字字元而已, 所以此例中目標字串 '123abc' 比對第一個字元 '1' 時就匹配而停止, 匹配的字元是第一個字元, 所以 Match 物件紀錄 match='1', 位置 span(0, 1) 即索引 0 . 

但用正規式 r'\d' 比對目標字串 'abc123' 不會匹配, 因為第一個字元 'a' 就不符合而馬上停止比對並傳回 None. 因為 re.match() 的傳回值不是 None 便是 Match 物件, 剛好可用 if 來判斷並輸出結果. 

如果要比對目標字串開頭的所有數字, 正規式要改為 r'\d+', 其中特殊符號 + 表示前面的字元必須出現至少一次, 故 r'\d+' 意思是數字字元必須連續出現 1 次以上, 例如 :

>>> pattern=re.compile(r'\d+')                       # 數字字元連續出現 1 次以上
>>> match=re.match(pattern, '123abc')      
>>> match     
<re.Match object; span=(0, 3), match='123'>     # 匹配的子字串是 '123'
>>> match=re.match(pattern, 'abc123')         # re.match() 要求從開頭就要符合故不匹配
>>> type(match)
<class 'NoneType'>                                              # 比對失敗傳回 None

可見用正規式 r'\d+' 比對目標字串 'abc123' 不匹配, 因為開頭字元 'a' 就不符合了, 比對馬上停止並傳回 None, 但比對 '123abc' 則會匹配最前面的三個數字字元 '123', 因為正規式 r'\d+' 要比對 1 個以上數字字元直到遇到 'a' 不符合而停止. 

上面的範例都是先呼叫 re.compile() 將正規式編譯成 Pattern 物件, 然後將其傳入 re.match() 當第一參數, 這種作法的好處是, 只需要編譯一次正規式, 所得到的 Pattern 物件可以一再地重複使用. 其實也可以不需要先呼叫 re.compile(), 直接將正規式字串傳入 re.match() 中當第一參數, 例如 :

>>> match=re.match(r'\d', '123abc')             # 從頭比對目標字串 '123abc'
>>> match     
<re.Match object; span=(0, 1)match='1'>        # 匹配的子字串是 '1'
>>> match=re.match(r'\d+', '123abc')      
>>> match     
<re.Match object; span=(0, 3)match='123'>     # 匹配的子字串是 '123'

此例在呼叫 re.match() 時直接傳入正規式字串當第一參數, 此函式會在隱性地在背後先呼叫 re.compile() 進行編譯後取得 Pattern 件再進行比對, 效能上比直接傳入 Pattern 物件要低. 所以如果相同的正規式要重複使用, 最好是先呼叫 re.compile() 先編譯成 Pattern 物件來用; 如果正規式只使用一次則沒差, 直接傳入正規式字串比較方便. 

另外一個比對函式 re.fullmatch() 用來比對整個目標字串是否符合正規式, 它不僅要求從目標字串開頭就要符合樣式, 一直到目標字串結尾都要符合! 亦即 re.fullmatch() 是整個目標字串都符合時才匹配, 這相當於在 re.match() 的正規式結尾添加一個 '$' 的效果, 例如 :

>>> match=re.fullmatch(r'[a-z]+', 'abcxyz')       # 整個字串從頭到尾都符合
>>> match    
<re.Match object; span=(0, 6), match='abcxyz'>    
>>> match=re.fullmatch(r'[a-z]+', 'hello world ')     # 中間的空格不符合樣式 (不匹配)  
>>> match     
>>> type(match)   
<class 'NoneType'>     

此例正規式 r'[a-z]+' 字元集 [a-z] 表示一個英文小寫字母, + 表示這種字母有 1 個以上, 所以用它來比對目標字串 'abcxyz' 會匹配, 但比對 'abc123' 或 'hello world' 不匹配, 因為前者並非整個字串都是英文小寫字母 (含有數字), 而後者是中間有一個空格字元. 其實 re.fullmatch() 的功能大部分都可以用 re.match() 來模擬, 只要將傳入 re.match() 的正規式結尾加一個 '$' 即可, 例如 :

>>> match=re.match(r'[a-z]+$', 'abcxyz')           # 等效的 re.match() 樣式
>>> match   
<re.Match object; span=(0, 6), match='abcxyz'>       

此例傳入 re.match() 的正規式字串結尾有個位置字元 '$', 表示目標字串必須以英文小寫字母結尾, 配合 re.match() 從頭開始就須符合才匹配的功能, 合起來就是目標字串從頭到尾都需符合, 相當於是 re.fullmatch() 的功能. 

函式 re.search() 會從頭搜尋整個目標字串是否符合樣式, 當它找到第一組 (即最前面那組) 匹配的子字串時就會停止搜尋, 並將匹配的子字串放入 Match 物件中傳回; 若搜尋到目標字串結尾不匹配就傳回 None, 例如 : 

>>> match=re.search(r'\d', 'abc123')            # 搜尋第一組匹配的子字串
>>> match       
<re.Match object; span=(3, 4), match='1'>       # 匹配 1 個字元

此例是在目標字串中從頭開始搜尋第一組符合正規式 r'\d' (一個數字字元) 的子字串, 當找到索引 3 的 '1' 的第一組時匹配, 搜尋馬上停止並傳回匹配字元 '1' 及其位置 sapn(3, 4), 雖然後面的字元 '2', 與 '3' 也都符合, 但 re.search() 的個性是只取一瓢飲. 如果正規式是 r'\d+' (一個以上的數字字元) 的話, re.search() 會傳回更多匹配字元, 例如 :

>>> match=re.search(r'\d+', 'abc123')            # 搜尋第一組匹配的子字串
>>> match     
<re.Match object; span=(3, 6), match='123'>      # 匹配 3 個字元

此例正規式 r'\d+' 匹配一個以上的數字字元, 所以當正規式求值器讀取到 '1' 發現匹配後會繼續讀取不會停止, 直到結尾或遇到第一個不符合字元為止, 並傳回第一組匹配的子字串 '123'. 此例若將目標字串改為 'abc123xyz' 結果相同, 正規式求值器在匹配後讀到第一個不符合的字元 'x' 才會停止. 

與 re.search() 淺嚐即止不同的是, re.findall() 會在目標字串中找出全部匹配的子字串 (我全都要!), 稱為迭代搜尋 (iterated search), 所有匹配的子字串會被依序放進串列中傳回, 不匹配則傳回一個空串列. re.findall() 的搜尋方式為非重疊 (non-overlapping) 搜尋, 亦即搜尋是由左向右盡可能地做最長匹配, 當匹配一個子字串後, 會從它的右邊繼續搜尋, 所以匹配 '123' 後, 下一個匹配不會是已匹配過的 '123' 內之 '23' 與 '3'.  例如 : 

>>> match=re.findall(r'\d', 'abc123')     # 搜尋所有匹配的子字串
>>> match     
['1', '2', '3']                                                   # 傳回三個匹配子字串的串列
>>> match=re.findall(r'\d', 'abcxyz')     # 搜尋所有匹配的子字串
>>> match    
[]                                                                 # 不匹配傳回空串列

此例是在目標字串 'abc123' 中搜尋全部匹配正規式 r'\d' 的子字串, 因為匹配的單位是一個數字字元, 所以總共會有 '1', '2', '3' 這三組匹配的子字串, 它們會被放在串列中傳回. 目標字串 'abcxyz' 則因為不匹配而傳回空字串. 

如果正規式改為匹配一個以上數字字元的 r'\d+', 則匹配單位就不是單一個數字字元, 而是多個字元, 例如 :

>>> match=re.findall(r'\d+', 'abc123')                # 搜尋所有匹配的子字串
>>> match    
['123']                                   
>>> match=re.findall(r'\d+', 'abc123xyz456')       
>>> match  
['123', '456'] 

上面第一個例子是在目標字串 'abc123' 中搜尋全部匹配正規式 r'\d+' 的子字串, 由於是搜尋一個以上的數字字元, 所以會在讀到 '1' 匹配後繼續搜尋直到尾端 '3' 後才停止, 故匹配子字串是 '123' 而非 '1', '2', '3', 因此傳回值是串列 ['123']. 第二個例子是在目標字串 'abc123xyz456' 中搜尋全部匹配正規式 r'\d+' 的子字串, 求值器找到了 '123' 與 '456' 這兩組匹配子字串. 

函式 re.sub() 與 re.findall() 一樣會搜尋整個目標字串中匹配的子字串, 但 re.findall() 只是將搜尋到的全部匹配子字串傳回, 而 re.sub() 則是將這些匹配子字串以指定的字串替換掉後將結果字串傳回, 如果不匹配就傳回目標字串本身 (因為找不到匹配子字串就沒有替換的對象, 所以就整欉好好送回去啦), 例如 : 

>>> result=re.sub(r'\d', 'xyz', 'abc123')     
>>> result   
'abcxyzxyzxyz'   

此例以 'xyz' 取代目標字串中匹配正規式 r'\d' 的所有子字串, 由於 r'\d' 匹配一個數字字元, 所以會有 '1', '2', '3' 這三組匹配子字串, 所以替換後就出現三個而非一個 'xyz' 了. 如果匹配單元是多個數字字元的 r'\d+', 則結果就不同了, 例如 : 

>>> result=re.sub(r'\d+', 'xyz', 'abc123')                  # 以 'xyz' 取代所有匹配子字串
>>> result    
'abcxyz'
>>> result=re.sub(r'\d+', '+++', 'abc123xyz456')     # 以 '+++' 取代所有匹配子字串     
>>> result     
'abc+++xyz+++'     

此兩例匹配單位是多個數字字元, 前者匹配子字串 '123' 被 'xyz' 替換; 後者有兩個匹配子字串 '123' 與 '456', 它們都被 '+++' 替換掉了. 

函式 re.split() 功能與 Python 字串物件的 spplit() 方法功能類似, 都可以將目標字串以特定分界符 (splitter) 為分界拆分為串列, 差別在於字串的 split() 方法是以固定的字元或字串當分界符; 而 re.split() 則是用正規式的匹配字串當作分界符, 彈性很大, 例如 : 

>>> '1 2 3 4   5'.split()               # 預設以空白 (含空格, 定位等) 為分界符
['1', '2', '3', '4', '5']
>>> '1 2 3 4   5'.split(' ')            # 指定以空格 ' ' 為界拆分
['1', '2', '3', '4', '', '', '5'] 

此例目標字串的 4 與 5 之間有 3 個空格, 當 split() 方法不傳入參數時多餘的空格會被忽略; 但若指定以空格為界拆分時會無法辨識這是連續的多餘空格, 因而傳回串列中 '4' 與 '5' 中間有兩個空字串. 將空格改用 + 取代較容易觀察 :

>>> '1+2+3+4+++5'.split('+')    # 以 '+' 為界拆分字串
['1', '2', '3', '4', '', '', '5']

如果使用正規表示法, 以正規式當作分界符就不會有這問題了 : 

>>> re.split(r'\s+', '1 2 3 4   5')     
['1', '2', '3', '4', '5']
>>> re.split(r'\++', '1+2+3+4+++5')         
['1', '2', '3', '4', '5']

可見傳回串列中就沒有空字串了, 第一例的正規式 r'\s+' 中的 \s 意思是空白字元 (包含空格與 Tab 等), 後面的 + 表示前面的空白出現 1 個以上. 第一例的正規式 r'\++' 中的 \+ 表示 + 字元本身, 因為 + 在正規表示法中為特殊字元 (量詞), 故須用 \ 來跳脫量詞的意思, 後面那個 + 才是量詞, 表示前面的 + 字元出現 1 次以上.   


2. 呼叫 Pattern 物件的方法來比對, 搜尋, 或取代 :

除了直接呼叫上述 re 模組的函式進行字串的比對, 搜尋, 與替代等作業外, 第二種作法是呼叫  Pattern 物件之方法, 因為 Pattern 物件亦實作了與 re 模組函式功能相同的 match(), fullmatch(), search(), findall() 以及 sub() 等方法, 例如 : 

>>> import re   
>>> pattern=re.compile(r"\d")     # 編譯後傳回 Pattern 物件
>>> members=dir(pattern)           # 查詢 Pattern 物件之成員
>>> for mbr in members:              # 走訪 Pattern 物件的成員
    obj=eval('pattern.' + mbr)         # 取得成員
    if not mbr.startswith('_'):           # 跳過內部成員
        print(mbr, type(obj))     
        
findall <class 'builtin_function_or_method'>   
finditer <class 'builtin_function_or_method'>   
flags <class 'int'>   
fullmatch <class 'builtin_function_or_method'>   
groupindex <class 'dict'>   
groups <class 'int'>   
match <class 'builtin_function_or_method'>
pattern <class 'str'>
scanner <class 'builtin_function_or_method'>
search <class 'builtin_function_or_method'>
split <class 'builtin_function_or_method'>
sub <class 'builtin_function_or_method'>
subn <class 'builtin_function_or_method'>

這些物件方法的介面與上面 re 模組的同名函式功能相同, 唯一的差別只是不需要傳入正規式字串或 Pattern 物件 (因為它自己就是代表正規式的 Pattern 物件), 摘要如下表 : 

 Pattern 物件常用方法 說明
 match(msg) 以 pattern 從頭比對 msg, 匹配傳回 Match 物件, 否則傳回 None
 fullmatch(msg) 與 match() 相同, 差別是頭尾都須符合 (正規式結為自動加上 $)
 search(msg [, flag=0]) 以 pattern 比對整個 msg, 匹配傳回 Match 物件, 否則傳回 None
 findall(msg [, flag=0]) 在 msg 中搜尋匹配樣式 pattern 的所有子字串, 將結果以 list 傳回
 split(msg [, flag=0]) 將 msg 以匹配 pattern 樣式之子字串拆分, 將結果以 list 傳回
 sub(rep, msg[, flag=0]) 在 msg 中搜尋匹配樣式 pattern 的所有子字串並以 rep 取代

例如 : 

>>> import re   
>>> pattern=re.compile(r"\d")                      # 比對一個數字
>>> pattern.match('123abc')         
<re.Match object; span=(0, 1), match='1'>        # 匹配第一個字元
>>> pattern=re.compile(r"\d+")                    # 比對 1 個以上字元
>>> pattern.match('123abc')         
<re.Match object; span=(0, 3), match='123'>    # 匹配前三個字元  
>>> pattern.fullmatch('123')                           # 整個字串從頭到尾都符合
<re.Match object; span=(0, 3), match='123'>
>>> pattern.search('abc123')                          # 搜尋第一組匹配的子字串
<re.Match object; span=(3, 6), match='123'>
>>> pattern.findall('abc123xyz456')              # 搜尋全部匹配的子字串
['123', '456']
>>> pattern.sub('+++', 'abc123xyz456')        # 替換
'abc+++xyz+++'
>>> pattern=re.compile(r'\s+')                       # 以空白字元為界拆分
>>> pattern.split('1 2 3 4   5')         
['1', '2', '3', '4', '5']
>>> pattern=re.compile(r'\++')                   
>>> pattern.split('1+2+3+4+++5')                  # 以 + 字元為界拆分
['1', '2', '3', '4', '5']

可見結果與呼叫 re 模組的函式是一樣的. 


四. 正規式語法測試 :

了解如何使用 re 模組進行比對, 搜尋, 與替換後, 即可來測試上面所述的正規式語法 :

1. 列首列尾位置的錨定字元 (anchor) :

特殊字元 ^ 與 $ 不是用來匹配任何實際的字元, 而是用來匹配一列中的開頭與結尾位置, 也就是所謂的錨定 (anchor) 功能 : ^ 用來錨定列首位置; 而 $ 則是用來錨定列尾位置. 如果同時使用了 ^ 與 $ 表示整個目標字串必須符合該樣式才算匹配, 例如正規式 r'^hello' 會匹配以 'hello' 開頭的字串, 正規式 r'\d$' 會匹配以數字字元結尾之字串, 而正規式 r'^\d$' 則會匹配從頭至尾都是數字的字串. 比較特殊的是 r'^$', 此正規式可用來比對一個空列 (就是只有一個換列字元, 沒別的). 

首先來測試用 '^' 字元來限制匹配發生於起始位置 : 

>>> re.search(r'hello', 'hello world, hello tony')        # 沒有用 '^' 限制匹配之起始位置
<re.Match object; span=(0, 5), match='hello'>
>>> re.search(r'^hello', 'hello world, hello tony')      # 用 '^' 限制匹配於起始位置
<re.Match object; span=(0, 5), match='hello'>

此例目標字串中有兩個 'hello', 但因 re.search() 只匹配找到的第一組子字串即停止不會繼續往下找, 所以此例正規式有無加起始位置字元 '^' 結果相同, 都是匹配第一個 'hello'. 但同樣的目標字串如果用 re.findall() 遍尋就有差別了, 例如 :

>>> re.findall(r'hello', 'hello world, hello tony')        # 沒有用 '^' 限制匹配之起始位置
['hello', 'hello'] 
>>> re.findall(r'^hello', 'hello world, hello tony')      # 用 '^' 限制匹配之起始位置 
['hello']
>>> re.findall(r'^hello', 'hi world, hello tony')           # 找不到在字串開頭的 'hello'
[]   

可見如果正規式沒有冠 '^' 的話, re.findall() 會遍尋所有匹配的字串, 所以會傳回兩個 'hello'; 但冠了 '^' 後便限制只能匹配在開始位置上的 'hello' 了, 如果目標字串開頭位置並無 'hello' 那就會傳回空串列. 

正規式的尾端若是 '$' 字元則會限制只有在目標字串以 '$' 前面的樣式結尾才會匹配, 例如 :

>>> re.search(r'world$', 'hello world')                       # 匹配 (以 'world' 結尾)
<re.Match object; span=(6, 11), match='world'>   
>>> re.search(r'world$', 'hello world!') == None      # 不匹配 (不是以 'world' 結尾)
True

此例正規式 r'world$' 表示目標字串須以 'world' 結尾才匹配, 故第一例匹配, 但第二例因為多了一個結尾的驚嘆號, 所以不匹配. 下面是與數字有關的範例 : 

>>> re.findall(r'\d+', '10+20=30')        # 沒有用 '$' 限制匹配之結束位置
['10', '20', '30']
>>> re.findall(r'\d+$', '10+20=30')      # 用 '$' 限制匹配之結束位置 
['30']

第一例用 '$' 限制匹配之結束位置, 所以 re.findall() 會傳回全部匹配之數字, 第二例正規式 r\d+$' 只匹配結尾的數字字串, 所以只傳回最後面的 '30'. 

若於正規式首尾同時使用 '^' 與 '$' 表示目標字串必須整個都符合這兩個位置標示字元之間的樣式才算匹配, 例如 r'^\d+$' 會匹配全部都是數字的字串 : 

>>> re.findall(r'^\d+$', '10+20=30')      # 不匹配 : 含有非數字 '+' 與 '=' 
[]
>>> re.findall(r'^\d+$', '102030')           # 匹配 : 全為數字
['102030']
>>> re.match(r'^\d+$', '102030')      
<re.Match object; span=(0, 6), match='102030'>      # 比對是否全為數字
>>> re.fullmatch(r'\d+', '102030')    
<re.Match object; span=(0, 6), match='102030'>       # 比對是否全為數字

第一例因目標字串中含有非數字的 '+' 與 '=' 故不匹配; 第二例與第三例全為數字故匹配; 第四例則是用 re.fullmatch() 來比對, 由於此函式本來就是從頭到尾比對都要符合才匹配, 所以就不需要用到 '^' 與 '$' 了 (用也沒關係, 結果相同), 所以後兩例是等效的. 


2. 字元集 (character set) :

字元集是放在中括號 [] 裡的字元集合, 但不管裡面有多少個字元, 它只代表一個字元, 亦即括號內只是列舉候選字元而已, 只有其中一個會出線, 雖然前後順序不重要, 但為了可讀性通常會照易讀的順序排列, 例如 [13579] 會比 [95173] 容易看懂. 從邏輯上來看, 字元集內的所有字元彼此是 OR (或) 的關係, 亦即只輸出它們之中的任一個字元, 相對而言, 字元集外的字元則是 AND (且) 的關係, 例如 r'gr[ae]y' 匹配 'gray' 或 'grey', 但 r'gray' 則只能匹配 'gray'. 

字元集中有五個特殊字元用法必須注意, 其中 - 與 ^ 為字元集自己定義的詮釋字元, 它們與字元集外面的 - 與 ^ 用法不同 : 
  • 位於字元中間用來列舉範圍的連字號 - (字元集內的詮釋字元)
  • 位於開頭表示否定的 ^ 字元 (字元集內的詮釋字元) 
  • 右中括號字元 ]
  • 單引號 '
  • 雙引號 "
除了引號與右中括號, 所有的特殊字元在字元集內都不需要跳脫 (但要跳脫也可以). 其中引號要視整個正規式使用哪一種括號而定, 用單引號則字元集內的單引號要跳脫; 用雙引號則字元集內的雙引號要跳脫 (故不管三七二十一, 字元集內的引號全部都跳脫最保險), 總之, 字元集內外的語法是不一樣的, 字元集有自己的詮釋字元 (即連字號 - 與 ^ 字元), 它們的用法在字元集內外是不同的.  如果說正規表示法是內嵌於程式語言中的小型語言, 則字元集可說是內嵌於正規表示法中的迷你語言

要在字元集內窮舉 Unicode 編碼上的一段連續字元區間很長很麻煩, 字元集提供 "範圍標記法" 來簡化, 此法以中間的連字號表示一個 Unicode 值的區間, 例如 [0-9] 等於是 [012345679], 而 [a-z] 表示所有小寫字母. 若要表示連字號本身, 則連字號要放在中括號的開頭或結尾, 或者用倒斜線跳脫, 例如要列舉數字字元或連字號要用 [-0-9] 或 [0-9-] 表示, 此正規式列舉了 0~9 與 - 共 11 個字元. 注意, 使用範圍標記法時順序很重要, 必須 Unicode 值小的在前 Unicode 值大的在後, 否則會出現 'bad caracter range' 錯誤, 例如 [9-0] 或 [z-a] 就不是合法的正規式, 例如 :

>>> re.match(r'[9-0]', '123')               # [9-0] 不合法, 應該用 [0-9]
Traceback (most recent call last):
......(略)......
    raise source.error(msg, len(this) + 1 + len(that))
re.error: bad character range 9-0 at position 1    

正確範例如下 : 

>>> re.match(r'[0-9]', '123abc')                   # 從頭開始匹配一個數字字元
<re.Match object; span=(0, 1), match='1'>    
>>> re.search(r'[0-9]+', 'abc123xyz')          # 搜尋第一組 1 個以上的數字字元
<re.Match object; span=(3, 6), match='123'>
>>> re.findall(r'[a-z]+', 'abc123xyz')          # 搜尋全部 1 個以上的小寫英文字母字元
['abc', 'xyz']
>>> re.search(r'#[0-9a-fA-F]+', 'color:#01FA7C;')     # 搜尋 16 進位色碼
<re.Match object; span=(6, 13), match='#01FA7C'>   

從上面最後一例的正規式可知, 字元集內可以使用多組範圍標記, 例如 r'[a-zA-Z]' 會匹配所有的英文字母, 各組範圍標記的前後關係不重要, 因此也可以寫成 r'[A-Za-z]'. 

注意, 如果要比對連字號本身必須將連字號緊鄰中括號 [ 或 ], 否則必須用 \ 跳脫, 不能放在中間, 例如下面範例都是比對一個 0, 9 或 - 字元 :

>>> re.findall(r'[-09]', 'abc0-179')       # 搜尋全部 0, 9 或 - 字元 (- 放開頭)
['0', '-', '9']
>>> re.findall(r'[09-]', 'abc0-179')       # 搜尋全部 0, 9 或 - 字元 (- 放結尾)
['0', '-', '9']
>>> re.findall(r'[0\-9]', 'abc0-179')      # 搜尋全部 0, 9 或 - 字元 (用 \ 跳脫)
['0', '-', '9']

字元集另外一個有特殊用途的是否定字元 '^', 如上所述, 此字元在正規式中也用來標示開始位置, 但在字元集內意思不一樣 (字元集內外有各自的詮釋字元), 放在 [ 後面開頭位置表示否定, 稱為排除型字元集, 表示匹配未列出隻字元, 例如 [^0-9] 表示非數字字元, 等於簡記法的 \D. 但要注意, 不能把 [^a] 理解為是 'a' 字元以外的任何字元都匹配, 因為這樣子空字元 '' 也應該要匹配才對; 實際上意思不是這樣,  [^a] 正確的意思是 : 有一個字元 (所以不能是空字元), 當它不是 'a' 字元時就匹配. 

範例如下 : 

>>> re.match(r'[^0-9]', 'abc123')                            # 從頭比對非數字字元 (匹配 'a')
<re.Match object; span=(0, 1), match='a'>    
>>> re.findall(r'[^0-9]', '123A4(5y6*7^8$9-0')     # 搜尋全部非數字字元 
['A', '(', 'y', '*', '^', '$', '-']

可見 re.match() 匹配開頭第一個非數字字元後就停止比對並傳回 Match 物件, 而 re.findall() 則是找出全部非數字字元後已串列傳回. 注意, 字元集裡的 '^' 若不是在開頭位置 (即 '[' 後面) 就是一般字元, 不需要用倒斜線 \ 跳脫, 例如 : 

>>> re.findall(r'[abc\-^(*]', '1a2^3(4*5c6-')       # 搜尋字元集內的指定字元
['a', '^', '(', '*', 'c', '-']

此例中連字號因為不是放在頭尾所以需要跳脫, 其它特殊字元都不需要跳脫 ('^' 不是在開頭位置屬於一般字元故不需跳脫). 


3. 選項 (option) 或多選結構 (alternative) :

管線字元 '|' 是正規表示法的詮釋字元, 可用來串接無限多個子正規式, 整個選項結構被視為單一元素, 只要其中任一個子正規式匹配, 那麼整個選項結構就達成匹配; 但若每一個選項都不匹配, 則整個選項結構就不匹配. 所以就每個子正規式的比對結果來說 | 的各選項之間是 OR 的關係, 在邏輯上功能類似字元集, 某些正規式可分別用字元集或選項來實現, 例如 r'gr[ae]y' 等同於 r'gray|grey' 或更精簡的 r'gr(a|e)y', 但注意別無限上綱把兩者畫上等號, 它們的作用範圍有差別, 字元集是字元級別的 OR; 而選項結構則是子正規式級別的 OR. 除此之外, 字元集的候選範圍在中括號 [] 內部, 而選項結構卻可無限延伸, 但可以用群組來限制選項的範圍, 例如 r'gr(a|e)y 中 | 的範圍不會超過括號. 


五. Match 物件與正規式的分組 : 

特殊字元小括號 () 的用途是將正規式字串分成好幾個匹配群組, 這種用法稱為分群或分組 (grouping), 分組是正規表示法中的最重要的語法之一, 因為求值器會標記 (tagging) 前面已匹配的群組以便後續參照或引用 (reference). 

呼叫 re.match() 或 re.search 函式進行比對或搜尋時會傳回 Match 物件, 此物件提供了許多方法來存取正規式中的群組匹配之結果. 可用 dir() 與 eval() 來檢視 Match 物件的成員 :  

>>> import re
>>> match=re.match(r'\d', '123abc')    # 匹配傳回 
>>> members=dir(match)                      # 查詢 Match 物件之成員
>>> for mbr in members:                       # 走訪 Match 物件的成員
    obj=eval('match.' + mbr)                    # 取得成員
    if not mbr.startswith('_'):                    # 跳過內部成員 
        print(mbr, type(obj))                       # 顯示成員之類型
        
end <class 'builtin_function_or_method'>   
endpos <class 'int'>
expand <class 'builtin_function_or_method'>
group <class 'builtin_function_or_method'>
groupdict <class 'builtin_function_or_method'>
groups <class 'builtin_function_or_method'>
lastgroup <class 'NoneType'>
lastindex <class 'NoneType'>
pos <class 'int'>
re <class 're.Pattern'>
regs <class 'tuple'>
span <class 'builtin_function_or_method'>
start <class 'builtin_function_or_method'>
string <class 'str'>

藍色粗體者為較常用的物件成員, 其中 lastindex 為屬性 (整數), 記錄匹配的最後一個分組之索引 (也就是能存取的最大分組索引 n), 其餘為物件方法, 摘要說明如下表 : 

 Match 物件的方法  說明
 group(n)  傳回與第 n 個群組匹配的字串, 預設 n=0 會傳回整個匹配字串
 groups()  將各群組匹配之字串依序組成一個 tuple 傳回
 groupdict()  傳回所有具名群組之名稱與匹配字串組成的字典, 鍵:值=name:text
 start(n)  傳回第 n 個群組匹配字串在目標字串中的起始位置索引
 end(n)  傳回第 n 個群組匹配字串在目標字串中的結束位置索引
 span(n)  將第 n 個群組匹配字串起始與結束索引以 tuple 傳回, 即 (start(n), end(n))

其中 group() 方法會傳回指定群組索引之匹配字串, 沒有傳入群組索引預設為 0, 即 group()=(0), 會傳回各群組匹配的全部字串; groups() 方法無傳入參數, 它是將各群組匹配的字串依序組成 tuple 傳回來; groupdict() 方法則是傳回具名群組織名稱與匹配字串組成之字典. 而 span(), start(), end() 都是用來查詢指定群組匹配字串在目標字串中的索引位置. 

下面是分組的一個簡單範例 :

>>> import re
>>> match=re.match(r'(a+)(b+)', 'aabbbccc')    # 含有兩個分組的正規式  
>>> match       
<re.Match object; span=(0, 5), match='aabbb'>      # 匹配子字串 'aabbb'
>>> match.lastindex                                              # 最大的分組索引
2

由 lastindex 屬性值可知有兩個匹配群組, 2 是最後一個分組的索引, 而群組 0 是全部匹配群組合在一起之匹配字串, 可以呼叫 Match 物件的 group() 方法查詢各群組之匹配字串, 例如 : 

>>> match.group()                # 各群組的匹配字串
'aabbb'
>>> match.group(0)              # 各群組匹配的全部字串
'aabbb'
>>> match.group(1)              # 群組 1 的匹配字串
'aa'
>>> match.group(2)              # 群組 2 的匹配字串
'bbb'
>>> match.groups()               # 傳回各群組匹配字串組成之 tuple
('aa', 'bbb')
>>> match.groupdict()          # 傳回各具名群組名稱與匹配字串組成之 dict                  
{}                                             # 無具名群組故傳回空字典
>>> match.span(1)                # 第一群組匹配字串的索引範圍
(0, 2)
>>> match.start(1)                # 第一群組匹配字串的開始索引
0
>>> match.end(1)                  # 第一群組匹配字串的結束索引 (不含)
2
>>> match.span(2)                 # 第二群組匹配字串的索引範圍
(2, 5)
>>> match.start(2)                 # 第二群組匹配字串的開始索引
2
>>> match.end(2)                   # 第二群組匹配字串的結束索引 (不含)
5

如果正規式沒有分組, 則 group(n) 方法只能傳入 0 或不傳入參數, 即只能存取整體的匹配字串, 存取與分組相關的資料會出現錯誤, 例如 : 

>>> match=re.match(r'a+b+', 'aabbbccc')          # 匹配 1 個以上之 a 與 b 字元
>>> match       
<re.Match object; span=(0, 5), match='aabbb'>   
>>> match.group()                            # 全部匹配字串
'aabbb'
>>> match.group(0)                          # 全部匹配字串
'aabbb'
>>> match.group(1)                          # 此匹配沒有分群
Traceback (most recent call last):
  File "<pyshell>", line 1, in <module>
IndexError: no such group

電話號碼的比對就是分組派上用場之處, 以固網電話為例, 電話號碼分成前面的區碼與後面的用戶號碼兩段, 這兩段用正規式表示便可用兩個群組來代表, 這樣就能分別標記區碼與用戶號碼了. 不過電話號碼的記法習慣不同, 有用 - 號分隔的, 也有用小括號將趨碼括起來的, 它們相對應的正規式如下表所示 :

 電話號碼 正規式
 07-1234567 或 02-12345678 (\d{2})-(\d{7, 8})
 (07)1234567 或 (02)12345678 (\(\d{2}\))(\d{7, 8})
 (07)-1234567 或 (02)-12345678 (\(\d{2}\))-(\d{7, 8})

注意, 台灣的固網用戶號碼長度一般是 7 碼, 但或 8 碼台北與台中則為 8 碼. 上表中第一種記法之正規式很直觀, 就是用量詞字元 {} 限定前面數字字元的長度, {2} 表示限定 2 碼, 而 {7, 8} 表示限定碼長為 7 碼或 8 碼. 第二種記法使用小括號 () 括住區碼, 但由於 () 在正規式中有特定用途 (就是分組啦), 要用到括號本身就必須在前面加倒斜線來跳脫 (escape), 第三種記法則只是在區碼小括弧後面多一個 dash. 其實後面這兩個記法可以用量詞 ? (0 或 1 個) 來合併為 (\(\d{2}\))-?(\d{7, 8}). 如果要比對這三種記法, 可以用管線符號 | 將其與第一個正規式合併 :

pattern=re.compile(r'(\d{2})-(\d{7,8})|(\(\d{2}\))-?(\d{7,8})')

注意, 量詞 {m,n} 的 m 與 n 中間不可有空格. 

下面就用這個正規式樣本來從文本中搜尋電話號碼 :

>>> pattern=re.compile(r'(\d{2})-(\d{7,8})|(\(\d{2}\))-?(\d{7,8})')   
>>> pattern.match('07-12345678')                                 # 從開頭比對是否符合
<re.Match object; span=(0, 11), match='07-12345678'>
>>> pattern.search('家:07-12345678, 公司:02-87654321, 手機:0933-123456')    
<re.Match object; span=(3, 14), match='07-12345678'>   # 找到第一組匹配就停止
>>> pattern.findall("家:07-1234567, 公司:02-87654321, 手機:0933-123456")    
[('07', '1234567', '', ''), ('02', '87654321', '', '')]                     # 沒有匹配的分組傳回空字串
>>> pattern.findall("家:(07)1234567, 公司:(02)-87654321, 手機:0933-123456")     
[('', '', '(07)', '1234567'), ('', '', '(02)', '87654321')]                # 沒有匹配的分組傳回空字串

此例傳入 match() 的字串開頭就符合樣式所以匹配 (若目標字串是 '家:07-1234567' 就不會匹配), search() 方法則找到第一組匹配字串就停止, 所以後面的 02-87654321 不會被找到; 而 findall() 則會依據分組傳回串列. 從兩個 findall() 範例可知, 每一個分組的比對結果都會被放進 tuple 中, 所以沒有匹配的分組會傳回空字串. 


五. 用旗標與修飾符改變比對模式 : 

以上的範例皆未用到旗標 (flag), 在 re 模組的函式與 Pattern 物件的方法中, 旗標都是有預設值的備選參數, 例如預設有分大小寫, 採用單列模式, 以及關閉註釋功能等. 旗標在正規表示法中的功能是用來改變預設的比對模式 (match mode), 在 re 模組中是以內建常數形式存在. 另外正規表示法還有所謂修飾符 (modifier) 的語法也可以修改比對模式, 每種旗標都有相對應的修飾符語法.  

常用的比對模式旗標有下列四個 : 
  • re.IGNORECASE (簡寫 re.I) : 不分大小寫, 對應之修飾符為 (?i)
  • re.DOTALL (簡寫 re.S) : 單列模式, 對應之修飾符為 (?s)
  • re.MULTILINE (簡寫 re.M) : 多列模式, 對應之修飾符為 (?m)
  • re.VERBOSE (簡寫 re.X) : 註解模式, 對應之修飾符為 (?x)

1. 不分大小寫模式旗標 re.IGNORECASE (或 re.I) : 

此旗標是 re 模組的常數 re.IGNORECASE (簡寫為 re.I), 用來將正規式的比對模式由預設的有分大小寫更改為不分大小寫, 其值事實上是整數 2, 例如 : 

>>> re.IGNORECASE    
<RegexFlag.IGNORECASE: 2>   
>>> re.I                                              # 簡寫
<RegexFlag.IGNORECASE: 2>      
>>> re.IGNORECASE == 2    
True
>>> re.I == 2   
True

正規表示法比對時預設是有分大小寫, 亦即 r'ok' 只匹配 'ok' 而不會匹配 'OK', 'Ok', 或 'oK'. 如果要匹配這四種字元排列必須用 r'[oO][kK]', 例如 :

>>> re.match(r'ok', 'OK') == None               # 不匹配 (大小寫不符)
True
>>> re.match(r'[oO][kK]', 'OK')                   # 匹配 (不分大小寫)
<re.Match object; span=(0, 2), match='OK'>  
>>> re.match(r'[oO][kK]', 'ok')                     # 匹配 (不分大小寫)
<re.Match object; span=(0, 2), match='ok'>
>>> re.match(r'[oO][kK]', 'oK')                    # 匹配 (不分大小寫)
<re.Match object; span=(0, 2), match='oK'>
>>> re.match(r'[oO][kK]', 'Ok')                    # 匹配 (不分大小寫)
<re.Match object; span=(0, 2), match='Ok'>

這種作法當字串較長時就會很繁冗, 但用 re.I 旗標就可以輕易解決此問題, 例如 :

>>> re.match(r'ok', 'ok', re.I)   
<re.Match object; span=(0, 2), match='ok'>
>>> re.match(r'ok', 'OK', re.I)    
<re.Match object; span=(0, 2), match='OK'>  
>>> re.match(r'ok', 'Ok', re.I)  
<re.Match object; span=(0, 2), match='Ok'>  
>>> re.match(r'ok', 'oK', re.I)  
<re.Match object; span=(0, 2), match='oK'>
>>> re.match(r'OK', 'ok', re.I)    
<re.Match object; span=(0, 2), match='ok'>

可見傳入旗標 re.I 後不管目標字串的每個字元是大小寫都能匹配, 而且正規式也是不分大小寫. 

除了使用 re.I 旗標外, 還可以在正規式前面冠上修飾符 (?i) 將比對模式改為不分大小寫 (修飾符看起來像是群組但其實不是, 顧名思義它就是修飾後面正規式的意義而已), 例如 : 

>>> re.match(r'(?i)ok', 'OK')    
<re.Match object; span=(0, 2), match='OK'>
>>> re.match(r'(?i)ok', 'Ok')    
<re.Match object; span=(0, 2), match='Ok'>
>>> re.match(r'(?i)ok', 'oK')    
<re.Match object; span=(0, 2), match='oK'> 
>>> re.match(r'(?i)OK', 'ok')    
<re.Match object; span=(0, 2), match='ok'>

使用修飾符的好處是正規式幾乎可以原封不動搬到其他程式語言中不需修改 (除了 Javascript 外都可以, Javascript 是在正規式尾端以 /i 指定不分大小寫), 


2. 單列模式旗標 re.DOTALL (或 re.S): 

此單列模式跟正規式中小數點字元的功能有關, 小數點常被誤認為是匹配任何字元的萬用字元, 其實在預設模式下它有一個例外, 即不包含換列字元 '\n', 用 re.match() 與 re.search() 進行比對與搜尋 r'.*' 樣式時若遇到換列字元會停止, 例如 :

>>> re.match(r'.*', 'Trust me,You can make it')      # 無換列字元, 全部都匹配
<re.Match object; span=(0, 24), match='Trust me,You can make it'>  
>>> re.match(r'.*', 'Trust me\nYou can make it')    # 換列字元 \n 使比對 r'.*' 停止
<re.Match object; span=(0, 8), match='Trust me'> 
>>> re.search(r'.*', 'Trust me\nYou can make it')    # 換列字元 \n 使搜尋 r'.*' 停止        
<re.Match object; span=(0, 8), match='Trust me'> 

此例正規式 r'.*' 匹配 0 個或更多個除 \n 以外的字元, 當 re.match() 或 re.search() 讀取到 \n 字元時會因為不匹配而停止比對或搜尋, 結果只傳回 \n 字元前面的 'Trust me' 而已. 如果傳入旗標 re.DOTALL (或簡寫 re.S) 將比對模式從預設模式改為單列模式, 它會把小數點字元的例外情形 (不含 \n) 去除 (變成包含 \n), 使其能代表所有的字元 (此即 DOT=ALL 之意), 由於此旗標能跨越換列字元, 整個目標字串看起來似乎是單列一樣, 故稱為單列旗標, 例如 : 

>>> re.match(r'.*', 'Trust me\nYou can make it', re.DOTALL)      # 單列模式  
<re.Match object; span=(0, 24), match='Trust me\nYou can make it'>
>>> re.search(r'.*', 'Trust me\nYou can make it',re.DOTALL)      # 單列模式
<re.Match object; span=(0, 24), match='Trust me\nYou can make it'>

可見傳入 re.DOTALL (re.S) 後比對與搜尋就能跨越換列字元 \n 了. 

小數點萬用字元不包含換列字元 \n 問題除了用單列模式旗標解決外, 還可以使用修飾符, re.DOTALL 旗標對應的修飾符為 (?s), 把它放在正規式最前面就可以切換到單列模式了, 例如 : 

>>> re.match(r'(?s).*', 'Trust me\nYou can make it')      # 使用單列模式修飾符
<re.Match object; span=(0, 24), match='Trust me\nYou can make it'>  
>>> re.search(r'(?s).*', 'Trust me\nYou can make it')      # 使用單列模式修飾符
<re.Match object; span=(0, 24), match='Trust me\nYou can make it'> 

可見效果與使用旗標是一樣的. 

另外, 萬用字元除了使用正規式 r'.*' 配合 re.DOTALL 旗標改變比對模式外, 萬用字元還可以用正規式 r'[\s\S]*' 來表示, 其中 \s 表示所有空白字元 (包括空格, TAB 等), 而 \S 表示非空白字元, 兩個互斥的集合放在字元集中相當於是所有字元, 這樣就不需要用到單列模式旗標了. 例如 : 

>>> re.match(r'[\s\S]*', 'Trust me\nYou can make it')      
<re.Match object; span=(0, 24), match='Trust me\nYou can make it'>
>>> re.search(r'[\s\S]*', 'Trust me\nYou can make it')      
<re.Match object; span=(0, 24), match='Trust me\nYou can make it'>

可見效果與使用 re.DOTALL 旗標相同. 除了可用 [\s\S] 代表任何字元外, 也可以用  [\d\D] 或  [\w\W], 所以這種用法的缺點就是缺乏解讀上的單一性.


3. 多列模式旗標 re.MULTILINE (或 re.M) : 

多列模式似乎與上面的單列模式相對應, 但其實兩個模式毫不相干, 單列模式改變的是小數點字元的比對規則; 而多列模式改變的則是 ^ 與 $ 的比對規則. 在預設模式下, ^ 與 $ 表示整個目標字串的開頭與結束位置, 而在多列模式下, ^ 與 $ 表示目標字串內每一列的開頭與結束位置, 這對於要做逐列比對樣式的工作非常有用, 例如下面這個多列字串 :

1. Hello World
Welcome
2. Trust me, you can make it

在預設模式下用 re.findall() 無法直接找出多列字串中全部數字字元開頭的列, 例如 : 

>>> str='1. Hello World\nWelcome\n2. Trust me, you can make it' 
>>> re.findall(r'^\d.*', str)    
['1. Hello World']     

這是因為正規式 r'^\d.*' 匹配以數字字元開頭的任意的長度字元, 但是在預設模式下小數點並不包含換列字元 \n, 所以碰到第一個 \n 就停止搜尋了. 但若改為多列模式, 則 ^ 與 $ 會被重新定義為每列的起始與結束位置, 這樣就可以直接用正規式 r'^\d.*' 來比對是否為數字開頭的列, 例如 : 

>>> str='1. Hello World\nWelcome\n2. Trust me, you can make it' 
>>> re.findall(r'^\d.*', str, re.MULTILINE)        # 多列模式 ^ 字元表示每列開頭
['1. Hello World', '2. Trust me, you can make it']

此例的目標字串有三列, 其中第一列與第三列才是以數字字元開頭的, 所以傳回這兩列. 除了使用旗標, 還可以在正規式開頭冠上 (?m) 修飾符設定為多列模式, 例如 : 

>>> str='1. Hello World\nWelcome\n2. Trust me, you can make it'      
>>> re.findall(r'(?m)^\d.*', str)     
['1. Hello World', '2. Trust me, you can make it']    

可見結果與上面使用旗標是一樣的. 使用修飾符的好處是正規式可移植到除 Javascript 以外的程式語言皆可使用, 而旗標的用法則因語言而異.  

如果要在每列結尾加上句點則要用 $ 字元, 例如 : 

>>> re.sub(r'$', '.', str, re.MULTILINE)    
'1. Hello World\nWelcome\n2. Trust me, you can make it.'

但似乎無效 (why?), 改用修飾符 (?m) 則可以 : 

>>> re.sub(r'(?m)$', '.', str)    
'1. Hello World.\nWelcome.\n2. Trust me, you can make it.'


4. 註解模式旗標 : 

當正規式很長很複雜時可讀性會較差, 這時可將正規式以長字串分列表示, 同時以 re.VERBOSE  (或簡寫 re.X) 旗標設定為註解模式, 這樣就可以在每列後面用 # 加上註解說明, 例如上面用來比對固網電話號碼的正規式 : 

r'(\d{2})-(\d{7,8})|(\(\d{2}\))-?(\d{7,8})'   

全都寫成一列不容易懂, 即使在底下以文字說明也不直觀, 改用註解模式即可將正規式分列說明 : 

>>> pattern=r'''(\d{2})-(\d{7,8})|      # 區碼無括弧, 例如 02-12345678
            (\(\d{2}\))-?(\d{7,8})                # 區碼有括弧, 例如 (02)-12345678
         '''    
>>> re.match(pattern, '07-1234567', re.VERBOSE)     
<re.Match object; span=(0, 11), match='07-1234567'>
>>> re.match(pattern, '02-87654321're.X)                  # 簡寫
<re.Match object; span=(0, 11), match='02-87654321'>    

除了使用 re.VERBOSE 或 re.X 旗標外, 還可以在正規式最前面冠上 (?x) 修飾符來設定為註解模式 (注意是小寫 x), 例如 : 

 >>> pattern=r'''(?x)          # 註解模式修飾符
            (\d{2})-(\d{7,8})|      # 區碼無括弧, 例如 02-12345678
            (\(\d{2}\))-?(\d{7,8})  # 區碼有括弧, 例如 (02)-12345678
         '''
>>> re.match(pattern, '07-12345678')       # 不需要旗標
<re.Match object; span=(0, 11), match='07-12345678'>

此例使用了修飾符 (?x) 所以函式中就不用傳入 re.VERBOSE 旗標了. 

參考 : 


PS : 這篇我寫很久了 (大概一年多), 因太多事情打斷一直寫不完, 每次重拾又掉入 start from scratch 地獄, 來來回回改了很多遍, 已經忘記第一版是長怎樣了, 哈哈, 今天終於搞定了 (花了三周). 但也別高興得太早, 正規表示法這魔頭是沒那麼好降伏的, 這玩意兒每次重新複習完都會讓你有 from zero to hero 的感覺, 沒有錯, 這時大概就是走火入魔了.