Python正規表示式

Python正規表示式:不一定要會,但會了超省力

「正規表示式」(Regualr expression) 看了就讓人頭暈,一堆難懂的符號與文字。

尤其,正規這兩個字讓人覺得莫名恐懼。

如果你覺得「正規表示式」的名稱讓人摸不著頭緒,你可以試著從英文來理解,然後在心中為它取一個你比較喜歡的中文名稱,也許就不會那麼排斥了。

「正規表示式」是 Regualr expression 的中文翻譯,由兩個英文單字組成,regular expression

regular 是規則的意思,expression 則是表達的意思,中文翻譯把 Regualr expression 翻譯成「正規表示式」,代表一種有規則、有規律的表達方式。

也有人把 Regular expression 翻譯為正則表達式、規則運算式、正規表式法

其實你也可以不必去記這些中文名稱,知道 Regular expression 就是指,有規則的表達方式就可以了,重要的是讓它的功能幫助到你。

接下來我們會介紹正規表示式的功用,比較使用及不使用正規表示式的差異,這個比較文有些冗長,你可以使用快速閱讀,直接跳到你需要的操作說明段落。

Python正規表示式的功用

面對「正規表示式」你可能還會有這些問題:

  • 一定要學會「正規表示式」嗎?
  • 「正規表示式」可以幹嘛?
  • 「正規表示式」功能是什麼?
  • 「正規表示式」是不是很難?
  • 不會「正規表示式」就不能學會寫程式嗎?

首先,你不一定要會「正規表示式」也是可以寫程式。

學會什麼都是你的選擇,你可以不會「正規表示式」,依然可以編寫 Python 程式碼,讓程式運作;就好像你可以不會開車、也不會搭乘交通工具,就是只會走路,你還是可以從台北移動到高雄,只是會比較費力、費時。

「Python正規表示式」可以做什麼?

關於這個問題,讓我們回到學程式的目的。

why you learn coding
why you learn coding

最大的目,是要當個收入高、有技術涵養的工程師?跟上趨勢,擁有不易被社會淘汰的專長?

這可能是許多人學程式語言的目的。

不過就本質上而言,編寫程式語言的目的,是要利用程式幫助我們處理事情,將繁瑣複雜的事簡單化,讓規律的事情自動化。

而「正規表示式」的功用就是:幫你讓複雜的程式碼規律化、簡單化。

學會正規表示式的好處

Python 自動化的樂趣》這本書將「正規表示式」講解的很淺顯易懂,作者AI Sweigart 這麼形容,’會’與’不會’「正規表示式」的差異:

會用「正規表示式」(Regualr expression)意味著可以用3個步驟就解決問題,而不需要耗費3,000步才能搞定。

好處總是比較出來才會知道,我們來看看《Python 自動化的樂趣》這本書,所舉的例子,讓你評估一下是否有必要學會「正規表示式」。

python自動化的樂趣中文版-2
python自動化的樂趣中文版-2

情境是這樣的:

設計一個可以辨別電話號碼的程式,讓你可以在一大堆的資料中,用程式快速幫你抓出電話號碼。

要查找的電話號碼格式:

02-1111-2222,區碼二碼數字 – 四碼數字 – 四碼數字。

不使用正規表示式

不使用「Python正規表示式」的情況下,我們設計一個名稱為 isPhoneNumber() 的函式。

首先檢查長度是不是12個位元,再依序檢查每個位置是數字或是 – 符號,每一個條件都必需成立,才會判斷為電話號碼。

函式內,寫滿了 if 陳述式與 for 迴圈,一個字元一個字元的檢查,資料是不是數字及 – 符號,全部符合的情況才會返回 True,True 代表某段文字是電話號碼,False 代表不是電話號碼。

最後面的四行 print() 程式碼,執行電話號碼的辨別工作。

