2.36. 群組與錨點

樣式所能做的不只是判斷「這個字串符合」而已 -- 它還能把比對到的片段拆解開來,並以名稱將每一段交給應用程式。樣式中某一部分外加的括號會使它成為擷取群組;接著比對物件便會將每個群組以個別的子字串形式公開出來。

2.36.1. 擷取群組

將樣式的任何一部分以 (...) 包起來即可擷取它所比對到的內容:

>>> import re
>>> m = re.search(r'temp (\d+) at (\d+)s', 'temp 42 at 137s ok')
>>> m.group(0)
'temp 42 at 137s'
>>> m.group(1)
'42'
>>> m.group(2)
'137'
  • 群組 0 永遠是整個比對結果。

  • 群組 1、2、... 是擷取到的子字串,依其開頭括號由左至右編號。

  • 以超過最後一個群組的索引呼叫 match.group() 會引發 IndexError

一種常見的樣式是「比對一個已知的結構,並將其中變動的部分擷取為整數」:

def parse_temp(line):
    m = re.search(r'temp (\d+) at (\d+)s', line)
    if not m:
        return None
    return int(m.group(1)), int(m.group(2))

2.36.2. 非擷取群組

括號也會將子運算式分組,使量詞能套用到整個群組。這正是 r'(ab)+' 中分組的唯一目的 -- 「一個或多個 ab」。ab 顯示為群組 1 只是個副作用。

若要分組但不擷取,請使用 (?:...):

>>> re.search(r'(?:ab)+', 'xababy').group(0)
'abab'

當樣式為了結構而使用分組、但並不在意把每個片段取出時,非擷取群組可讓群組編號保持整潔。

2.36.3. 錨點

錨點並不比對字元 -- 它們比對的是位置

  • ^ -- 字串的開頭。

  • $ -- 字串的結尾。

錨點正是使 re.match()re.search() 行為不同的原因。re.match(p, s) 等同於 re.search('^' + p, s):它強制樣式從位置 0 開始。接著在樣式結尾加上 $,便會使樣式比對整個字串而非其他內容:

>>> re.search(r'^\d+$', '12345')
<match num=1>
>>> re.search(r'^\d+$', '12345 ok') is None
True

在 MicroPython 的 re 中,^$ 永遠表示傳給 re.search()整個字串的開頭與結尾。沒有 re.MULTILINE 旗標可讓它們在每個內嵌換行處進行比對,而 $ 也不會比對結尾 \n 之前的位置 -- 它必須是輸入的絕對結尾。若要取得逐行的行為,請先以換行符分割輸入,再對每一行執行樣式。

2.36.4. 字元集

方括號定義一組明確的字元集合。比對會從該集合中剛好消耗一個字元。

  • [abc] -- abc 其中之一。

  • [a-z] -- a-z 範圍內(含端點)的一個字元。

  • [a-zA-Z0-9] -- 字母或數字。三個範圍結合在一起。

  • [^abc] -- 不是 abc 之一。^ 只有在它是方括號內的第一個字元時才表示否定。

範例:

>>> re.search(r'[A-F0-9]{6}', 'colour #1a2b3c rest').group(0)
'1A2B3C'
>>> re.search(r'[A-F0-9]{6}', 'colour #1a2b3c rest') is None
True

由於字面文字為小寫,第一次呼叫實際上會回傳 None。MicroPython 的 re 沒有 re.IGNORECASE 旗標 -- 若要不分大小寫地比對,請將兩種大小寫都寫進集合中:

>>> re.search(r'[A-Fa-f0-9]{6}', 'colour #1a2b3c rest').group(0)
'1a2b3c'

字元類別的捷徑(\d\s\w 及其否定形式)也可用於 [...] 之內:[\w-] 表示「文字字元或一個字面破折號」。

2.36.5. 貪婪量詞與惰性量詞

量詞 *+?{m,n} 預設是貪婪的 -- 它們會在樣式其餘部分仍能成立的前提下比對盡可能多的字元。通常這正是想要的結果;但有時並非如此:

>>> re.search(r'<(.+)>', 'a <b> <c> d').group(1)
'b> <c'

貪婪的 .+ 會一路抓取到最後一個 >。附加 ? 會使量詞變為惰性的 -- 它會比對盡可能少的字元:

>>> re.search(r'<(.+?)>', 'a <b> <c> d').group(1)
'b'

惰性形式會停在第一個 >。從字串中擷取成對的分隔符時,惰性量詞會不斷派上用場。

2.36.6. 替換中的反向參照

re.sub() 可在替換字串中透過 \1\2、... 反向參照擷取到的群組。替換會使用擷取到的片段重寫每一次比對:

>>> re.sub(r'(\d+)\.(\d+)', r'\2.\1', 'swap 12.34 and 5.6')
'swap 34.12 and 6.5'

每次比對都會擷取兩個數字,而替換會把它們互換。\g<1> 是相同效果的另一種語法 -- 當替換字串中的下一個字元是數字時很有用(r'\g<1>0' 是在群組 1 後附加一個字面零,而非讀成「群組 10」)。

2.36.7. 哪些功能無法使用

在此提醒一下 MicroPython 的 re 支援哪些功能,以防來自 CPython 的某個樣式跑到這裡而讓你感到意外:

  • 前瞻 (?=...) 與後顧 (?<=...) -- 未實作。

  • 具名群組 (?P<name>...) 與具名反向參照 (?P=name) -- 未實作。

  • re.IGNORECASEre.MULTILINEre.DOTALL 這類旗標常數 -- 不予理會。請自行建立不分大小寫的集合,或自行預先分割輸入。

  • match.groups()match.span()match.start()match.end() 等方法被限制在某個 ROM 等級,而目前出貨的 OpenMV 開發板都未啟用該等級。依賴它們的程式碼將無法在相機上執行。

有了樣式、群組與錨點,相機上的正規表示式工具集小到可以一坐之間學會,又豐富到足以勝任除上下文相關剖析以外的所有工作。