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]--a、b、c其中之一。[a-z]--a-z範圍內(含端點)的一個字元。[a-zA-Z0-9]-- 字母或數字。三個範圍結合在一起。[^abc]-- 不是a、b、c之一。^只有在它是方括號內的第一個字元時才表示否定。
範例:
>>> 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.IGNORECASE、re.MULTILINE、re.DOTALL這類旗標常數 -- 不予理會。請自行建立不分大小寫的集合,或自行預先分割輸入。match.groups()、match.span()、match.start()與match.end()等方法被限制在某個 ROM 等級,而目前出貨的 OpenMV 開發板都未啟用該等級。依賴它們的程式碼將無法在相機上執行。
有了樣式、群組與錨點,相機上的正規表示式工具集小到可以一坐之間學會,又豐富到足以勝任除上下文相關剖析以外的所有工作。