>>> def isPhoneNumber(text):
>>>     if len(text) != 12:
>>>         return False
>>>     for i in range(0,2):
>>>         if not text[i].isdecimal():
>>>             return False
>>>     if text[2] != '-':
>>>         return False
>>>     for i in range(4,7):
>>>         if not text[i].isdecimal():
>>>             return False
>>>     if text[7] != '-':
>>>         return False
>>>     for i in range(8,11):
>>>         if not text[i].isdecimal():
>>>             return False
>>>     return True 
>>>  
>>> print('this is a number: 02-1234-5555')
>>> print(isPhoneNumber('02-1234-5555'))
>>> print('this is a number: oh no! regular expression')
>>> print(isPhoneNumber('oh no! regular expression'))

執行程式後,得到的結果

this is a number: 02-1234-5555
True
this is a number: oh no! regular expression
False

成功辨識出 02-1234-5555 是電話號碼,而 oh no! regular expression 不是電話號碼。

接著我們再將最後四行的 print() 程式碼調整一下,搭配 for 迴圈與 if 陳述式,減少你打一大堆 print(),調整後如下:

>>> Data = 'Please call David at 02-8888-1688 by today. 02-9888-9898 is his office number.'
>>> for i in range(len(Data)):
>>>     number = Data [i:i+12]
>>>     if isPhoneNumber(number):
>>>         print('Phone Number: ' + number)
>>> print('Please call him/her as soon as possible.')

執行結果如下方所示:

Phone Number: 02-8888-1688
Phone Number: 02-9888-9898
Please call him/her as soon as possible.

這是不使用正規表示式,來找尋、比對資料的方法,你需要幫每個字元設定條件,以判別是不是電話號碼的格式。

使用正規表示式

而如果使用「Python正規表示式」來檢查,會是什麼狀況呢?

「正規表示式」有強大的比對功能,為多種字元條件設定了特定的符號,例如要找數字,以 \d 表示,就會讓 Python 去判別字元是不是數字。 

一樣是針對找電話號碼情境,使用「正規表示式」的程式碼會是這樣:

>>> import re
>>> phoneNumRegex = re.compile(r'\d\d-\d\d\d\d-\d\d\d\d')
>>> result = phoneNumRegex.findall('Please call David at 02-8888-1688 by today. 02-9888-9898 is his office number.')
>>> number = ', '.join(result)
>>> print('Phone Number: ' + number)
>>> print('Please call him/her as soon as possible.')

沒錯,如果你會使用「正規表示式」,就可以很簡潔的你做一個功能一樣的小程式。

執行結果:

Phone Number: 02-8888-1688, 02-9888-9898
Please call him/her as soon as possible.

程式碼減短不少,並且也可以順利找到文字中的電話號碼。

除此以外,調整規則也會方便很多。

兩種方法都可以在辨別出字串的電話號碼,使用正規表示式方便也簡短很多。

而且你也不用在各個 if、for 之間數著你的 range() 內的引數應該設多少,第幾個位置該是數字、第幾個位置該是 – 符號。

前面的例子中,不使用正規表示式的程式碼需要 23 行,使用正規表示式則只要 6行 (本文例子的字串,因螢幕寬度限制,變為多行的仍以單行計算)。

使用正規表示式中的 compile()函式、findall()方法、\d ,取代一大堆 if 、for ,的確省了不少力氣。

這樣比較下來,有激起你想學習正規表示式的動力嗎?

如果有,就接著往下看吧。

開始編寫正規表示式

前面找電話號碼的例子,印證使用「正規表示式」讓我們的程式碼簡潔許多,也免去了思考一大堆 if 陳述句for 迴圈 ,要怎麼使用的恰到好處的燒腦狀況。

該怎麼在 Python 中使用「正規表示式」呢?

最重要的第一步: 匯入re 模組

因為「正規表示式」沒有內建在 Python 中,所以使用前,必須在程式碼的最開頭匯入 re 模組。

怎麼匯入?就是打上:

>>>import re

import 就是匯入的意思,只要你要匯入任何模組,都是用 import 並在其後加上模組名稱。re 是 regular expression 模組的名稱。

