Große Zahlen in Dreiergruppen aufteilen: Lookaround
(Auszug aus "Reguläre Ausdrücke" von Jeffrey E. F. Friedl)
Große Zahlen werden zur besseren Lesbarkeit oft in Dreiergruppen aufgeteilt. (Anmerkung: Bei Nichtproportionalschriften werden die Dreiergruppen in den USA durch Kommas, in Deutschland durch Punkte und in der Schweiz durch Hochkommas abgetrennt; im Schriftsatz durch ein Spatium: 82 499 213. (Anm. d. Ü.)) Der Befehl
print "Deutschland hat $pop Einwohner\n";
gibt vielleicht so etwas wie »Deutschland hat 82499213 Einwohner« aus, aber die meisten Leute würden wohl automatisch »82.499.213« schreiben. Wie lässt sich das mit einem regulären Ausdruck erreichen?
Wenn wir die Dreiergruppen bilden, zählen wir die Ziffern von der rechten Seite her ab. Es wäre nett, wenn man das mit regulären Ausdrücken direkt nachbilden könnte, aber reguläre Ausdrücke arbeiten nun mal von links nach rechts. Wenn wir das Verfahren etwas umformulieren - »Punkte so zwischen Ziffern setzen, dass rechts davon nur Dreiergruppen von Ziffern vorkommen und mindestens eine Ziffer links davon verbleibt« -, dann können wir das mit dem relativ neuen Feature Lookaround ziemlich einfach erreichen.
Lookaround-Konstrukte sind mit den Metazeichen für Wortgrenzen (˹\b˼) oder den Ankern ˹^˼ und ˹$˼ insofern verwandt, als sie nicht auf eigentlichen Text passen, sondern auf eine Position, eine bestimmte Stelle zwischen den Zeichen im Suchstring. Aber die Lookaround-Konstrukte sind wesentlich allgemeiner als die Spezialfälle Wortgrenze und Zeilenanker.
Eine Form des Lookarounds ist das Lookahead: Der Suchstring wird in Vorwärtsrichtung (nach rechts) nach einem Unterausdruck abgesucht, und der entsprechende Teil der Regex ist erfolgreich, wenn der Unterausdruck passt. Ein positives Lookahead oder eine positive vorausschauende Zusicherung wird mit der besonderen Klammerung ˹(?=...)˼ angegeben, also beispielsweise mit ˹(?=\d)˼ - dieser Unterausdruck ist an Stellen im Suchstring erfolgreich, bei denen rechts davon eine Ziffer steht. Das Pendant Lookbehind, die zurückblickende Zusicherung, untersucht den String in der Rückwärtsrichtung (nach links). Für das Lookbehind wird die Klammerung ˹(?<=...)˼ verwendet, also beispielsweise ˹(?<=\d)˼, die an Positionen passt, bei denen links eine Ziffer steht (anders gesagt: bei Stellen nach einer Ziffer).
Lookaround »verbraucht« keinen Text
Ein wichtiger Punkt beim Lookaround und ähnlichen Konstrukten ist, dass sie keine Zeichen aus dem Suchtext »konsumieren«, obwohl sie den String nach vorn oder nach hinten durchsuchen. Das ist zunächst sicher verwirrend, deshalb veranschauliche ich das hier an einem Beispiel. Die Regex ˹Jeffrey˼ findet im folgenden Text den unterstrichenen Teil:
... by Jeffrey Friedl.
Der gleiche Unterausdruck als vorausschauende Zusicherung (Lookahead) passt nur gerade an der markierten Position:
... by ▵Jeffrey Friedl.
Beim Lookahead wird der Suchtext zwar auf den Unterausdruck überprüft, aber es wird nur die Position gefunden, an der der Unterausdruck passt, nicht der eigentliche Text, auf den der Unterausdruck gepasst hat. Wenn wir das mit einem normalen Unterausdruckwie etwa ˹Jeff˼ kombinieren, der wirklichen Text erkennt, können wir genauer suchen. Der kombinierte reguläre Ausdruck ˹(?=Jeffrey)Jeff˼ erkennt den String »Jeff«, aber nur dann, wenn er als Teil von »Jeffrey« auftritt. In der nächsten Abbildung ist das genauer dargestellt.
Abbildung: Ablauf der Mustersuche mit ˹(?=Jeffrey)Jeff˼.
Der Ausdruck erkennt die folgenden Zeichen:
... by Jeffrey Friedl.
Das würde auch nur ˹Jeff˼ leisten, aber der zusammengesetzte Ausdruck passt nicht auf:
... by Thomas Jefferson
Der Teil ˹Jeff˼ würde auch auf diese Zeile passen, aber es gibt keine Position im Suchstring, bei der auch ˹(?=Jeffrey)˼ passen würde - also gibt es keinen Treffer für den gesamten Ausdruck.
Vielleicht werden die Möglichkeiten dieses Konstrukts nicht sofort klar. Konzentrieren Sie sich darauf, was eine »vorausschauende Zusicherung« ist - keine Angst, Sie werden bald realistischere Anwendungen sehen.
Vielleicht ist es erhellend zu sehen, dass ˹(?=Jeffrey)Jeff˼ ganz genau das Gleiche bewirkt wie ˹Jeff(?=rey)˼. Beide passen auf die vier Zeichen »Jeff«, aber nur, wenn sie Teil von »Jeffrey« sind.
Die Reihenfolge der Teile des regulären Ausdrucks ist dabei wichtig. ˹Jeff(?=Jeffrey)˼ passt in keinem der oben behandelten Fälle, es passt auf »Jeff«, aber nur, wenn darauf »Jeffrey« folgt.
Die syntaktische Notation für den Lookaround ist wieder recht unschön, wie die für die nicht-einfangenden Klammern »(?:...)« von Lösung 5. Das Besondere daran ist die »öffnende Klammer«, die aus mehreren Zeichen besteht. Es gibt eine Reihe von diesen »öffnenden Klammern«, die alle mit den zwei Zeichen »(?« beginnen. Das dritte Zeichen (nach dem Fragezeichen) definiert die Funktion des ganzen Klammerausdrucks. Sie kennen bereits die nicht-einfangenden Klammern »(?:...)«, die vorausschauende Zusicherung (Lookahead) »(?=...)« und die zurückblickende Zusicherung (Lookbehind) »(?<=...)«; Sie werden noch weitere kennenlernen.
Weitere Beispiele zum Lookahead
Ja, wir werden gelegentlich zu den Dreiergruppen bei großen Zahlen kommen - aber zunächst brauchen Sie noch etwas mehr Übung mit den Lookaround-Konstrukten. Die Aufgabe besteht darin, die deutsche Genitivform »Jeffs« durch den englischen Genitiv »Jeff's« zu ersetzen. Das geht auch ohne Lookaround ganz einfach mit s/Jeffs/Jeff's/g. (Sie erinnern sich: Das /g steht für »globales Ersetzen«.) Wir können das mit den Wortgrenzen verbessern: s/\bJeffs\b/Jeff's/g.
Wir könnten noch raffinierter vorgehen und Klammern benutzen, s/\b(Jeff)(s)\b/$1'$2/g, aber das ist für diese einfache Aufgabe doch etwas übertrieben. Wir bleiben also vorläufig bei s/\bJeffs\b/Jeff's/g. Vergleichen Sie das nun mit:
s/\bJeff(?=s\b)/Jeff'/g
Der einzige Unterschied besteht darin, dass nun das ˹s\b˼ in einer Lookahead-Klammer steht.
In der folgenden Abbildung wird gezeigt, wie diese Regex einen Treffer findet. Parallel zur Regex wurde das ›s‹ aus dem Ersatztext entfernt.
Abbildung: Ablauf der Mustersuche bei ˹\bJeff(?=s\b)˼.
Nachdem ˹Jeff˼ erfolgreich gefunden wurde, wird der Lookahead ausprobiert. Dieser ist nur dann erfolgreich, wenn der Unterausdruck ausgehend von der aktuellen Position passt (d. h. wenn auf ›Jeff‹ ein ›s‹ und eine Wortgrenze folgt). Weil aber ˹s\b˼ im Lookahead-Unterausdruck vorkommt, ist das damit erkannte ›s‹ nicht Teil des gesamten Treffers. Sie erinnern sich: ˹Jeff˼ erkennt Text und konsumiert diesen, der Lookahead-Teil passt nur auf eine Position im String. Der einzige Vorteil des Lookahead-Konstrukts besteht in dieser Situation darin, dass in manchen Fällen ein Treffer der gesamten Regex verhindert wird, in anderen nicht. Wir können damit die ganze Regex ˹Jeffs˼ anwenden, aber der Treffer umfasst nur ˹Jeff˼.
Warum sollten wir so tun, als ob wir weniger gefunden hätten, als wir tatsächlich gefunden haben? In vielen Fällen geht es darum, den auf den Treffer folgenden Text mit anderen, späteren Teilen der Regex weiter zu untersuchen oder mit der gleichen Regex erneut. So werden wir beim Beispiel zu den Dreiergruppen bei großen Zahlen vorgehen. Bei diesem kleinen Beispiel geht es darum, die ganze Regex ˹Jeffs˼ zu finden - denn nur dann soll ein Apostroph eingefügt werden - aber wir können so den Ersatztext kleiner halten. Weil der gesamte Treffer das ›s‹ nicht enthält, brauchen wir es im Ersatztext auch nicht anzufügen.
Sowohl die Regex als auch der Ersatztext sind also verschieden, aber das Resultat ist das gleiche. Bisher sieht das alles nach einer Trockenübung aus, aber ich steuere auf ein bestimmtes Ziel hin.
Beim Übergang von der ersten Regex zur zweiten wurde das ˹s˼ am Ende der Haupt-Regex in den Lookahead-Teil verschoben. Können wir etwas Ähnliches auch mit dem Teil ˹Jeff˼ tun und diesen in einen Lookbehind-Teil verschieben? Das ergäbe ˹(?<=\bJeff)(?=s\b)˼, in normaler Sprache ausgedrückt etwa: »Finde eine Stelle, von der aus wir ›Jeff‹ finden, wenn wir rückwärtsblicken, und ›s‹, wenn wir nach vorn schauen.« An dieser Position soll der Apostroph eingesetzt werden. Damit bekommen wir diese Substitution:
s/(?<=\bJeff)(?=s\b)/'/g
Langsam wird es interessant. Die Regex als Ganzes erkennt bzw. konsumiert überhaupt keinen Text, sie findet einen Treffer bei einer Position im String, gerade dort, wo der Apostroph hin soll. An dieser Stelle wird das »Nichts« durch den Ersatzstring ersetzt. In folgender Abbildung ist das illustriert. Einen ganz ähnlichen Effekt haben wir gesehen, als wir mit s/^/|>●/ ein ›|>●‹ vorn in die E-Mail-Zeilen einfügten.
Abbildung: Ablauf der Mustersuche bei ˹(?<=\bJeff)(?=s\b)˼.
Würde die Funktion der Regex verändert, wenn wir die zwei Lookaround-Konstrukte vertauschten? Anders gefragt: Was bewirkt s/(?=s\b)(?<=\bJeff)/'/g? Die Auflösung finden Sie in Lösung 8.
Zusammenfassung »Jeffs«
In der folgenden Tabelle sind die verschiedenen Ansätze für die Transformation von Jeffs in Jeff's aufgeführt.
Tabelle: Ansätze für das »Jeffs«-Problem.
Lösung | Kommentar |
---|---|
s/\bJeffs\b/Jeff's/g | Die einfachste, naheliegendste, klarste und effizienteste Lösung; die, die ich benutzen würde, wenn ich nicht andere Methoden demonstrieren würde. Kein Lookaround, »konsumiert« den ganzen String ›Jeffs‹. |
s/\b(Jeff)(s)\b/$1'$2/g | Kompliziert und doch ohne besondere Vorteile. Konsumiert den ganzen String ›Jeffs‹. |
s/\bJeff(?=s\b)/Jeff'/g | Das ›s‹ wird hier nicht aufgebraucht. Dieser Ansatz ist sonst kaum von praktischem Nutzen außer zu Illustrationszwecken. |
s/(?<=\bJeff)(?=s\b)/'/g | Diese Regex »konsumiert« überhaupt keinen Text. Mit Lookahead und Lookbehind wird eine Position im String gefunden, an der ein Hochkomma eingesetzt wird. Illustriert das Lookaround-Prinzip. |
s/(?=s\b)(?<=\bJeff)/'/g | Die gleiche Regex wie oben, aber die Lookahead- und Lookbehind-Teile sind vertauscht. Weil diese Tests keine Zeichen aus dem Suchstring verbrauchen, spielt die Reihenfolge keine Rolle. |
Ich möchte noch ein kleines Problem zu diesen regulären Ausdrücken präsentieren, bevor wir zum Beispiel mit den Dreiergruppen zurückkehren. Wenn ohne Berücksichtigung von Groß- und Kleinschreibung nach »Jeffs« gesucht werden soll, jedoch nach dem Einsetzen des Apostrophs die vorherige Schreibung erhalten bleiben soll - bei welchen der vier Substitutionen kann dazu einfach der /i-Modifikator angefügt werden? Ein Hinweis: Mit zwei Lösungen aus der Tabelle funktioniert das nicht korrekt. Finden Sie heraus, welches die anderen zwei Lösungen sind und warum es mit diesen funktioniert. Die Auflösung finden Sie unter Lösung 9.
Zurück zu den Dreiergruppen bei großen Zahlen...
Sie haben wahrscheinlich gemerkt, dass die Verbindung zwischen dem »Jeff«-Beispiel und den Dreiergruppen darin besteht, dass wir ein Zeichen an einer bestimmten Stelle, an einer Position, im String einfügen wollen und dass wir diese Position mit einem regulären Ausdruck beschreiben können.
Wir hatten festgestellt, dass wir die Tausenderpunkte bei einer Position so einfügen wollen, »dass rechts davon nur Dreiergruppen von Ziffern vorkommen und mindestens eine Ziffer links davon verbleibt«. Die zweite Bedingung können wir mit einer zurückblickenden Zusicherung einfach erfüllen. Es genügt, eine Ziffer links von der gesuchten Position zu haben, also ˹(?<=\d)˼.
Die Bedingung, dass »rechts davon nur Dreiergruppen von Ziffern vorkommen« dürfen, ist etwas schwieriger umzusetzen. Eine Dreiergruppe wird durch ˹\d\d\d˼ erkannt, das ist klar. Wir können diese Dreiergruppe in ein ˹(...)+˼ einpacken und erlauben so eine Dreiergruppe oder auch mehrere. Danach dürfen aber keine einzelnen Ziffern oder Zweiergruppen vorkommen; der String muss nach der letzten Dreiergruppe enden. Das erreichen wir mit dem Anker ˹$˼. Der Unterausdruck ˹(\d\d\d)+$˼ erkennt eine Folge von Dreiergruppen von Ziffern bis zum Ende des Suchstrings. Wenn er aber in eine Lookahead-Klammer eingesetzt wird, passt er nur auf die Positionen, bei denen diese Folgen beginnen können, das sind beispielsweise die hier markierten: ›▵123▵456▵789‹. Das ist etwas zu viel: Wir wollen vor der allerersten Ziffer keinen Punkt - also fügen wir ˹(?<=\d)˼ hinzu und schränken die möglichen Positionen weiter ein.
Das Programmstück
$pop =~ s/(?<=\d)(?=(\d\d\d)+$)/./g;
print "Deutschland hat $pop Einwohner\n";
gibt tatsächlich das gewünschte »Deutschland hat 82.499.213 Einwohner« aus. Die Klammern um ˹\d\d\d˼ sind normale, einfangende Klammern. Hier werden sie nur zum Gruppieren benutzt, damit das Pluszeichen auf die gesamte Dreiergruppe wirkt; das Abspeichern in $1 wird nicht benötigt.
Ich hätte die nicht-einfangenden Klammern ˹(?:...)˼ verwenden können, die Sie in Lösung 5 kennengelernt haben; das ergäbe die Regex ˹(?<=\d)(?=(?:\d\d\d)+$)˼. Das ist insofern »besser«, als es genauer ist - jemand, der den Code liest, muss sich nicht fragen, was mit dem Text in $1 passiert. Es wäre etwas effizienter, weil der gefundene String nicht abgespeichert werden muss. Andererseits ist der Ausdruck mit ˹(?:...)˼ noch schwerer lesbar als mit dem einfacheren ˹(...)˼, und in diesem Fall habe ich der klareren Darstellung den Vorzug gegeben. Solche Faktoren muss man bei regulären Ausdrücken häufig abwägen. Ich persönlich verwende ˹(?:...)˼, wenn es vom Problem her angemessen ist (also in diesem Fall), wenn ich aber etwas demonstrieren will (wie meistens in diesem Buch), lasse ich alles Überflüssige weg.
Wortgrenzen und negatives Lookaround
Nehmen wir an, Sie wollen mit dieser Methode eine Zahl in Dreiergruppen aufteilen, die Teil eines längeren Strings ist, zum Beispiel:
$text = "Deutschland hat 82499213 Einwohner";
.
.
.
$text =~ s/(?<=\d)(?=(\d\d\d)+$)/,/g;
print "$text\n";
So funktioniert das leider nicht, weil das ˹$˼ verlangt, dass am Ende der Folge von Dreiergruppen auch der String zu Ende ist. Wir können das Dollarzeichen aber nicht weglassen, sonst würden die Tausenderpunkte bei jeder Position eingefügt, bei der es eine Ziffer zur Linken und mindestens drei zur Rechten gibt; wir erhielten »...hat 8.2.4.9.9.213 Ein...«.
Es mag etwas merkwürdig anmuten, aber wir könnten das Dollarzeichen durch ein Metazeichen ersetzen, das bei Wortgrenzen passt, also durch \b. Obwohl es hier nur um Zahlen und Ziffern geht, funktioniert das. Das liegt an der etwas merkwürdigen Auffassung von Perl, was denn ein Wort ist. In Perl und in den meisten anderen Programmen steht ˹\w˼ für ein Wortzeichen, für ein alphanumerisches Zeichen inklusive des Unterstrichs. Eine Wortgrenze ist nun eine Position, bei der auf der einen Seite ein solches Zeichen steht (in unserem Fall eine Ziffer) und auf der anderen Seite ein anderes Zeichen (z.B. das Ende der Zeile oder ein Leerzeichen).
Diese Formulierung »auf der einen Seite dies, auf der anderen Seite jenes« kommt uns doch bekannt vor, nicht? Genau so sind wir beim »Jeffs«-Beispiel vorgegangen. Ein Unterschied besteht jedoch: Im vorliegenden Fall soll auf der einen Seite ein Zeichen nicht gefunden werden. Es stellt sich heraus, dass das, was wir bisher Lookahead und Lookbehind genannt haben, richtigerweise positives Lookahead und positives Lookbehind genannt werden sollte, weil sie auf Positionen passen, bei denen der Unterausdruck in den Klammern passt. Wie Sie aus der folgenden Tabelle ersehen können, gibt es auch die negativen Pendants dazu, also negatives Lookahead und negatives Lookbehind. Diese sind erfolgreich, wenn der Unterausdruck in den Klammern nicht passt.
Tabelle: Vier Typen von Lookaround.
Typ | Konstrukt | Erfolgreich, wenn der geklammerte Unterausdruck ... |
Positives Lookbehind | (?<=...) | ... auf der linken Seite gefunden wird. |
Negatives Lookbehind | (?<!...) | ... auf der linken Seite nicht gefunden wird. |
Positives Lookahead | (?=...) | ... auf der rechten Seite gefunden wird. |
Negatives Lookahead | (?!...) | ... auf der rechten Seite nicht gefunden wird. |
Wenn eine Wortgrenze eine Position ist, bei der auf der einen Seite ˹\w˼ passt und auf der anderen Seite ˹\w˼ nicht passt, dann können wir mit ˹(?<!\w)(?=\w)˼ einen Wortanfang und mit dem Gegenstück ˹(?=\w)(?<!\w)˼ ein Wortende erkennen. Wir können beides zusammensetzen und mit ˹(?<!\w)(?=\w)|(?=\w)(?<!\w)˼ das Metazeichen ˹\b˼ für die Wortgrenze imitieren. In einer Programmiersprache wie Perl, die ˹\b˼ unterstützt, ist das natürlich etwas albern, aber die Ausdrücke für Wortanfang und -ende können ganz hilfreich sein.
Für unser Dreiergruppenproblem reicht aber ˹(?!\d)˼ statt ˹$˼, so können wir das Ende der Folge von Dreiergruppen erkennen. Damit erhalten wir:
$text =~ s/(?<=\d)(?=(\d\d\d)+(?!\d))/./g;
Das funktioniert jetzt auch bei beispielsweise »ein Ton von 12345Hz«, und das ist gut - aber leider auch bei Texten wie »... in den 1970ern ...«, wo es schlecht hinpasst. Alle vorgenannten Lösungen setzen einen Punkt in »... 1970 ...« ein. Man muss wissen, mit was für Texten man es zu tun hat, nur dann kann man eine Regex fachgerecht einsetzen. Wenn der Text Jahreszahlen enthält, ist das Aufteilen in Dreiergruppen sicher eine schlechte Idee.
Bei all diesen Ansätzen haben wir ein negatives Lookahead benutzt, ˹(?!\w)˼ oder ˹(?!\d)˼, und so verlangt, dass eine bestimmte Klasse von Zeichen nicht gefunden werden darf. Sie erinnern sich vielleicht an das Metazeichen ˹\D˼, das für »ein Zeichen, das keine Ziffer ist« steht, und denken vielleicht, dass man dieses statt des negativen Lookaheads nehmen könnte. Das wäre ein Fehler. ˹\D˼ verlangt nach einem tatsächlichen Zeichen, aber dieses Zeichen darf keine Ziffer sein. Wenn nach der letzten Dreiergruppe aber gar nichts mehr folgt, kann ˹\D˼ nicht passen.
Dreiergruppen ohne Lookbehind
Unterstützung für das Lookbehind ist nicht so häufig, und es wird auch seltener verwendet als das Lookahead. Lookbehind ist neuer, in Perl wurde dieses Feature erst Jahre nach dem Lookahead-Konstrukt eingebaut. In Perl gibt es jetzt beides, aber das ist nicht in allen Programmiersprachen so, die reguläre Ausdrücke unterstützen. Es kann also ganz nützlich sein, auch eine Lösung für das Problem »Große Zahlen in Dreiergruppen aufteilen« zu haben, die ohne Lookbehind auskommt. Beim Ansatz
$text =~ s/(\d)(?=(\d\d\d)+(?!\d))/$1./g;
wurden einfach die Klammern um das erste ˹\d˼ durch normale, einfangende Klammern ersetzt; dafür muss der eingefangene und in $1 abgespeicherte Text im Ersatztext wieder eingesetzt werden, genau vor dem Tausenderpunkt.
Was machen wir, wenn auch das Lookahead nicht unterstützt wird? Wir können wohl wieder ˹\b˼ statt des einen Lookahead-Konstrukts ˹(?!\d)˼ nehmen, aber können wir nach dem gleichen Schema wie beim Lookbehind auch das Lookahead-Konstrukt ersetzen? Funktioniert die folgende Substitution?
$text =~ s/(\d)((\d\d\d)+\b)/$1.$2/g;
Die Auflösung finden Sie in Lösung 10.
<< zurück | vor >> |
Tipp der data2type-Redaktion: Zum Thema Reguläre Ausdrücke bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an: |
Copyright der deutschen Ausgabe © 2008 by O’Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Reguläre Ausdrücke" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
O’Reilly Verlag GmbH & Co. KG, Balthasarstr. 81, 50670 Köln