2.35. 樣式基礎

正規表示式是一種用來以形式(而非確切內容)描述字串的小型語言。當一個字串符合與否取決於它是否遵循某種你能描述卻無法一一列舉的樣式時,就該使用它 -- 「一連串數字後接一個單位」、「以 ERROR 開頭並以數字結尾的一行」、「這些副檔名中任一個、以任意順序、帶有選用的 v 前綴」。

只有在單純的字串方法行不通時,才動用 re

上述每一種方法都比等效的正規表示式更快、更易讀,也更不容易出錯。當字串的形式重要而確切子字串無關緊要時,才使用正規表示式。

2.35.1. 你會用到的四樣東西

MicroPython 的 re 模組公開了四樣東西:

  • re.compile() -- 將樣式字串轉成一個可重複使用的已編譯樣式物件。

  • re.match() -- 在字串的開頭嘗試比對樣式。樣式錨定在位置 0。

  • re.search() -- 在字串的任何位置嘗試比對樣式。回傳第一個比對結果。

  • re.sub() -- 找出每一個比對結果並加以替換。

相對於 CPython 值得注意的缺漏:沒有 re.findall、沒有 re.finditer、模組層級沒有 re.split(改由已編譯樣式提供 split 方法)、沒有 re.fullmatch、也沒有像 re.IGNORECASE 這類旗標常數。在 CPython 上你會用到這些功能的地方,請以迴圈搭配 re.search() 自行建構等效作法。

2.35.2. 第一個樣式

樣式 r'\d+' 會比對一個或多個數字:

>>> import re
>>> m = re.search(r'\d+', 'sensor reading 42 ok')
>>> m.group(0)
'42'

有幾點值得注意:

  • 樣式是以原始字串r'...')撰寫的,如此 \d 中的反斜線才會傳達給 re,而不會被當成 Python 字串跳脫字元處理。正規表示式樣式請一律使用原始字串。

  • re.search() 成功時回傳一個比對物件,失敗時回傳 None。呼叫 match.group() 之前請務必先檢查。

  • m.group(0) 是樣式所比對到的完整文字。群組 1、2、... 會在樣式包含擷取括號後才出現。

同樣的樣式搭配 re.match() 會回傳 None,因為該字串並非以數字開頭:

>>> re.match(r'\d+', 'sensor reading 42 ok') is None
True
>>> re.match(r'\d+', '42 readings')
<match num=1>

2.35.3. 樣式的組成片段

大多數實用的樣式都是由一小組片段組成的。在 MicroPython 中可用的有:

字面字元 -- 任何非特殊字元都比對它自己。hello 比對 hello

特殊字元 -- . ^ $ * + ? { } [ ] \ | ( ) 都具有以下說明的意義。若要按字面比對其中之一,請以反斜線跳脫它:\. 比對一個字面句點。

字元類別 -- 常見字元集合的簡寫:

  • \d -- 任何數字 0-9

  • \D -- 任何非數字

  • \s -- 任何空白字元(空格、定位字元、換行)

  • \S -- 任何非空白字元

  • \w -- 任何「文字」字元:字母、數字、底線

  • \W -- 任何非文字字元

  • . -- 除換行外的任何字元

量詞 -- 前一個片段必須比對的次數:

  • * -- 零個或多個(貪婪)

  • + -- 一個或多個(貪婪)

  • ? -- 零個或一個

  • {n} -- 剛好 n

  • {m,n} -- 介於 mn 之間(含端點)

組合運用:\d{3}-\d{4} 比對三個數字、一個破折號、四個數字。\s+ 比對一個或多個空白字元。hello.*world 比對 hello、任意內容(包括空無一物),然後是 world

備註

貪婪意味著量詞會在仍能讓樣式其餘部分成立的前提下,盡可能多地消耗輸入。對 hello x world y world 而言,hello.*world 中的 .* 會比對到仍能在結尾留下一個 world 的最長字段 -- 它擷取的是 x world y,而非較短的 x+{m,n} 範圍形式也是如此:引擎會取它能取到的最長比對,只有在樣式其餘部分失敗時才退讓。

2.35.4. 替換

re.sub() 會找出每一個比對結果並以一個字串加以替換。替換內容可透過 \1\2、... 參照擷取到的群組(稍後與其餘群組語法一併介紹)。在沒有群組的情況下,re.sub 就是針對正規表示式進行的直接尋找與替換:

>>> re.sub(r'\s+', ' ', 'too    many   spaces')
'too many spaces'

>>> re.sub(r'\d+', 'N', 'log 12, log 345, log 6')
'log N, log N, log N'

第三個引數是要處理的字串;結果是一個將每一個比對結果都替換掉的新字串。

2.35.5. 分割 -- 僅適用於已編譯的樣式

模組層級並沒有 re.split。若要依正規表示式分割,請先編譯樣式,再呼叫其 split 方法:

>>> sep = re.compile(r'\s*,\s*')
>>> sep.split('a , b,c ,  d')
['a', 'b', 'c', 'd']

選用的第二個引數可限制分割的次數上限:

>>> sep.split('a, b, c, d', 2)
['a', 'b', 'c, d']

2.35.6. 為重複使用而編譯

如果同一個樣式會執行許多次 -- 在迴圈內或在高頻函式中 -- 請編譯它一次並重複使用該已編譯物件:

digit_run = re.compile(r'\d+')

def first_number(line):
    m = digit_run.search(line)
    return int(m.group(0)) if m else None

對已編譯物件呼叫 pattern.match()pattern.search(),與模組層級的函式效果相同,但省去了每次呼叫時重新編譯的開銷。

2.35.7. 比對不到任何內容的樣式

特別有三種樣式會讓開發者栽跟頭:

  • .* 會比對到空字串。re.search(r'.*', s).group(0) 對任何輸入都會回傳 ''

  • 含有未跳脫特殊字元的樣式是語法錯誤。re.compile(r'cost: $5') 會引發 ValueError,因為 $ 表示「字串結尾」。請改用 r'cost: \$5'

  • 句點 . 不會比對換行。若要跨換行比對,請以 [\s\S] 明確處理換行,或每次只餵入一行。

有了這些片段,樣式幾乎能比對任何固定形式的文字片段。而要從比對結果中把結構化的資料取回,則需要擷取群組。