匯入re 模組,絕對是你要開始編寫「正規表示式」的第一步,沒有匯入re模組,你寫得再好的「正規表示式」也不會執行,只會得到錯誤。

NameError: name 're' is not defined

當你看到這個錯誤訊息,就是你忘記匯入 re 模組了,快在程式碼開頭加上 import re 吧!

建立 Regex 物件

匯入 re 模組就像是找到一部車子,你還要學會怎麼發動引擎才能開始讓車子運轉,送你到達目的地。

要啟動引擎,要先找到鑰匙孔或者是啟動的按鍵。

建立 Regex 物件就是啟動的關鍵。

Regex 就是 Regular expression 的縮寫,所以Regex 物件就是指,正規表示式物件。

要建立 Regex 物件,要使用 re 模組中的 compile()函式,以 re.compile()來表示。

compile 是編譯的意思,在compiel() 函式的括號中填入你需要的規則,也就是編譯一個規則,放到括號中。

以找電話號碼為例,我們設定要找的號碼格式是:02-1111-2222,區碼二碼數字- 四碼數字 – 四碼數字。

在正規表示式中,\d 代表一個只能是數字的字元,我們就用 \d 來建立電話號碼格式。

>>>phoneNumRegex = re.compile(r'\d\d-\d\d\d\d-\d\d\d\d')

建立的好的 Regex 物件,我們把它指派給一個好懂得變數,方便之後使用,才不用一直打 re.compile(r’\d\d-\d\d\d\d-\d\d\d\d’),只要打出 phoneNumRegex ,就代表你所設定的規則。

眼尖的你可能會發現,compile() 括號內怎麼有個 r,要求原始字串。

這是因為正規表示式,會用到許多符號,可能會與 Python 內建的功能相衝突,所以會在你需要的格式前面加上 r ,代表原始字串 raw

關於原始字串,你可以在《Python字串(string)基礎與20種常見操作》這篇文章的「顯示原始字串」段落,喚起你對原始字串的印象。

正規表示式入門操作

「正規表示式」有很多種形態的規則,不過我們先學幾個基本的用法就好,等到對正規表示式的觀念建立了,再去拓展、延伸。

不需要一開始就給自己過多的資訊量,被博大精深的正規表示式嚇跑了,總是要先有入門,建立基礎後,再藉著基礎知識去鑽研更深一層的知識。

就像是學開車,如果教練第一天就教你全部的操作方法,但你連發動引擎都不會,一定會覺得開車超難的,而且還要求你瞭解汽車三萬多個零件的名稱、用途、功能、故障排除,以及所有的交通法規,你大概會覺得這輩子是不可能學會開車了。

資訊過多無法消化
資訊過多無法消化

現在,我們就先來學習「正規表示式 」的入門操作吧!

比對 Regex 物件

建立好 Regex 物件後,也就建立好你要比對資料的規則了。

比對資料可以使用 search() 或 findall() 方法,search()只會找出符合規則的一筆資料,findall()則是會把所有符合的資料都找出來給你,所以才叫 find all。

這兩個方法找東西的成果不一樣,操作上也有點不同。

使用search()方法

search()方法要結合Regex物件使用,下方的例子中,phoneNumRegex 是我們為了找電話號碼建立的物件名稱。

在 phoneNumRegex 後方加個句點,輸入search(),並在括號內填入你要比對、查找的資料。

如果有找到符合規則的,便會得到一個 Match 物件;如果沒有找到,則會得到 None。

match 的英文意思是吻合的意思。

通常我們會將 Match 物件指派給 mo 這個變數,mo 是 match object 的簡稱,跟momo購物沒有關係。

>>> phoneNumRegex = re.compile(r'\d\d-\d\d\d\d-\d\d\d\d')
>>> mo = phoneNumRegex.search('Call me at 02-8888-1688 by today.')
>>> print(mo.group())

而呼叫 Match 物件需要使用 group()方法,才可以把找到的東西呈現出來,也就是程式才會顯示出比對正確的字串。

