2.36. Gruppen und Anker

Ein Muster kann mehr aussagen als nur „diese Zeichenkette passt“ – es kann die übereinstimmenden Teile herauslösen und jeden einzelnen davon namentlich an die Anwendung übergeben. Klammern um einen Teil eines Musters machen daraus eine Capturing Group; das Match-Objekt stellt dann jede Gruppe als eigene Teilzeichenkette bereit.

2.36.1. Capturing Groups

Schließe einen beliebigen Teil eines Musters in (...) ein, um zu erfassen, worauf er passt:

>>> 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'
  • Gruppe 0 ist immer das gesamte Match.

  • Die Gruppen 1, 2, … sind die erfassten Teilzeichenketten, von links nach rechts nach ihrer öffnenden Klammer nummeriert.

  • Ein Aufruf von match.group() mit einem Index hinter der letzten Gruppe löst einen IndexError aus.

Ein häufiges Muster lautet „eine bekannte Struktur matchen und die variablen Teile als Ganzzahlen erfassen“:

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. Non-Capturing Groups

Klammern gruppieren außerdem einen Teilausdruck, sodass ein Quantor auf die gesamte Gruppe angewendet werden kann. Das ist in r'(ab)+' der einzige Zweck der Gruppierung – „ein oder mehrere ab“. Dass ab als Gruppe 1 erscheint, ist ein Nebeneffekt.

Um zu gruppieren, ohne zu erfassen, verwende (?:...):

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

Non-Capturing Groups halten die Gruppennummern übersichtlich, wenn ein Muster die Gruppierung zur Strukturierung nutzt, sich aber nicht dafür interessiert, die einzelnen Teile herauszuziehen.

2.36.3. Anker

Anker matchen kein Zeichen – sie matchen eine Position.

  • ^ – Anfang der Zeichenkette.

  • $ – Ende der Zeichenkette.

Anker sind der Grund, warum sich re.match() und re.search() unterschiedlich verhalten. re.match(p, s) entspricht re.search('^' + p, s): Es zwingt das Muster, an Position 0 zu beginnen. Wird zusätzlich $ an das Ende eines Musters gesetzt, muss das Muster die gesamte Zeichenkette und nichts anderes matchen:

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

^ und $ bedeuten in MicroPython re stets den Anfang und das Ende der gesamten Zeichenkette, die an re.search() übergeben wird. Es gibt kein re.MULTILINE-Flag, das sie an jedem eingebetteten Zeilenumbruch matchen lässt, und $ matcht auch nicht die Position vor einem abschließenden \n – es muss das absolute Ende der Eingabe sein. Um ein zeilenweises Verhalten zu erhalten, teile die Eingabe zuerst an den Zeilenumbrüchen auf und wende das Muster auf jede Zeile an.

2.36.4. Zeichenmengen

Eckige Klammern definieren eine explizite Menge von Zeichen. Das Match verbraucht genau ein Zeichen aus der Menge.

  • [abc] – eines von a, b, c.

  • [a-z] – ein Zeichen aus dem Bereich a-z (einschließlich).

  • [a-zA-Z0-9] – Buchstaben oder Ziffern. Drei kombinierte Bereiche.

  • [^abc]kein a, b oder c. Das ^ negiert nur dann, wenn es das erste Zeichen innerhalb der Klammern ist.

Beispiele:

>>> 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

Der erste Aufruf liefert in der Praxis None, weil der literale Text in Kleinbuchstaben vorliegt. MicroPython re besitzt kein re.IGNORECASE-Flag – um ohne Berücksichtigung der Groß-/Kleinschreibung zu matchen, schreibe beide Schreibweisen in die Menge:

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

Die Klassenkürzel (\d, \s, \w und ihre negierten Formen) können auch innerhalb von [...] verwendet werden: [\w-] bedeutet „Wortzeichen oder ein literaler Bindestrich“.

2.36.5. Gierige und genügsame Quantoren

Die Quantoren *, +, ? und {m,n} sind standardmäßig gierig – sie matchen so viele Zeichen, wie der Rest des Musters noch zulässt. Oft ist genau das gewünscht; manchmal jedoch nicht:

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

Das gierige .+ hat alles bis zum letzten > aufgegriffen. Durch Anhängen von ? wird der Quantor genügsam – er matcht so wenig wie möglich:

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

Die genügsame Form stoppt beim ersten >. Genügsame Quantoren tauchen ständig auf, wenn man ausgeglichene Begrenzer aus einer Zeichenkette extrahiert.

2.36.6. Rückwärtsverweise bei der Ersetzung

re.sub() kann in der Ersetzungszeichenkette über \1, \2, … auf erfasste Gruppen zurückverweisen. Die Ersetzung schreibt jedes Match anhand der erfassten Teile um:

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

Jedes Match erfasst zwei Zahlen, und die Ersetzung vertauscht sie. \g<1> ist eine alternative Syntax für dasselbe – nützlich, wenn das nächste Zeichen in der Ersetzung eine Ziffer ist (r'\g<1>0', um eine literale Null an Gruppe 1 anzuhängen, statt „Gruppe 10“ zu lesen).

2.36.7. Was nicht verfügbar ist

Eine Erinnerung daran, was MicroPython re nicht unterstützt, falls ein Muster aus CPython hier landet und dich überrascht:

  • Lookahead (?=...) und Lookbehind (?<=...) – nicht implementiert.

  • Benannte Gruppen (?P<name>...) und benannte Rückwärtsverweise (?P=name) – nicht implementiert.

  • Flag-Konstanten wie re.IGNORECASE, re.MULTILINE, re.DOTALL – werden nicht berücksichtigt. Erstelle die Menge ohne Beachtung der Groß-/Kleinschreibung selbst oder teile die Eingabe vorab auf.

  • Die Methoden match.groups(), match.span(), match.start() und match.end() sind an eine ROM-Stufe gekoppelt, die kein ausgeliefertes OpenMV-Board aktiviert. Code, der auf sie angewiesen ist, läuft nicht auf der Cam.

Mit Mustern, Gruppen und Ankern ist das Regex-Werkzeug auf der Cam klein genug, um es in einer Sitzung zu erlernen, und reichhaltig genug, um alles außer kontextsensitivem Parsing zu erledigen.