Python 規則運算式

規則運算式是一種功能強大的語言,可用來比對文字模式。本頁將簡單介紹規則運算式本身,以便用於 Python 練習,並示範規則運算式在 Python 中如何運作。Python 的「re」模組支援規則運算式。

在 Python 中,規則運算式搜尋通常寫成:

match = re.search(pat, str)

re.search() 方法會使用規則運算式模式和字串,並在字串中搜尋該模式。如果搜尋成功,search() 會傳回比對物件,否則傳回 None。因此,搜尋通常會在隨後緊接著出現 if-statement 以測試搜尋是否成功,如以下範例所示,搜尋「word:」模式後接 3 個字母的字 (詳見下方說明):

import re

str = 'an example word:cat!!'
match = re.search(r'word:\w\w\w', str)
# If-statement after search() tests if it succeeded
if match:
  print('found', match.group()) ## 'found word:cat'
else:
  print('did not find')

程式碼 match = re.search(pat, str) 會將搜尋結果儲存在名為「match」的變數中。然後 if-statement 會測試比對結果。如果搜尋成功,則條件為 match.group() 就是相符的文字 (例如「word:cat」)。否則,如果比對結果為 False (無更明確),表示搜尋並未成功,且沒有相符文字。

「r」就會在模式字串的開頭指定 Python 的「raw」字串,在沒有變更的情況下傳遞反斜線,這對規則運算式非常有用 (Java 不需要使用此功能!)。建議您一律使用「r」撰寫模式字串只是養成習慣

基本模式

規則運算式的功用在於可指定模式,而不只是固定字元。以下是符合單一字元的最基本格式:

  • a、X、9、<-- 一般字元只會比對名稱本身。因為以下字元的涵義,所以不會與自己配對: 。^ $ * + ?{ [ ] \ |( ) (詳情如下)
  • 。(半形句號) -- 比對新行「\n」以外的任何單一字元
  • \w -- (小寫 w) 與 "word" 相符字元:字母或數字或底線 [a-zA-Z0-9_]。請注意,雖然「字詞」是序曲,它只會比對一個字詞字元,而非整個字詞。\W (大寫 W) 會比對任何非文字字元。
  • \b -- 字詞和非字詞之間的邊界
  • \s -- (小寫 s) 會比對單一空白字元 -- 空格、換行字元、返回、定位點、表單 [ \n\r\t\f]。\S (大寫 S) 會比對任何非空白字元。
  • \t、\n、\r -- Tab、換行符號、Return
  • \d -- 十進位數字 [0-9] (部分舊版規則運算式公用程式不支援 \d,但皆支援 \w 和 \s)
  • ^ = start、$ = end -- 比對字串的開頭或結尾
  • \ -- 禁止「特殊性」字元組合例如,請使用 \。如要比對半形句號,或 \\ 則可比對斜線。如果不確定字元是否具有特殊意義 (例如「@」),請嘗試在符號前面加上斜線 (\@)。如果逸出序列無效 (例如 \c),Python 程式就會暫停並顯示錯誤。

基本範例

笑話:三眼中是什麼叫豬?啊!

以下是在字串內搜尋模式的基本規則:

  • 搜尋會從字串開始到結束,在找到第一個相符項目時停止
  • 所有模式都必須相符,但不包括部分字串
  • 如果 match = re.search(pat, str) 成功,則比對並非「無」,且特定 match.group() 會包含相符文字。
  ## Search for pattern 'iii' in string 'piiig'.
  ## All of the pattern must match, but it may appear anywhere.
  ## On success, match.group() is matched text.
  match = re.search(r'iii', 'piiig') # found, match.group() == "iii"
  match = re.search(r'igs', 'piiig') # not found, match == None

  ## . = any char but \n
  match = re.search(r'..g', 'piiig') # found, match.group() == "iig"

  ## \d = digit char, \w = word char
  match = re.search(r'\d\d\d', 'p123g') # found, match.group() == "123"
  match = re.search(r'\w\w\w', '@@abcd!!') # found, match.group() == "abc"

重複

使用 + 和 * 在模式中指定重複項目,會變得更加有趣

  • + -- 左側出現 1 次以上的模式,例如「i+」= 一或多個 i
  • * -- 模式左側出現 0 次以上
  • 嗎?-- 將模式左側比對 0 或 1 次

最左側 &最大

首先,搜尋會找出與模式最左側的相符項,再嘗試運用最多的字串,亦即「+」和「*」會盡可能拉高一點 (+ 和 * 都表示為「貪婪」)。

重複範例

  ## i+ = one or more i's, as many as possible.
  match = re.search(r'pi+', 'piiig') # found, match.group() == "piii"

  ## Finds the first/leftmost solution, and within it drives the +
  ## as far as possible (aka 'leftmost and largest').
  ## In this example, note that it does not get to the second set of i's.
  match = re.search(r'i+', 'piigiiii') # found, match.group() == "ii"

  ## \s* = zero or more whitespace chars
  ## Here look for 3 digits, possibly separated by whitespace.
  match = re.search(r'\d\s*\d\s*\d', 'xx1 2   3xx') # found, match.group() == "1 2   3"
  match = re.search(r'\d\s*\d\s*\d', 'xx12  3xx') # found, match.group() == "12  3"
  match = re.search(r'\d\s*\d\s*\d', 'xx123xx') # found, match.group() == "123"

  ## ^ = matches the start of string, so this fails:
  match = re.search(r'^b\w+', 'foobar') # not found, match == None
  ## but without the ^ it succeeds:
  match = re.search(r'b\w+', 'foobar') # found, match.group() == "bar"

電子郵件範例

假設您要從「xyz alice-b@google.com purple monkey」字串中找到電子郵件地址。我們會透過這個實際範例來示範更多規則運算式功能。請嘗試使用 r'\w+@\w+' 模式:

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'\w+@\w+', str)
  if match:
    print(match.group())  ## 'b@google'