執行程式後,得到下方的結果:

02-8888-1688

search()方法可以幫助我們找對比對成功的第一筆電話,所以如果你都只要找出一筆資料,你已經學會如何讓正規表示式為你所用了。

如果你要找到所有的電話,或是所有符合規則的資料,那你可以再學一個方法,findall()方法。

使用findall() 方法

findall()方法一看就是會幫你把所有符合規則的資料找齊,find all 不就說明了一切。

findall() 方法的操作與 search() 方法相似,也是在 Regex物件後方加個句號,放上 findall(),並在括號內填入你要比對的目標資料。

下方是我們前面提過的例子,把比對出來的資料都指派給 result 這個變數。

>>> phoneNumRegex = re.compile(r'\d\d-\d\d\d\d-\d\d\d\d')
>>> mo = phoneNumRegex.findall('Please call David at 02-8888-1688 by today. 02-9888-9898 is his office number.')
>>> print(mo)

程式執行結果:

['02-8888-1688', '02-9888-9898']

findall() 與 search() 不同的是:

  • 你不需要使用 group() 就可以呈現出比對成功的結果。
  • findall()方法返回串列(list),串列內的項目是字串(string);然而 search() 方法擇返回字串(string)。
    也就是說兩個方法所得到的產出,資料型態是不同的,前者是串列(list),後者是字串(string)。資料型態不同,就會影響你運用資料的方式!

分組後,比對結果的差異

前面的例子,我們沒有將 re.compile() 函式中的規則分組。

這裡的分組是指幫規則分區塊,用小括號來區分。

將 re.compile() 函式中的規則分組後,search() 及findall()返回的結果,會與沒有分組時有些差異。

下方的例子,我們將電話號碼分組,區碼二碼數字- 四碼數字 – 四碼數字,分出了三組。

>>>phoneNumRegex = re.compile(r'(\d\d)-(\d\d\d\d)-(\d\d\d\d)')

分組後使用search()方法,可指定要返回的值

規則進行分組後,你可以在 group()的括號內填入組別,來取得指定的比對資料。

下方例子,我們把規則分成三組 (\d\d)-(\d\d\d\d)-(\d\d\d\d),一個括號代表一組。

>>> phoneNumRegex = re.compile(r'(\d\d)-(\d\d\d\d)-(\d\d\d\d)')
>>> mo = phoneNumRegex.search('Please call me at 02-8888-1688 or 02-3333-2323 by today.')
>>> print(mo.group(3))
>>> print(mo.group(0))

這裡要注意的是,group(0) 或 group() 代表全部,group(1) 代表第一組,以此類推,這裡的排序方式又跟 index 要從0開始算起有點不一樣,需要留意一下。

執行程式後,你會得到這樣的結果:

1688
02-8888-1688

將規則以括號分組後,你可以更精準地取用比對出來的資料,例如你找出了 1000筆電話,但你只需要區碼,便可以用 group(1),只顯示第一組的資料,也就是區碼。

分組後使用findall()方法,返回值為內含tuple的串列

同樣的,我們把規則分成三組,來看看 findall() 返回的值是什麼。

>>> phoneNumRegex = re.compile(r'(\d\d)-(\d\d\d\d)-(\d\d\d\d)')
>>> result = phoneNumRegex.findall('Please call David at 02-8888-1688 by today. 02-9888-9898 is his office number.')
>>> print(result)

執行程式碼的結果:

[('02', '8888', '1688'), ('02', '9888', '9898')]

結果還是會產生串列(list),但串列(list)的第一層變成tuple,tuple內才是字串(string)。

如果你希望比對的成果可以有完整電話號碼,你可以在規則的最外層再加上括號。

>>> phoneNumRegex = re.compile(r'((\d\d)-(\d\d\d\d)-(\d\d\d\d))')
>>> result = phoneNumRegex.findall('Please call David at 02-8888-1688 by today. 02-9888-9898 is his office number.')
>>> print(result)

