Zurückschauen, nach Vorne schauen
|
🪛
|
Im folgenden werden die Begriffe Lookahead- und Lookbehind-"Gruppen" benutzt. |
Häufig will man gegen einen Ausdruck matchen, aber nur einen Teil des Ausdrucks eigentlich als Wert haben.
Dafür kann man Groups benutzen, benannte oder Anonyme.
result = re.findall("<(.*?)>", "Sometext <p><h2>Neuer Absatz</h2><b>Dicker text</b>")
... for i in result:
... print(i)
p
h2
/h2
b
/b
Gematcht wird auf <.?> → also auf Zeichenketten die in <> stehen.
Durch die Gruppe → () → (.?) ist in der Ergebnismenge nur der Inhalt dieser, also der Text in den Tags enthalten.
Look-Behind
Es kommt vor das eine Gruppe nur matchen soll wenn davor ein bestimmter Ausdruck matcht.
Nehmen wir an, wir wollen in folgendem String:
Sometext <p><h2>Neuer Absatz</h2><div><b>Dicker text</b></div>"
Den fett geschriebenen Text erhalten, aber nur wenn das <b>-Tag in einem <div> ist.
result = re.findall("<div><b>(.*?)</b>", "Sometext <p><h2>Neuer Absatz</h2><div><b>Dicker text</b></div>")
for i in result:
print(i)
Dicker text
Das funktioniert soweit ganz gut über normale Gruppen.
Schwierig wird es wenn sich Gruppen überschneiden.
Also wenn das obige Beispiel den Text in <b> und nur wenn davor ein <div> kommt erfassen soll und alle Tags zusätzlich.
result = re.finditer("<.*?>|<div><b>(.*?)</b>", "Sometext <p><h2>Neuer Absatz</h2><div><b>Dicker text</b></div>")
... for i in result:
... print(i.group())
<p>
<h2>
</h2>
<div>
<b>
</b>
</div>
Finditer durchläuft den String und wendet iterativ/fortlaufend den regulären Ausdruck darauf an.
Der reguläre Ausdruck sucht nach Tags <.?> oder der Zeichenfolge <div><b>(.?)</b> .
Wie man im Ergebnis sieht werden nur die Tags gefunden, aber nicht die Zeichenfolge <div><b>(.*?)</b> .
Das liegt daran wie der String abgearbeitet wird:
| 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
< |
/ |
h |
2 |
> |
< |
d |
i |
v |
> |
< |
b |
> |
D |
i |
c |
k |
e |
r |
t |
e |
x |
t |
< |
/ |
b |
> |
< |
/ |
d |
i |
v |
> |
Der Einfachhalthalber beginnen wir mit der Betrachtung kurz vor dem eigentlichen Problem.
-
Zeichen 13-17 → </h2> wird durch den ersten Teil des regulären Ausdrucks <.*?>__ konsumiert
-
danach wird der Pointer auf Position 18, also < gesetzt
-
der reguläre Ausdruck wird ab der neuen Position erneut angewendet
-
die Zeichenfolge 18-22 → <div> wird durch den ersten Teil des regulären Ausdrucks <.*?> konsumiert
-
danach wird der Pointer auf Position 23, also < gesetzt
-
der reguläre Ausdruck wird ab der neuen Position erneut angewendet
-
die Zeichenfolge 23-25 → <b> wird durch den ersten Teil des regulären Ausdrucks <.*?> konsumiert
-
danach wird der Pointer auf Position 26, also D gesetzt
An der letzten Stelle (ab Zeichen 26 und folgende) könnte an sich der zweite Teil des regulären Ausdrucks matchen <div><b>(.*?)</b> .
Genaugenommen würde der Abschnitt auf den die zweite Hälfte des Ausdrucks zutrifft an Zeichen 18 beginnen mit <div>.
Und genau darin besteht das Problem, die erste Hälfte des regulären Ausdrucks hat alles bis Zeichen 26 konsumiert, hier ist der erste Abschnitt der nicht mehr durch die erste Hälfte des regulären Ausdrucks abgedeckt wäre, alles vor Zeichen 26 ist "konsumiert" und nicht mehr "zugänglich".
Der verbleibende String ist "Dicker text</b></div>", zum matchen der zweiten Hälfte des reulären Ausdrucks fehlt __</div><b> und daher matcht die zweite Hälfte des regulären Ausdrucks nie.
An dieser Stelle kommen Lookbehind-Assertions ins Spiel.
Diese werden ähnlich Gruppen definiert:
(?<=Lookbehind-Regex)ActualRegex
-
(?⇐ → fixer Part der ein eine Lookbehind-"Gruppe" einleitet
-
Lookbehind-Regex → an dieser Stelle sollte der reguläre Ausdruck stehen der rückwärts gesucht werden soll. Dieser Ausdruck wird nicht Teil des Ergebnisses
-
ActualRegex → An dieser Stelle sollte der reguläre Ausdruck stehen, dieser geht in das Ergebnis ein also zum Beispiel so:
(?<=<div><b>).*?
Hier würde wenn .? geguckt ob davor <div><b> steht.
Ist das der Fall wird alles nach <b> durch .? erfasst und als Ergebnis zurückgegeben.
Obiges Beispiel würde dann so aussehen:
result = re.finditer("<.*?>|(?<=<div><b>)(.*?)</b>", "Sometext <p><h2>Neuer Absatz</h2><div><b>Dicker text</b></div>")
... for i in result:
... print(i.group())
<p>
<h2>
</h2>
<div>
<b>
Dicker text</b>
</div>
Wie man sieht wird die zweite Hälfte des regulären Ausdrucks aktiv und resultiert in Dicker text</b>.
Gleichzeitig werden aber <div> und <b>__ weiterhin von der ersten Hälfte des regulären Ausdrucks erfasst. Die Lookbehind-Gruppe ändert daran nichts.
|
📝
|
Zusammengefasst: Man kann mit Look-Behind-Gruppen also effektiv "rückwärts" gucken, in Teile des Strings die bereits verarbeitet wurden. |
|
🪛
|
Look-Behind-Gruppen können keine Quantifier benutzen. |
Lookahead
Die "Lösung" aus dem vorherigen Beispiel hat ein Problem, die Ergenisliste sieht so aus:
<p>
<h2>
</h2>
<div>
<b>
Dicker text</b>
</div>
Das Ziel war aber sowohl alle Tags, als auch den Text zwischen <div><b> und dem darauffolgenden <b> zu erhalten, also den Teil:
<div><b>Dicker text</b>
Die Prüfung das hinter dem Text ein </b> folgt soll erhalten bleiben, aber nicht wie jetzt Teil des Ergebnisses sein, sondern das Tag soll als eigenständiges Ergebnis auftauchen (wie die anderen Tags).
Ähnlich wie Lookbehind-"Groups", kann man auch Lookahead-"Groups" nutzen.
ActualRegex(?=Lookahead-Regex)
-
(?= → Zeichenfolge für eine Lookahead-"Gruppe"
-
ActualRegex → der reguläre Ausdruck der wenn er matcht und der Lookahead ebenfalls matcht als Ergebnis zurückgegeben wird
-
Lookahead-Regex → der reguläre Ausdruck der "vorwärts" geschaut werden soll. Dieser wird nicht Teil des Ergebnisses
Also zum Beispiel:
(.*?)(?=</b>)
Passt man das Beispiel entsprechend an:
result = re.finditer("<.*?>|(?<=<div><b>)(.*?)(?=</b>)", "Sometext <p><h2>Neuer Absatz</h2><div><b>Dicker text</b></div>")
... for i in result:
... print(i.group())
<p>
<h2>
</h2>
<div>
<b>
Dicker text
</b>
</div>
Bekommt man wie gewünscht alle Tags separat und den Text zwischen <div><b> und </b>
Schaut man sich den relevanten Teil des Strings an:
| 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
D |
i |
c |
k |
e |
r |
t |
e |
x |
t |
< |
/ |
b |
> |
< |
/ |
d |
i |
v |
> |
Dann:
-
Position 26 D wird durch den ersten Teil des regulären Ausdrucks nicht erfasst <.?>, aber durch den zweiten Teil (?⇐<div><b>)(.?)(?=</b>) - genaugenommen dieser Teil (.?)
-
es wird anschließend der Lookbehind-Teil (?⇐<div><b>) geprüft, also das was vor Position 26 steht - das matcht
-
da (.*?) non-greedy ist wird der Lookahead-Teil (?=</b>) anschließend ausgeführt und gegen die folgenden Zeichen (27-30) geprüft - dieser matcht nicht
-
der Pointer wird auf Position 27 verschoben
-
es wird geprüft ob der "normale" Regex-Teil der zweiten Hälfte des regulären Ausdrucks immer noch wahr ist → (.*?) gegen i → das matcht
-
es wird wieder der Lookahead-Teil wie am Beginn des Paragrafs ausgeführt und da der nicht match der Rest des Blocks wiederholt
-
der Block wird abgearbeitet bis enweder der eigentliche Regex-Teil nicht mehr zutrifft (was hier nicht passiert)
-
oder der Lookahead-Teil matcht - Position 38 - 41 aka. </b>
-
damit ist die zweite Hälfte des regulären Ausdrucks erfüllt und das Ergebnis - nur der Teil der durch den "normalen" regulären Ausdruck (.*?) abgedeckt wird - Dicker text wird als Ergebnis gespeichert
-
der Pointer steht auf dem Zeichen direkt hinter dem "normalen" regulären Ausdruck, also Position 38 aka. < - der Lookahead-Ausdruck verschiebt den Pointer nicht
-
Position 38 - 41 matcht die erste Hälfte des regulären Ausdrucks <.*?> → </b> und wird gespeichert
-
der Pointer wird dann auf Position 42 gesetzt
-
der Vorgang wiederholt sich
|
📝
|
Lookahead-"Gruppen" ändern nicht die Position des "Pointers", der Pointer bleibt unmittelbar hinter dem dem "normalen" regular Expression stehen, auf den die Lookahead-"Gruppe folgt (so diese matcht). |