Python 規則運算式

規則運算式是比對文字模式的強大語言。本頁提供規則運算式的基本簡介,不僅足以滿足我們的 Python 練習,還會說明規則運算式在 Python 中的運作方式。Python「re」模組支援規則運算式。

在 Python 中,規則運算式搜尋的寫法一般如下:

match = re.search(pat, str)

re.search() 方法採用規則運算式模式和字串,可在字串中搜尋該模式。如果搜尋成功,search() 會傳回比對物件,否則會傳回 None。因此,搜尋通常會緊接著加上 if 陳述式,以測試搜尋是否成功。如以下範例所示,搜尋的是「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 會測試相符項目 -- 如果 true 搜尋成功,且 match.group() 是相符的文字 (例如「word:cat」)。否則,如果比對為 false (無意義更明確),即代表搜尋失敗,且沒有相符的文字。

位於模式字串開頭的「r」會指定 Python「原始」字串,這個字串會在未經變更的情況下傳遞反斜線,對規則運算式非常有用 (Java 需要此功能!)。建議您一律以 'r' 表示模式字串,就像習慣一樣。

基本模式

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

  • a、X、9、< -- 一般字元就是完全相符的內容。這個中繼字元因為具有特殊意義,所以不相符: 。^ $ * + ? { [ ] \ | ( ) (詳情如下)
  • 。(句點) -- 比對任何字元,但換行字元「\n」除外
  • \w -- (小寫 w) 會比對「字詞」字元:字母、數字或底線 [a-zA-Z0-9_]。請注意,雖然「字詞」是詞彙表,但只會比對單一字詞字元,而不是整個字詞。\W (大寫 W) 會比對任何非字詞字元。
  • \b -- 字詞和非字詞之間的邊界
  • \s -- (小寫 s) 會比對單一空白字元 -- 空格、換行、歸位、定位鍵、表單 [ \n\r\t\f]。\S (大寫 S) 會比對任何非空白字元。
  • \t、\n、\r -- 分頁標籤、換行、傳回
  • \d -- 十進位數字 [0-9] (部分舊版規則運算式公用程式不支援 \d,但都支援 \w 和 \s)
  • ^ = start, $ = end -- 與字串的開頭或結尾
  • \ -- 禁止字元的「專長」。例如,使用「\.」比對句號或「\\」來比對斜線。如果您不確定字元是否具有特殊意義,例如「@」,可以試著在前面加上斜線 (\@)。如果字元不是有效的逸出序列 (例如 \c),Python 程式就會因錯誤而暫停。

基本範例

笑話:你不知道有三眼的豬怎麼叫?piiig!

規則運算式的基本搜尋規則為:

  • 搜尋作業會從開始到結束的順序,在找到的第一個相符項目時停止
  • 所有模式都必須相符,但並非所有字串
  • 如果 match = re.search(pat, str) 成功,就不會比對 None,在特定 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 紫色猴子」中尋找電子郵件地址。我們會使用這個範例展示更多規則運算式功能。以下是使用 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) 是與左括號對應的比對文字,而 match.group(2) 則是與左括號 2 對應的文字。純 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 和群組

括號 ( ) 群組機制可與 findall() 合併使用。如果模式包含 2 個以上的括號群組,則 findall() 會傳回 *元組* 的清單,而不會傳回字串清單。每個元組代表一個模式比對,元組內則是 group(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() 的結果。如果模式沒有相符結果,請試著縮減模式,將部分部分移除,導致比對過多。沒有比對項目時,因為沒有具體問題可供查看,所以你無法進行任何進度。比對到太多字詞後,您就可以逐步將關鍵字加緊,只達成您想要的內容。

選項

重新函式會接受修改模式比對行為的選項。options 旗標會以額外引數形式新增至 search() 或 findall() 等,例如 re.search(pat, str, re.IGNORECASE)。

  • IGNORECASE -- 會忽略大小寫差異,因此「a」會同時符合「a」和「A」。
  • DOTALL -- 允許句點 (.) 來比對換行符號,通常與新行的內容相符。這可能會使你不斷進步,認為「.*」與所有內容相符,但預設不會超過線條的結尾。請注意,\s (空白字元) 包含換行符號,因此,如果您想比對出一串可能含換行字元的空白字元,只要使用 \s* 即可
  • MULTILINE -- 在由多行組成的字串內,允許 ^ 和 $ 比對每一行的開頭和結尾。^/$ 通常只會與整個字串的開頭和結尾相符。

貪婪與非貪婪 (選填)

這是選擇性的區段,顯示運動不需要的進階規則運算式技巧。

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

假設您嘗試將每個標記與「(<.*>)」模式配對:首先是比對的第一個項目。

結果雖有些出乎意料,但「.*」的貪婪見法會將其與整個「<b>foo</b>」和「<i></i>」相符,視為一大相符。問題是 .* 盡可能解決這個問題,而不是在開頭 > 停止 (又稱「貪婪」)。

為規則運算式新增了「?」符號例如「.*?」或「.+?」,請將開頭變更為非政府機關。但現在他們會盡快停止。因此,「(<.*?>)」模式只會取得「<b>」做為第一個相符項目,而「</b>」為第二個相符項目,依此類推,則分別為每個 <..> 配對進行轉換。樣式通常通常會使用「.*?」,後面緊接著一些具體標記 (在本例中為 >),強迫執行 .*? 的執行。

源自 Perl 的擴充功能,而包含 Perl 副檔名的規則運算式稱為「Perl 相容規則運算式 -- pcre」。Python 支援 pcre 支援。許多指令列公用程式等工具都有接受電腦模式的旗標。

這是一種普遍的舊技術,用於編碼這個概念「所有這些字元,但停在 X 點」,還是使用方括號樣式。就上述範例而言,您可以寫出模式,但不用用 .* 來取得所有字元,請使用 [^>]* 略過所有非 > 的字元 (開頭的 ^「反轉」就是方括號集,因此符合括號中的任何字元)。

替代 (選用)

re.sub(pat,replace, str) 函式會搜尋指定字串中的所有模式例項並加以取代。替換字串可包含「\1」、「\2」,這些參照是指原始相符文字中的「\1」、「\2」參照群組(1)、group(2) 的文字,以此類推。

以下的例子將會搜尋所有電子郵件地址,並將其變更為保留使用者 (\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 練習