在此情況下,搜尋並「找不到完整電子郵件地址」,因為 \w 與「-」並不相符或「.」網址。我們使用下方的規則運算式功能來修正這個問題。

方括號

用於表示一組字元,因此 [abc] 與「a」相符或「b」或「c」。\w 和 \s 等代碼位於方括號內,但是有一個例外:點 (.) 代表常值點。電子郵件問題時,使用方括號可輕鬆加上 '.'。和「-」這些字元集會顯示在 @ 前後,格式為「[\w.-]+@[\w.-]+」取得完整的電子郵件地址:

  match = re.search(r'[\w.-]+@[\w.-]+', str)
  if match:
    print(match.group())  ## 'alice-b@google.com'
敬上 (更多方括號功能) 您也可以使用破折號來表示範圍,因此 [a-z] 會比對所有小寫英文字母。如要在不表示範圍的情況下使用破折號,請在最後輸入破折號 (例如:[abc-].方括號 (^) 的開頭會反轉,因此 [^ab] 是指「a」以外的任何字元或「b」。

群組擷取

「群組」功能中,即可挑選要擷取的部分相符文字。假設我們想要分別擷取使用者名稱和主機的電子郵件問題。方法是在格式中的使用者名稱和主機前後加上括號 ( ),例如:r'([\w.-]+)@([\w.-]+)'。在此情況下,括號不會變更模式相符的項目,而是建立邏輯運算子「群組」。。搜尋成功時,match.group(1) 是對應第 1 個左括號的相符文字,而 match.group(2) 是對應第 2 個左括號的對應文字。plain match.group() 依然是完整的比對文字。

  str = 'purple alice-b@google.com monkey dishwasher'
  match = re.search(r'([\w.-]+)@([\w.-]+)', str)
  if match:
    print(match.group())   ## 'alice-b@google.com' (the whole match)
    print(match.group(1))  ## 'alice-b' (the username, group 1)
    print(match.group(2))  ## 'google.com' (the host, group 2)

規則運算式的常見工作流程之一,就是為要搜尋的項目編寫模式,新增括號群組來擷取所需部分。

Findall

findall() 可能是 Re 模組中最強大的單一函式。上面,我們使用了 re.search() 來尋找與模式最相符的項目。findall() 會找出「所有」相符項目,並以字串形式傳回其結果,每個字串都代表一個相符項目。
  ## Suppose we have a text with many email addresses
  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'

  ## Here re.findall() returns a list of all the found email strings
  emails = re.findall(r'[\w\.-]+@[\w\.-]+', str) ## ['alice@google.com', 'bob@abc.com']
  for email in emails:
    # do something with each found email string
    print(email)

Findall With Files

在檔案方面,您可能經常需要編寫迴圈來疊代檔案的各行,接著可以在每一行呼叫 findall()。而是讓 findall() 代您進行疊代,效果更好!只要將整個檔案文字饋送到 findall(),即可讓它傳回單一步驟中所有相符的清單 (提醒您,f.read() 會傳回單一字串中檔案的完整文字):

  # Open file
  f = open('test.txt', encoding='utf-8')
  # Feed the file text into findall(); it returns a list of all the found strings
  strings = re.findall(r'some pattern', f.read())

findall 和 Groups

括號 ( ) 的群組機制可以與 findall() 合併使用。如果模式包含 2 個以上的括號群組, findall() 會傳回 *tuples* 清單,而非傳回字串清單。每個元組代表一個符合模式的一個相符項目,元組內部則是群組(1)、group(2) .. 資料。因此,如果在電子郵件模式中加入 2 個括號群組, findall() 就會傳回元組清單,每個長度 2 都含有使用者名稱和主機,例如(「alice」、「google.com」)。

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  tuples = re.findall(r'([\w\.-]+)@([\w\.-]+)', str)
  print(tuples)  ## [('alice', 'google.com'), ('bob', 'abc.com')]
  for tuple in tuples:
    print(tuple[0])  ## username
    print(tuple[1])  ## host

取得元組清單後,您就可以在其中進行迴圈,為每個元組執行一些運算。如果模式不包含括號,findall() 會傳回找到的字串清單,如上述範例所示。如果模式包含一組括號, findall() 會傳回與該單一群組相對應的字串清單。(罕見的選用功能: 有時您在模式中使用了括號 ( ) 分組,但您不想擷取這些分組。在此情況下,請在開頭加上「?:」括號,例如:(?: ) 和左括號不算群組結果。)

RE 工作流程和偵錯

規則運算式模式 僅含幾個字元 就足以容納所有資訊,但由於字元相當密集,您還是可能需要花很多時間偵錯模式。設定執行階段,以便您能夠輕鬆執行模式並顯示其相符的內容,例如:在小型測試文字上執行,然後輸出 findall() 結果。如果模式比對不到任何項目,請嘗試縮小模式,移除部分模式,獲得太多相符結果。如果沒有相符的結果,就無法取得任何進度。如果兩者比對太多,您可以逐步加緊,讓廣告活動只符合您的需求。

選項

re 函式會採用選項來修改模式比對的行為。Option 旗標會額外新增引數至 search() 或 findall() 等,例如:re.search(pat, str, re.IGNORECASE)。

  • IGNORECASE -- 是否要在比對時忽略大小寫差異,所以「a」同時符合「a」和「A」
  • DOTALL -- 允許點 (.) 比對換行,一般是符合換行符號以外的字元。這樣可能會使您升高,您認為 .* 符合所有條件,但預設不會超過線條的結尾。請注意,\s (空白字元) 含有換行符號,因此如果要比對所有可能包含換行的空白字元,只要使用 \s*
  • 多行 - 在由多行組成的字串中,允許 ^ 和 $ 比對每一行的開頭和結尾。通常 ^/$ 只會比對整個字串的開頭和結尾。

灰色與非貪婪 (選填)

此為選用部分,說明練習中不需使用更進階的規則運算式技巧。

假設您有包含標記的文字:<b>foo</b>和「依」<i></i>

假設您嘗試將每個標記與模式「(<.*>)」配對-- 先滿足什麼條件?

這個結果有點出乎意料,但「.*」的貪婪顯示部分會使該部分與「<b>foo</b>」整個相符和「so on」<i></i>和大型競爭問題在於 .* 盡可能盡可能增加,而不會在第一個時間點停止 >(也稱為「貪婪」)。

這是在規則運算式中加入「?」的延伸,,例如「.*?」或「.+?」,將狀態變更為非貪婪現在它們會盡快停止。所以模式「(<.*?>)」只會取得「<b>」第一個相符項目,「</b>」與第二個比對相符,而取得每個 <..>配對。樣式通常會使用 .*?後面緊接著一些具體標記 (在此範例中為 >),然後是 .*?指令執行 Pod

「*?」源自 Perl,包含「Perl」擴充功能的規則運算式則稱為「Perl 相容規則運算式 -- pcre」。Python 包含純粹平台支援。許多指令列公用程式等都具有接受特定模式的旗標。

這是一種廣為使用的技術,用於編寫這種「所有字元,但在 X 停止」的概念。使用方括號樣式針對上述指令,您可以編寫模式,但如要取得所有字元,而非 .*,請使用 [^>]*,這會略過所有不是 > 的字元。(開頭的 ^ 會「反轉」方括號集合,因此會比對方括號中沒有的字元)。

替代字詞 (選用)

re.sub(pat, replace, str) 函式會在指定字串中搜尋並取代所有模式例項。替換字串可包含「\1」、「\2」。

下方範例將搜尋並變更所有電子郵件地址,將使用者 (\1) 保留為使用者 (\1),但主機則保留 yo-yo-dyne.com。

  str = 'purple alice@google.com, blah monkey bob@abc.com blah dishwasher'
  ## re.sub(pat, replacement, str) -- returns new string with all replacements,
  ## \1 is group(1), \2 group(2) in the replacement
  print(re.sub(r'([\w\.-]+)@([\w\.-]+)', r'\1@yo-yo-dyne.com', str))
  ## purple alice@yo-yo-dyne.com, blah monkey bob@yo-yo-dyne.com blah dishwasher

運動

如要練習規則運算式,請參閱 Baby Names 練習