最外層的括號會被當成第一組,結果就像這樣:

[('02-8888-1688', '02', '8888', '1688'), ('02-9888-9898', '02', '9888', '9898')]

需要特別注意的,當 findall() 返回的返回的串列,第一層資料變成 tuple後,需要留意資料的操作上侷限,因為 tuple 操作較少,例如字串方法 join() 就不能用了。

不過你還是可以直接把值指派給不同變數,來呈現出你要的資料型態,再進行操作。

例如,你可以不在最外圍使用一個括號,而以另一種方法來獲得完整的電話號碼。

這個方法將 findall() 方法回傳的結果,再進行操作,取出各個數字,用 + 運算子串成字串。

>>> phoneNumRegex = re.compile(r'(\d\d)-(\d\d\d\d)-(\d\d\d\d)')
>>> result = phoneNumRegex.findall('Please call David at 02-8888-1688 by today. 02-9888-9898 is his office number.')
>>> for i in range(len(result)):
>>>     area, first, second = result[i][0], result[i][1],result[i][2]
>>>     print(area + '-' + first + '-' + second)

執行程式你可得到這樣的結果:

02-8888-1688
02-9888-9898

鄭重恭喜你,學會「正規表示式」了!

基本上,只要你會匯入 re 模組,利用 compile() 函式建立 Regex 物件,使用 search() 及 group() 或 findall() 方法來找到符合規則的資料,你就已經會使用「正規表示式」。

只不過因為你只會 \d 這個規則,所以你只會從資料中找出你要的數字,例如電話、價錢、郵遞區號、樂透號碼等,如果你要找的是e-mail、網址等其他格式的目標呢?

如果希望可以制定更多元的規則,你可以再多了解還有哪些規則可以讓你使用,幫助你比對資料,這樣就可以擴大你的「正規表示式」功力了。

各種幫助你比對的工具

你需要比對的資訊可能不只數字,有可能是 Email、網址,或是你要把比對得到的姓名隱藏起來,以***表示。

接下來介紹常見的字元規則,讓你使用「正規表示式」時,有更多工具可以使用。

常見的字元分類

從找電話號碼的例子中,你知道 \d 代表任何數字字元,0-9的數字都會符合 \d 的代表的規則,其實還有很多類似的字元分類,常見的幾個列在下面的表格中。

分類符號

規則含義

符合的例子

\d

從0到9的數字

123

\w

任何的字母、數字及底線符號_

yes123_或YES123_

\s

空白字元,包括空格、定位符號空格(tab)、換行符號

有點難呈現,請用規則想像一下

\D

\d規則以外的字元。即除了數字以外的字元

abc或ABC

\W

\w規則以外的字元。即除了數字、字母、底線以外的字元

,或 -

\S

\s規則以外的字元。即除了\d空白字元以外的字元

123或yes123_或,

具有特殊功能的符號

這些符號在正規表示式中,具有特殊功能,如果只是要表示這個符號,需要加上反斜線,例如你要比對句點,要打上\.,或是使用自訂規則[\]。

符號

功能

.

比對任何字元,但是換行符號除外。

|

可以比對多個規則,第一個符合為比對結果。

?

比對符合0次或1次。或者代表節儉的比對,比對最少的就停止。
.*? 比對到符合的就停止,不貪婪。

*

比對符合0次或多次,或者代表貪婪的比對,盡可能比對最多。

.*  代表比對所有字元,貪婪的一直找下去。

+

比對符合1次或多次。

[ ]

自訂比對格式。如為除外的比對,左側中括號加上^。
[^123],表示比對沒有123以外的字元。

{ }

指定比對的次數。
{3}代表三次,{3,5}代表三到五次,逗號前後可以擇一省略。

( )

將規則分組。

^

比對開頭符合規則的字串。在[]中代表除外,位置必須緊接在[之後。

$

比對結尾符合規則的字串。
同時使用,^與$符號,代表模式要一模一樣。

compile()函式第二個引數

compile()函式可加入第二個引數,增加設定規則的彈性。

名稱

功能

re.IGNORECASE 或 re.I

比對時忽略大小寫

re.DOTALL

比對時包含所有的字元,換行符號也包括

re.VERBOSE

在compile()中,使用 # 註釋功能。多行字串中,為了對齊使用的空格,不會被列為比對的規則

需要注意的是,第二個引數只能放一個值,如果三個功能你都需要用到,可以使用 | 符號,將之組合起來使用。

>>>requestRegex = re.compile('luck', re.I|re.DOTALL|re.VERBOSE)

這些工具,可以幫助你建立更便捷「正規表示式」,查看表格如有不明白之處,《Python 自動化的樂趣》這本書,對於相關符號的解釋與範例,可以再幫助你理解。

如果書本的閱讀讓你苦惱,你還可以透由課程教學,更快的理解 Python「正規表示式」,作者Al SweigartUdemy上的課程解說,相信會讓你學會該怎麼使用這些操作。

使用sub()方法取代指定的字串

相信你一定很常看部分隱藏的名字,例如下方的得獎名單。

本圖為移民署中獎名單

sub() 方法可以幫你製作類似這樣的效果,把比對到的姓名隱藏起來,以***表示。

2個步驟教你用 sub() 方法辦到。

第一步 找出目標

先利用 compile() 設定可以找到目標的規則,並利用 sub() ,第一個引數指定要替代的字串,第二個引數為要進行取代的字串。

下方的例子是個得獎名單,列出了最大獎到三獎,我們要獎得獎人的名字隱藏起來,先用 HIDE 取代 (輸入你想取代的字元,此處以 HIDE 舉例)。

>>> import re
>>> nameRegex = re.compile(r'prize - \w+')
>>> result = nameRegex.sub('HIDE', 'first prize - Phoebe, second prize - Vivi, third prize - Ming')
>>> print(result)

測試一下結果,執行程式後,得到下方的結果。

first HIDE, second HIDE, third HIDE

沒問題,名字都被取代了。

第二步 將規則分組

我們希望可以顯示得獎者名字的第一個字,其餘以***取代。

首先,我們需要為compile()裡的規則,進行分組,將要留下的字元用小括號分組。

>>> nameRegex = re.compile(r'prize - (\w)\w+')

接著在 sub() 中填入你要取代的模式,\1 代表輸入分組的第一組,即每個得獎著名字的第一個字母,其後則以***取代。

>>>result = nameRegex.sub(r' \1***', 'first prize - Phoebe, second prize - Vivi, third prize - Ming')

如果你區分了很多組,\1 、\2 、\3 分別代表第一組、第二組、第三組,以此類推來運用。

完整的程式碼會是這樣:

>>> import re
>>> nameRegex = re.compile(r' - (\w)\w*')
>>> result = nameRegex.sub(r' \1***', 'first prize - Phoebe, second prize - Vivi, third prize - Ming')
>>> print(result)

執行後的結果:

first prize P***, second prize V***, third prize M***

書籍及課程資訊

如果你想再看多一點Python「正規表示式」的例子與實作,推薦閱讀《Python 自動化的樂趣》這本書的第七章。

python自動化的樂趣中文版-2
python自動化的樂趣中文版-2

如果你想透由課程教學,更快的理解 Python「正規表示式」,作者Al SweigartUdemy上的課程解說得相當清楚。

「正規表示式」課程在第10堂課,以7個影片,用將近2個小時的課程講解這個部分。

作者的線上課程跟書本的講解順序略有不同,影片有提到的書本都有,只是線上課程畢竟是口語的教學,相對書本好懂很多,如果理解本文或書本上有碰到難點,相信這堂課可以為你帶來很多收穫。

關於更多的Python 「正規表示式」資訊,你還可以參考 Python 官方文件說明,鉅細靡遺將各種用法都列出來了。

延伸閱讀:

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *