This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Die Informationen in diesem Buch wurden mit größter Sorgfalt erarbeitet. Dennoch können Fehler nicht vollständig ausgeschlossen werden. Verlag, Autoren und Übersetzer übernehmen keine juristische Verantwortung oder irgendeine Haftung für eventuell verbliebene Fehler und deren Folgen. Alle Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt und sind möglicherweise eingetragene Warenzeichen. Der Verlag richtet sich im Wesentlichen nach den Schreibweisen der Hersteller. 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.
Kommentare und Fragen können Sie gerne an uns richten: O’Reilly Verlag Balthasarstr. 81 50670 Köln Tel.: 0221/9731600 Fax: 0221/9731608 E-Mail: [email protected]
Die Originalausgabe erschien 2009 unter dem Titel Regular Expressions Cookbook im Verlag O’Reilly Media, Inc.
Die Darstellung einer Spitzmaus im Zusammenhang mit dem Thema Reguläre Ausdrücke ist ein Warenzeichen von O’Reilly Media, Inc.
Bibliografische Information Der Deutschen Bibliothek Die Deutsche Bibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.ddb.de abrufbar.
Lektorat: Alexandra Follenius & Susanne Gerbert, Köln Korrektorat: Sibylle Feldmann, Düsseldorf Satz: Tim Mergemeier, Reemers Publishing Services GmbH, Krefeld, www.reemers.de Umschlaggestaltung: Michael Oreal, Köln Produktion: Karin Driesen & Andrea Miß, Köln Belichtung, Druck und buchbinderische Verarbeitung: Druckerei Kösel, Krugzell; www.koeselbuch.de ISBN 978-3-89721-957-1 Dieses Buch ist auf 100% chlorfrei gebleichtem Papier gedruckt.
Im letzten Jahrzehnt ist die Beliebtheit regulärer Ausdrücke deutlich angestiegen. Heutzutage gibt es in allen verbreiteten Programmiersprachen mächtige Bibliotheken zur Verarbeitung regulärer Ausdrücke. Zum Teil bietet die Sprache sogar selbst die entsprechenden Möglichkeiten. Viele Entwickler nutzen diese Features, um den Anwendern ihrer Applikationen das Suchen und Filtern der Daten mithilfe regulärer Ausdrücke zu ermöglichen. Reguläre Ausdrücke sind überall. Es gibt viele Bücher, die sich mit regulären Ausdrücken befassen. Die meisten machen ihren Job ganz gut – sie erklären die Syntax und enthalten ein paar Beispiele sowie eine Referenz. Aber es gibt keine Bücher, die Lösungen vorstellen. Lösungen, die auf regulären Ausdrücken basieren und für eine ganze Reihe von praktischen Problemen aus der realen Welt genutzt werden können. Bei solchen Problemen geht es vor allem um Fragen zu Texten auf einem Computer und um Internetanwendungen. Wir, Steve und Jan, haben uns dazu entschieden, diese Lücke mit diesem Buch zu füllen. Wir wollten vor allem zeigen, wie Sie reguläre Ausdrücke in Situationen verwenden können, in denen weniger Erfahrene im Umgang mit regulären Ausdrücken sagen würden, das sei nicht möglich, oder in denen Softwarepuristen der Meinung sind, ein regulärer Ausdruck sei nicht das richtige Tool für diese Aufgabe. Da reguläre Ausdrücke heute überall zu finden sind, sind sie oft auch als Tool verfügbar, das von Endanwendern genutzt werden kann. Auch Programmierer können durch die Verwendung einiger weniger regulärer Ausdrücke viel Zeit sparen, etwa wenn sie Informationen suchen und verändern müssen. Die Alternative ist oft, Stunden oder Tage mit dem Umsetzen in prozeduralen Code zu verbringen oder eine Bibliothek von dritter Seite zu nutzen.
Gefangen im Gewirr der verschiedenen Versionen Vergleichbar mit anderen beliebten Dingen der IT-Branche, gibt es auch reguläre Ausdrücke in vielen unterschiedlichen Ausprägungen mit unterschiedlicher Kompatibilität. Das hat dazu geführt, dass es diverse Varianten eines regulären Ausdrucks gibt, die sich nicht immer gleich verhalten oder die teilweise gar nicht funktionieren.
| XI
Viele Bücher erwähnen, dass es unterschiedliche Varianten gibt, und führen auch einige der Unterschiede auf. Aber immer wieder lassen sie bestimmte Varianten unerwähnt – vor allem wenn in diesen Varianten Features fehlen –, statt auf alternative Lösungen und Workarounds hinzuweisen. Das ist frustrierend, wenn Sie mit unterschiedlichen Varianten regulärer Ausdrücke in den verschiedenen Anwendungen oder Programmiersprachen arbeiten müssen. Saloppe Bemerkungen in der Literatur wie „jeder nutzt mittlerweile reguläre Ausdrücke im Perl-Stil“ bagatellisieren leider eine ganze Reihe von Inkompatibilitäten. Selbst Pakete im „Perl-Stil“ besitzen entscheidende Unterschiede, und Perl entwickelt sich ja auch noch weiter. Solche oberflächlichen Äußerungen können für Programmierer trostlose Folgen haben und zum Beispiel dazu führen, dass sie eine halbe Stunde oder mehr damit verbringen, nutzlos im Debugger herumzustochern, statt die Details ihrer Implementierung für reguläre Ausdrücke zu kontrollieren. Selbst wenn sie herausfinden, dass ein Feature, auf dem sie aufbauen, nicht vorhanden ist, wissen sie nicht immer, wie sie stattdessen vorgehen können. Dieses Buch ist das erste, das die am meisten verbreiteten und umfangreichsten Varianten regulärer Ausdrücke nebeneinander aufführt – und zwar durchgängig im ganzen Buch.
Für wen dieses Buch gedacht ist Sie sollten dieses Buch lesen, wenn Sie regelmäßig am Computer mit Text zu tun haben – egal ob Sie einen Stapel Dokumente durchsuchen, Text in einem Texteditor bearbeiten oder Software entwickeln, die Text durchsuchen oder verändern soll. Reguläre Ausdrücke sind für diese Aufgaben exzellente Hilfsmittel. Das Reguläre Ausdrücke Kochbuch erklärt Ihnen alles, was Sie über reguläre Ausdrücke wissen müssen. Sie brauchen kein Vorwissen, da wir selbst einfachste Aspekte regulärer Ausdrücke erklären werden. Wenn Sie schon Erfahrung mit regulären Ausdrücken haben, werden Sie eine Menge Details kennenlernen, die in anderen Büchern oder Onlineartikeln häufig einfach übergangen werden. Sind Sie jemals über eine Regex gestolpert, die in einer Anwendung funktioniert hat, in einer anderen aber nicht, werden Sie die in diesem Buch detailliert und gleichwertig behandelten Beschreibungen zu sieben der verbreitetsten Varianten regulärer Ausdrücke sehr hilfreich finden. Wir haben das ganze Buch als Kochbuch aufgebaut, sodass Sie direkt zu den Themen springen können, die Sie interessieren. Wenn Sie dieses Buch von vorne bis hinten durchlesen, werden Sie am Ende Meister regulärer Ausdrücke sein. Mit diesem Buch erfahren Sie alles, was Sie über reguläre Ausdrücke wissen müssen, und noch ein bisschen mehr – unabhängig davon, ob Sie Programmierer sind oder nicht. Wenn Sie reguläre Ausdrücke in einem Texteditor nutzen wollen, in einem Suchtool oder in irgendeiner Anwendung, die ein Eingabefeld „Regex“ enthält, können Sie dieses Buch auch ganz ohne Programmiererfahrung lesen. Die meisten Rezepte bieten Lösungen an, die allein auf einem oder mehreren regulären Ausdrücken basieren. XII | Vorwort
Die Programmierer unter Ihnen erhalten in Kapitel 3 alle notwendigen Informationen zum Implementieren regulärer Ausdrücke in ihrem Quellcode. Dieses Kapitel geht davon aus, dass Sie mit den grundlegenden Features der Programmiersprache Ihrer Wahl vertraut sind, aber Sie müssen keine Erfahrung mit dem Einsatz regulärer Ausdrücke in Ihrem Quellcode mitbringen.
Behandelte Technologien .NET, Java, JavaScript, PCRE, Perl, Python und Ruby kommen nicht ohne Grund auf dem Rückseitentext vor. Vielmehr stehen diese Begriffe für die sieben Varianten regulärer Ausdrücke, die in diesem Buch behandelt werden, wobei alle sieben gleichermaßen umfassend beschrieben werden. Insbesondere haben wir versucht, alle Uneinheitlichkeiten zu beschreiben, die wir in diesen verschiedenen Varianten finden konnten. Das Kapitel zur Programmierung (Kapitel 3) enthält Code-Listings in C#, Java, JavaScript, PHP, Perl, Python, Ruby und VB.NET. Auch hier gibt es zu jedem Rezept Lösungen und Erläuterungen für alle acht Sprachen. Damit gibt es in diesem Kapitel zwar einige Wiederholungen, aber Sie können die Abhandlungen über Sprachen, an denen Sie nicht interessiert sind, gern überspringen, ohne etwas in der Sprache zu verpassen, die Sie selbst anwenden.
Aufbau des Buchs In den ersten drei Kapiteln dieses Buchs geht es um nützliche Tools und grundlegende Informationen, die eine Basis für die Verwendung regulärer Ausdrücke bilden. Jedes der folgenden Kapitel stellt dann eine Reihe von regulären Ausdrücken vor, die bestimmte Bereiche der Textbearbeitung behandeln. Kapitel 1, Einführung in reguläre Ausdrücke, erläutert die Rolle regulärer Ausdrücke und präsentiert eine Reihe von Tools, die das Erlernen, Aufbauen und Debuggen erleichtern. Kapitel 2, Grundlagen regulärer Ausdrücke, beschreibt alle Elemente und Features regulärer Ausdrücke zusammen mit wichtigen Hinweisen zu einer effektiven Nutzung. Kapitel 3, Mit regulären Ausdrücken programmieren, stellt Coding-Techniken vor und enthält Codebeispiele für die Verwendung regulärer Ausdrücke in jeder der in diesem Buch behandelten Programmiersprachen. Kapitel 4, Validierung und Formatierung, enthält Rezepte für den Umgang mit typischen Benutzereingaben, wie zum Beispiel Datumswerten, Telefonnummern und Postleitzahlen in den verschiedenen Staaten. Kapitel 5, Wörter, Zeilen und Sonderzeichen, behandelt häufig auftretende Textbearbeitungsaufgaben, wie zum Beispiel das Testen von Zeilen auf die An- oder Abwesenheit bestimmter Wörter.
Vorwort | XIII
Kapitel 6, Zahlen, zeigt, wie man Integer-Werte, Gleitkommazahlen und viele andere Formate in diesem Bereich aufspürt. Kapitel 7, URLs, Pfade und Internetadressen, zeigt Ihnen, wie Sie mit den Strings umgehen, die im Internet und in Windows-Systemen für das Auffinden von Inhalten genutzt werden. Kapitel 8, Markup und Datenaustausch, dreht sich um das Bearbeiten von HTML, XML, Comma-Separated Values (CSV) und Konfigurationsdateien im INI-Stil.
Konventionen in diesem Buch Die folgenden typografischen Konventionen werden in diesem Buch genutzt: Kursiv Steht für neue Begriffe, URLs, E-Mail-Adressen, Dateinamen und Dateierweiterungen. Feste Breite
Wird genutzt für Programme, Programmelemente wie Variablen oder Funktionsnamen, Werte, die das Ergebnis einer Ersetzung mithilfe eines regulären Ausdrucks sind, und für Elemente oder Eingabetexte, die einem regulären Ausdruck übergeben werden. Dabei kann es sich um den Inhalt eines Textfelds in einer Anwendung handeln, um eine Datei auf der Festplatte oder um den Inhalt einer String-Variablen. Feste Breite, kursiv
Zeigt Text, der vom Anwender oder durch den Kontext bestimmt werden sollte. ‹RegulärerzAusdruck›
Steht für einen regulären Ausdruck, entweder allein oder so, wie Sie ihn in das Suchfeld einer Anwendung eingeben würden. Leerzeichen in regulären Ausdrücken werden durch graue Kreise wiedergegeben, außer im Free-Spacing-Modus. «Textzzuzersetzen»
Steht für den Text, der bei einer Suchen-und-Ersetzen-Operation durch den regulären Ausdruck gefunden wird und dann ersetzt werden soll. Leerzeichen im zu ersetzenden Text werden mithilfe grauer Kreise dargestellt. Gefundener Text
Steht für den Teil des Texts, der zu einem regulären Ausdruck passt. ...
Graue Punkte in einem regulären Ausdruck weisen darauf hin, dass Sie diesen Bereich erst mit Leben füllen müssen, bevor Sie den regulären Ausdruck nutzen können. Der Begleittext erklärt, was Sie dort eintragen können. (CR), (LF) und (CRLF) CR, LF und CRLF in Rahmen stehen für die echten Zeichen zum Zeilenumbruch in Strings und nicht für die Escape-Zeichen \r, \n und \r\n. Solche Strings können entstehen, wenn man in einem mehrzeiligen Eingabefeld die Eingabetaste drückt oder im Quellcode mehrzeilige String-Konstanten genutzt werden, wie zum Beispiel die Verbatim-Strings in C# oder Strings mit dreifachen Anführungszeichen in Python.
XIV | Vorwort
Der „Wagenrücklauf“-Pfeil, den Sie vielleicht auf Ihrer Tastatur auf der Eingabetaste sehen, wird genutzt, wenn wir eine Zeile auftrennen müssen, damit sie auf die Druckseite passt. Geben Sie den Text in Ihrem Quellcode ein, sollten Sie hier nicht die Eingabetaste drücken, sondern alles auf einer Zeile belassen. Dieses Icon steht für einen Tipp, einen Vorschlag oder eine allgemeine Anmerkung.
Dieses Icon steht für einen Warnhinweis.
Die Codebeispiele verwenden Dieses Buch ist dazu da, Ihnen bei Ihrer Arbeit zu helfen. Sie können den Code dieses Buchs in Ihren Programmen und Dokumentationen verwenden. Sie brauchen uns nicht um Erlaubnis zu fragen, solange Sie nicht einen beachtlichen Teil des Codes wiedergeben. Beispielsweise benötigen Sie keine Erlaubnis, um ein Programm zu schreiben, das einige Codeteile aus diesem Buch verwendet. Für den Verkauf oder die Verbreitung einer CDROM mit Beispielen aus O’Reilly-Büchern brauchen Sie auf jeden Fall unsere Erlaubnis. Die Beantwortung einer Frage durch das Zitieren dieses Buchs und seiner Codebeispiele benötigt wiederum keine Erlaubnis. Wenn Sie einen erheblichen Teil der Codebeispiele dieses Buchs in die Dokumentation Ihres Produkts einfügen, brauchen Sie eine Erlaubnis.
Wir freuen uns über einen Herkunftsnachweis, bestehen aber nicht darauf. Eine Referenz enthält i.d.R. Titel, Autor, Verlag und ISBN, zum Beispiel: „Reguläre Ausdrücke Kochbuch von Jan Goyvaerts & Steven Levithan, Copyright 2010, O’Reilly Verlag, ISBN 9783-89721-957-1.“ Wenn Sie denken, Ihre Verwendung unserer Codebeispiele könnte den angemessenen Gebrauch oder die hier erteilte Erlaubnis überschreiten, nehmen Sie einfach mit uns über [email protected] Kontakt auf.
Danksagung Wir danken Andy Oram, unserem Lektor bei O’Reilly Media, Inc., für seine Begleitung bei diesem Projekt vom Anfang bis zum Ende. Ebenso danken wir Jeffrey Friedl, Zak Greant, Nikolaj Lindberg und Ian Morse für ihre sorgfältigen fachlichen Korrekturen, durch die dieses Buch umfassender und genauer wurde.
Vorwort | XV
KAPITEL 1
Einführung in reguläre Ausdrücke
Wenn Sie dieses Kochbuch aufgeschlagen haben, sind Sie wahrscheinlich schon ganz erpicht darauf, ein paar der hier beschriebenen seltsamen Strings mit Klammern und Fragezeichen in Ihren Code einzubauen. Falls Sie schon so weit sind: nur zu! Die verwendbaren regulären Ausdrücke sind in den Kapiteln 4 bis 8 aufgeführt und beschrieben. Aber wenn Sie zunächst die ersten Kapitel dieses Buchs lesen, spart Ihnen das langfristig möglicherweise eine Menge Zeit. So finden Sie in diesem Kapitel zum Beispiel eine Reihe von Hilfsmitteln – einige wurden von Jan, einem der beiden Autoren, erstellt –, mit denen Sie einen regulären Ausdruck testen und debuggen können, bevor Sie ihn in Ihrem Code vergraben, wo Fehler viel schwieriger zu finden sind. Außerdem zeigen Ihnen diese ersten Kapitel, wie Sie die verschiedenen Features und Optionen regulärer Ausdrücke nutzen können, um Ihr Leben leichter zu machen. Sie werden verstehen, wie reguläre Ausdrücke funktionieren, um deren Performance zu verbessern, und Sie lernen die subtilen Unterschiede kennen, die in den verschiedenen Programmiersprachen existieren – selbst in unterschiedlichen Versionen Ihrer bevorzugten Programmiersprache. Wir haben uns also mit diesen Grundlageninformationen viel Mühe gegeben und sind recht zuversichtlich, dass Sie sie lesen werden, bevor Sie mit der Anwendung regulärer Ausdrücke beginnen – oder spätestens dann, wenn Sie nicht mehr weiterkommen und Ihr Wissen aufstocken wollen.
Definition regulärer Ausdrücke Im Rahmen dieses Buchs ist ein regulärer Ausdruck eine bestimmte Art von Textmuster, das Sie in vielen modernen Anwendungen und Programmiersprachen nutzen können. Mit ihm können Sie prüfen, ob eine Eingabe zu einem Textmuster passt, Sie können Texte eines bestimmten Musters in einer größeren Datei finden, durch anderen Text oder durch eine veränderte Version des bisherigen Texts ersetzen, einen Textabschnitt in eine Reihe von Unterabschnitten aufteilen – oder auch sich selbst ins Knie schießen. Dieses Buch hilft Ihnen dabei, genau zu verstehen, was Sie tun, um Katastrophen zu vermeiden.
| 1
Geschichte des Begriffs „regulärer Ausdruck“ Der Begriff regulärer Ausdruck kommt aus der Mathematik und der theoretischen Informatik. Dort steht er für eine Eigenschaft mathematischer Ausdrücke namens Regularität. Solch ein Ausdruck kann als Software mithilfe eines deterministischen endlichen Automaten (DEA) implementiert werden. Ein DEA ist ein endlicher Automat, der kein Backtracking nutzt. Die Textmuster, die von den ersten grep-Tools genutzt wurden, waren reguläre Ausdrücke im mathematischen Sinn. Auch wenn der Name geblieben ist, sind aktuelle reguläre Ausdrücke im Perl-Stil keine regulären Ausdrücke im mathematischen Sinn. Sie sind mit einem nicht deterministischen endlichen Automaten (NEA) implementiert. Später werden Sie noch mehr über Backtracking erfahren. Alles, was ein normaler Entwickler aus diesem Textkasten mitnehmen muss, ist, dass ein paar Informatiker in ihren Elfenbeintürmen sehr verärgert darüber sind, dass ihr wohldefinierter Begriff durch eine Technologie überlagert wurde, die in der realen Welt viel nützlicher ist.
Wenn Sie reguläre Ausdrücke sinnvoll einsetzen, vereinfachen sie viele Programmierund Textbearbeitungsaufgaben oder ermöglichen gar erst deren Umsetzung. Ohne sie bräuchten Sie Dutzende, wenn nicht Hunderte von Zeilen prozeduralen Codes, um zum Beispiel alle E-Mail-Adressen aus einem Dokument zu ziehen – Code, der nicht besonders spannend zu schreiben ist und der sich auch nur schwer warten lässt. Mit dem passenden regulären Ausdruck, wie er in Rezept 4.1, zu finden ist, braucht man nur ein paar Zeilen Code, wenn nicht sogar nur eine einzige. Aber wenn Sie versuchen, mit einem einzelnen regulären Ausdruck zu viel auf einmal zu machen, oder wenn Sie Regexes auch dort nutzen, wo sie eigentlich nicht sinnvoll sind, werden Sie folgendes Statement nachvollziehen können:1 Wenn sich manche Menschen einem Problem gegenübersehen, denken sie: „Ah, ich werde reguläre Ausdrücke nutzen.“ Jetzt haben sie zwei Probleme.
Dieses zweite Problem taucht jedoch nur auf, wenn diese Menschen die Anleitung nicht gelesen haben, die Sie gerade in den Händen halten. Lesen Sie weiter. Reguläre Ausdrücke sind ein mächtiges Tool. Wenn es bei Ihrer Arbeit darum geht, Text auf einem Computer zu bearbeiten oder zu extrahieren, wird Ihnen ein solides Grundwissen über reguläre Ausdrücke viele Überstunden ersparen.
Viele Varianten regulärer Ausdrücke Okay, der Titel des vorigen Abschnitts war gelogen. Wir haben gar nicht definiert, was reguläre Ausdrücke sind. Das können wir auch nicht. Es gibt keinen offiziellen Standard, der genau definiert, welche Textmuster reguläre Ausdrücke sind und welche nicht. Wie 1 Jeffrey Friedl hat die Geschichte dieses Zitats in seinem Blog verfolgt: http://regex.info/blog/2006-09-15/247.
2 | Kapitel 1: Einführung in reguläre Ausdrücke
Sie sich vorstellen können, hat jeder Designer einer Programmiersprache und jeder Entwickler einer textverarbeitenden Anwendung eine andere Vorstellung davon, was genau ein regulärer Ausdruck tun sollte. Daher sehen wir uns einem ganzen Reigen von Varianten regulärer Ausdrücke gegenüber. Glücklicherweise sind die meisten Designer und Entwickler faul. Warum sollte man etwas total Neues aufbauen, wenn man kopieren kann, was schon jemand anderer gemacht hat? Im Ergebnis lassen sich alle modernen Varianten regulärer Ausdrücke, einschließlich derer, die in diesem Buch behandelt werden, auf die Programmiersprache Perl zurückverfolgen. Wir nennen diese Varianten reguläre Ausdrücke im Perl-Stil. Ihre Syntax ist sehr ähnlich und meist auch kompatibel, aber eben nicht immer. Autoren sind ebenfalls faul. Meistens verwenden wir den Ausdruck Regex oder Regexp, um einen einzelnen regulären Ausdruck zu bezeichnen, und Regexes für den Plural. Regex-Varianten entsprechen nicht eins zu eins den Programmiersprachen. Skriptsprachen haben meist ihre eigene, eingebaute Regex-Variante. Andere Programmiersprachen nutzen Bibliotheken, um eine Unterstützung regulärer Ausdrücke anzubieten. Manche Bibliotheken gibt es für mehrere Sprachen, während man bei anderen Sprachen auch aus unterschiedlichen Bibliotheken wählen kann. Dieses einführende Kapitel kümmert sich nur um die Varianten regulärer Ausdrücke und ignoriert vollständig irgendwelche Programmierüberlegungen. Kapitel 3 beginnt dann mit den Codebeispielen, sodass Sie schon mal in Kapitel 3, Mit regulären Ausdrücken programmieren, spicken könnten, um herauszufinden, mit welchen Varianten Sie arbeiten werden. Aber ignorieren Sie erst einmal den ganzen Programmierkram. Die im nächsten Abschnitt vorgestellten Tools ermöglichen einen viel einfacheren Weg, die Regex-Syntax durch „Learning by Doing“ kennenzulernen.
Regex-Varianten in diesem Buch In diesem Buch haben wir die Regex-Varianten ausgewählt, die heutzutage am verbreitetsten sind. Es handelt sich bei allen um Regex-Varianten im Perl-Stil. Manche der Varianten besitzen mehr Features als andere. Aber wenn zwei Varianten das gleiche Feature besitzen, haben sie meist auch die gleiche Syntax dafür. Wir werden auf die wenigen, aber nervigen Unregelmäßigkeiten hinweisen, wenn wir ihnen begegnen. All diese Regex-Varianten sind Teile von Programmiersprachen und Bibliotheken, die aktiv entwickelt werden. Aus der Liste der Varianten können Sie ersehen, welche Versionen in diesem Buch behandelt werden. Im weiteren Verlauf des Buchs führen wir die Varianten ohne Versionen auf, wenn die vorgestellte Regex überall gleich funktioniert. Das ist nahezu immer der Fall. Abgesehen von Fehlerkorrekturen, die Grenzfälle betreffen, ändern sich Regex-Varianten im Allgemeinen nicht, es sei denn, es werden neue Features ergänzt, die einer Syntax Bedeutung verleihen, die vorher als Fehler angesehen wurde:
Definition regulärer Ausdrücke | 3
Perl Perls eingebaute Unterstützung für reguläre Ausdrücke ist der Hauptgrund dafür, dass Regexes heute so beliebt sind. Dieses Buch behandelt Perl 5.6, 5.8 und 5.10. Viele Anwendungen und Regex-Bibliotheken, die behaupten, reguläre Ausdrücke im Perl- oder einem zu Perl kompatiblen Stil zu nutzen, tun das meist gar nicht. In Wirklichkeit nutzen sie eine Regex-Syntax, die der von Perl ähnlich ist, aber nicht die gleichen Features unterstützt. Sehr wahrscheinlich verwenden sie eine der Regex-Varianten aus dem Rest der Liste. Diese Varianten nutzen alle den Perl-Stil. PCRE PCRE ist die C-Bibliothek „Perl-Compatible Regular Expressions”, die von Philip Hazel entwickelt wurde. Sie können diese Open Source-Bibliothek unter http://www.pcre.org herunterladen. Dieses Buch behandelt die Versionen 4 bis 7. Obwohl die PCRE behauptet, zu Perl kompatibel zu sein, und dies vermutlich mehr als alle anderen Varianten in diesem Buch auch ist, setzt sie eigentlich nur den PerlStil um. Manche Features, wie zum Beispiel die Unicode-Unterstützung, sind etwas anders, und Sie können keinen Perl-Code mit Ihrer Regex mischen, wie es bei Perl selbst möglich ist. Aufgrund der Open Source-Lizenz und der ordentlichen Programmierung hat die PCRE Eingang in viele Programmiersprachen und Anwendungen gefunden. Sie ist in PHP eingebaut und in vielen Delphi-Komponenten verpackt. Wenn eine Anwendung behauptet, reguläre Ausdrücke zu nutzen, die „zu Perl kompatibel“ sind, ohne aufzuführen, welche Regex-Variante genau genutzt wird, ist es sehr wahrscheinlich PCRE. .NET Das .NET Framework von Microsoft stellt über das Paket System.Text.RegularExpressions eine vollständige Regex-Variante im Perl-Stil bereit. Dieses Buch behandelt die .NET-Versionen 1.0 bis 3.5. Im Prinzip gibt es aber nur zwei Versionen von System.Text.RegularExpressions: 1.0 und 2.0. In .NET 1.1, 3.0 und 3.5 gab es keine Änderungen an den Regex-Klassen. Jede .NET-Programmiersprache, unter anderem C#, VB.NET, Delphi for .NET und sogar COBOL.NET, hat einen vollständigen Zugriff auf die .NET-Variante der Regexes. Wenn eine Anwendung, die mit .NET entwickelt wurde, eine RegexUnterstützung anbietet, können Sie ziemlich sicher sein, dass sie die .NET-Variante nutzt, selbst wenn sie behauptet, „reguläre Ausdrücke von Perl“ zu nutzen. Eine wichtige Ausnahme bildet das Visual Studio (VS) selbst. Die in VS integrierte Entwicklungsumgebung (IDE) nutzt immer noch die gleiche alte Regex-Variante, die sie von Anfang an unterstützt hat. Und die ist überhaupt nicht im Perl-Stil nutzbar. Java Java 4 ist das erste Java-Release, das durch das Paket java.util.regex eine eingebaute Unterstützung regulärer Ausdrücke anbietet. Schnell wurden dadurch die verschiedenen Regex-Bibliotheken von dritter Seite verdrängt. Abgesehen davon, dass es sich um ein Standardpaket handelt, bietet es auch einen vollständige Regex-Variante im Perl-Stil an und hat eine ausgezeichnete Performance, selbst im Vergleich zu
4 | Kapitel 1: Einführung in reguläre Ausdrücke
Anwendungen, die in C geschrieben wurden. Dieses Buch behandelt das Paket java.util.regex in Java 4, 5 und 6. Wenn Sie Software verwenden, die in den letzten paar Jahren mit Java entwickelt wurden, wird jede Unterstützung regulärer Ausdrücke vermutlich auf die Java-Variante zurückgreifen. JavaScript In diesem Buch nutzen wir den Begriff JavaScript, um damit die Variante regulärer Ausdrücke zu beschreiben, die in Version 3 des ECMA-262-Standards definiert ist. Dieser Standard ist die Basis der Programmiersprache ECMAScript. Diese ist besser bekannt durch ihre Implementierungen JavaScript und JScript in den verschiedenen Webbrowsern. Internet Explorer 5.5 bis 8.0, Firefox, Opera und Safari implementieren alle Version 3 von ECMA-262. Trotzdem gibt es in allen Browsern unterschiedliche Grenzfälle, in denen sie vom Standard abweichen. Wir weisen auf solche Probleme hin, wenn sie eine Rolle spielen. Ist es möglich, auf einer Website mit einem regulären Ausdruck suchen oder filtern zu können, ohne auf eine Antwort vom Webserver warten zu müssen, wird die Regex-Variante von JavaScript genutzt, die die einzige browserübergreifende RegexVariante auf Clientseite ist. Selbst VBScript von Microsoft und ActionScript 3 von Adobe nutzen sie. Python Python unterstützt reguläre Ausdrücke durch sein Modul re. Dieses Buch behandelt Python 2.4 und 2.5. Die Unterstützung regulärer Ausdrücke in Python hat sich seit vielen Jahren nicht geändert. Ruby Die Unterstützung regulärer Ausdrücke in Ruby ist wie bei Perl Teil der Sprache selbst. Dieses Buch behandelt Ruby 1.8 und 1.9. Eine Standardkompilierung von Ruby 1.8 verwendet die Variante regulärer Ausdrücke, die direkt im Quellcode von Ruby implementiert ist. Eine Standardkompilierung von Ruby 1.9 nutzt die Oniguruma-Bibliothek für reguläre Ausdrücke. Ruby 1.8 kann so kompiliert werden, dass es Oniguruma verwendet, und Ruby 1.9 so, dass es die ältere Ruby-Regex-Variante nutzt. In diesem Buch bezeichnen wir die native Ruby-Variante als Ruby 1.8 und die Oniguruma-Variante als Ruby 1.9. Um herauszufinden, welche Ruby-Regex-Variante Ihre Site nutzt, versuchen Sie, den regulären Ausdruck ‹a++› zu verwenden. Ruby 1.8 wird Ihnen mitteilen, dass der reguläre Ausdruck ungültig ist, da es keine possessiven Quantoren unterstützt, während Ruby 1.9 damit einen String finden wird, der aus einem oder mehreren Zeichen besteht. Die Oniguruma-Bibliothek ist so entworfen, dass sie abwärtskompatibel zu Ruby 1.8 ist und neue Features so ergänzt, dass keine bestehenden Regexes ungültig werden. Die Implementatoren haben sogar Features darin gelassen, die man wohl besser entfernt hätte, wie zum Beispiel die Verwendung von (?m), mit der der Punkt auch Zeilenumbrüche findet, wofür andere Varianten (?s) nutzen.
Definition regulärer Ausdrücke | 5
Suchen und Ersetzen mit regulären Ausdrücken Suchen und Ersetzen ist eine Aufgabe, für die reguläre Ausdrücke häufig eingesetzt werden. Solch eine Funktion übernimmt einen Ausgangstext, einen regulären Ausdruck und einen Ersetzungsstring als Eingabe. Die Ausgabe ist der Ausgangstext, in dem alle Übereinstimmungen mit dem regulären Ausdruck durch den Ersetzungstext ausgetauscht wurden. Obwohl der Ersetzungstext kein regulärer Ausdruck ist, können Sie bestimmte Syntaxelemente nutzen, um einen dynamischen Ersetzungstext aufzubauen. Bei allen Varianten können Sie den Text einfügen, der durch den regulären Ausdruck gefunden wurde, oder einen Teil davon. In den Rezepten 2.20 und 2.21 wird das beschrieben. Manche Varianten unterstützen auch noch das Einfügen von passendem Kontext im Ersetzungstext, wie in Rezept 2.22 zu lesen ist. In Kapitel 3, Rezept 3.16 erfahren Sie, wie man für jede Übereinstimmung einen anderen Ersetzungstext erzeugen kann.
Viele Varianten des Ersetzungstexts Unterschiedliche Ideen von unterschiedlichen Softwareentwicklern haben dazu geführt, dass es viele verschiedene Varianten regulärer Ausdrücke gibt. Jede davon hat eine andere Syntax und unterstützt andere Features. Bei den Ersetzungstexten ist das nicht anders. Tatsächlich gibt es sogar noch mehr Varianten als bei den regulären Ausdrücken selbst. Es ist schwer, eine Engine für reguläre Ausdrücke aufzubauen. Die meisten Programmierer bevorzugen es, eine bestehende zu nutzen, aber es ist recht einfach, eine Funktion zum Suchen und Ersetzen auf einer bestehenden Regex-Engine aufzubauen. So gibt es viele Varianten für Ersetzungstexte in Regex-Bibliotheken, die nicht von sich aus bereits Funktionen zum Ersetzen anbieten. Glücklicherweise haben alle Varianten regulärer Ausdrücke in diesem Buch entsprechende Varianten für den Ersetzungstext, mit Ausnahme der PCRE. Diese Lücke in PCRE macht den Programmierern, die damit arbeiten, das Leben schwer. Die Open Source-PCRE-Bibliothek enthält keinerlei Funktionen für Ersetzungen. Das heißt, alle Anwendungen und Programmiersprachen, die auf PCRE aufbauen, müssen ihre eigenen Funktionen bereitstellen. Die meisten Programmierer versuchen, eine bestehende Syntax zu kopieren, aber sie machen es nie exakt gleich. Dieses Buch behandelt die folgenden Ersetzungstextvarianten. In „Viele Varianten regulärer Ausdrücke“ auf Seite 2, finden Sie weitere Informationen über die Regex-Varianten, die zu den entsprechenden Ersetzungstextvarianten gehören: Perl Perl besitzt eine eingebaute Unterstützung für Ersetzungen mit regulären Ausdrücken. Dazu nutzt es den Operator s/regex/replace/. Die Perl-Variante für Ersetzungen gehört zu der Perl-Variante für reguläre Ausdrücke. Dieses Buch behandelt Perl 5.6 bis Perl 5.10. In der letzten Version ist eine Unterstützung für benannte Rückwärtsreferenzen im Ersetzungstext hinzugekommen, so wie auch bei den regulären Ausdrücken nun benannte einfangende Gruppen enthalten sind. 6 | Kapitel 1: Einführung in reguläre Ausdrücke
PHP In diesem Buch bezieht sich die PHP-Variante für den Ersetzungstext auf die Funktion preg_replace. Diese Funktion nutzt die PCRE-Variante für die regulären Ausdrücke und die PHP-Variante für den Ersetzungstext. Andere Programmiersprachen, die PCRE nutzen, verwenden nicht die gleiche Ersetzungstextvariante wie PHP. Abhängig davon, woher die Designer Ihrer Programmiersprache ihre Inspiration beziehen, kann die Ersetzungstextsyntax der von PHP gleichen oder eine beliebige andere Variante aus diesem Buch nutzen. PHP bietet zudem noch die Funktion ereg_replace. Diese Funktion nutzt eine andere Variante für reguläre Ausdrücke (POSIX ERE) und auch eine andere Variante für die Ersetzungstexte. PHPs ereg-Funktionen werden in diesem Buch nicht behandelt. .NET Das Paket System.Text.RegularExpressions stellt eine ganze Reihe von Funktionen zum Suchen und Ersetzen bereit. Die .NET-Variante für den Ersetzungstext gehört zur .NET-Variante für die regulären Ausdrücke. Alle Versionen von .NET verwenden die gleiche Ersetzungstextvariante. Die neuen Features in .NET 2.0 für die regulären Ausdrücke haben keinen Einfluss auf die Syntax für den Ersetzungstext. Java Das Paket java.util.regex enthält Funktionen zum Suchen und Ersetzen. Dieses Buch behandelt Java 4, 5 und 6. Alle Versionen greifen auf die gleiche Ersetzungstextsyntax zurück. JavaScript In diesem Buch nutzen wir den Begriff JavaScript, um sowohl auf die Variante für den Ersetzungstext zu verweisen als auch auf die für die regulären Ausdrücke. Beides ist in Edition 3 des ECMA-262-Standards definiert. Python Pythons Modul re stellt eine Funktion sub bereit, mit der gesucht und ersetzt werden kann. Die Python-Variante für den Ersetzungstext gehört zur Python-Variante für reguläre Ausdrücke. Dieses Buch behandelt Python 2.4 und 2.5. Die RegexUnterstützung ist bei Python in den letzten Jahren sehr stabil gewesen. Ruby Die Unterstützung regulärer Ausdrücke in Ruby ist Bestandteil der Sprache selbst und auch die Funktion zum Suchen und Ersetzen. Dieses Buch behandelt Ruby 1.8 und 1.9. Eine Standardkompilierung von Ruby 1.8 nutzt die Variante für reguläre Ausdrücke, die direkt im Quellcode von Ruby definiert ist, während eine Standardkompilierung von Ruby 1.9 die Oniguruma-Bibliothek für reguläre Ausdrücke nutzt. Ruby 1.8 kann so kompiliert werden, dass Oniguruma verwendet wird, während Ruby 1.9 so kompiliert werden kann, dass die alte Regex-Variante von Ruby genutzt wird. In diesem Buch bezeichnen wir die native Ruby-Variante als Ruby 1.8 und die Oniguruma-Variante als Ruby 1.9.
Suchen und Ersetzen mit regulären Ausdrücken | 7
Die Syntax für Ersetzungstexte ist in Ruby 1.8 und 1.9 die gleiche, nur dass Ruby 1.9 auch noch benannte Rückwärtsreferenzen im Ersetzungstext unterstützt. Benannte einfangende Gruppen sind ein neues Feature bei den regulären Ausdrücken von Ruby 1.9.
Tools für das Arbeiten mit regulären Ausdrücken Sofern Sie nicht schon längere Zeit mit regulären Ausdrücken gearbeitet haben, empfehlen wir Ihnen, Ihre ersten Experimente in einem Tool durchzuführen und nicht in Quellcode. Die Beispiel-Regexes in diesem Kapitel und in Kapitel 2, sind einfache reguläre Ausdrücke, die keine zusätzliche Maskierung in einer Programmiersprache (oder sogar in einer Unix-Shell) brauchen. Sie können diese regulären Ausdrücke direkt in das Suchfeld einer Anwendung eingeben. Kapitel 3 erklärt, wie Sie reguläre Ausdrücke in Ihren Quellcode einbauen können. Will man einen regulären Ausdruck als String mit Anführungszeichen versehen, wird er noch schwerer lesbar, weil sich dann die Maskierungsregeln für Strings mit denen für reguläre Ausdrücke vermischen. Wir heben uns das für Rezept 3.1, auf. Haben Sie einmal die Grundlagen regulärer Ausdrücke verstanden, werden Sie auch durch den BackslashDschungel finden. Die in diesem Abschnitt beschriebenen Tools ermöglichen es Ihnen auch, reguläre Ausdrücke zu debuggen, die Syntax zu prüfen und andere Informationen zu bekommen, die in den meisten Programmierumgebungen nicht erhältlich sind. Wenn Sie also reguläre Ausdrücke in Ihren Anwendungen entwickeln, ist es für Sie vielleicht sinnvoll, einen komplizierten regulären Ausdruck in einem dieser Tools aufzubauen, bevor Sie ihn in Ihrem Programm einsetzen.
RegexBuddy RegexBuddy (Abbildung 1-1) ist das Tool mit den (zum Zeitpunkt der Entstehung dieses Buchs) meisten verfügbaren Features zum Erstellen, Testen und Implementieren regulärer Ausdrücke. Es bietet die einzigartige Möglichkeit, alle Varianten regulärer Ausdrücke zu emulieren, die in diesem Buch behandelt werden, und sogar zwischen den verschiedenen Varianten zu konvertieren. RegexBuddy wurde von Jan Goyvaerts entworfen und entwickelt, einem der Autoren dieses Buchs. Dadurch wurde Jan zu einem Experten für reguläre Ausdrücke und mithilfe von RegexBuddy war Koautor Steven in der Lage, sich so gut mit regulären Ausdrücken vertraut zu machen, dass er sich an diesem Buch beteiligen konnte. Wenn der Screenshot (Abbildung 1-1) ein bisschen unruhig aussieht, liegt das daran, dass wir einen Großteil der Panels auf den Bildschirm gebracht haben, um zu zeigen, was RegexBuddy alles drauf hat. Die Standardansicht ordnet alle Panels hübsch in Registerkarten an. Sie können sie aber auch abreißen, um sie auf einen zweiten Monitor zu ziehen.
8 | Kapitel 1: Einführung in reguläre Ausdrücke
Abbildung 1-1: RegexBuddy
Um einen der regulären Ausdrücke aus diesem Buch auszuprobieren, tippen Sie ihn einfach in das Eingabefeld im oberen Bereich des RegexBuddy-Fensters ein. RegexBuddy stellt den Ausdruck dann automatisch mit Syntax-Highlighting dar und macht deutlich auf Fehler und fehlende Klammern aufmerksam. Das Create-Panel baut automatisch eine detaillierte Analyse in englischer Sprache auf, während Sie die Regex eintippen. Durch einen Doppelklick auf eine Beschreibung im Baum mit der Struktur des regulären Ausdrucks können Sie diesen Teil des regulären Ausdrucks bearbeiten. Neue Teile lassen sich in Ihren regulären Ausdruck per Hand oder über den Button Insert Token einfügen. Bei Letzterem können Sie dann aus einem Menü auswählen, was Sie haben wollen. Wenn Sie sich zum Beispiel nicht an die komplizierte Syntax für einen positiven Lookahead erinnern, können Sie RegexBuddy bitten, die passenden Zeichen für Sie einzufügen. Geben Sie einen Beispieltext im Test-Panel ein – indem Sie ihn entweder eintippen oder hineinkopieren. Wenn der Highlight-Button aktiv ist, markiert RegexBuddy automatisch den Text, der zur Regex passt. Einige dieser Buttons werden Sie wahrscheinlich am häufigsten nutzen: List All Gibt eine Liste mit allen Übereinstimmungen aus.
Tools für das Arbeiten mit regulären Ausdrücken | 9
Replace Der Replace-Button am oberen Fensterrand zeigt ein neues Fenster an, in dem Sie den Ersetzungstext eingeben können. Der Replace-Button im Test-Panel zeigt Ihnen dann den Ausgangstext an, nachdem die Ersetzungen vorgenommen wurden. Split (der Button im Test-Panel, nicht der am oberen Fensterrand) Behandelt den regulären Ausdruck als Separator und teilt den Ausgangstext in Tokens auf, die zeigen, wo in Ihrem Text Übereinstimmungen gefunden wurden. Klicken Sie auf einen dieser Buttons und wählen Sie Update Automatically, damit RegexBuddy die Ergebnisse dynamisch aktualisiert, wenn Sie Ihre Regex oder den Ausgangstext anpassen. Um genau zu sehen, wie Ihre Regex funktioniert (oder warum nicht), klicken Sie im TestPanel auf eine hervorgehobene Übereinstimmung oder auf eine Stelle, an der die Regex nicht funktioniert hat, und danach auf den Button Debug. RegexBuddy wechselt dadurch zum Debug-Panel und zeigt den gesamten Prozess zum Finden von Übereinstimmungen Schritt für Schritt. Klicken Sie in die Ausgabe des Debuggers, um herauszufinden, welches Regex-Token zu dem Text passte, den Sie angeklickt haben. Im Use-Panel wählen Sie Ihre bevorzugte Programmiersprache aus. Dann wählen Sie eine Funktion, um den Quellcode für das Implementieren Ihrer Regex zu erzeugen. Die Quellcode-Templates von Regex-Buddy lassen sich mit dem eingebauten Template-Editor problemlos bearbeiten. Sie können neue Funktionen und sogar neue Sprachen ergänzen oder bestehende anpassen. Möchten Sie Ihre Regex mit einer größeren Datenmenge testen, wechseln Sie zum GREPPanel, um eine beliebige Anzahl von Dateien und Ordnern zu durchsuchen (und eventuell Ersetzungen vorzunehmen). Wenn Sie in Code, den Sie warten, eine Regex finden, kopieren Sie sie in die Zwischenablage – einschließlich der umschließenden Anführungszeichen oder Schrägstriche. In RegexBuddy klicken Sie auf den Paste-Button und wählen den String-Stil Ihrer Programmiersprache aus. Ihre Regex wird dann in RegexBuddy als pure Regex erscheinen – ohne die zusätzlichen Anführungszeichen oder Maskierungen, die für String-Literale notwendig sind. Mit dem Copy-Button erzeugen Sie einen String in der gewünschten Syntax, sodass Sie ihn in Ihren Quellcode einfügen können. Sobald Sie mehr Erfahrung haben, können Sie im Library-Panel eine praktische Bibliothek mit regulären Ausdrücke aufbauen. Ergänzen Sie die dort abgelegten Regexes auf jeden Fall um eine detaillierte Beschreibung und einen Test-String. Reguläre Ausdrücke können sehr kryptisch sein, selbst für Experten. Haben Sie dennoch ein Problem damit, eine passende Regex aufzubauen, klicken Sie auf das Forum-Panel und dann auf den Login-Button. Falls Sie RegexBuddy gekauft haben, erscheint das Anmeldefenster. Klicken Sie auf OK, sind Sie direkt mit dem Benutzerforum von RegexBuddy verbunden. Steven und Jan sind dort häufig zu finden.
10 | Kapitel 1: Einführung in reguläre Ausdrücke
RegexBuddy läuft unter Windows 98, ME, 2000, XP und Vista. Falls Sie eher Linux oder Apple bevorzugen, lässt sich RegexBuddy auch gut mit VMware, Parallels, CrossOver Office und (mit ein paar Einschränkungen) auch mit WINE betreiben. Sie können eine kostenlose Testversion von RegexBuddy unter http://www.regexbuddy.com/RegexBuddyCookbook.exe herunterladen. Abgesehen vom Benutzerforum ist die Testversion sieben Tage lang uneingeschränkt nutzbar.
RegexPal RegexPal (Abbildung 1-2) ist ein Online-Testtool für reguläre Ausdrücke. Es wurde von Steven Levithan erstellt, einem der Autoren dieses Buchs. Sie brauchen dazu lediglich einen modernen Webbrowser. RegexPal ist vollständig in JavaScript geschrieben. Daher unterstützt es nur die JavaScript-Variante für reguläre Ausdrücke, so wie sie in dem von Ihnen verwendeten Webbrowser implementiert ist.
Abbildung 1-2: RegexPal
Um einen der regulären Ausdrücke auszuprobieren, die in diesem Buch vorgestellt werden, rufen Sie http://www.regexpal.com auf. Geben Sie die Regex in das Feld ein, in dem Enter regex here. steht. RegexPal zeigt Ihren regulären Ausdruck automatisch mit SyntaxHighlighting an, wodurch Sie auch Syntaxfehler in der Regex erkennen können. RegexPal ist sich der Probleme unterschiedlicher Implementierungen in den verschiedenen
Tools für das Arbeiten mit regulären Ausdrücken | 11
Browsern bewusst, die Ihnen das Leben ganz schön schwer machen können. Wenn daher eine bestimmte Syntax in manchen Browsern nicht korrekt arbeitet, wird RegexPal diese als Fehler hervorheben. Jetzt geben Sie einen Beispieltext in das Feld mit dem Inhalt Enter test data here. ein. RegexPal hebt automatisch die Übereinstimmungen zu Ihrer Regex hervor. Es gibt keine Buttons, die man anklicken muss – das macht RegexPal zu einem der angenehmsten Onlinetools für das Testen regulärer Ausdrücke.
Weitere Onlinetools für Regexes Es ist einfach, ein simples Onlinetool für das Testen regulärer Ausdrücke aufzubauen. Wenn Sie ein paar grundlegende Web-Entwicklungskenntnisse besitzen, reichen die Informationen aus Kapitel 3 aus, um selbst etwas zu bauen. Hunderte von Entwicklern haben das schon getan, ein paar haben Features ergänzt, die erwähnenswert sind.
regex.larsolavtorvik.com Lars Olav Torvik hat ein tolles kleines Testtool für reguläre Ausdrücke unter http://regex.larsolavtorvik.com bereitgestellt (siehe Abbildung 1-3). Zunächst wählen Sie die Variante aus, mit der Sie arbeiten wollen, indem Sie im oberen Bereich der Seite auf deren Namen klicken. Lars bietet PHP PCRE, PHP POSIX und JavaScript an. PHP PCRE, die PCRE-Variante, die wir in diesem Buch behandeln, wird von der PHP-Funktion preg genutzt. POSIX ist eine alte und recht eingeschränkte RegexVariante, die von der PHP-Funktion ereg verwendet wird, aber in diesem Buch keine weitere Erwähnung findet. Wenn Sie JavaScript auswählen, arbeiten Sie mit der JavaScriptImplementierung Ihres Browsers. Geben Sie Ihren regulären Ausdruck in das Pattern-Feld und Ihren Ausgangstext in das Subject-Feld ein. Einen Moment später wird im Matches-Feld Ihr Ausgangstext mit den hervorgehobenen Übereinstimmungen angezeigt. Das Code-Feld enthält eine einzelne Codezeile, die Ihre Regex auf Ihren Ausgangstext anwendet. Kopieren Sie diese Zeile in Ihren Codeeditor, brauchen Sie die Regexp nicht selbst in ein String-Literal umzuwandeln. Strings oder Arrays, die vom Code zurückgegeben werden, finden Sie im ResultFeld. Da Lars Ajax-Technologie verwendet hat, um seine Site aufzubauen, sind die Ergebnisse für alle Varianten immer nach kurzer Zeit verfügbar. Um dieses Tool nutzen zu können, müssen Sie online sein, da auf dem Server PHP verarbeitet wird. Die zweite Spalte zeigt eine Liste mit Regex-Befehlen und -Optionen an. Diese hängen davon ab, welche Variante Sie gewählt haben. Zu den Befehlen gehören üblicherweise solche zum Finden von Übereinstimmungen (match), zum Ersetzen von Text (replace) und zum Aufteilen von Texten (split). Die Optionen enthalten die üblichen Verdächtigen, wie zum Beispiel das Ignorieren von Groß- und Kleinschreibung, aber auch implementierungsspezifische Optionen. Diese Befehle und Optionen werden in Kapitel 3, beschrieben.
12 | Kapitel 1: Einführung in reguläre Ausdrücke
Abbildung 1-3: regex.larsolavtorvik.com
Nregex http://www.nregex.com (Abbildung 1-4) ist ein unkompliziertes Online-Testtool für Regexes, das von David Seruyange mit .NET-Technologie gebaut wurde. Die Site erwähnt zwar nicht, welche Variante sie nutzt, aber als dieses Buch hier geschrieben wurde, war es .NET 1.x. Das Layout der Seite ist ein bisschen verwirrend. Sie geben Ihren regulären Ausdruck in das Feld unter dem Text Regular Expression ein und setzen die Regex-Optionen mithilfe der Kontrollkästchen darunter. Geben Sie Ihren Ausgangstext in das große Feld am unteren Ende der Seite ein, wobei Sie den Standardtext If I just had $5.00 then "she" wouldn't be so @#$! mad. ersetzen. Wenn Ihr Text von einer Webseite kommt, geben Sie die URL in das Feld Load Target From URL ein und klicken auf den Button Load, der sich darunter befindet. Möchten Sie eine Datei auf Ihrer Festplatte als Ausgangsbasis nehmen, klicken Sie auf den Button Browse, wählen die gewünschte Datei aus und klicken dann auf den Button Load unter dem entsprechenden Eingabefeld.
Tools für das Arbeiten mit regulären Ausdrücken | 13
Abbildung 1-4: Nregex
Ihr Ausgangstext wird im Feld Matches & Replacements in der Mitte der Webseite nochmals erscheinen, wobei die Übereinstimmungen zur Regex hervorgehoben sind. Wenn Sie etwas in das Feld Replacement String eingeben, wird stattdessen das Ergebnis des Ersetzungsvorgangs angezeigt. Wenn Ihr regulärer Ausdruck ungültig ist, erscheint: ... Das Auswerten der Regex wird mit .NET-Code auf dem Server durchgeführt, daher müssen Sie für diese Site online sein. Arbeiten die automatischen Aktualisierungen langsam – vielleicht weil Ihr Ausgangstext sehr groß ist –, markieren Sie das Kontrollkästchen Manually Evaluate Regex über dem Eingabefeld für Ihren regulären Ausdruck, um einen Evaluate-Button zu erhalten. Diesen können Sie dann anklicken, um das Feld Matches & Replacements zu aktualisieren.
14 | Kapitel 1: Einführung in reguläre Ausdrücke
Rubular Michael Lovitt hat ein minimalistisches Regex-Testtool unter http://www.rubular.com (Abbildung 1-5) bereitgestellt, wobei die Regex-Variante von Ruby 1.8 genutzt wird.
Abbildung 1-5: Rubular
Geben Sie Ihren regulären Ausdruck im Feld Your regular expression zwischen den beiden Schrägstrichen ein. Sie können Groß- und Kleinschreibung ignorieren, wenn Sie in das kleine Feld nach dem zweiten Schrägstrich ein i tippen. Genauso können Sie die Option Punkt findet auch Zeilenumbruch durch die Eingabe eines m im gleichen Feld aktivieren. im schaltet beide Optionen ein. Auch wenn diese Konventionen ein wenig unpraktisch zu sein scheinen, wenn Sie mit Ruby noch nicht so vertraut sind, entsprechen Sie dennoch der Syntax /regex/im, die in Ruby genutzt wird, um einen regulären Ausdruck anzugeben. Tragen Sie Ihren Ausgangstext in das Feld Your test string ein und warten Sie einen Moment. Es wird ein neues Feld Match result auf der rechten Seite erscheinen, in dem Ihr Ausgangstext mit allen Übereinstimmungen hervorgehoben zu finden ist.
myregexp.com Sergey Evdokimov hat eine ganze Reihe von Regex-Testtools für Java-Entwickler geschrieben. Die Homepage unter http://www.myregexp.com (Abbildung 1-6) bietet auch eine Onlineversion an. Dabei handelt es sich um ein Java-Applet, das in Ihrem Browser
Tools für das Arbeiten mit regulären Ausdrücken | 15
läuft. Dafür muss die Java 4-Runtime (oder eine neuere) auf Ihrem Computer installiert sein. Das Applet nutzt das Paket java.util.regex (das seit Java 4 existiert), um Ihren regulären Ausdruck auszuwerten. In diesem Buch bezieht sich die Java-Variante auf dieses Paket.
Abbildung 1-6: myregexp.com
Geben Sie Ihren regulären Ausdruck in das Feld Regular Expression ein. Mit dem Menü Flags können Sie die gewünschten Regex-Optionen setzen. Drei der Optionen sind auch als Kontrollkästchen direkt auf der Oberfläche zu finden. Wenn Sie eine Regex testen wollen, die schon als String in Java-Code vorhanden ist, kopieren Sie den gesamten String in die Zwischenablage. Im myregexp.com-Tester wählen Sie im Edit-Menü den Punkt Paste Regex from Java String aus. Im gleichen Menü können Sie auch Copy Regex for Java Source aufrufen, wenn Sie mit der Bearbeitung des regulären Ausdrucks fertig sind. In diesem Menü gibt es außerdem ähnliche Einträge für JavaScript und XML.
16 | Kapitel 1: Einführung in reguläre Ausdrücke
Unterhalb des regulären Ausdrucks gibt es vier Registerkarten, auf denen vier verschiedene Tests ausgeführt werden können: Find Alle Übereinstimmungen zum regulären Ausdruck werden im Ausgangstext hervorgehoben. Dies sind die von der Java-Methode Matcher.find() gefundenen Elemente. Match Prüft, ob der reguläre Ausdruck mit dem Ausgangstext vollständig übereinstimmt. Wenn das der Fall ist, wird der gesamte Text hervorgehoben. Die Methoden String.matches() und Matcher.matches() gehen so vor. Split Das zweite Feld auf der rechten Seite zeigt das Array mit Strings an, das von String.split() oder Pattern.split() zurückgegeben wird. Replace Geben Sie einen Ersetzungstext ein, zeigt das Feld auf der rechten Seite den von String.replaceAll() oder Matcher.replaceAll() zurückgegebenen Text an. Sie finden die anderen Regex-Tools von Sergey über die Links oben auf der Seite http://www.myregexp.com. Eines ist ein Plug-in für Eclipse, das andere eines für IntelliJ IDEA.
reAnimator Oliver Steeles reAnimator unter http://osteele.com/tools/reanimator (Abbildung 1-7) bringt keine tote Regex ins Leben zurück. Aber es ist ein nettes kleines Tool, das eine grafische Darstellung des endlichen Automaten ausgibt, den eine Regex-Engine nutzt, um einen regulären Ausdruck umzusetzen. Die Syntax des reAnimator ist sehr eingeschränkt. Es lassen sich alle in diesem Buch behandelten Varianten nutzen. Jede Regex, die Sie im reAnimator darstellen können, wird in allen Varianten funktionieren, die in diesem Buch beschrieben sind, aber das Gegenteil ist definitiv nicht der Fall. Das liegt daran, dass die regulären Ausdrücke von reAnimator im mathematischen Sinn regulär sind. Der Kasten „Geschichte des Begriffs ,regulärer Ausdruck’“ auf Seite 2 erklärt das kurz. Gehen Sie zum Pattern-Feld im oberen Bereich der Seite und klicken Sie auf den Edit-Button. Geben Sie nun Ihren regulären Ausdruck ein und klicken Sie auf Set. Tippen Sie langsam den Ausgangstext in das Input-Feld ein. Beim Eintippen jedes einzelnen Zeichens bewegen sich bunte Bälle durch den Automaten, um zu zeigen, wo sich der Endpunkt aufgrund Ihrer bisherigen Eingabe befindet. Blaue Bälle stehen für eine vom Automaten akzeptierte Eingabe, die aber noch mehr benötigt, um eine vollständige Übereinstimmung zu erreichen. Grüne Bälle zeigen, dass der endliche Automat das ganze Muster abbilden kann. Wenn keine Bälle zu sehen sind, kann der Automat mit der Eingabe nichts anfangen.
Tools für das Arbeiten mit regulären Ausdrücken | 17
Abbildung 1-7: reAnimator
reAnimator zeigt Ihnen nur dann eine Übereinstimmung, wenn der reguläre Ausdruck zum ganzen Eingabetext passt – so also ob Sie ihn zwischen die Anker ‹^› und ‹$› gesetzt hätten. Das ist eine weitere Eigenschaft von Ausdrücken, die im mathematischen Sinn regulär sind.
Weitere Desktop-Tools für das Arbeiten mit regulären Ausdrücken Expresso Expresso (nicht zu verwechseln mit dem koffeinhaltigen Espresso) ist eine .NET-Anwendung, mit der reguläre Ausdrücke erstellt und getestet werden können. Das Programm lässt sich unter http://www.ultrapico.com/Expresso.htm herunterladen. Um es zu nutzen, muss das .NET Framework 2.0 auf Ihrem Computer installiert sein. Beim Download handelt es sich um eine Testversion, die 60 Tage kostenlos nutzbar ist. Danach müssen Sie das Programm registrieren, da ansonsten ein Großteil der Funktionalität nicht mehr nutzbar ist. Das Registrieren ist kostenfrei, aber Sie müssen den Jungs von Ultrapico Ihre E-Mail-Adresse mitteilen. Der Registrierungsschlüssel wird per E-Mail verschickt.
18 | Kapitel 1: Einführung in reguläre Ausdrücke
Expresso präsentiert sich, wie in Abbildung 1-8 gezeigt. Der Bereich Regular Expression, in den Sie Ihren regulären Ausdruck eingeben, ist immer sichtbar. Es gibt kein SyntaxHighlighting. Im Bereich Regex Analyzer wird automatisch eine kurze Analyse Ihres regulären Ausdrucks in englischer Sprache aufgebaut. Auch er ist stets sichtbar.
Abbildung 1-8: Expresso
Im Designmodus können Sie am unteren Ende des Fensters die Regex-Optionen setzen, zum Beispiel Ignore Case. Den meisten Platz auf dem Bildschirm nimmt eine Reihe von Registerkarten ein, auf denen Sie das Regex-Token auswählen können, das Sie einfügen wollen. Wenn Sie zwei Monitore nutzen oder auch einen großen, klicken Sie auf den Undock-Button, um die Registerkarten zu „lösen“. Dann können Sie Ihren regulären Ausdruck auch im anderen Modus (Testmodus) erstellen. Im Testmodus geben Sie Ihren Ausgangstext in der linken unteren Ecke ein. Dann klicken Sie auf den Button Run Match, um eine Liste aller Übereinstimmungen im Bereich Search Results zu erhalten. Es gibt keine Hervorhebungen des Ausgangstexts. Klicken Sie auf eine Übereinstimmung in den Ergebnissen, um die entsprechende Stelle im Ausgangstext anzuwählen. Tools für das Arbeiten mit regulären Ausdrücken | 19
Die Expression Library enthält eine Liste mit Beispiel-Regexes und eine mit den zuletzt verwendeten. Ihre Regex wird dieser Liste immer dann hinzugefügt, wenn Sie Run Match anklicken. Sie können die Bibliothek über das Menü Library im Hauptmenü anpassen.
The Regulator The Regulator kann von http://sourceforge.net/projects/regulator heruntergeladen werden. Dabei handelt es sich um eine weitere .NET-Anwendung für das Erstellen und Testen regulärer Ausdrücke. Die neueste Version erfordert .NET 2.0 oder neuer. Ältere Versionen für .NET 1.x können immer noch heruntergeladen werden. The Regulator ist Open Source, und man muss weder etwas bezahlen, noch muss man sich registrieren. Im Regulator passiert alles in einem Fenster (Abbildung 1-9). Auf der Registerkarte New Document geben Sie Ihren regulären Ausdruck ein. Es gibt ein automatisches SyntaxHighlighting, aber Syntaxfehler in Ihrer Regex werden nicht hervorgehoben. Per Rechtsklick wählen Sie das Regex-Token aus, das Sie aus einem Menü einfügen wollen. Sie können die Optionen für reguläre Ausdrücke über die Buttons in der Toolbar setzen. Die Icons sind ein wenig kryptisch. Warten Sie auf den Tooltipp, um zu verstehen, welche Einstellung Sie mit welchem Button vornehmen.
Abbildung 1-9: The Regulator
Rechts unterhalb des Bereichs für Ihre Regex klicken Sie auf den Input-Button, um den Bereich anzuzeigen, in dem Ihr Ausgangstext eingegeben werden kann. Klicken Sie auf den Button Replace with, um den Ersetzungstext einzugeben, wenn Sie suchen und ersetzen wollen. Links unterhalb der Regex können Sie die Ergebnisse Ihrer Regex-Operation sehen. Diese werden nicht automatisch aktualisiert, Sie müssen stattdessen auf einen der Buttons Match, Replace oder Split in der Toolbar klicken. Es gibt für die Eingabe kein Highlighting. Klicken Sie auf eine Übereinstimmung in den Ergebnissen, um sich den entsprechenden Teil im Ausgangstext anzeigen zu lassen.
20 | Kapitel 1: Einführung in reguläre Ausdrücke
Das Panel Regex Analyzer zeigt eine einfache Analyse Ihres regulären Ausdrucks in englischer Sprache. Diese wird allerdings nicht automatisch erstellt und ist auch nicht interaktiv. Um die Analyse zu aktualisieren, wählen Sie im Menü View den Punkt Regex Analyzer, auch wenn sie schon sichtbar ist. Klicken Sie dagegen auf die Analyse, wird nur der Textcursor bewegt.
grep Der Name grep leitet sich vom Befehl g/re/p ab, der im Unix-Texteditor ed eine Suche mithilfe eines regulären Ausdrucks durchführt. Dieser Befehl wurde so häufig genutzt, dass mittlerweile alle Unix-Systeme ein eigenes grep-Tool haben, um Dateien mit regulären Ausdrücken zu durchsuchen. Wenn Sie Unix, Linux oder OS X nutzen, geben Sie in einem Terminal-Fenster man grep ein, um alles darüber zu lernen. Die folgenden drei Tools sind Windows-Anwendungen, die das tun, was auch grep tut, und sogar mehr.
PowerGREP PowerGREP wurde von Jan Goyvaerts entwickelt, einem der Autoren dieses Buchs, und ist vermutlich das umfassendste grep-Tool, das für Microsoft Windows verfügbar ist (Abbildung 1-10). PowerGREP nutzt eine eigene Regex-Variante, die das Beste aller in diesem Buch besprochenen Varianten kombiniert. Diese Variante wird in RegexBuddy als „JGsoft“ bezeichnet. Um schnell eine Suche mit regulären Ausdrücken durchzuführen, klicken Sie im ActionMenü einfach auf Clear und geben Ihren regulären Ausdruck in das Suchfeld des ActionPanels ein. Klicken Sie dann auf einen Ordner im Panel File Selector und wählen Sie im Menü File Selector entweder Include File or Folder oder Include Folder and Subfolders. Dann wählen Sie im Action-Menü Execute aus, um Ihre Suche zu starten. Um Suchergebnisse auch zu ersetzen, wählen Sie in der linken oberen Ecke des ActionPanels in der Auswahlliste Action Type den Eintrag search-and-replace aus. Es erscheint dann unterhalb des Suchfelds ein Replace-Feld. Geben Sie hier Ihren Ersetzungstext ein. Alle anderen Schritte laufen genau so ab wie beim reinen Suchen. PowerGREP besitzt die einmalige Fähigkeit, bis zu drei Listen mit regulären Ausdrücken gleichzeitig nutzen zu können, wobei jede Liste eine beliebige Anzahl von Regexes enthalten kann. Während die beiden vorigen Absätze beschrieben haben, wie Sie einfache Suchen durchführen können, die auch jedes andere grep-Tool ermöglicht, muss man schon ein bisschen in der umfangreichen Dokumentation zu PowerGREP lesen, um alle Möglichkeiten ausschöpfen zu können.
Tools für das Arbeiten mit regulären Ausdrücken | 21
Abbildung 1-10: PowerGREP
PowerGREP läuft unter Windows 98, ME, 2000, XP und Vista. Sie können eine kostenlose Testversion über http://www.powergrep.com/PowerGREPCookbook.exe herunterladen. Abgesehen von der Möglichkeit, Ergebnisse und Bibliotheken speichern zu können, ist die Testversion für 15 Tage vollständig nutzbar. In dieser Zeit lassen sich zwar keine Ergebnisse aus dem Results-Panel abspeichern, aber Suchen-und-Ersetzen-Vorgänge ändern trotzdem Ihre Dateien, so wie es die vollständige Version auch tut.
Windows Grep Windows Grep (http://www.wingrep.com) ist eines der ältesten grep-Tools für Windows. Seine Oberfläche ist zwar schon etwas angestaubt (Abbildung 1-11), aber es erledigt das, was es soll, ohne Probleme. Dabei wird eine eingeschränkte Regex-Variante namens POSIX ERE unterstützt. Bei den unterstützten Features wird die gleiche Syntax genutzt wie bei den Varianten in diesem Buch. Windows Grep ist Shareware, Sie können es also kostenlos herunterladen, aber es wird erwartet, dass Sie es bezahlen, wenn Sie längerfristig damit arbeiten wollen. Um mit einer Suche zu beginnen, wählen Sie im Search-Menü den Eintrag Search. Das sich daraufhin öffnende Fenster sieht je nach gewähltem Modus unterschiedlich aus – es gibt im Options-Menü einen Beginner Mode und einen Expert Mode. Anfänger erhalten
22 | Kapitel 1: Einführung in reguläre Ausdrücke
Abbildung 1-11: Windows Grep
einen Wizard, der sie durch die einzelnen Schritte führt, während Experten einen Dialog mit Registerkarten vorfinden. Wenn Sie eine Suche eingerichtet haben, führt Windows Grep sie sofort aus und zeigt Ihnen eine Liste von Dateien an, in denen Treffer gefunden wurden. Klicken Sie auf eine der Dateien, um die Übereinstimmungen zu sehen. Per Doppelklick öffnen Sie die Datei. Mit All Matches im View-Menü zeigt das untere Panel alles an. Um ein Suchen und Ersetzen durchzuführen, wählen Sie im Search-Menü den Eintrag Replace.
RegexRenamer RegexRenamer (Abbildung 1-12) ist eigentlich kein grep-Tool. Statt den Inhalt von Dateien zu durchsuchen, sucht und ersetzt es in Dateinamen. Sie können das Programm auf http://regexrenamer.sourceforge.net herunterladen. RegexRenamer benötigt das .NET Framework von Microsoft in der Version 2.0.
Tools für das Arbeiten mit regulären Ausdrücken | 23
Abbildung 1-12: RegexRenamer
Geben Sie Ihren regulären Ausdruck in das Feld Match ein, den Ersetzungstext ins Feld Replace. Klicken Sie auf /i, um Groß- und Kleinschreibung zu ignorieren, oder auf /g, um alle Übereinstimmungen in einem Dateinamen zu ersetzen statt nur die erste. Mit /x wird zur Freiformsyntax gewechselt, was hier nicht sehr sinnvoll ist, da Sie nur eine Zeile haben, um Ihren regulären Ausdruck einzugeben. Nutzen Sie den Baum auf der linken Seite, um den Ordner auszuwählen, der die umzubenennenden Dateien enthält. Sie können in der rechten oberen Ecke eine Dateimaske oder einen Regex-Filter definieren. Damit wird die Liste der Dateien, auf die Ihre Regex zum Suchen und Ersetzen angewandt werden soll, eingeschränkt. Es ist viel praktischer, eine Regex zum Filtern und eine zum Ersetzen zu nutzen, statt beides mit einer einzigen Regex abhandeln zu wollen.
Beliebte Texteditoren Die meisten modernen Texteditoren bieten zumindest eine grundlegende Unterstützung für reguläre Ausdrücke an. Beim Suchen oder beim Ersetzen finden Sie im Allgemeinen eine Checkbox, mit der reguläre Ausdrücke genutzt werden können. Manche Editoren, wie zum Beispiel EditPad Pro, nutzen zudem reguläre Ausdrücke für verschiedenste Features bei der Textbearbeitung, wie zum Beispiel beim Syntax-Highlighting oder zum Erstellen von Klassen- und Funktionslisten. Die Dokumentation jedes Editors beschreibt alle diese Features. Einige der beliebtesten Texteditoren mit einer Unterstützung regulärer Ausdrücke sind:
24 | Kapitel 1: Einführung in reguläre Ausdrücke
• Boxer Text Editor (PCRE) • Dreamweaver (JavaScript) • EditPad Pro (eigene Variante, die das Beste der Varianten aus diesem Buch kombiniert, in RegexBuddy als „JGsoft“ bezeichnet) • Multi-Edit (PCRE, wenn Sie die Option Perl auswählen) • NoteTab (PCRE) • UltraEdit (PCRE) • TextMate (Ruby 1.9 [Oniguruma])
Tools für das Arbeiten mit regulären Ausdrücken | 25
KAPITEL 2
Grundlagen regulärer Ausdrücke
Die in diesem Kapitel vorgestellten Probleme sind keine echten Probleme, die Ihr Chef oder Ihr Kunde von Ihnen gelöst haben will. Stattdessen sind es technische Probleme, denen Sie sich vielleicht gegenübersehen, wenn Sie reguläre Ausdrücke erstellen oder bearbeiten, mit denen die eigentlichen Probleme gelöst werden sollen. Das erste Rezept erklärt zum Beispiel, wie man literalen Text mit einem regulären Ausdruck finden kann. Das ist normalerweise nicht das eigentliche Ziel, da Sie keinen regulären Ausdruck brauchen, wenn Sie nur nach literalem Text suchen wollen. Aber wenn Sie eine Regex erstellen, werden Sie auch literalen Text finden wollen, daher müssen Sie wissen, welche Zeichen zu maskieren sind. Rezept 2.1 beschreibt Ihnen, wie das geht. Die Rezepte beginnen mit sehr einfachen Techniken für reguläre Ausdrücke. Wenn bereits Sie Regexes verwendet haben, reicht es wahrscheinlich, sie nur zu überfliegen, oder Sie überspringen sie sogar. Die Rezepte weiter unten in diesem Kapitel werden Ihnen dann aber sicherlich doch etwas Neues vermitteln, sofern Sie nicht schon Reguläre Ausdrücke von Jeffrey E. F. Friedl (O’Reilly) von vorne bis hinten durchgelesen haben. Wir haben die Rezepte in diesem Kapitel so zusammengestellt, dass jedes einen bestimmten Aspekt der Syntax regulärer Ausdrücke erklärt. Lesen Sie sie von Anfang bis Ende, um sich ein solides Wissen über regulären Ausdrücke anzueignen. Oder Sie schauen sich direkt die reguläre Ausdrücke aus der realen Welt an, die Sie in den Kapiteln 4 bis 8 finden, um dann dort den Verweisen auf dieses Kapitel zu folgen, wenn Sie sich einer Ihnen unbekannten Syntax gegenübersehen. Dieses Tutorium-Kapitel kümmert sich nur um reguläre Ausdrücke und ignoriert vollständig sämtliche Überlegungen zur Programmierung. Das nächste Kapitel ist das mit den ganzen Codebeispielen. Sie können schon mal einen Blick auf „Programmiersprachen und Regex-Varianten“ in Kapitel 3 werfen, um herauszufinden, welche Variante regulärer Ausdrücke Ihre Programmiersprache nutzt. Die Varianten, die in diesem Kapitel behandelt werden, wurden in „Regex-Varianten in diesem Buch“ auf Seite 3, vorgestellt.
| 27
2.1
Literalen Text finden
Problem Erstellen eines regulären Ausdrucks, der genau auf den so hübsch ausgedachten folgenden Satz passt: Die Sonderzeichen in der ASCII-Tabelle sind: !"#$%&'()*+,-./:;?@ [\]^_`{|}~.
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Jeder reguläre Ausdruck, der keines der zwölf Zeichen $()*+.?[\^{| enthält, findet einfach sich selbst. Um herauszufinden, ob sich in dem Text, den Sie gerade bearbeiten, Ein Loch ist im Eimer findet, suchen Sie einfach nach ‹EinzLochzistzimzEimer›. Es ist dabei egal, ob das Kontrollkästchen Regulärer Ausdruck in Ihrem Texteditor markiert ist oder nicht. Die zwölf Sonderzeichen, durch die die regulären Ausdrücke so spannend werden, heißen Metazeichen. Wenn Sie mit Ihrer Regex literal nach ihnen suchen wollen, müssen Sie sie maskieren, indem Sie einen Backslash vor sie setzen. Somit passt die Regex: \$\(\)\*\+\.\?\[\\\^\{\|
zum Text: $()*+.?[\^{|
Bemerkenswerterweise fehlen in dieser Liste die schließende eckige Klammer ], der Bindestrich - und die schließende geschweifte Klammer }. Die Klammer ]wird nur dann zu einem Metazeichen, wenn es vorher ein nicht maskiertes [ gab, und } nur nach einem nicht maskierten {. Es gibt keinen Grund, } jemals zu maskieren. Regeln für Metazeichen für die Blöcke zwischen [ und ] werden in Rezept 2.3 erläutert. Das Maskieren eines beliebigen anderen nicht alphanumerischen Zeichens ändert nichts daran, wie Ihr regulärer Ausdruck arbeitet – zumindest nicht, solange Sie mit einer der in diesem Buch behandelten Varianten arbeiten. Das Maskieren eines alphanumerischen Zeichens verpasst ihm entweder eine spezielle Bedeutung, oder es führt zu einem Syntaxfehler. User, die mit regulären Ausdrücken noch nicht so vertraut sind, maskieren häufig alle Sonderzeichen, die ihnen über den Weg laufen. Lassen Sie nicht jeden wissen, dass Sie ein Anfänger sind. Maskieren Sie weise. Ein Dschungel unnötiger Backslashs sorgt dafür,
28 | Kapitel 2: Grundlagen regulärer Ausdrücke
dass reguläre Ausdrücke schwerer zu lesen sind, insbesondere wenn alle diese Backslashs auch noch verdoppelt werden müssen, um die Regex als literalen String im Quellcode unterbringen zu können.
Regex-Optionen: Keine Regex-Varianten: Java 6, PCRE, Perl Perl, PCRE und Java unterstützen die Regex-Tokens ‹\Q› und ‹\E›. ‹\Q› unterdrückt die Bedeutung aller Metazeichen, einschließlich des Backslashs, bis ein ‹\E› kommt. Wenn Sie ‹\E› weglassen, werden alle Zeichen nach dem ‹\Q› bis zum Ende des Regex als Literale behandelt. Einziger Vorteil von ‹\Q...\E› ist, dass es leichter lesbar ist als ‹\.\.\.›. Obwohl Java 4 und 5 dieses Feature unterstützen, sollten Sie es nicht verwenden. Fehler in der Implementierung führen dazu, dass reguläre Ausdrücke mit ‹\Q...\E› zu etwas anderem passen, als Sie erwarten und als PCRE, Perl oder Java 6 finden. Diese Fehler wurden in Java 6 behoben, womit es sich genau so verhält wie PCRE und Perl.
Übereinstimmungen unabhängig von Groß- und Kleinschreibung ascii
Regex-Optionen: Ignorieren von Groß- und Kleinschreibung Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby (?i)ascii
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Standardmäßig reagieren reguläre Ausdrücke auf Groß- und Kleinschreibung. ‹regex› passt zu regex, aber nicht zu Regex, REGEX oder ReGeX. Um ‹regex› zu all diesen Texten passen zu lassen, muss die Regex Groß- und Kleinschreibung ignorieren. Bei den meisten Anwendungen lässt sich das über das Markieren eines Kontrollkästchens erledigen. Alle Programmiersprachen, die im nächsten Kapitel behandelt werden, haben eine Option oder Eigenschaft, die Sie setzen können, damit Ihre Regex nicht auf Großund Kleinschreibung reagiert. Rezept 3.4 im nächsten Kapitel erläutert, wie Sie die zu jeder Lösung aufgeführten Regex-Optionen in Ihrem Quellcode umsetzen können.
2.1 Literalen Text finden | 29
Wenn Sie das Ignorieren von Groß- und Kleinschreibung nicht außerhalb der Regex einschalten können, lässt sich das auch über den Modus-Modifikator ‹(?i)› erreichen, wie bei ‹(?i)regex›. Das funktioniert mit den Varianten .NET, Java, PCRE, Perl, Python und Ruby. .NET, Java, PCRE, Perl und Ruby unterstützen lokale Modus-Modifikatoren, die nur einen Teil des regulären Ausdrucks beeinflussen. ‹empfindlich(?i)unempfindlich(?-i) empfindlich› passt zu empfindlichUNEMPFINDLICHempfindlich, aber nicht zu EMPFINDLICHunempfindlichEMPFINDLICH. ‹(?i)› schaltet das Ignorieren von Groß- und Kleinschreibung für den Rest der Regex ein, während ‹(?-i)› dies wieder deaktiviert. Beide lokalen Modifikatoren funktionieren als Umschalter. Rezept 2.10 zeigt, wie man lokale Modus-Modifikatoren mit Gruppen statt mit Umschaltern nutzt.
Siehe auch Rezepte 2.3 und 5.14.
2.2
Nicht druckbare Zeichen finden
Problem Finden eines Strings mit den folgenden ASCII-Steuerzeichen: Bell, Escape, Form Feed, Line Feed, Carriage Return, horizontaler Tab, vertikaler Tab. Diese Zeichen haben die hexadezimalen ASCII-Codes 07, 1B, 0C, 0A, 0D, 09, 0B.
Lösung \a\e\f\n\r\t\v
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Python, Ruby \x07\x1B\f\n\r\t\v
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Python, Ruby \a\e\f\n\r\t\0x0B
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby
Diskussion Sieben der meistgenutzten ASCII-Steuerzeichen haben eigene Maskierungssequenzen. Sie bestehen alle aus einem Backslash, gefolgt von einem Buchstaben. Das ist die gleiche Syntax, die auch in vielen Programmiersprachen für String-Literale genutzt wird. Tabelle 2-1 zeigt die gebräuchlichsten nicht druckbaren Zeichen und ihre Repräsentation.
Der Standard ECMA-262 unterstützt ‹\a› und ‹\e› nicht. Daher nutzen wir für die JavaScript-Beispiele im Buch eine andere Syntax, auch wenn viele Browser ‹\a› und ‹\e› trotzdem unterstützen. Perl unterstützt ‹\v› nicht, daher müssen wir hier eine andere Syntax für den vertikalen Tab verwenden. Diese Steuerzeichen, wie auch ihre alternative Syntax, die im folgenden Abschnitt gezeigt wird, können in Ihrem regulären Ausdruck innerhalb und außerhalb von Zeichenklassen gleichermaßen genutzt werden.
Variationen zur Repräsentation nicht druckbarer Zeichen Die 26 Steuerzeichen \cG\x1B\cL\cJ\cM\cI\cK
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Ruby 1.9 Mit ‹\cA› bis ‹\cZ› können Sie eines der 26 Steuerzeichen finden, die in der ASCIITabelle die Positionen 1 bis 26 einnehmen. Das c muss dabei kleingeschrieben sein. In den meisten Varianten ist es egal, ob der dem c folgende Buchstabe klein- oder großgeschrieben ist. Wir empfehlen aber, immer einen Großbuchstaben zu nutzen, da Java dies benötigt. Die Syntax kann praktisch sein, wenn Sie es gewohnt sind, Steuerzeichen auf Konsolensystemen einzugeben, indem Sie die Strg-Taste zusammen mit einem Buchstaben drücken. Auf einem Terminal schickt Strg-H einen Rückschritt. In einer Regex findet ‹\cH› dementsprechend einen Rückschritt. Python und die klassische Ruby-Engine in Ruby 1.8 unterstützen diese Syntax nicht, die Oniguruma-Engine in Ruby 1.9 dagegen schon. Das Escape-Steuerzeichen an Position 27 in der ASCII-Tabelle lässt sich so mit dem englischen Alphabet nicht mehr abbilden, daher müssen wir in diesem Fall in unserem regulären Ausdruck ‹\x1B› nutzen.
2.2 Nicht druckbare Zeichen finden |
31
Der 7-Bit-Zeichensatz \x07\x1B\x0C\x0A\x0D\x09\x0B
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Ein kleines \x gefolgt von zwei hexadezimalen Ziffern (als Großbuchstaben) findet ein einzelnes Zeichen im ASCII-Set. Abbildung 2-1, zeigt, welche hexadezimalen Kombinationen von ‹\x00› bis ‹\x7F› zu welchem Zeichen im ganzen ASCII-Zeichensatz passen. Die Tabelle ist so aufgebaut, dass die erste hexadezimale Ziffer links von oben nach unten läuft, während die zweite Ziffer oben von links nach rechts läuft.
0 1 2 3 4 5 6 7
0 1 2 3 4 5 6 7 8 9 NUL SOH STX ETX EOT ENQ ACK BEL BS HT DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SP ! " # $ % & ' ( ) 0 1 2 3 4 5 6 7 8 9 @ A B C D E F G H I P Q R S T U V W X Y ` a b c d e f g h i p q r s t u v w x y
A B C LF VT FF SUB ESC FS * + , : ; < J K L Z [ \ j k l z { |
D CR GS = M ] m }
E SO RS . > N ^ n ~
F SI US / ? O _ o DEL
Abbildung 2-1ASCII-Tabelle
Es hängt von Ihrer Regex-Engine und der Codepage, in der Ihr Ausgangstext kodiert ist, ab, welche Zeichen von ‹\x80› bis ‹\xFF› passen. Wir empfehlen, ‹\x80› bis ‹\xFF› nicht zu verwenden. Stattdessen greifen Sie besser auf die Unicode-Codepoint-Token zurück, die in Rezept 2.7, beschrieben werden. Wenn Sie Ruby 1.8 nutzen oder PCRE ohne UTF-8-Unterstützung kompiliert haben, können Sie die Unicode-Codepoints nicht nutzen. Ruby 1.8 und PCRE ohne UTF-8 sind Regex-Engines für 8-Bit-Zeichen. Sie kennen keine Textkodierungen oder Zeichen aus mehr als einem Byte. ‹\xAA› findet in diesen Engines einfach das Byte 0xAA, egal welches Zeichen 0xAA darstellt oder ob 0xAA sogar Teil eines Multibyte-Zeichens ist.
Siehe auch Rezept 2.7.
32 | Kapitel 2: Grundlagen regulärer Ausdrücke
2.3
Ein oder mehrere Zeichen finden
Problem Erstellen eines regulären Ausdrucks, der alle üblichen Schreibfehler von Kalender findet, sodass Sie dieses Wort in einem Dokument finden können, ohne den Rechtschreibkünsten des Autors vertrauen zu müssen. Es soll für jeden Vokal ein a oder e möglich sein. Zudem soll ein anderer regulärer Ausdruck erstellt werden, um ein einzelnes hexadezimales Zeichen zu finden. Schließlich soll noch eine dritte Regex erstellt werden, um ein einzelnes Zeichen zu finden, das kein hexadezimales Zeichen ist.
Lösung Kalender mit Schreibfehlern K[ae]l[ae]nd[ae]r
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Hexadezimales Zeichen [a-fA-F0-9]
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Nicht hexadezimales Zeichen [^a-fA-F0-9]
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Die Notation mit eckigen Klammern wird als Zeichenklasse bezeichnet. Eine Zeichenklasse passt zu einem einzelnen Zeichen aus einer Liste möglicher Zeichen. Die drei Klassen in der ersten Regex passen entweder zu einem a oder einem e, und zwar unabhängig voneinander. Wenn Sie Kalender mit dieser Regex testen, passt die erste Zeichenklasse zu a, die zweite zu e und die dritte ebenfalls zu e. Außerhalb von Zeichenklassen sind zwölf Sonderzeichen Metazeichen. Innerhalb einer Zeichenklasse haben nur vier Zeichen eine besondere Bedeutung: \, ^, - und ]. Wenn Sie Java oder .NET nutzen, ist die öffnende eckige Klammer [ ebenfalls ein Metazeichen innerhalb einer Zeichenklasse. Alle anderen Zeichen sind Literale und ergänzen die Zei-
2.3 Ein oder mehrere Zeichen finden | 33
chenklasse. Der reguläre Ausdruck ‹[$()*+.?{|]› passt zu jedem der neun Zeichen innerhalb der eckigen Klammern. Der Backslash maskiert immer das ihm folgende Zeichen, so wie er es auch außerhalb von Zeichenklassen tut. Das maskierte Zeichen kann ein einzelnes Zeichen sein oder der Anfang bzw. das Ende eines Bereichs. Die anderen vier Metazeichen spielen ihre spezielle Bedeutung nur dann aus, wenn sie an einer bestimmten Position stehen. Es ist möglich, sie als literale Zeichen in eine Zeichenklasse mit aufzunehmen, ohne sie zu maskieren. Dazu muss man sie dort einfügen, wo sich ihre besondere Bedeutung nicht auswirkt. ‹[][^-]› macht das deutlich, wenn Sie nicht gerade eine JavaScript-Implementierung nutzen, die sich strikt an den Standard hält. Aber wir empfehlen Ihnen, diese Metazeichen immer zu maskieren, sodass die vorige Regex so aussehen sollte: ‹[\]\[\^\-]›. Durch das Maskieren der Metazeichen wird Ihr regulärer Ausdruck besser verständlich. Alphanumerische Zeichen können nicht mit einem Backslash maskiert werden. Macht man es trotzdem, führt das entweder zu einem Fehler, oder man erzeugt ein Regex-Token (etwas mit einer speziellen Bedeutung in einem regulären Ausdruck). Wenn wir bestimmte andere Regex-Tokens besprechen, wie zum Beispiel in Rezept 2.2, erwähnen wir, ob sie innerhalb von Zeichenklassen genutzt werden können. Alle diese Tokens bestehen aus einem Backslash und einem Zeichen, manchmal gefolgt von einer Reihe weiterer Zeichen. Somit findet ‹[\r\n]› ein Carriage Return (\r) oder einen Line Break (\n). Der Zirkumflex (^) negiert die Zeichenklasse, wenn Sie ihn direkt nach der öffnenden eckigen Klammer platzieren. Dadurch wird durch die Zeichenklasse jedes Zeichen gefunden, das sich nicht in der Liste befindet. Eine negierte Zeichenklasse passt zu Zeilenumbruchzeichen, sofern Sie sie nicht zur negierten Zeichenklasse hinzufügen. Der Bindestrich (-) erstellt einen Bereich, wenn er zwischen zwei Zeichen platziert wird. Der Bereich besteht dann aus der Zeichenklasse mit dem Zeichen vor dem Bindestrich, dem Zeichen nach dem Bindestrich und allen Zeichen, die in numerischer Reihenfolge dazwischenliegen. Um zu wissen, welche Zeichen das sind, müssen Sie sich die ASCII- oder Unicode-Zeichentabelle anschauen. ‹[A-z]› enthält alle Zeichen in der ASCII-Tabelle zwischen dem großen A und dem kleinen z. Der Bereich enthält auch einige Sonderzeichen, daher sollte man eher die Zeichenklasse ‹[A-Z\[\\\]\^_`a-z]› nutzen, die die Zeichen expliziter angibt. Wir empfehlen Ihnen, Bereiche nur zwischen zwei Ziffern oder zwischen zwei Buchstaben, die beide entweder Groß- oder Kleinbuchstaben sind, zu erstellen Umgekehrte Bereiche, wie zum Beispiel ‹[z-a]›, sind nicht erlaubt.
Variationen Abkürzungen [a-fA-F\d]
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby
34 | Kapitel 2: Grundlagen regulärer Ausdrücke
Sechs Regex-Tokens, die aus einem Backslash und einem Buchstaben bestehen, stehen jeweils für Abkürzungen von Zeichenklassen. Sie können diese sowohl innerhalb als auch außerhalb einer Zeichenklasse nutzen. ‹\d› und ‹[\d]› entsprechen beide einer einzelnen Ziffer. Jedes Regex-Token mit einem Kleinbuchstaben hat auch ein entsprechendes Token mit einem Großbuchstaben, der für das Gegenteil steht. Daher entspricht ‹\D› jedem Zeichen, das keine Ziffer ist, ist also identisch mit ‹[^\d]›. ‹\w› findet ein einzelnes Wortzeichen. Ein Wortzeichen ist ein Zeichen, das als Teil eines
Worts vorkommen kann. Dazu gehören Buchstaben, Ziffern und der Unterstrich. Diese Festlegung mutet ein bisschen seltsam an, aber sie wurde getroffen, da diese Zeichen üblicherweise auch für Bezeichner in Programmiersprachen erlaubt sind. ‹\W› passt dementsprechend zu jedem Zeichen, das nicht Teil eines Worts ist. In Java, JavaScript, PCRE und Ruby entspricht ‹\w› immer ‹[a-zA-Z0-9_]›. In .NET und Perl sind auch Zeichen und Ziffern aus allen anderen Schriftsystemen enthalten (Kyrillisch, Thailändisch und so weiter). In Python sind die anderen Schriftzeichen und -ziffern nur enthalten, wenn Sie beim Erzeugen der Regex die Option UNICODE oder U mitgeben. ‹\d› folgt in allen Varianten den gleichen Regeln. In .NET und Perl sind Ziffern aus anderen Schriftsystemen immer enthalten, während Python sie nur dann berücksichtigt, wenn Sie die Option UNICODE oder U mitgeben. ‹\s› findet jedes Whitespace-Zeichen. Dazu gehören Leerzeichen, Tabs und Zeilenumbrüche. In .NET, Perl und JavaScript passt ‹\s› auch zu jedem Zeichen, das durch den Unicode-Standard als Whitespace definiert ist. Beachten Sie, dass JavaScript für ‹\s› Unicode nutzt, für ‹\d› und ‹\w› aber ASCII. ‹\S› passt zu jedem Zeichen, das nicht von ‹\s› gefunden wird.
Das Ganze wird noch inkonsistenter, wenn wir ‹\b› hinzufügen. ‹\b› ist keine Abkürzung für eine Zeichenklasse, sondern eine Wortgrenze. Sie gehen vielleicht davon aus, dass ‹\b› Unicode unterstützt, wenn dies auch ‹\w› tut, und nur ASCII, wenn auch ‹\w› nur auf ASCII zurückgreift, aber das ist nicht immer der Fall. Der Abschnitt „Wortzeichen“ auf Seite 46 in Rezept 2.6 geht da näher drauf ein.
Groß- und Kleinschreibung ignorieren (?i)[A-F0-9]
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby (?i)[^A-F0-9]
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Das Ignorieren von Groß- und Kleinschreibung beeinflusst auch Zeichenklassen, egal ob durch eine externe Option gesetzt (siehe Rezept 3.4) oder durch einen Modus-Modifikator innerhalb der Regex (siehe Rezept 2.1). Die oben gezeigten beiden Regexes verhalten sich genau so wie die in der ursprünglichen Lösung.
2.3 Ein oder mehrere Zeichen finden | 35
JavaScript hält sich an die gleichen Regeln, unterstützt aber nicht ‹(?i)›. Um einen regulären Ausdruck in JavaScript Groß- und Kleinschreibung ignorieren zu lassen, setzen Sie beim Erstellen die Option /i.
Variantenspezifische Features Zeichenklassendifferenz in .NET [a-zA-Z0-9-[g-zG-Z]]
Dieser reguläre Ausdruck passt zu einem einzelnen hexadezimalen Zeichen, allerdings auf recht umständlichen Wegen. Die grundlegende Zeichenklasse passt zu jedem alphanumerischen Zeichen, und eine eingebettete Klasse zieht dann davon die Buchstaben g bis z ab. Diese eingebettete Klasse muss am Ende der Basisklasse erscheinen und durch ein Minuszeichen (einen Bindestrich) von ihr „abgezogen“ werden: ‹[Klasse-[Subtrahend]]›. Die Differenz von Zeichenklassen ist insbesondere im Zusammenhang mit UnicodeEigenschaften, -Blöcken und -Schriftsystemen nützlich. So findet zum Beispiel ‹\p{IsThai}› jedes Zeichen im Thai-Block. ‹\P{N}› findet jedes Zeichen, das keine Nummerneigenschaft besitzt. Kombiniert man beides mithilfe der Differenz, findet ‹[\p{IsThai}-[\P{N}]]› jede der zehn thailändischen Ziffern.
Zeichenklassenvereinigung, -differenz und -schnittmenge in Java [a-f[A-F][0-9]] [a-f[A-F[0-9]]]
Java erlaubt es, eine Zeichenklasse in einer anderen einzubetten. Wenn die eingebettete Klasse direkt enthalten ist, entspricht die Ergebnisklasse der Vereinigungsmenge beider Klassen. Sie können so viele Klassen einbetten, wie Sie wollen. Beide obigen Regexes haben den gleichen Effekt wie die ursprüngliche Regex ohne die zusätzlichen eckigen Klammern. [\w&&[a-fA-F0-9\s]]
Diese Regex könnte einen Preis in einem Regex-Verschleierungswettbewerb gewinnen. Die grundlegende Zeichenklasse passt zu jedem Wortzeichen. Die eingebettete Klasse findet jedes hexadezimale Zeichen und jeden Whitespace. Die Ergebnisklasse ist die Schnittmenge von beiden, wodurch nur hexadezimale Zeichen gefunden werden und sonst nichts. Da die Basisklasse kein Whitespace findet und die eingebettete Klasse nicht die Zeichen ‹[g-zG-Z_]›, werden alle aus der Ergebnis-Zeichenklasse herausgenommen und es verbleiben nur die hexadezimalen Zeichen. [a-zA-Z0-9&&[^g-zG-Z]]
Dieser reguläre Ausdruck findet ein einzelnes hexadezimales Zeichen, aber ebenfalls recht umständlich. Die grundlegende Zeichenklasse findet jedes alphanumerische Zeichen und eine eingebettete Klasse zieht dann davon die Buchstaben g bis z ab. Bei dieser
36 | Kapitel 2: Grundlagen regulärer Ausdrücke
eingebetteten Klasse muss es sich um eine negierte Zeichenklasse handeln, der zwei Kaufmanns-Und-Zeichen vorangestellt sind: ‹[Klasse&&[^Subtrahend]]›. Vereinigungen und Differenzen von Zeichenklassen sind besonders nützlich, wenn man mit Unicode-Eigenschaften, -Blöcken und -Schriftsystemen arbeitet. So findet zum Beispiel ‹\p{IsThai}› jedes Zeichen im Thai-Block. ‹\P{N}› findet jedes Zeichen, das keine Nummerneigenschaft besitzt. Zusammen findet ‹[\p{IsThai}-[\P{N}]]› jede der zehn thailändischen Ziffern. Wenn Sie sich über die feinen Unterschiede des Regex-Tokens ‹\p› wundern, werden Sie die Antworten darauf in Rezept 2.7 finden.
Siehe auch Rezepte 2.1, 2.2 und 2.7.
2.4
Ein beliebiges Zeichen finden
Problem Finden eines Zeichens in Anführungszeichen. Bereitstellen einer Lösung, die ein beliebiges Zeichen zwischen den Anführungszeichen findet, mit Ausnahme eines Zeilenumbruchs. Bereitstellen einer anderen Regex, die wirklich jedes Zeichen zulässt, auch Zeilenumbrüche.
Lösung Jedes Zeichen mit Ausnahme von Zeilenumbrüchen '.'
Regex-Optionen: Keine (die Option Punkt passt zu Zeilenumbruch darf nicht gesetzt sein) Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Jedes Zeichen einschließlich Zeilenumbrüchen '.'
Regex-Optionen: Punkt passt zu Zeilenumbruch Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby '[\s\S]'
Regex-Optionen: Keine Regex-Varianten .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
2.4 Ein beliebiges Zeichen finden | 37
Diskussion Jedes Zeichen außer Zeilenumbrüchen Der Punkt ist eines der ältesten und einfachsten Elemente regulärer Ausdrücke. Seine Bedeutung war schon immer die, ein beliebiges einzelnes Zeichen zu finden. Allerdings ist nicht ganz klar, was beliebiges Zeichen genau bedeutet. Die allerersten Tools, die reguläre Ausdrücke nutzten, verarbeiteten Dateien Zeile für Zeile, daher enthielt der Ausgangstext auch nie Zeilenumbrüche. Die in diesem Buch besprochenen Programmiersprachen verarbeiten den Ausgangstext als Ganzes, egal ob er Zeilenumbrüche enthält oder nicht. Wenn Sie wirklich eine zeilenbasierte Verarbeitung brauchen, müssen Sie etwas Code schreiben, der Ihren Text in ein Array mit Zeilen aufteilt und die Regex auf jede Zeile in diesem Array anwendet. Rezept 3.21 zeigt Ihnen, wie das geht. Larry Wall, der Entwickler von Perl, wollte, dass Perl sich so verhält wie die klassischen zeilenbasierten Tools, bei denen der Punkt niemals einen Zeilenumbruch (\n) fand. Alle anderen in diesem Buch behandelten Varianten haben sich daran orientiert. ‹.› findet daher jedes Zeichen mit Ausnahme eines Zeilenumbruchs.
Jedes Zeichen einschließlich Zeilenumbrüchen Wenn Ihr regulärer Ausdruck auch über mehr als eine Zeile hinaus arbeiten soll, müssen Sie die Option Punkt passt zu Zeilenumbruch aktivieren. Diese Option läuft unter verschiedenen Namen. Perl und viele andere nennt sie verwirrenderweise „Single Line“Modus, während Java sie als „Dot All“-Modus bezeichnet. In Rezept 3.4 im nächsten Kapitel finden Sie alle Details. Aber egal wie der Name dieser Option in Ihrer bevorzugten Programmiersprache lautet – bezeichnen Sie sie am besten als „Punkt passt zu Zeilenumbruch“-Modus. Denn genau das tut die Option. Bei JavaScript ist eine andere Lösung erforderlich, denn es besitzt diese Option nicht. Wie in Rezept 2.3 erläutert, findet ‹\s› jedes Whitespace-Zeichen, während ‹\S› jedes Zeichen findet, das nicht von ‹\s› gefunden wird. Kombiniert man das zu ‹[\s\S]›, erhält man eine Zeichenklasse, die alle Zeichen enthält, einschließlich der Zeilenumbrüche. ‹[\d\D]› und ‹[\w\W]› haben den gleichen Effekt.
Punkt-Missbrauch Der Punkt ist das am meisten missbrauchte Zeichen in regulären Ausdrücken. ‹\d\d.\d\d.\d\d› ist zum Beispiel kein guter regulärer Ausdruck, um ein Datum zu finden. Er passt zwar zu 16-05-08, aber auch zu 99/99/99. Schlimmer noch ist, dass er auch zu 12345678 passt. Ein guter regulärer Ausdruck, der nur gültige Datumswerte findet, wird in einem späteren Kapitel behandelt. Aber es ist sehr leicht, den Punkt durch eine passendere Zeichenklasse zu ersetzen. ‹\d\d[/.\-]\d\d[/.\-]\d\d› erlaubt einen Schrägstrich, einen Punkt oder einen Bindestrich als Datumstrenner. Diese Regex findet zwar immer noch 99/99/99, aber nicht mehr 12345678.
38 | Kapitel 2: Grundlagen regulärer Ausdrücke
Verwenden Sie den Punkt nur, wenn Sie wirklich jedes Zeichen zulassen wollen. In allen anderen Situationen sollten Sie eher eine Zeichenklasse oder eine negierte Zeichenklasse verwenden.
Variationen (?s)'.'
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python (?m)'.'
Regex-Optionen: Keine Regex-Varianten: Ruby Wenn Sie den „Punkt passt zu Zeilenumbruch“-Modus nicht außerhalb des regulären Ausdrucks anschalten können, haben Sie die Möglichkeit, einen Modus-Modifikator an den Anfang des regulären Ausdrucks zu setzen. Wir erläutern das Konzept der ModusModifikatoren und deren Fehlen in JavaScript in Abschnitt „Übereinstimmungen unabhängig von Groß- und Kleinschreibung“ auf Seite 29 unter Rezept 2.1. ‹(?s)› ist der Modus-Modifikator für den „Punkt passt zu Zeilenumbruch“-Modus in .NET, Java, PCRE, Perl und Python. Das s steht für „Singe Line“-Modus, den etwas ver-
wirrenden Namen in Perl. Die Terminologie ist so irritierend, dass der Entwickler von Rubys Regex-Engine sie falsch kopiert hat. Ruby nutzt ‹(?m)›, um den „Punkt passt zu Zeilenumbruch“-Modus einzuschalten. Abgesehen vom Buchstaben ist die Funktionalität vollkommen identisch. Die neue Engine in Ruby 1.9 nutzt weiterhin ‹(?m)› für „Punkt passt zu Zeilenumbruch“. Die Bedeutung von ‹(?m)› in Perl wird in Rezept 2.5 erläutert.
Siehe auch Rezepte 2.3, 3.4 und 3.21.
2.5
Etwas am Anfang und/oder Ende einer Zeile finden
Problem Erstellen von vier regulären Ausdrücken. Finden des Worts Alpha, aber nur dann, wenn es am Anfang des Ausgangstexts steht. Finden des Worts Omega, aber nur, wenn es ganz am Ende des Ausgangstexts steht. Finden des Worts Anfang, aber nur, wenn es am Anfang einer Zeile steht. Finden des Worts Ende, aber nur, wenn es am Ende einer Zeile steht.
2.5 Etwas am Anfang und/oder Ende einer Zeile finden | 39
Lösung Anfang des Texts ^Alpha
Regex-Optionen: Keine (^ und $ passen auf Zeilenumbrüche darf nicht gesetzt sein) Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python \AAlpha
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby
Ende des Texts Omega$
Regex-Optionen: Keine (^ und $ passen auf Zeilenumbrüche darf nicht gesetzt sein) Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python Omega\Z
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby
Zeilenanfang ^Anfang
Regex-Optionen: ^ und $ passen auf Zeilenumbrüche Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Zeilenende Ende$
Regex-Optionen: ^ und $ passen auf Zeilenumbrüche Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Anker und Zeilen Die Regex-Tokens ‹^›, ‹$›, ‹\A›, ‹\Z› und ‹\z› werden als Anker bezeichnet. Sie passen zu keinem Zeichen. Stattdessen passen sie zu bestimmten Positionen, wodurch der reguläre Ausdruck an diesen Positionen verankert wird. Eine Zeile ist der Teil des Ausgangstexts, der zwischen dem Anfang des Texts und einem Zeilenumbruch, zwischen zwei Zeilenumbrüchen oder zwischen einem Zeilenumbruch und dem Ende des Texts liegt. Wenn es im Text keine Zeilenumbrüche gibt, wird der
40 | Kapitel 2: Grundlagen regulärer Ausdrücke
ganze Text als eine Zeile angesehen. Daher besteht der folgende Text aus vier Zeilen, jeweils einer für eins, zwei, einem leeren String und vier: eins zwei vier
Der Text könnte in einem Programm als eins(LF) zwei (LF|LF) vier repräsentiert werden.
Anfang des Texts Der Anker ‹\A› passt immer zum Anfang des Ausgangstexts, noch vor dem ersten Zeichen. Das ist der einzige Ort, an dem er passt. Setzen Sie ‹\A› an den Anfang Ihres regulären Ausdrucks, um zu prüfen, ob der Ausgangstext mit dem Text beginnt, den Sie finden wollen. Das „A“ muss ein Großbuchstabe sein. JavaScript unterstützt kein ‹\A›. Der Anker ‹^› entspricht ‹\A›, solange Sie nicht die Option ^ und $ passen auf Zeilenumbrüche aktiviert haben. Diese Option ist standardmäßig für alle Regex-Varianten außer Ruby abgeschaltet. Ruby bietet auch keine Möglichkeit an, diese Option zu deaktivieren. Sofern Sie nicht JavaScript nutzen, empfehlen wir Ihnen, immer ‹\A› statt ‹^› zu nutzen. Die Bedeutung von ‹\A› ändert sich niemals, dadurch vermeidet man Irritationen oder Fehler, wenn man die Regex-Optionen setzt.
Ende des Texts Die Anker ‹\Z› und ‹\z› passen immer zum Ende des Ausgangstexts, und zwar nach dem letzten Zeichen. Setzen Sie ‹\Z› oder ‹\z› an das Ende Ihres regulären Ausdrucks, um zu prüfen, ob der Ausgangstext mit dem Text endet, den Sie finden wollen. .NET, Java, PCRE, Perl und Ruby unterstützen sowohl ‹\Z› als auch ‹\z›. Python unterstützt nur ‹\Z›. JavaScript unterstützt weder ‹\Z› noch ‹\z›. Der Unterschied zwischen ‹\Z› und ‹\z› wird dann relevant, wenn das letzte Zeichen Ihres Ausgangstexts ein Zeilenumbruch ist. In diesem Fall passt ‹\Z› zum einen am Ende des Ausgangstexts, aber auch direkt vor diesem Zeilenumbruch. Der Vorteil ist, dass Sie nach ‹Omega\Z› suchen können, ohne sich darum kümmern zu müssen, einen abschließenden Zeilenumbruch zu entfernen. Wenn Sie eine Datei Zeile für Zeile einlesen, nehmen einige Tools den Zeilenumbruch am Ende der Zeile mit auf, während andere ihn weglassen. ‹\Z› übergeht diesen Unterschied, während ‹\z› nur ganz am Ende des Ausgangstexts passt, sodass es nicht passt, wenn noch ein abschließender Zeilenumbruch folgt. Der Anker ‹$› entspricht ‹\Z›, sofern Sie die Option ^ und $ passen auf Zeilenumbrüche nicht eingeschaltet haben. Diese Option ist standardmäßig für alle Regex-Varianten außer Ruby ausgeschaltet. Ruby bietet keine Möglichkeit an, diese Option zu deaktivieren. Wie bei ‹\Z› passt ‹$› ganz am Ende des Ausgangstexts, aber auch direkt vor dem letzten, abschließenden Zeilenumbruch, wenn es einen gibt. 2.5 Etwas am Anfang und/oder Ende einer Zeile finden | 41
Um diese subtile und ein wenig konfuse Situation zu entwirren, wollen wir uns ein Beispiel in Perl anschauen. Davon ausgehend, dass $/ (der aktuelle Datensatztrenner) auf den Standardwert \n gesetzt ist, liest die folgende Perl-Anweisung eine einzelne Zeile vom Terminal ein (Standardeingabe): $line = ;
Perl belässt den Zeilenumbruch im Inhalt der Variablen $line. Daher wird ein Ausdruck wie ‹EndezderzEingabe.\z› nicht passen. Aber sowohl ‹EndezderzEingabe.\Z› als auch ‹EndezderzEingabe.$› werden gefunden, da sie den abschließenden Zeilenumbruch ignorieren. Um die Verarbeitung zu vereinfachen, entfernen Perl-Programmierer häufig die Zeilenumbrüche mit: chomp $line;
Danach werden alle drei Anker passen. (Technisch gesehen, entfernt chomp den aktuellen Datensatzseperator vom Ende eines Strings.) Sofern Sie nicht JavaScript nutzen, empfehlen wir, immer ‹\Z› statt ‹$› zu verwenden. Die Bedeutung von ‹\Z› ändert sich niemals, dadurch vermeidet man Irritationen oder Fehler, wenn man die Regex-Optionen setzt.
Anfang einer Zeile Standardmäßig passt ‹^› nur am Anfang des Ausgangstexts, so wie ‹\A›. Lediglich in Ruby passt ‹^› immer am Anfang einer Zeile. Bei allen anderen Varianten müssen Sie die Option einschalten, mit der der Zirkumflex und das Dollarzeichen auch bei Zeilenumbrüchen passen. Diese Option wird üblicherweise als „Multiline“-Modus bezeichnet. Verwechseln Sie das nicht mit dem „Singleline“-Modus, der besser als „Punkt passt zu Zeilenumbruch“-Modus bezeichnet werden sollte. Der „Multiline“-Modus betrifft nur den Zirkumflex und das Dollarzeichen, während der „Singleline“-Modus ausschließlich den Punkt beeinflusst, wie in Rezept 2.4 beschrieben wurde. Es ist ohne Probleme möglich, sowohl den „Singleline“- als auch den „Multiline“-Modus gleichzeitig einzuschalten. Standardmäßig sind beide Optionen ausgeschaltet. Mit den korrekten Optionen passt ‹^› am Anfang jeder Zeile des Ausgangstexts. Genauer gesagt, passt er vor dem ersten Zeichen in der Datei, so wie er es immer tut, und nach jedem Zeilenumbruchzeichen im Ausgangstext. Der Zirkumflex ist in ‹\n^› redundant, weil ‹^› immer nach einem ‹\n› passt.
Ende einer Zeile Standardmäßig passt ‹$› nur am Ende des Ausgangstexts oder vor dem letzten Zeilenumbruch, so wie ‹\Z›. Lediglich in Ruby passt ‹$› immer am Ende jeder Zeile. Bei allen anderen Varianten müssen Sie die „Multiline“-Option aktivieren, damit Zirkumflex und Dollarzeichen auch bei Zeilenumbrüchen passen.
42 | Kapitel 2: Grundlagen regulärer Ausdrücke
Mit den korrekt gesetzten Optionen passt ‹$› am Ende jeder Zeile im Ausgangstext. (Natürlich passt es auch nach dem letzten Zeichen im Text, weil das immer das Ende einer Zeile ist.) Das Dollarzeichen ist in ‹$\n› überflüssig, weil ‹$› immer vor ‹\n› passt.
Finden von Text ohne Inhalt Ein regulärer Ausdruck kann problemlos aus nichts mehr als einem oder mehreren Ankern bestehen. Solch ein regulärer Ausdruck hat dann an jeder Stelle, an der der Anker passt, ein Suchergebnis der Länge null. Wenn Sie mehrere Anker kombinieren, müssen alle Anker an der gleichen Stelle passen, damit die Regex etwas findet. Sie können solch einen regulären Ausdruck beim Suchen und Ersetzen nutzen. Ersetzen Sie ‹\A› oder ‹\Z›, um dem gesamten Ausgangstext etwas voranzustellen oder hinten anzufügen. Ersetzen Sie ‹^› oder ‹$› im „^ und $ passen bei Zeilenumbruch“-Modus, um jeder Zeile im Ausgangstext etwas voranzustellen oder am Ende anzufügen. Kombinieren Sie zwei Anker, um auf leere Zeilen oder eine fehlende Eingabe zu testen. ‹\A\Z› passt beim leeren String, aber auch bei einem String, der nur einen einzelnen Zeilenumbruch enthält. ‹\A\z› passt nur beim leeren String, ‹^$› im „^ und $ passen bei Zeilenumbruch“-Modus passt bei jeder leeren Zeile im Ausgangstext.
Variationen (?m)^Anfang
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python (?m)Ende$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python Wenn sich der „^ und $ passen bei Zeilenumbruch“-Modus nicht außerhalb des regulären Ausdrucks aktivieren lässt, können Sie einen Modus-Modifikator an den Anfang des regulären Ausdrucks setzen. Das Konzept von Modus-Modifikatoren und die fehlende Unterstützung in JavaScript werden in Abschnitt „Übereinstimmungen unabhängig von Groß- und Kleinschreibung“ auf Seite 29 in Rezept 2.1 erläutert. ‹(?m)› ist der Modus-Modifikator für den „^ und $ passen bei Zeilenumbruch“-Modus in .NET, Java, PCRE, Perl und Python. Das m steht für „Multiline“-Modus. Das ist die
verwirrende Perl-Bezeichnung für „^ und $ passen bei Zeilenumbruch“. Wie schon erläutert hat diese Terminologie so verwirrt, dass der Entwickler von Rubys Regex-Engine es falsch kopiert hat. Ruby nutzt ‹(?m)›, um den „Punkt passt zu Zeilenumbruch“-Modus einzuschalten. Rubys ‹(?m)› hat nichts mit dem Zirkumflex- und Dollar-Anker zu tun. In Ruby passen ‹^› und ‹$› immer am Anfang und Ende jeder Zeile.
2.5 Etwas am Anfang und/oder Ende einer Zeile finden | 43
Abgesehen von der unglücklichen Verwechslung der Buchstaben ist die Entscheidung von Ruby, ‹^› und ‹$› exklusiv für Zeilen zu nutzen, eine gute Entscheidung. Sofern Sie nicht JavaScript nutzen, empfehlen wir Ihnen, das auch in Ihre eigenen regulären Ausdrücke zu übernehmen. Jan hat diese Idee beim Design von EditPad Pro und PowerGREP umgesetzt. Sie werden kein Kontrollkästchen finden, das den Text „^ und $ passen bei Zeilenumbruch“ enthält, aber es gibt natürlich eins für „Punkt passt zu Zeilenumbruch“. Sofern Sie Ihren regulären Ausdruck nicht mit ‹(?-m)› beginnen, müssen Sie ‹\A› und ‹\Z› nutzen, um Ihre Regex am Anfang oder Ende Ihrer Datei zu verankern.
Siehe auch Rezepte 3.4 und 3.21.
2.6
Ganze Wörter finden
Problem Erstellen einer Regex, die rot in Mein Auto ist rot findet, aber nicht in rotieren oder Graubrot. Erstellen einer anderen Regex, die rot in Karotte findet, aber in keinem der vorigen drei Strings.
Lösung Wortgrenzen \brot\b
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Nicht-Wortgrenzen \Brot\B
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Wortgrenzen Das Regex-Token ‹\b› wird als Wortgrenze bezeichnet. Es passt an den Anfang oder das Ende eines Worts. Allein liefert es ein Ergebnis ohne Länge zurück. ‹\b› ist ein Anker, so wie die Tokens, die im vorigen Abschnitt vorgestellt wurden.
44 | Kapitel 2: Grundlagen regulärer Ausdrücke
Genau genommen passt ‹\b› an diesen drei Stellen: • Vor dem ersten Zeichen des Texts, wenn das erste Zeichen ein Wortzeichen ist. • Nach dem letzten Zeichen des Texts, wenn das letzte Zeichen ein Wortzeichen ist. • Zwischen zwei Zeichen im Text, wenn eines ein Wortzeichen und das andere kein Wortzeichen ist. Keine der in diesem Buch behandelten Varianten hat eigene Tokens, die nur vor oder nur nach einem Wort passen. Sofern Sie keine Regexes erstellen wollen, die nur aus Wortgrenzen bestehen, sind diese auch nicht notwendig. Die Tokens vor oder nach dem ‹\b› in Ihrem regulären Ausdruck legen fest, wo ‹\b› passen kann. Das ‹\b› in ‹\bx› und ‹!\b› passt nur am Wortanfang. Das ‹\b› in ‹x\b› und ‹\b!› kann nur am Ende eines Worts passen. ‹x\bx› und ‹!\b!› können niemals irgendwo passen. Um eine Suche nach ganzen Wörtern mit regulären Ausdrücken durchzuführen, stecken Sie das Wort einfach zwischen zwei Wortgrenzen, so wie wir es mit ‹\brot\b› gemacht haben. Das erste ‹\b› benötigt das ‹r›, damit es am Anfang des Texts oder nach einem Nicht-Wortzeichen passt. Das zweite ‹\b› benötigt das ‹t›, damit es entweder am Ende des Strings oder vor einem Nicht-Wortzeichen passt. Zeilenumbrüche sind Nicht-Wortzeichen. ‹\b› passt also nach einem Zeilenumbruch, wenn diesem direkt ein Wortzeichen folgt. Auch passt es vor einem Zeilenumbruch, wenn direkt davor ein Wortzeichen steht. So wird also ein Wort, das eine ganze Zeile einnimmt, auch von einer „Wortsuche“ gefunden. ‹\b› wird nicht vom „Multiline“-Modus oder ‹(?m)› beeinflusst. Das ist einer der Gründe dafür, dass dieses Buch den „Multiline“-Modus als „^ und $ passen bei Zeilenumbruch“-Modus bezeichnet.
Nicht-Wortgrenzen ‹\B› passt an jeder Position im Ausgangstext, an der ‹\b› nicht passt, und ‹\B› passt an
jeder Position, die nicht der Anfang oder das Ende eines Worts ist. Genauer gesagt, passt ‹\B› an diesen fünf Stellen: • • • • •
Vor dem ersten Zeichen des Texts, wenn das erste Zeichen kein Wortzeichen ist. Nach dem letzten Zeichen des Texts, wenn das letzte Zeichen kein Wortzeichen ist. Zwischen zwei Wortzeichen. Zwischen zwei Nicht-Wortzeichen. Beim leeren String.
‹\Brot\B› passt zu rot in Karotte, aber nicht zu Mein Auto ist rot, rotieren oder Graubrot.
Um das Gegenteil einer reinen Wortsuche durchzuführen (also Mein Auto ist rot auszuschließen, aber Karotte, rotieren und Graubrot zu finden), müssen Sie eine Alternation nutzen, um ‹\Brot› und ‹rot\B› zu ‹\Brot|rot\B› zu kombinieren. ‹\Brot› findet rot in Karotte und Graubrot. ‹rot\B› findet rot in rotieren (und Karotte, wenn sich ‹\Brot› nicht schon darum gekümmert hat). Rezept 2.8 beschreibt die Alternation. 2.6 Ganze Wörter finden | 45
Wortzeichen Jetzt haben wir die ganze Zeit über Wortgrenzen geredet, aber nicht darüber, was ein Wortzeichen ist. Ein Wortzeichen ist ein Zeichen, das als Teil eines Worts vorkommen kann. Der Abschnitt „Abkürzungen“ auf Seite 34 in Rezept 2.3 hat erklärt, welche Zeichen in ‹\w› enthalten sind, womit ein einzelnes Wortzeichen gefunden wird. Leider gilt für ‹\b› nicht das Gleiche. Auch wenn alle Varianten in diesem Buch ‹\b› und ‹\B› unterstützen, unterscheiden sie sich darin, welche Zeichen für sie Wortzeichen sind. .NET, JavaScript, PCRE, Perl, Python und Ruby lassen ‹\b› zwischen zwei Zeichen passend sein, bei denen eines durch ‹\w› gefunden wird und das andere durch ‹\W›. ‹\B› passt immer zwischen zwei Zeichen, die entweder durch ‹\w› oder ‹\W› gefunden werden. JavaScript, PCRE und Ruby betrachten nur ASCII-Zeichen als Wortzeichen. ‹\w› entspricht ‹[a-zA-Z0-9_]›. Bei diesen Varianten können Sie eine Wortsuche in Sprachen vornehmen, die nur die Buchstaben A bis Z, aber keine diakritischen Zeichen nutzen, wie zum Beispiel Englisch. Es lassen sich dort aber keine Wortsuchen in anderen Sprachen vornehmen, wie zum Beispiel im Spanischen oder im Russischen. .NET und Perl behandeln Buchstaben und Ziffern aus allen Schriftsystemen als Wortzeichen. Bei diesen Varianten können Sie eine Wortsuche in jeder Sprache durchführen, auch in solchen, die keine lateinischen Buchstaben nutzen. Python lässt Ihnen die Wahl. Nicht-ASCII-Zeichen werden nur dann berücksichtigt, wenn Sie beim Erstellen der Regex die Option UNICODE oder U mitgeben. Diese Option beeinflusst ‹\b› und ‹\w› gleichermaßen. Java verhält sich inkonsistent. ‹\w› passt nur auf ASCII-Zeichen, ‹\b› berücksichtigt dagegen Unicode und funktioniert mit jeder Sprache. In Java findet ‹\b\w\b› einen einzelnen englischen Buchstaben, eine Ziffer oder den Unterstrich, der in keiner Sprache der Welt als Teil eines Worts vorkommt. ‹\b кошка \b› findet das russische Wort für „rot”, da ‹\b› Unicode unterstützt. Aber ‹\w+› wird kein russisches Wort finden, weil ‹\w› nur für ASCII-Zeichen ausgelegt ist.
Siehe auch Rezept 2.3.
46 | Kapitel 2: Grundlagen regulärer Ausdrücke
2.7
Codepoints, Eigenschaften, Blöcke und Schriftsysteme bei Unicode
Problem Verwenden eines regulären Ausdrucks, um das Trademark-Zeichen (™) zu finden. Dazu soll sein Unicode-Codepoint angegeben und nicht ein vorhandenes Trademark-Zeichen kopiert werden. Wenn Sie kein Problem mit dem Kopieren haben, ist das TrademarkZeichen einfach nur ein weiteres literales Zeichen, selbst wenn Sie es nicht direkt über Ihre Tastatur eingeben können. Literale Zeichen werden in Rezept 2.1 behandelt. Erstellen eines regulären Ausdrucks, der jedes Zeichen mit der Unicode-Eigenschaft „Währungssymbol“ findet. Unicode-Eigenschaften werden auch als Unicode-Kategorien bezeichnet. Erstellen eines regulären Ausdrucks, der jedes Zeichen im Unicode-Block „Greek Extended“ findet. Erstellen eines regulären Ausdrucks, der jedes Zeichen findet, das laut Unicode-Standard Teil des griechischen Schriftsystems ist. Erstellen eines regulären Ausdrucks, der ein Graphem findet, also das, was üblicherweise als Zeichen angesehen wird: ein Basiszeichen mit all seinen Ergänzungen.
Lösung Unicode-Codepoint \u2122
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, Python Diese Regex funktioniert in Python nur, wenn sie als Unicode-String eingegeben wird: u"\u2122". \x{2122}
Regex-Optionen: Keine Regex-Varianten: PCRE, Perl, Ruby 1.9 PCRE muss mit UTF-8-Unterstützung kompiliert werden. In PHP muss die UTF-8Unterstützung durch den Muster-Modifikator /u aktiviert werden. Ruby 1.8 unterstützt keine Unicode-Regexes.
2.7 Codepoints, Eigenschaften, Blöcke und Schriftsysteme bei Unicode | 47
Unicode-Eigenschaft oder -Kategorie \p{Sc}
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Ruby 1.9 PCRE muss mit UTF-8-Unterstützung kompiliert werden. In PHP muss die UTF-8Unterstützung durch den Muster-Modifikator /u aktiviert werden. JavaScript und Python unterstützen keine Unicode-Eigenschaften. Ruby 1.8 unterstützt gar keine UnicodeRegexes.
Unicode-Block \p{IsGreekExtended}
Regex-Optionen: Keine Regex-Varianten: .NET, Perl \p{InGreekExtended}
Regex-Optionen: Keine Regex-Varianten: Java, Perl JavaScript, PCRE, Python und Ruby unterstützen keine Unicode-Blöcke.
Unicode-Schriftsystem \p{Greek}
Regex-Optionen: Keine Regex-Varianten: PCRE, Perl, Ruby 1.9 Für eine Unterstützung von Unicode-Schriftsystemen (Skripten) wird die PCRE 6.5 benötigt, die mit UTF-8-Unterstützung kompiliert werden muss. In PHP wird die UTF-8Unterstützung durch den Muster-Modifikator /u aktiviert. .NET, JavaScript und Python unterstützen keine Unicode-Schriftsysteme. Ruby 1.8 unterstützt gar keine UnicodeRegexes.
Unicode-Graphem \X
Regex-Optionen: Keine Regex-Varianten: PCRE, Perl PCRE und Perl haben ein eigenes Token für das Finden von Graphemen, sie unterstützen aber auch den Workaround mit Unicode-Eigenschaften. \P{M}\p{M}*
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Ruby 1.9
48 | Kapitel 2: Grundlagen regulärer Ausdrücke
PCRE muss mit UTF-8-Unterstützung kompiliert werden. In PHP muss die UTF-8Unterstützung durch den Muster-Modifikator /u aktiviert werden. JavaScript und Python unterstützen keine Unicode-Eigenschaften. Ruby 1.8 unterstützt gar keine UnicodeRegexes.
Diskussion Unicode-Codepoint Ein Codepoint ist ein Eintrag in der Unicode-Zeichendatenbank, jedoch nicht unbedingt das Gleiche wie ein Zeichen – abhängig davon, was Sie mit „Zeichen“ meinen. Was auf dem Bildschirm erscheint, wird in Unicode als Graphem bezeichnet. Der Unicode-Codepoint U+2122 steht für das „Trademark“-Zeichen. Sie können ihn, abhängig von der genutzten Variante, mit ‹\u2122› oder ‹\x{2122}› finden. Bei der ‹\u›-Syntax muss man genau vier hexadezimale Ziffern angeben. Sie können sie also nur für die Unicode-Codepoints von U+0000 bis U+FFFF nutzen. Bei der ‹\x›-Syntax lassen sich beliebig viele hexadezimale Ziffern angeben, wodurch alle Codepoints von U+000000 bis U+10FFFF unterstützt werden. U+00E0 finden Sie zum Beispiel mit ‹\x{E0}› oder ‹\x{00E0}›. Codepoints ab U+100000 werden nur sehr selten genutzt und auch durch Fonts und Betriebssysteme eher wenig unterstützt. Codepoints können innerhalb und außerhalb von Zeichenklassen genutzt werden.
Unicode-Eigenschaften oder -Kategorien Jeder Unicode-Codepoint hat genau eine Unicode-Eigenschaft oder passt zu einer einzelnen Unicode-Kategorie. Diese Begriffe haben dabei die gleiche Bedeutung. Es gibt 30 Unicode-Kategorien, die in sieben übergeordneten Kategorien zusammengefasst sind: ‹\p{L}›
Jegliche Buchstaben beliebiger Sprachen. ‹\p{Ll}›
Kleinbuchstaben, für die es auch Großbuchstaben gibt. ‹\p{Lu}›
Großbuchstaben, für die es auch Kleinbuchstaben gibt. ‹\p{Lt}›
Ein Buchstabe, der am Anfang eines Worts steht, wenn nur der erste Buchstabe des Worts großgeschrieben ist. ‹\p{Lm}›
Ein Sonderzeichen, das als Buchstabe verwendet wird. ‹\p{Lo}›
Ein Buchstabe oder Ideogramm, das keine Varianten in Klein- oder Großschreibung besitzt.
2.7 Codepoints, Eigenschaften, Blöcke und Schriftsysteme bei Unicode | 49
‹\p{M}›
Ein Zeichen, das dafür gedacht ist, mit anderen Zeichen kombiniert zu werden (Akzente, Umlaute, umschließende Kästchen und so weiter). ‹\p{Mn}›
Ein Zeichen, das dafür gedacht ist, mit einem anderen Zeichen kombiniert zu werden, und keinen zusätzlichen Raum einnimmt (zum Beispiel Akzente und Umlaute). ‹\p{Mc}›
Ein Zeichen, das dafür gedacht ist, mit einem anderen Zeichen kombiniert zu werden, und zusätzlichen Raum einnimmt (zum Beispiel Akzente in vielen östlichen Sprachen). ‹\p{Me}›
Ein Zeichen, das andere Zeichen umschließt (Kreise, Quadrate, Tastenkappen und so weiter). ‹\p{Z}›
Jeglicher Whitespace oder unsichtbare Trenner. ‹\p{Zs}›
Ein Whitespace-Zeichen, das unsichtbar ist, aber Raum einnimmt. ‹\p{Zl}›
Das Zeilentrennzeichen U+2028. ‹\p{Zp}›
Das Absatztrennzeichen U+2029. ‹\p{S}›
Mathematische Symbole, Währungssymbole, Dingbats, Zeichen für Kästen und so weiter. ‹\p{Sm}›
Mathematische Symbole. ‹\p{Sc}›
Währungssymbole. ‹\p{Sk}›
Ein Modifikator als eigenes Zeichen. ‹\p{So}›
Verschiedene Symbole, die keine mathematischen Symbole, Währungssymbole oder Modifikatorzeichen sind. ‹\p{N}›
Numerische Zeichen in allen Schriftsystemen. ‹\p{Nd}›
Eine Ziffer von 0 bis 9 in jeglichem Schriftsystem mit Ausnahme ideografischer Schriften. ‹\p{Nl}›
Eine Zahl, die wie ein Buchstabe aussieht, zum Beispiel eine römische Ziffer. ‹\p{No}›
Eine hoch- oder tiefgestellte Ziffer oder eine Zahl, die keine Ziffer von 0 bis 9 ist (außer Zahlen aus ideografischen Schriften). 50 | Kapitel 2: Grundlagen regulärer Ausdrücke
‹\p{P}›
Jegliches Satzzeichen. ‹\p{Pd}›
Striche (Bindestriche, Gedankenstriche und so weiter). ‹\p{Ps}›
Öffnende Klammern. ‹\p{Pe}›
Schließende Klammern. ‹\p{Pi}›
Öffnende Anführungszeichen. ‹\p{Pf}›
Schließende Anführungszeichen. ‹\p{Pc}›
Ein Satzzeichen, das Wörter verbindet, wie zum Beispiel ein Unterstrich. ‹\p{Po}›
Satzzeichen, die kein Strich, keine Klammer, kein Anführungszeichen und kein Verbindungszeichen sind. ‹\p{C}›
Unsichtbare Steuerzeichen und ungenutzte Codepoints. ‹\p{Cc}›
Ein Steuerzeichen im Bereich ASCII 0x00…0x1F oder Latin-1 0x80…0x9F. ‹\p{Cf}›
Eine unsichtbare Formatanweisung. ‹\p{Co}›
Codepoints, die für eigene Anwendungen reserviert sind. ‹\p{Cs}›
Eine Hälfte eines Stellvertreterpaars in UTF-16-Kodierung. ‹\p{Cn}›
Codepoints, denen kein Zeichen zugewiesen wurde. ‹\p{Ll}› passt zu einem einzelnen Codepoint, der die Eigenschaft Ll oder „Kleinbuchstabe“ besitzt. ‹\p{L}› ist eine einfachere Schreibweise der Zeichenklasse ‹[\p{Ll}\p{Lu}\ p{Lt}\p{Lm}\p{Lo}]›. Diese findet einen einzelnen Codepoint in einer der „Buchstaben“-
Kategorien. ‹\P› ist die negierte Version von ‹\p›. ‹\P{Ll}› passt zu einem einzelnen Codepoint, der nicht die Eigenschaft Ll besitzt. ‹\P{L}› passt zu einem einzelnen Codepoint, der keine der „Buchstaben“-Eigenschaften besitzt. Das ist nicht das Gleiche wie ‹[\P{Ll}\P{Lu}\P{Lt}\P{Lm}\P{Lo}]›, da mit Letzterem alle Codepoints gefunden werden. ‹\P{Ll}› passt zu den Codepoints mit der Eigenschaft Lu (und jeder anderen Eigenschaft außer Ll), während ‹\P{Lu}› die Codepoints mit Ll enthält. Kombiniert man beide in einer Codepoint-Klasse, werden immer alle möglichen Codepoints gefunden.
2.7 Codepoints, Eigenschaften, Blöcke und Schriftsysteme bei Unicode | 51
Unicode-Block Die Zeichendatenbank von Unicode teilt alle Codepoints in Blöcke auf. Jeder Block besteht aus einem zusammenhängenden Bereich von Codepoints. Die Codepoints U+0000 bis U+FFFF sind in 105 Blöcke unterteilt: U+0000…U+007F
‹\p{InBasic_Latin}›
U+0080…U+00FF
‹\p{InLatin-1_Supplement}›
U+0100…U+017F
‹\p{InLatin_Extended-A}›
U+0180…U+024F
‹\p{InLatin_Extended-B}›
U+0250…U+02AF
‹\p{InIPA_Extensions}›
U+02B0…U+02FF
‹\p{InSpacing_Modifier_Letters}›
U+0300…U+036F
‹\p{InCombining_Diacritical_Marks}›
U+0370…U+03FF
‹\p{InGreek_and_Coptic}›
U+0400…U+04FF
‹\p{InCyrillic}›
U+0500…U+052F
‹\p{InCyrillic_Supplementary}›
U+0530…U+058F
‹\p{InArmenian}›
U+0590…U+05FF
‹\p{InHebrew}›
U+0600…U+06FF
‹\p{InArabic}›
U+0700…U+074F
‹\p{InSyriac}›
U+0780…U+07BF
‹\p{InThaana}›
U+0900…U+097F
‹\p{InDevanagari}›
U+0980…U+09FF
‹\p{InBengali}›
U+0A00…U+0A7F
‹\p{InGurmukhi}›
U+0A80…U+0AFF
‹\p{InGujarati}›
U+0B00…U+0B7F
‹\p{InOriya}›
U+0B80…U+0BFF
‹\p{InTamil}›
U+0C00…U+0C7F
‹\p{InTelugu}›
U+0C80…U+0CFF
‹\p{InKannada}›
U+0D00…U+0D7F
‹\p{InMalayalam}›
U+0D80…U+0DFF
‹\p{InSinhala}›
U+0E00…U+0E7F
‹\p{InThai}›
U+0E80…U+0EFF
‹\p{InLao}›
U+0F00…U+0FFF
‹\p{InTibetan}›
U+1000…U+109F
‹\p{InMyanmar}›
U+10A0…U+10FF
‹\p{InGeorgian}›
U+1100…U+11FF
‹\p{InHangul_Jamo}›
U+1200…U+137F
‹\p{InEthiopic}›
52 | Kapitel 2: Grundlagen regulärer Ausdrücke
U+13A0…U+13FF
‹\p{InCherokee}›
U+1400…U+167F
‹\p{InUnified_Canadian_Aboriginal_Syllabics}›
U+1680…U+169F
‹\p{InOgham}›
U+16A0…U+16FF
‹\p{InRunic}›
U+1700…U+171F
‹\p{InTagalog}›
U+1720…U+173F
‹\p{InHanunoo}›
U+1740…U+175F
‹\p{InBuhid}›
U+1760…U+177F
‹\p{InTagbanwa}›
U+1780…U+17FF
‹\p{InKhmer}›
U+1800…U+18AF
‹\p{InMongolian}›
U+1900…U+194F
‹\p{InLimbu}›
U+1950…U+197F
‹\p{InTai_Le}›
U+19E0…U+19FF
‹\p{InKhmer_Symbols}›
U+1D00…U+1D7F
‹\p{InPhonetic_Extensions}›
U+1E00…U+1EFF
‹\p{InLatin_Extended_Additional}›
U+1F00…U+1FFF
‹\p{InGreek_Extended}›
U+2000…U+206F
‹\p{InGeneral_Punctuation}›
U+2070…U+209F
‹\p{InSuperscripts_and_Subscripts}›
U+20A0…U+20CF
‹\p{InCurrency_Symbols}›
U+20D0…U+20FF
‹\p{InCombining_Diacritical_Marks_for_Symbols}›
U+2100…U+214F
‹\p{InLetterlike_Symbols}›
U+2150…U+218F
‹\p{InNumber_Forms}›
U+2190…U+21FF
‹\p{InArrows}›
U+2200…U+22FF
‹\p{InMathematical_Operators}›
U+2300…U+23FF
‹\p{InMiscellaneous_Technical}›
U+2400…U+243F
‹\p{InControl_Pictures}›
U+2440…U+245F
‹\p{InOptical_Character_Recognition}›
U+2460…U+24FF
‹\p{InEnclosed_Alphanumerics}›
U+2500…U+257F
‹\p{InBox_Drawing}›
U+2580…U+259F
‹\p{InBlock_Elements}›
U+25A0…U+25FF
‹\p{InGeometric_Shapes}›
U+2600…U+26FF
‹\p{InMiscellaneous_Symbols}›
U+2700…U+27BF
‹\p{InDingbats}›
U+27C0…U+27EF
‹\p{InMiscellaneous_Mathematical_Symbols-A}›
U+27F0…U+27FF
‹\p{InSupplemental_Arrows-A}›
U+2800…U+28FF
‹\p{InBraille_Patterns}›
U+2900…U+297F
‹\p{InSupplemental_Arrows-B}›
2.7 Codepoints, Eigenschaften, Blöcke und Schriftsysteme bei Unicode | 53
U+2980…U+29FF
‹\p{InMiscellaneous_Mathematical_Symbols-B}›
U+2A00…U+2AFF
‹\p{InSupplemental_Mathematical_Operators}›
U+2B00…U+2BFF
‹\p{InMiscellaneous_Symbols_and_Arrows}›
U+2E80…U+2EFF
‹\p{InCJK_Radicals_Supplement}›
U+2F00…U+2FDF
‹\p{InKangxi_Radicals}›
U+2FF0…U+2FFF
‹\p{InIdeographic_Description_Characters}›
U+3000…U+303F
‹\p{InCJK_Symbols_and_Punctuation}›
U+3040…U+309F
‹\p{InHiragana}›
U+30A0…U+30FF
‹\p{InKatakana}›
U+3100…U+312F
‹\p{InBopomofo}›
U+3130…U+318F
‹\p{InHangul_Compatibility_Jamo}›
U+3190…U+319F
‹\p{InKanbun}›
U+31A0…U+31BF
‹\p{InBopomofo_Extended}›
U+31F0…U+31FF
‹\p{InKatakana_Phonetic_Extensions}›
U+3200…U+32FF
‹\p{InEnclosed_CJK_Letters_and_Months}›
U+3300…U+33FF
‹\p{InCJK_Compatibility}›
U+3400…U+4DBF
‹\p{InCJK_Unified_Ideographs_Extension_A}›
U+4DC0…U+4DFF
‹\p{InYijing_Hexagram_Symbols}›
U+4E00…U+9FFF
‹\p{InCJK_Unified_Ideographs}›
U+A000…U+A48F
‹\p{InYi_Syllables}›
U+A490…U+A4CF
‹\p{InYi_Radicals}›
U+AC00…U+D7AF
‹\p{InHangul_Syllables}›
U+D800…U+DB7F
‹\p{InHigh_Surrogates}›
U+DB80…U+DBFF
‹\p{InHigh_Private_Use_Surrogates}›
U+DC00…U+DFFF
‹\p{InLow_Surrogates}›
U+E000…U+F8FF
‹\p{InPrivate_Use_Area}›
U+F900…U+FAFF
‹\p{InCJK_Compatibility_Ideographs}›
U+FB00…U+FB4F
‹\p{InAlphabetic_Presentation_Forms}›
U+FB50…U+FDFF
‹\p{InArabic_Presentation_Forms-A}›
U+FE00…U+FE0F
‹\p{InVariation_Selectors}›
U+FE20…U+FE2F
‹\p{InCombining_Half_Marks}›
U+FE30…U+FE4F
‹\p{InCJK_Compatibility_Forms}›
U+FE50…U+FE6F
‹\p{InSmall_Form_Variants}›
U+FE70…U+FEFF
‹\p{InArabic_Presentation_Forms-B}›
U+FF00…U+FFEF
‹\p{InHalfwidth_and_Fullwidth_Forms}›
U+FFF0…U+FFFF
‹\p{InSpecials}›
54 | Kapitel 2: Grundlagen regulärer Ausdrücke
Ein Unicode-Block ist ein zusammenhängender, mit anderen Blöcken nicht überlappender Bereich von Codepoints. Auch wenn viele Blöcke die Namen von Unicode-Schriftsystemen und Unicode-Kategorien tragen, entsprechen sie ihnen nicht zu 100% Prozent. Der Name eines Blocks beschreibt nur seine wichtigsten Anwendungen. Der Währungsblock enthält nicht die Symbole für Dollar und Yen. Diese finden sich aus historischen Gründen in den Blöcken Basic_Latin und Latin-1_Supplement. Um ein beliebiges Währungssymbol zu finden, müssen Sie \p{Sc} statt \p{InCurrency} nutzen. Die meisten Blöcke enthalten auch nicht zugewiesene Codepoints, die durch die Eigenschaft ‹\p{Cn}› gekennzeichnet sind. Keine andere Unicode-Eigenschaft und keines der Unicode-Schriftsysteme enthält nicht zugewiesene Codepoints. Die Syntax mit ‹\p{InBlockName}› funktioniert bei .NET und Perl. Java verwendet eine Syntax mit ‹\p{IsBlockName}›. Perl unterstützt ebenfalls die Variante mit Is, aber wir empfehlen, bei In zu bleiben, um nicht mit Unicode-Schriftsystemen durcheinanderzukommen. Bei Schriftsystemen unterstützt Perl nämlich ‹\p{Script}› und ‹\p{IsScript}›, aber nicht ‹\p{InScript}›.
Unicode-Schriftsysteme Jeder Unicode-Codepoint ist Teil genau eines Unicode-Schriftsystems (abgesehen von den nicht zugewiesenen Codepoints, die zu keinem Schriftsystem gehören). Die zugewiesenen Codepoints bis U+FFFF gehören zu einem der folgenden Schriftsysteme (Skripten): ‹\p{Common}› ‹\p{Arabic}› ‹\p{Armenian}› ‹\p{Bengali}› ‹\p{Bopomofo}› ‹\p{Braille}› ‹\p{Buhid}› ‹\p{CanadianAboriginal}› ‹\p{Cherokee}› ‹\p{Cyrillic}› ‹\p{Devanagari}› ‹\p{Ethiopic}› ‹\p{Georgian}› ‹\p{Greek}› ‹\p{Gujarati}› ‹\p{Gurmukhi}› ‹\p{Han}› ‹\p{Hangul}› ‹\p{Hanunoo}›
Ein Schriftsystem ist eine Gruppe von Codepoints, die von einem bestimmten menschlichen Schreibsystem genutzt wird. Manche Schriftsysteme, wie zum Beispiel Thai, entsprechen einer einzelnen Sprache. Andere, wie Latin, umfassen mehrere Sprachen. Einige Sprachen bestehen aber auch aus mehreren Schriftsystemen. So gibt es zum Beispiel kein japanisches Unicode-Schriftsystem, stattdessen stellt Unicode die Schriftsysteme Hiragana, Katakana, Han und Latin bereit, aus denen japanische Dokumente normalerweise erstellt werden. Wir haben das Schriftsystem Common an erster Stelle aufgeführt, auch wenn es damit aus der alphabetischen Reihenfolge herausfällt. Dieses Schriftsystem beinhaltet alle Arten von Zeichen, die für viele Schriftsysteme genutzt werden, wie zum Beispiel Satzzeichen, Whitespace und verschiedene Symbole.
Unicode-Graphem Der Unterschied zwischen Codepoints und Zeichen wird dann relevant, wenn es sich um Kombinationszeichen handelt. Der Unicode-Codepoint U+0061 steht für „Latin small letter a“ (also das kleine „a“), während U+00E0 für „Latin small letter a with grave accent“ steht (also das kleine „à“). Beide stellen etwas dar, was die meisten Leute als Buchstaben beschreiben würden. U+0300 ist das Kombinationszeichen „combining grave accent“. Es kann nur nach einem Buchstaben sinnvoll verwendet werden. Ein String, der aus den Unicode-Codepoints U+0061 und U+0300 besteht, wird als à dargestellt, genauso wie U+00E0. Das Kombinationszeichen U+0300 wird dabei über dem Zeichen U+0061 angezeigt. Der Grund für diese beiden unterschiedlichen Vorgehensweisen beim Anzeigen eines Buchstaben mit Akzent liegt darin, dass viele alte Zeichensätze „a mit Gravis“ als einzelnes Zeichen kodieren. Die Designer von Unicode dachten, es wäre nützlich, sowohl alte Zeichensätze eins zu eins abbilden als auch den Unicode-Weg gehen zu können, bei dem Akzente und Grundbuchstaben getrennt sind. Mit Letzterem lassen sich nämlich beliebige Kombinationen erzeugen, die nicht von den alten Zeichensätzen unterstützt wurden. Für Sie als Regex-Anwender ist es wichtig zu wissen, dass alle in diesem Buch behandelten Regex-Varianten mit Codepoints und nicht mit grafischen Zeichen arbeiten. Wenn wir sagen, dass der reguläre Ausdruck ‹.› zu einem einzelnen Zeichen passt, passt er in Wirklichkeit nur zu einem einzelnen Codepoint. Besteht Ihre Text aus den beiden Codepoints U+0061 und U+0300, die zum Beispiel in einer Programmiersprache wie Java als String-Literal "\u0061\u0300" dargestellt werden können, wird der Punkt nur den Codepoint U+0061, also das a finden, ohne dabei den Akzent U+0300 zu berücksichtigen. Die Regex ‹..› wird beides finden.
56 | Kapitel 2: Grundlagen regulärer Ausdrücke
Perl und PCRE bieten ein spezielles Regex-Token ‹\X› an, das zu jedem einzelnen Unicode-Graphem passt. Im Endeffekt ist es die Unicode-Version des altehrwürdigen Punkts. Es passt zu jedem Unicode-Codepoint, der kein Kombinationszeichen ist, und schließt alle direkt darauffolgenden Kombinationszeichen ein, wenn es welche gibt. Mit ‹\P{M}\p{M}*› kann man das Gleiche mit der Unicode-Eigenschaftssyntax erreichen. ‹\X› findet zwei Übereinstimmungen im Text àà, egal wie dieser kodiert wurde. Wenn er als "\u00E0\u0061\u0300" kodiert ist, wird die erste Übereinstimmung "\u00E0" und die zweite "\u0061\u0300" sein.
Variationen Negierte Form Das großgeschriebene ‹\P› ist die negierte Form des kleinen ‹\p›. So passt zum Beispiel ‹\P{Sc}› zu jedem Zeichen, das nicht die Unicode-Eigenschaft „Währungssymbol“ besitzt. ‹\P› wird von allen Varianten für alle Eigenschaften, Blöcke und Schriftsprachen unterstützt, die auch ‹\p› anbieten.
Zeichenklassen Alle Varianten lassen Tokens der Form ‹\u›, ‹\x›, ‹\p› und ‹\P› auch innerhalb von Zeichenklassen zu – sofern sie sie überhaupt unterstützen. Das vom Codepoint repräsentierte Zeichen oder die Zeichen in der Kategorie, dem Block oder dem Schriftsystem werden der Zeichenklasse hinzugefügt. So können Sie zum Beispiel ein Zeichen finden, das entweder ein öffnendes Anführungszeichen, ein schließendes Anführungszeichen oder das Trademark-Symbol (U+2122) ist, indem Sie folgende Zeichenklasse nutzen: [\p{Pi}\p{Pf}\x{2122}]
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Ruby 1.9
Alle Zeichen auflisten Wenn die von Ihnen genutzte Regex-Variante keine Kategorien, Blöcke oder Schriftsysteme von Unicode unterstützt, können Sie die Zeichen, die sich in der Kategorie, im Block oder im Schriftsystem befinden, in einer Zeichenklasse aufführen. Bei Blöcken ist das sehr einfach – jeder Block besteht einfach aus einem Bereich zwischen zwei Codepoints. Der Block „Greek Extended“ besteht aus den Zeichen U+1F00 bis U+1FFF: [\u1F00-\u1FFF]
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, Python [\x{1F00}-\x{1FFF}]
Regex-Optionen: Keine Regex-Varianten: PCRE, Perl, Ruby 1.9
2.7 Codepoints, Eigenschaften, Blöcke und Schriftsysteme bei Unicode | 57
Bei den meisten Kategorien und in vielen Schriftsystemen muss man für die Zeichenklasse eine lange Liste einzelner Codepoints und kurzer Bereiche angeben. Die Zeichen jeder Kategorie und vieler Schriftsysteme sind über die ganze Unicode-Tabelle verstreut. Dies ist das griechische Schriftsystem: [\u0370-\u0373\u0375\u0376-\u0377\u037A\u037B-\u037D\u0384\u0386 \u0388-\u038A\u038C\u038E-\u03A1\u03A3-\u03E1\u03F0-\u03F5\u03F6 \u03F7-\u03FF\u1D26-\u1D2A\u1D5D-\u1D61\u1D66-\u1D6A\u1DBF\u1F00-\u1F15 \u1F18-\u1F1D\u1F20-\u1F45\u1F48-\u1F4D\u1F50-\u1F57\u1F59\u1F5B\u1F5D \u1F5F-\u1F7D\u1F80-\u1FB4\u1FB6-\u1FBC\u1FBD\u1FBE\u1FBF-\u1FC1 \u1FC2-\u1FC4\u1FC6-\u1FCC\u1FCD-\u1FCF\u1FD0-\u1FD3\u1FD6-\u1FDB \u1FDD-\u1FDF\u1FE0-\u1FEC\u1FED-\u1FEF\u1FF2-\u1FF4\u1FF6-\u1FFC \u1FFD-\u1FFE\u2126]
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, Python Wir haben diesen regulären Ausdruck aufgebaut, indem wir die Liste für das griechische Schriftsystem aus http://www.unicode.org/Public/UNIDATA/Scripts.txt kopierten und dann drei reguläre Ausdrücke nutzten, um sie per Suchen und Ersetzen anzupassen: 1. Suchen nach dem regulären Ausdruck ‹;.*› und Ersetzen seiner Übereinstimmun-
gen durch leeren Text. Das entfernt die Kommentare. Wenn unabsichtlich jeglicher Text gelöscht wird, machen Sie den Vorgang rückgängig und schalten die Option Punkt passt zu Zeilenumbruch ab. 2. Suchen nach ‹^› mit aktiver Option ^ und $ passen zu Zeilenumbruch und Ersetzen durch «\u». Das versieht die Codepoints mit \u. Mit einem Ersetzen von ‹\.\.› durch «-\u» werden die Bereiche angepasst. 3. Schließlich wird ‹\s+› durch leeren Text ersetzt, um die Zeilenumbrüche zu entfernen. Ergänzt man jetzt noch die eckigen Klammern, ist die Regex fertig. Eventuell müssen Sie zusätzlich ganz am Anfang der Zeichenklasse ein \u ergänzen und/oder es am Ende entfernen. Das hängt davon ab, ob Sie führende oder abschließende Leerzeilen aus Scripts.txt mitkopiert haben. Das mag recht aufwendig erscheinen, aber Jan brauchte dafür weniger als eine Minute. Das Erstellen der Beschreibung erforderte mehr Zeit. Für die \x{}-Syntax ist es genauso einfach: 1. Suchen nach dem regulären Ausdruck ‹;.*› und Ersetzen seiner Übereinstimmun-
gen durch leeren Text. Das entfernt die Kommentare. Wenn unabsichtlich jeglicher Text gelöscht wird, machen Sie den Vorgang rückgängig und schalten die Option Punkt passt zu Zeilenumbruch ab. 2. Suchen nach ‹^› mit aktiver Option ^ und $ passen zu Zeilenumbruch und Ersetzen durch «\x{». Das versieht die Codepoints mit \x{. Mit einem Ersetzen von ‹\.\.› durch «}-\x{» werden die Bereiche angepasst. 3. Schließlich wird ‹\s+› durch «}» ersetzt, um die schließende geschweifte Klammer zu ergänzen und die Zeilenumbrüche zu entfernen. Fügt man jetzt noch die eckigen Klammern hinzu, ist die Regex fertig. Eventuell müssen Sie ganz am Anfang der Zei-
58 | Kapitel 2: Grundlagen regulärer Ausdrücke
chenklasse ein \x{ ergänzen und/oder es am Ende entfernen. Das hängt davon ab, ob Sie führende oder abschließende Leerzeilen aus Scripts.txt mitkopiert haben. Das Ergebnis ist: [\x{0370}-\x{0373}\x{0375}\x{0376}-\x{0377}\x{037A}\x{037B}-\x{037D} \x{0384}\x{0386}\x{0388}-\x{038A}\x{038C}\x{038E}-\x{03A1} \x{03A3}-\x{03E1}\x{03F0}-\x{03F5}\x{03F6}\x{03F7}-\x{03FF} \x{1D26}-\x{1D2A}\x{1D5D}-\x{1D61}\x{1D66}-\x{1D6A}\x{1DBF} \x{1F00}-\x{1F15}\x{1F18}-\x{1F1D}\x{1F20}-\x{1F45}\x{1F48}-\x{1F4D} \x{1F50}-\x{1F57}\x{1F59}\x{1F5B}\x{1F5D}\x{1F5F}-\x{1F7D} \x{1F80}-\x{1FB4}\x{1FB6}-\x{1FBC}\x{1FBD}\x{1FBE}\x{1FBF}-\x{1FC1} \x{1FC2}-\x{1FC4}\x{1FC6}-\x{1FCC}\x{1FCD}-\x{1FCF}\x{1FD0}-\x{1FD3} \x{1FD6}-\x{1FDB}\x{1FDD}-\x{1FDF}\x{1FE0}-\x{1FEC}\x{1FED}-\x{1FEF} \x{1FF2}-\x{1FF4}\x{1FF6}-\x{1FFC}\x{1FFD}-\x{1FFE}\x{2126} \x{10140}-\x{10174}\x{10175}-\x{10178}\x{10179}-\x{10189} \x{1018A}\x{1D200}-\x{1D241}\x{1D242}-\x{1D244}\x{1D245}]
Regex-Optionen: Keine Regex-Varianten: PCRE, Perl, Ruby 1.9
Siehe auch http://www.unicode.org ist die offizielle Website des Unicode Consortium, von der Sie alle offiziellen Unicode-Dokumente, Zeichentabellen und so weiter herunterladen können. Unicode ist ein umfangreiches Thema, zu dem ganze Bücher geschrieben wurden. Eines davon ist Unicode Explained von Jukka K. Korpela (O’Reilly). Wir können nicht alles, was Sie über die Codepoints, Eigenschaften, Blöcke und Schriftsysteme wissen sollten, in einem Abschnitt beschreiben. Wir haben noch nicht einmal erklärt, warum Sie sich damit beschäftigen sollten – Sie sollten es einfach. Die bequeme Einfachheit der erweiterten ASCII-Tabelle ist in der heutigen globalisierten Welt ein einsames Plätzchen.
2.8
Eine von mehreren Alternativen finden
Problem Erstellen eines regulären Ausdrucks, der beim wiederholten Anwenden auf den Text Maria, Julia und Susanne gingen zu Marias Haus nacheinander Maria, Julia, Susanne und dann wieder Maria findet. Weitere Suchen sollten kein Ergebnis mehr liefern.
Lösung Maria|Julia|Susanne
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
2.8 Eine von mehreren Alternativen finden | 59
Diskussion Der vertikale Balken, auch Pipe-Symbol genannt, teilt den regulären Ausdruck in mehrere Alternativen auf. ‹Maria|Julia|Susanne› passt zu Maria oder Julia oder Susanne. Bei jeder Suche passt immer nur ein Name, aber es kann immer ein anderer sein. Alle Varianten regulärer Ausdrücke, die in diesem Buch behandelt werden, nutzen eine Regex-gesteuerte Engine. Die Engine ist einfach die Software, die dafür sorgt, dass der reguläre Ausdruck genutzt werden kann. Regex-gesteuert1 heißt, dass alle möglichen Permutationen des regulären Ausdrucks an jeder Stelle des Ausgangstexts ausprobiert werden, bevor die Regex mit dem nächsten Zeichen fortfährt. Wenn Sie ‹Maria|Julia|Susanne› auf Maria, Julia und Susanne gingen zu Marias Haus anwenden, wird die Übereinstimmung Maria direkt am Anfang des Strings gefunden. Wenden Sie die gleiche Regex auf den Rest des Strings an – indem Sie zum Beispiel in Ihrem Texteditor auf Weitersuchen klicken –, versucht die Regex-Engine, ‹Maria› auf das erste Komma im String anzuwenden. Das geht schief. Dann versucht sie, ‹Julia› an der gleichen Stelle zu finden, was ebenfalls misslingt, genauso wie der Versuch, ‹Susanne› am Komma zu finden. Erst dann springt die Regex-Engine zum nächsten Zeichen im String weiter. Aber auch beim ersten Leerzeichen wird keine der drei Alternativen gefunden. Geht die Engine nun zum J weiter, wird die erste Alternative ‹Maria› nicht gefunden. Dann wird versucht, die zweite Alternative ‹Julia› beim J zu finden. Das passt zu Julia. Die Regex-Engine hat endlich Erfolg. Beachten Sie, dass Julia gefunden wurde, obwohl es im Ausgangstext ein weiteres Vorkommen von Maria gibt und ‹Maria› in der Regex vor ‹Julia› steht. Zumindest in diesem Fall ist die Reihenfolge der Alternativen im regulären Ausdruck egal. Der reguläre Ausdruck findet die am weitesten links stehende Übereinstimmung. Der Text wird von links nach rechts überprüft, und es wird bei jedem Schritt versucht, alle Alternativen im regulären Ausdruck anzuwenden. Sobald eine der Alternativen passt, wird an der ersten Position abgebrochen. Wenn wir im restlichen Text weitersuchen, wird Susanne gefunden. Die vierte Suche findet dann erneut Maria. Bitten Sie die Engine jedoch um eine fünfte Suche, wird diese fehlschlagen, da keine der drei Alternativen zum verbleibenden String s Haus passt. Die Reihenfolge der Alternativen im regulären Ausdruck ist nur dann von Belang, wenn zwei von ihnen an der gleichen Position im Text passen können. Die Regex ‹Julia|Juliane› besitzt zwei Alternativen, die an der gleichen Position im Text Ihr Name ist Juliane passen. Es gibt im regulären Ausdruck keine Wortgrenzen. Die Tatsache, dass ‹Julia› das Wort Juliane in Ihr Name ist Juliane nur teilweise abdeckt, spielt keine Rolle. 1 Es gibt auch textgesteuerte Engines. Der Hauptunterschied liegt darin, dass eine textgesteuerte Engine jedes Zeichen im Text nur einmal aufsucht, während eine Regex-gesteuerte Engine jedes Zeichen eventuell mehrfach anfasst. Textgesteuerte Engines sind viel schneller, lassen sich aber nur mit regulären Ausdrücken im echten mathematischen Sinn nutzen (beschrieben am Anfang von Kapitel 1). Die tollen regulären Ausdrücke im PerlStil, die dieses Buch so interessant machen, lassen sich nur durch eine Regex-gesteuerte Engine umsetzen.
60 | Kapitel 2: Grundlagen regulärer Ausdrücke
‹Julia|Juliane› passt zu Julia in Ihr Name ist Juliane, weil eine Regex-gesteuerte
Engine ungeduldig ist. Während sie den Text von links nach rechts durchforstet, um die am weitesten links stehende Übereinstimmung zu finden, sucht sie auch in den Alternativen der Regex von links nach rechts. Sobald sie eine Alternative findet, die passt, bricht sie ab. Wenn ‹Julia|Juliane› das J in Ihr Name ist Juliane erreicht, passt die erste Alternative ‹Julia›. Die zweite Alternative wird gar nicht ausprobiert. Weisen wir die Engine an, nach einer zweiten Übereinstimmung zu suchen, ist vom Ausgangstext nur noch das ne übrig. Darauf passt keine der beiden Alternativen. Es gibt zwei Möglichkeiten, Julia davon abzuhalten, Juliane die Show zu stehlen. Eine ist, die längere Alternative an den Anfang zu stellen: ‹Juliane|Julia›. Besser und zuverlässiger ist es allerdings, genauer zu sagen, was wir wollen: Wir suchen nach Namen, und Namen sind vollständige Wörter. Reguläre Ausdrücke kümmern sich nicht um Wörter, aber sie können auf Wortgrenzen Rücksicht nehmen. Daher werden sowohl ‹\bJulia\b|\bJuliane\b› als auch ‹\bJuliane\b|\bJulia\b› den Namen Juliane in Ihr Name ist Juliane finden. Aufgrund der Wortgrenzen passt nur eine Alternative. Die Reihenfolge ist dabei unwichtig. Rezept 2.12 erklärt, warum ‹\bJuliane?\b› die beste Lösung ist.
Siehe auch Rezept 2.9.
2.9
Gruppieren und Einfangen von Teilen des gefundenen Texts
Problem Verbessern des regulären Ausdrucks zum Finden von Maria, Julia oder Susanne, indem die Übereinstimmung ein ganzes Wort sein soll. Durch Gruppieren soll ein Paar Wortgrenzen für die ganze Regex reichen, anstatt dass für jede Alternative Paare genutzt werden müssen. Erstellen eines regulären Ausdrucks, der jedes Datum im Format yyyy-mm-dd findet und dabei das Jahr, den Monat und den Tag getrennt einfängt. Ziel ist, mit diesen drei Werten im Code, der die Regex-Auswertung startet, arbeiten zu können. Es soll davon ausgegangen werden, dass alle Datumswerte in Ausgangstext gültig sind. Der reguläre Ausdruck muss sich nicht mit Werten wie 9999-99-99 herumschlagen, da sie nicht vorkommen.
2.9 Gruppieren und Einfangen von Teilen des gefundenen Texts | 61
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Der Alternationsoperator, der im vorigen Abschnitt beschrieben wurde, hat (nahezu) den höchsten Rang aller Regex-Operatoren. Wenn Sie probieren, ‹\bMaria|Julia|Susanne\b› zu nutzen, sind die drei Alternativen ‹\bMaria›, ‹Julia› und ‹Susanne\b›. Diese Regex findet Julia in Ihr Name ist Julia. Möchten Sie, dass in Ihrer Regex etwas von der Alternation ausgeschlossen wird, müssen Sie die Alternativen gruppieren. Das geschieht durch (normale, runde) Klammern. Sie haben, wie in den meisten Programmiersprachen, den allerhöchsten Rang. ‹\b(Maria|Julia|Susanne)\b› hat drei Alternativen – ‹Maria›, ‹Julia› und ‹Susanne› – zwischen zwei Wortgrenzen. Diese Regex passt nicht zu Ihr Name ist Juliane. Wenn die Regex-Engine im Ausgangstext das J in Juliane erreicht, passt die erste Wortgrenze. Dann kümmert sich die Engine um die Gruppe. Die erste Alternative in der Gruppe, ‹Maria›, schlägt fehl. Die zweite Alternative ‹Julia› ist erfolgreich. Die Engine verlässt die Gruppe. Nun bleibt noch das ‹\b›. Die Wortgrenze passt aber nicht zwischen das a und das ne am Ende des Texts. Somit gibt es bei J keine Übereinstimmung. Ein Klammernpaar ist nicht nur eine Gruppe, sondern sogar eine einfangende Gruppe. Bei der Regex für Maria, Julia und Susanne ist das Einfangen nicht so nützlich, da es nur um das Finden selbst geht. Das Einfangen lässt sich dann gebrauchen, wenn es nur um Teile der Regex geht, wie zum Beispiel bei ‹\b(\d\d\d\d)-(\d\d)-(\d\d)\b›. Dieser reguläre Ausdruck passt zu einem Datum im Format yyyy-mm-dd. Die Regex ‹\b\d\d\d\d-\d\d-\d\d\b› macht das Gleiche. Da dieser reguläre Ausdruck keine Alternation oder Wiederholung verwendet, wird die Gruppierungsfunktion der Klammern nicht benötigt. Aber das Einfangen ist sehr praktisch. Die Regex ‹\b(\d\d\d\d)-(\d\d)-(\d\d)\b› hat drei Gruppen zum Einfangen. Gruppen werden durchnummeriert, indem die öffnenden Klammern von links nach rechts gezählt werden. Somit ist ‹(\d\d\d\d)› die Gruppe 1, ‹(\d\d)› Gruppe 2, und das zweite ‹(\d\d)› ist Gruppe 3. Während des Findens speichert die Regex-Engine den Inhalt der so gefundenen Gruppen, sobald die schließende Klammer erreicht wird. Findet unsere Regex den Wert 200805-24, wird 2008 für die erste Gruppe gespeichert, 05 für die zweite Gruppe und 24 für die dritte Gruppe.
62 | Kapitel 2: Grundlagen regulärer Ausdrücke
Es gibt drei Möglichkeiten, den „gefangenen“ Text zu verwenden. Rezept 2.10 in diesem Kapitel erklärt, wie Sie den gefangenen Text nochmals innerhalb der gleichen Regex zum Suchen nutzen können. Rezept 2.21 zeigt, wie Sie den gefangenen Text in den Ersetzungstext einfügen, wenn Sie suchen und ersetzen. Rezept 3.9 im nächsten Kapitel erläutert, wie Ihre Anwendung die Teile der Regex selbst nutzen kann.
Variationen Nicht-einfangende Gruppen In der Regex ‹\b(Maria|Julia|Susanne)\b› benötigten wir die Klammern nur für das Gruppieren. Anstatt eine einfangende Gruppe zu verwenden, können wir auch eine nicht-einfangende Gruppe nutzen: \b(?:Maria|Julia|Susanne)\b
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Die drei Zeichen ‹(?:› öffnen eine nicht-einfangende Gruppe. Die Klammer ‹)› schließt sie. Die nicht-einfangende Gruppe bietet die gleiche Funktionalität zum Gruppieren, fängt aber nichts ein. Wenn die öffnenden Klammern einfangender Gruppen gezählt werden, um ihre Position zu bestimmen, werden die Klammern nicht-einfangender Gruppen nicht mitgezählt. Das ist der Hauptvorteil nicht-einfangender Gruppen: Sie können sie einer bestehenden Regex hinzufügen, ohne die Referenzen auf die durchnummerierten einfangenden Gruppen durcheinanderzubringen. Ein weiterer Vorteil nicht-einfangender Gruppen ist eine höhere Geschwindigkeit. Wenn Sie für eine bestimmte Gruppe keine Rückwärtsreferenzen nutzen (Rezept 2.10), sie nicht in den Ersetzungstext einfügen (Rezept 2.21) und sie auch nicht in Ihrem Quellcode nutzen wollen (Rezept 3.9), erzeugt eine einfangende Gruppe nur unnötigen Overhead, den Sie durch eine nicht-einfangende Gruppe vermeiden können. In der Praxis werden Sie die Performanceunterschiede kaum spüren, sofern Sie die Regex nicht in einer großen Schleife und/oder mit vielen Daten verwenden.
Gruppen mit Modus-Modifikatoren In der Variation „Übereinstimmungen unabhängig von Groß- und Kleinschreibung“ von Rezept 2.1 haben wir erklärt, dass .NET, Java, PCRE, Perl und Ruby lokale ModusModifikatoren unterstüzen, indem sie Modusschalter nutzen: ‹empfindlich(?i) unempfindlich(?-i)empfindlich›. Auch wenn diese Syntax ebenfalls Klammern enthält, geht es bei Schaltern wie ‹(?i)› auch nicht um Gruppierungen.
2.9 Gruppieren und Einfangen von Teilen des gefundenen Texts | 63
Statt Schalter zu nutzen, können Sie Modus-Modifikatoren in einer nicht-einfangenden Gruppe verwenden: \b(?i:Maria|Julia|Susanne)\b
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Ruby empfindlich(?i:unempfindlich)empfindlich
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Ruby Durch das Hinzufügen von Modus-Modifikatoren zu einer nicht-einfangenden Gruppe wird der Modus für den Teil des regulären Ausdrucks innerhalb der Gruppe gesetzt. Die vorigen Einstellungen werden mit der schließenden Klammer wieder zurückgesetzt. Da das Berücksichtigen von Groß- und Kleinschreibung das Standardverhalten ist, wird so nur dieser Teil innerhalb der Gruppe unempfindlich für die Schreibweise: (?i:...)
Sie können mehrere Modifikatoren kombinieren: ‹(?ism:Gruppe)›. Mit einem Minuszeichen schalten Sie die Modifikatoren ab: ‹(?-ism:Gruppe)› schaltet alle drei Optionen ab. ‹(?i-sm)› schaltet die Unempfindlichkeit für Groß- und Kleinschreibung ein (i) und sowohl Punkt passt zu Zeilenumbruch (s) als auch Zirkumflex und Dollar passen zu Zeilenumbruch (m) ab. Diese Optionen werden in den Rezepten 2.4 und 2.5 erläutert.
Siehe auch Rezepte 2.10, 2.11, 2.21 und 3.9.
2.10 Vorher gefundenen Text erneut finden Problem Erstellen eines regulären Ausdrucks, der „magische“ Datumswerte im Format yyyy-mmdd findet. Ein Datum ist magisch, wenn Jahr (ohne Jahrhundert), Monat und Tag den gleichen Wert haben. So ist zum Beispiel 2008-08-08 ein magisches Datum. Sie können davon ausgehen, dass alle Datumswerte im Ausgangstext gültig sind. Der reguläre Ausdruck muss sich nicht mit Werten wie 9999-99-99 herumschlagen. Es müssen nur die magischen Datumswerte gefunden werden.
Lösung \b\d\d(\d\d)-\1-\1\b
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
64 | Kapitel 2: Grundlagen regulärer Ausdrücke
Diskussion Um schon vorher gefundenen Text weiter hinten in einer Regex nochmals zu finden, müssen wir zunächst den ersten Text einfangen. Das machen wir mit einer einfangenden Gruppe, wie es in Rezept 2.9 gezeigt wurde. Danach können wir den gleichen Text irgendwo in der Regex mithilfe einer Rückwärtsreferenz erneut suchen. Sie können die ersten neun einfangenden Gruppen referenzieren, indem Sie einen Backslash, gefolgt von einer einzelnen Ziffer zwischen eins und neun nutzen. Für die Gruppen 10 bis 99 nutzen Sie ‹\10› bis ‹\99›. Verwenden Sie nicht ‹\01›. Das ist entweder eine oktale Maskierung oder ein Fehler. Wir verwenden in diesem Buch keine oktalen Maskierungen, da die hexadezimalen Maskierungen wie ‹\xFF› viel einfacher zu verstehen sind.
Wenn der reguläre Ausdruck ‹\b\d\d(\d\d)-\1-\1\b› den Wert 2008-08-08 vorfindet, passen die ersten ‹\d\d› zu 20. Die Regex-Engine betritt dann die einfangende Gruppe und merkt sich die Position, die sie im Ausgangstext erreicht hat. Die ‹\d\d› innerhalb der Gruppe passen zu 08, und die Engine erreicht das Ende der Gruppe. Damit wird die erste Teilübereinstimmung 08 als einfangende Gruppe 1 abgespeichert. Das nächste Token ist der Bindestrich, der als Literal passt. Dann kommt die Rückwärtsreferenz. Die Regex-Engine holt sich den Inhalt der ersten einfangenden Gruppe: 08. Sie versucht nun, diesen Text als Literal zu finden. Wenn sich der reguläre Ausdruck nicht um Groß- und Kleinschreibung kümmert, wird der eingefangene Text dementsprechend gefunden. Somit war die Rückwärtsreferenz erfolgreich. Der nächste Bindestrich und die weitere Rückwärtsreferenz passen ebenfalls. Schließlich passt die Wortgrenze zum Ende des Ausgangstexts, und das Gesamtergebnis ist gefunden: 2008-08-08. Die einfangende Gruppe hat immer noch den Wert 08 gespeichert. Wenn eine eingefangene Gruppe wiederholt wird – sei es durch einen Quantor (Rezept 2.12) oder durch eine Rückwärtsreferenz (Rezept 2.13) –, wird der gespeicherte Wert jedes Mal überschrieben, wenn die einfangende Gruppe etwas Passendes findet. Eine Rückwärtsreferenz auf die Gruppe passt nur zu dem Text, der als Letztes von der Gruppe eingefangen wurde. Wenn die gleiche Regex auf 2008-05-24 2007-07-07 angewandt wird, passt zunächst ‹\b\d\d(\d\d)› auf 2008, womit 08 für die erste (und einzige) Gruppe abgespeichert wird. Als Nächstes passt der Bindestrich. Die Rückwärtsreferenz versucht, ‹08› zu finden, entdeckt aber nur 05. Da es keine weiteren Alternativen im regulären Ausdruck gibt, beendet die Engine den Versuch. Damit werden die Ergebnisse aller einfangenden Gruppen gelöscht. Wenn die Engine anschließend einen neuen Versuch startet und mit der ersten 0 im Text beginnt, enthält ‹\1› gar keinen Text.
2.10 Vorher gefundenen Text erneut finden | 65
Bei der Verarbeitung von 2008-05-24 2007-07-07 passt die Gruppe das nächste Mal, wenn ‹\b\d\d(\d\d)› den Wert 2007 findet. Nun wird 07 abgelegt. Dann passt der Bindestrich. Jetzt versucht die Rückwärtsreferenz, ‹07› zu finden. Das ist erfolgreich, genauso wie der nächste Bindestrich, die Rückwärtsreferenz und die Wortgrenze. Es wurde also 2007-0707 gefunden. Da die Regex-Engine von links nach rechts vorgeht, müssen Sie die einfangenden Klammern vor die Rückwärtsreferenz setzen. Die regulären Ausdrücke ‹\b\d\d\1-(\d\d)-\1› und ‹\b\d\d\1-\1-(\d\d)\b› werden niemals etwas finden. Da die Rückwärtsreferenz vor der einfangenden Gruppe ausgewertet wird, besitzt er noch keinen Wert. Wenn Sie nicht gerade mit JavaScript arbeiten, schlägt eine Rückwärtsreferenz immer fehl, wenn er auf eine Gruppe verweist, die noch nicht ausgewertet wurde. Eine Gruppe, die noch nicht ausgewertet wurde, ist nicht das Gleiche wie eine Gruppe, die eine Übereinstimmung der Länge null eingefangen hat. Eine Rückwärtsreferenz auf eine Gruppe mit der Länge null ist immer erfolgreich. Wenn ‹(^)\1› am Anfang des Texts erfolgreich gefunden wurde, enthält die erste einfangende Gruppe die Übereinstimmung des Zirkumflex mit Null-Länge. Damit ist auch ‹\1› erfolgreich. In der Praxis kann das passieren, wenn der Inhalt einer einfangenden Gruppe vollständig optional ist. JavaScript ist die einzige uns bekannte Variante, die mit der jahrzehntelangen Tradition der Rückwärtsreferenzen bricht. In JavaScript, zumindest in Implementierungen, die dem JavaScript-Standard folgen, ist eine Rückwärtsreferenz auf eine Gruppe, die noch nicht ausgewertet wurde, immer erfolgreich – so wie eine Rückwärtsreferenz auf eine Gruppe mit NullLänge. Daher passt ‹\b\d\d\1-\1-(\d\d)\b› in JavaScript zu 12--34.
Siehe auch Rezepte 2.9, 2.11, 2.21 und 3.9.
2.11 Teile des gefundenen Texts einfangen und benennen Problem Erstellen eines regulären Ausdrucks, der jedes Datum im Format yyyy-mm-dd findet. Bereitstellen separater einfangender Gruppen für das Jahr, den Monat und den Tag. Ziel ist es, mit diesen Werten im Code bequem arbeiten zu können. Dazu sollen dem eingefangenen Text die beschreibenden Namen „Jahr“, „Monat“ und „Tag“ zugewiesen werden. Erstellen eines weiteren regulären Ausdrucks, der „magische“ Datumswerte im Format yyyy-mm-dd findet. Ein Datum ist magisch, wenn Jahr (ohne Jahrhundert), Monat und Tag den gleichen Wert haben. So ist zum Beispiel 2008-08-08 ein magisches Datum. Einfangen der magischen Zahl (in diesem Beispiel 08) und Benennen mit „magisch“.
66 | Kapitel 2: Grundlagen regulärer Ausdrücke
Es kann davon ausgegangen werden, dass alle Datumswerte im Ausgangstext gültig sind. Die regulären Ausdrücke müssen sich nicht mit Werten wie 9999-99-99 herumschlagen.
Regex-Optionen: Keine Regex-Varianten: PCRE 4 und neuer, Perl 5.10, Python
Diskussion Benannte Captures Die Rezepte 2.9 und 2.10 beschreiben einfangende Gruppen und Rückwärtsreferenzen. Genauer gesagt, haben diese Rezepte nummerierte einfangende Gruppen und nummerierte Rückwärtsreferenzen genutzt. Jede Gruppe erhält automatisch eine Zahl, die Sie für die Rückwärtsreferenz nutzen können. Moderne Regex-Varianten unterstützen neben den nummerierten auch benannte einfangende Gruppen. Der einzige Unterschied zwischen benannten und nummerierten Gruppen ist, dass Sie den erstgenannten einen beschreibenden Namen zuweisen können, statt
2.11 Teile des gefundenen Texts einfangen und benennen | 67
sich mit automatisch vorgegebenen Zahlen herumschlagen zu müssen. Benannte Gruppen lassen Ihren regulären Ausdruck lesbarer und wartbarer werden. Fügt man eine einfangende Gruppe in eine bestehende Regex ein, können sich die Zahlen ändern, die den einfangenden Gruppen zugewiesen sind. Selbstvergebene Namen bleiben dagegen bestehen. Python nutzte die erste Regex-Variante, die benannte Captures unterstützte. Dort wurde die Syntax ‹(?PRegex)› genutzt. Der Name musste aus Wortzeichen bestehen, die durch ‹\w› gefunden werden können. ‹(?P› ist die öffnende Klammer der Gruppe, während ‹)› die schließende Klammer ist. Die Designer der .NET-Klasse Regex entwickelten ihre eigene Syntax für benannte Captures, die zwei Versionen ermöglicht. ‹(?Regex)› simuliert die Python-Syntax, nur ohne das P. Der Name muss aus Wortzeichen bestehen, die durch ‹\w› gefunden werden können. ‹(?› ist die öffnende Klammer der Gruppe, während ‹)› die schließende Klammer ist. Die spitzen Klammern in der Syntax sind nervig, wenn Sie in XML kodieren oder dieses Buch per DocBook XML schreiben wollen. Das ist der Grund für die Alternative in .NET: ‹(?'Name'Regex)›. Die spitzen Klammern wurden durch einfache Anführungszeichen ersetzt. Sie können sich aussuchen, welche Syntax Sie nutzen wollen. Die Funktionalität ist identisch. Vielleicht aufgrund der größeren Beliebtheit von .NET gegenüber Python scheint die .NET-Syntax diejenige zu sein, die Entwickler von anderen Regex-Bibliotheken lieber kopieren. Perl 5.10 nutzt sie genauso wie die Oniguruma-Engine in Ruby 1.9. PCRE hat die Python-Syntax vor langer Zeit kopiert, als Perl noch keine benannten Captures unterstützte. PCRE 7, die Version, die in Perl 5.10 neue Features ergänzt hat, unterstützt sowohl die .NET-Syntax als auch die Python-Syntax. Vielleicht als Anerkennung des Erfolgs von PCRE unterstützt Perl 5.10 in einer Art umgekehrter Kompatibilität auch die Python-Syntax. In PCRE und Perl 5.10 ist die Funktionalität der .NET-Syntax und der Python-Syntax für benannte Captures identisch. Wählen Sie die Syntax aus, die für Sie am nützlichsten ist. Wenn Sie in PHP entwickeln und wollen, dass Ihr Code auch mit älteren Versionen von PHP arbeitet, die dementsprechend ältere Versionen von PCRE nutzen, verwenden Sie am besten die Python-Syntax. Wenn Sie keine Kompatibilität zu älteren Versionen brauchen und auch mit .NET oder Ruby arbeiten, erleichtert die .NET-Syntax das Kopieren zwischen all diesen Sprachen. Sind Sie unsicher, nutzen Sie die Python-Syntax für PHP/PCRE. Leute, die Ihren Code mit einer älteren Version von PCRE kompilieren, werden nicht so glücklich sein, wenn die Regexes in Ihrem Code plötzlich nicht mehr funktionieren. Wenn Sie eine Regex nach .NET oder Ruby kopieren, ist das Löschen von ein paar P kein so großer Aufwand. Die Dokumentationen zu PCRE 7 und Perl 5.10 erwähnen die Python-Syntax kaum, aber sie ist auf jeden Fall nicht veraltet. Bei PCRE und PHP empfehlen wir sie sogar.
68 | Kapitel 2: Grundlagen regulärer Ausdrücke
Benannte Rückwärtsreferenzen Mit den benannten Captures kommen auch benannte Rückwärtsreferenzen. So, wie benannte einfangende Gruppe funktional identisch mit nummerierten einfangenden Gruppen sind, sind auch benannte Rückwärtsreferenzen funktional identisch mit nummerierten Rückwärtsreferenzen. Sie lassen sich nur einfacher lesen und warten. Python nutzt die Syntax ‹(?P=Name)›, um eine Rückwärtsreferenz für die Gruppe Name zu erzeugen. Auch wenn diese Syntax Klammern nutzt, ist die Rückwärtsreferenz keine Gruppe. Sie können zwischen den Namen und die schließende Klammer nichts anderes schreiben. Eine Rückwärtsreferenz ‹(?P=Name)› ist genauso wie ‹\1› ein singuläres RegexToken. .NET nutzt die Syntax ‹\k› und ‹\k'Name'›. Beide Versionen sind funktional identisch und können beliebig kombiniert werden. Eine benannte Gruppe, die in der Syntax mit spitzen Klammern erstellt wurde, kann mit der Anführungszeichensyntax für die Rückwärtsreferenz genutzt werden und umgekehrt. Wir empfehlen Ihnen dringend, benannte und nummerierte Gruppen nicht zusammen in einer Regex zu nutzen. Die verschiedenen Varianten haben unterschiedliche Regeln für das Durchnummerieren unbenannter Gruppen, die zwischen benannten Gruppen auftauchen. Perl 5.10 und Ruby 1.9 haben die .NET-Syntax übernommen, folgen aber nicht den Regeln von .NET für das Nummerieren benannter Gruppen oder das Mischen nummerierter Gruppen mit benannten Gruppen. Anstatt die Unterschiede zu erläutern, empfehle ich einfach, benannte und nummerierte Gruppen nicht zusammen zu nutzen. Vermeiden Sie Chaos und geben Sie entweder allen unbenannten Gruppen einen Namen oder sorgen Sie dafür, dass sie nicht einfangend sind.
Siehe auch Rezepte 2.9, 2.10, 2.21 und 3.9.
2.12 Teile der Regex mehrfach wiederholen Problem Erstellen von regulären Ausdrücken, die zu folgenden Arten von Zahlen passen: • einem Googol (eine Dezimalzahl mit 100 Stellen), • einer Hexadezimalzahl mit 32 Bit, • einer Hexadezimalzahl mit 32 Bit und einem optional Suffix h, • einer Gleitkommazahl mit einem optionalen ganzzahligen Anteil, einem verpflichtenden Nachkommateil und einem optionalen Exponenten. Jeder Teil kann eine beliebige Zahl von Stellen haben.
2.12 Teile der Regex mehrfach wiederholen | 69
Lösung Googol \b\d{100}\b
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Feste Wiederholung Der Quantor ‹{n}› wiederholt das vorige Regex-Token n Mal, wobei n eine positive Zahl ist. Das ‹\d{100}› in ‹\b\d{100}\b› findet also einen Text mit 100 Ziffern. Sie könnten das Gleiche auch erreichen, indem Sie 100 Mal ‹\d› eingeben. ‹{1}› wiederholt das vorige Token ein Mal, so als gäbe es keinen Quantor. ‹ab{1}c› ist die gleiche Regex wie ‹abc›. ‹{0}› wiederholt das vorige Token null Mal, womit es im Endeffekt aus dem regulären Ausdruck entfernt wird. ‹ab{0}c› ist die gleiche Regex wie ‹ac›.
Variable Wiederholung Bei der variablen Wiederholung nutzen wir den Quantor ‹{n,m}›, wobei n eine positive Zahl und m größer als n ist. ‹\b[a-f0-9]{1,8}\b› passt zu einer hexadezimalen Zahl mit einer bis acht Stellen. Bei einer variablen Wiederholung wird die Reihenfolge der Alternativen wichtig. Rezept 2.13 geht an dieser Stelle mehr ins Detail.
70 | Kapitel 2: Grundlagen regulärer Ausdrücke
Wenn n und m gleich sind, haben wir wieder die feste Wiederholung. ‹\b\d{100,100}\b› ist die gleiche Regex wie ‹\b\d{100}\b›.
Unendliche Wiederholung Der Quantor ‹{n,}›, bei dem n eine positive Zahl ist, ermöglicht unendlich viele Wiederholungen. Im Prinzip ist eine unendliche Wiederholung eine variable Wiederholung ohne Obergrenze. ‹\d{1,}› passt zu einer oder mehr Ziffern und ist das Gleiche wie ‹\d+›. Ein Plus nach
einem Regex-Token, bei dem es sich nicht um einen Quantor handelt, bedeutet „eins oder mehr“. Rezept 2.13 erklärt die Bedeutung eines Pluszeichens nach einem Quantor. ‹\d{0,}› passt zu null oder mehr Ziffern und ist das Gleiche wie ‹\d*›. Der Stern bedeutet immer „null oder mehr“. Neben der unendlichen Wiederholung sorgen ‹{0,}› und
der Stern auch dafür, dass das vorige Token optional wird.
Etwas optional machen Wenn wir eine variable Wiederholung nutzen, bei der n auf null gesetzt wurde, machen wir damit das Token, das vor dem Quantor steht, optional. ‹h{0,1}› passt zu einem ‹h› – oder gar keinem. Wenn es kein h gibt, führt ‹h{0,1}› zu einer Übereinstimmung mit Null-Länge. Nutzen Sie ‹h{0,1}› als regulären Ausdruck allein, findet dieser eine Übereinstimmung mit Null-Länge vor jedem Zeichen im Text, das kein h ist. Jedes h führt dagegen zu einer Übereinstimmung mit einem Zeichen (dem h). ‹h?› ist das Gleiche wie ‹h{0,1}›. Ein Fragezeichen nach einem gültigen und vollständigen Regex-Token, das kein Quantor ist, bedeutet „null oder ein Mal“. Das nächste Rezept beschreibt die Bedeutung eines Fragezeichens nach einem Quantor. Ein Fragezeichen oder ein anderer Quantor direkt nach einer öffnenden Klammer führt zu einem Syntaxfehler. Perl und die darauf aufbauenden Varianten nutzen diese Form, um der Regex-Syntax „Perl-Erweiterungen“ hinzuzufügen. Vorige Rezepte haben nicht-einfangende Gruppen und benannte einfangende Gruppen vorgestellt, die alle ein Fragezeichen nach einer öffnenden Klammer als Teil ihrer Syntax verwenden. Diese Fragezeichen sind keine Quantoren, sondern einfach Teil der Syntax für nicht-einfangende und benannte einfangende Gruppen. Die folgenden Rezepte werden noch mehr Arten von Gruppen vorstellen, die eine Syntax mit ‹(?› nutzen.
Wiederholende Gruppen Wenn Sie einen Quantor nach der schließenden Klammer einer Gruppe setzen, wird die ganze Gruppe wiederholt. ‹(?:abc){3}› ist das Gleiche wie ‹abcabcabc›.
2.12 Teile der Regex mehrfach wiederholen | 71
Quantoren können verschachtelt werden. ‹(e\d+)?› passt zu einem e, gefolgt von einer oder mehreren Ziffern, oder zu einer leeren Übereinstimmung. Bei unserer Regex für die Gleitkommazahl ist dies der optionale Exponent. Einfangende Gruppen können wiederholt werden. Wie in Rezept 2.9 beschrieben, wird die Übereinstimmung der Gruppe jedes Mal gespeichert, wenn die Engine die Gruppe verlässt. Dabei wird der alte Wert immer überschrieben. ‹(\d\d){1,3}› passt zu einem String mit zwei, vier oder sechs Ziffern. Die Engine verlässt diese Gruppe drei Mal. Wenn die Regex 123456 findet, wird die einfangende Gruppe den Wert 56 enthalten, da 56 bei der letzten Iteration der Gruppe gespeichert wurde. Die anderen beiden Übereinstimmungen durch die Gruppe, 12 und 34, können nicht ausgelesen werden. ‹(\d\d){3}› findet den gleichen Text wie ‹\d\d\d\d(\d\d)›. Wenn Sie alle zwei, vier oder sechs Ziffern einfangen wollen und nicht nur die letzten beiden, müssen Sie die einfangende Gruppe um den Quantor herum einrichten und nicht die einfangende Gruppe wiederholen: ‹((?:\d\d){1,3})›. Hier nutzen wir eine nicht-einfangende Gruppe, um die Gruppierungsfunktion von der einfangenden Gruppe zu übernehmen. Wir hätten auch zwei einfangende Gruppen verwenden können: ‹((\d\d){1,3})›. Wenn diese letzte Regex 123456 findet, enthält ‹\1› den Wert 123456 und ‹\2› den Wert 56.
Die Regex-Engine von .NET ist die einzige, die es Ihnen erlaubt, alle Iterationen einer wiederholten einfangenden Gruppe auszulesen. Wenn Sie direkt die Eigenschaft Value der Gruppe abfragen, die einen String zurückliefert, werden Sie 56 erhalten, so wie bei allen anderen Regex-Engines auch. Rückwärtsverweise im regulären Ausdruck und im Ersetzungstext nutzen ebenfalls die 56, aber wenn Sie die CaptureCollection der Gruppe verwenden, erhalten Sie einen Stack mit 56, 34 und 12.
Siehe auch Rezepte 2.9, 2.13, 2.14.
2.13 Minimale oder maximale Wiederholung auswählen Problem Ein Paar XHTML-Tags der Form
und
und den Text dazwischen finden. Der Text zwischen den Tags kann andere XHTML-Tags enthalten.
Lösung
.*?
Regex-Optionen: Punkt passt zu Zeilenumbruch Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
72 | Kapitel 2: Grundlagen regulärer Ausdrücke
Diskussion Alle in Rezept 2.12 behandelten Quantoren sind gierig (greedy), das heißt, sie versuchen, so häufig wie möglich wiederholt zu werden und erst dann mit der Regex weiterzumachen, wenn es keine weitere Übereinstimmung mehr gibt. Damit kann es schwer werden, Tags in XHTML (von XML abgeleitet; damit braucht jedes öffnende Tag ein schließendes) paarweise zu behandeln. Schauen Sie sich den folgenden einfachen Ausschnitt eines XHTML-Texts an:
Die <em>Aufgabe ist es, den Anfang eines Absatzes zu finden.
Dann müssen Sie das Ende des Absatzes finden.
Es gibt hier zwei öffnende
-Tags und zwei schließende
-Tags. Sie wollen das erste
mit dem ersten
finden, da beide zusammen einen einzelnen Absatz auszeichnen. Beachten Sie, dass dieser Absatz ein verschachteltes <em>-Tag enthält, daher kann die Regex nicht einfach abbrechen, wenn sie einem \d+)› entspricht, passt nicht auf abc123. ‹\w++› findet abc123 vollständig. Dann versucht die Regex-Engine, ‹\d++› am Ende des Texts zu finden. Da es aber keine weiteren Buchstaben gibt, schlägt ‹\d++› fehl. Ohne sich Backtracking-Positionen zu merken, ist diese
Suche dann nicht erfolgreich.
2.14 Unnötiges Backtracking vermeiden | 77
‹(?>\w+\d+)› hat zwei gierige Quantoren innerhalb der gleichen atomaren Gruppe. In der
Gruppe wird das Backtracking normal durchgeführt. Die Backtracking-Positionen werden erst verworfen, wenn die Engine die gesamte Gruppe verlässt. Lautet der Text abc123, passt ‹\w+› zu abc123. Der gierige Quantor merkt sich Backtracking-Positionen. Wenn ‹\d+› nichts findet, rückt ‹\w+› ein Zeichen heraus. ‹\d+› passt dann zu 3. Jetzt verlässt die Engine die atomare Gruppe und verwirft alle Backtracking-Positionen, die sie sich für ‹\w+› und ‹\d+› gemerkt hat. Da das Ende der Regex erreicht wurde, macht das hier auch keinen Unterschied. Es gibt aber ein Suchergebnis. Falls das Ende noch nicht erreicht ist, so wie in ‹(?>\w+\d+)\d+›, hätten wir die gleiche Situation wie bei ‹\w++\d++›. Für das zweite ‹\d+› wäre am Ende des Texts nichts mehr übrig. Da die Backtracking-Positionen weggeworfen wurden, kann die Regex-Engine nur aufgeben. Possessive Quantoren und atomare Gruppen helfen nicht nur beim Optimieren regulärer Ausdrücke, sie können durchaus auch zu anderen Übereinstimmungen führen, da die Zeichen herausfallen, die durch das Backtracking erreicht worden wären. Dieses Rezept zeigt Ihnen, wie Sie possessive Quantoren und atomare Gruppen nutzen, um kleinere Optimierungen vorzunehmen. Die wirken sich eventuell nicht einmal messbar auf die Geschwindigkeit aus. Das nächste Rezept wird aber zeigen, wie man durch atomare Gruppen auch für drastische Unterschiede sorgen kann.
Siehe auch Rezepte 2.12 und 2.15.
2.15 Aus dem Ruder laufende Wiederholungen verhindern Problem Mit einem einzelnen regulären Ausdruck eine komplette HTML-Datei abdecken und auf korrekt verschachtelte html-, head-, title- und body-Tags prüfen. Der reguläre Ausdruck muss bei HTML-Dateien mit nicht korrekt genutzten Tags effizient fehlschlagen.
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert, Punkt passt zu Zeilenumbruch Regex-Varianten: .NET, Java, PCRE, Perl, Ruby JavaScript und Python unterstützen keine atomaren Gruppen. Es gibt daher bei diesen beiden Varianten keine Möglichkeit, ein unnötiges Backtracking zu vermeiden. Wenn Sie
78 | Kapitel 2: Grundlagen regulärer Ausdrücke
in diesen Sprachen programmieren, können Sie das Problem umgehen, indem Sie für jedes dieser Tags eine literale Textsuche durchführen und dabei nach dem jeweils nächsten Tag vom Endpunkt der vorherigen Suche aus suchen.
Diskussion Die korrekte Lösung für dieses Problem lässt sich einfacher verstehen, wenn wir mit folgender naiven Lösung beginnen: .*?.*?.*? .*?.*?]*>.*?.*?
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert, Punkt passt zu Zeilenumbruch Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Wenn Sie diese Regex mit einer korrekten HTML-Datei testen, funktioniert sie wunderbar. ‹.*?› springt über alles andere hinweg, weil wir Punkt passt zu Zeilenumbruch aktiviert haben. Der genügsame Stern stellt sicher, dass die Regex immer nur ein Zeichen weitergeht und jedes Mal prüft, ob das nächste Tag passt. Die Rezepte 2.4 und 2.13 erläutern das genauer. Aber wenn der Ausgangstext nicht alle diese HTML-Tags besitzt, kommen Sie mit dieser Regex in Schwierigkeiten. Am schlimmsten ist es, wenn fehlt. Stellen Sie sich vor, die Regex-Engine hat alle vorigen Tags gefunden und ist nun damit beschäftigt, das letzte ‹.*?› zu erweitern. Da ‹› nie passen kann, wird ‹.*?› immer weiter bis zum Ende der Datei expandiert. Ist das Ende der Datei erreicht, schlägt die Regex fehl. Aber das ist noch nicht das Ende der Geschichte. Alle anderen sechs ‹.*?› haben sich eine Backtracking-Position gemerkt, die es ihnen erlaubt, weiter zu wachsen. Wenn das letzte ‹.*?› erfolglos war, wird das vorherige erweitert, das dann Schritt für Schritt findet. Der gleiche Text war schon vorher durch das literale ‹› in der Regex gefunden worden. Dieses ‹.*?› wird aber auch bis zum Ende der Datei erweitert, ebenso wie alle vorherigen genügsamen Punkte. Erst wenn das erste ‹.*?› das Ende der Datei erreicht, wird die Regex-Engine erkennen, dass die Suche erfolglos war. Dieser reguläre Ausdruck hat im schlimmsten Fall eine Komplexität von O(n7), also die Länge des Ausgangstexts in siebter Potenz. Es gibt sieben genügsame Punkte, die potenziell bis zum Ende der Datei erweitert werden. Wenn die Datei doppelt so groß ist, kann die Regex 128 Mal mehr Schritte brauchen, um zu erkennen, dass sie nicht passt. Wir nennen das katastrophales Backtracking. Es wird so viel Backtracking durchgeführt, dass die Regex entweder ewig läuft oder Ihre Anwendung zum Absturz bringt. Manche Regex-Implementierungen sind schlau und brechen solche aus dem Ruder laufenden Versuche ab, aber selbst dann wird die Regex für die Performance Ihrer Anwendung vernichtend sein.
2.15 Aus dem Ruder laufende Wiederholungen verhindern | 79
Das katastrophale Backtracking ist eine Form eines Phänomens, das als kombinatorische Explosion bekannt ist. Viele orthogonale Bedingungen treffen zusammen, und alle Kombinationen müssen ausprobiert werden. Sie könnten auch sagen, dass die Regex ein kartesisches Produkt der verschiedenen Wiederholungsoperatoren ist.
Die Lösung ist, atomare Gruppen zu nutzen, um das überflüssige Backtracking zu unterbinden. Es gibt für das sechste ‹.*?› keine Notwendigkeit, sich weiter auszudehnen, wenn ‹› gefunden wurde. Wird ‹› nicht gefunden, wird auch ein Erweitern des sechsten genügsamen Punkts kein schließendes html-Tag aus dem Hut zaubern können. Damit sich ein Quantor-Regex-Token nicht weiter ausdehnt, wenn das darauffolgende Element passt, stecken Sie beides – den Quantor-Teil der Regex und das folgende Element – zusammen in eine atomare Gruppe: ‹(?>.*?)›. Jetzt verwirft die RegexEngine alle Positionen für ‹.*?›, wenn ‹› gefunden wird. Wird später ‹› nicht gefunden, hat die Regex-Engine schon den Teil ‹.*?› vergessen, und es gibt keine zusätzlichen Erweiterungen. Wenn wir das Gleiche für alle anderen ‹.*?› in der Regex machen, wird sich keine von ihnen erweitern. Obwohl es immer noch sieben genügsame Punkte in der Regex gibt, werden sie sich nie gegenseitig überlappen. Damit verringert sich die Komplexität des regulären Ausdrucks auf O(n), ist also linear im Verhältnis zur Länge des Ausgangstexts. Ein regulärer Ausdruck kann nie effizienter sein.
Variationen Möchten Sie wirklich einmal ein katastrophales Backtracking beobachten, wenden Sie die Regex ‹(x+x+)+y› auf den Text xxxxxxxxxx an. Wenn schnell einen Misserfolg gemeldet wird, fügen Sie dem Text ein x hinzu. Wiederholen Sie das so lange, bis die Regex sehr langsam wird oder Ihre Anwendung abstürzt. Es werden nicht viele zusätzliche x benötigt werden, sofern Sie nicht Perl nutzen. Von den in diesem Buch behandelten Regex-Varianten ist nur Perl in der Lage, zu erkennen, dass der reguläre Ausdruck zu komplex ist, um anschließend die Suche ohne einen Crash abzubrechen. Die Komplexität dieser Regex ist O(2n). Wenn ‹y› nicht gefunden wird, wird die RegexEngine alle möglichen Wiederholungspermutationen für jedes ‹x+› und die sie enthaltende Gruppe durchprobieren. Eine solche Permutation kann so sein, dass ‹x+› auf xxx passt, das zweite ‹x+› auf x und die Gruppe dann drei weitere Male wiederholt wird, wobei jedes ‹x+› zu x passt. Bei zehn x gibt es 1.024 solcher Permutationen. Wenn wir die Anzahl auf 32 erhöhen, kommen wir auf über 4 Milliarden Permutationen. Damit wird jede Regex-Engine straucheln, sofern sie keine Sicherheitssysteme besitzt, mit deren Hilfe sie die Suche abbrechen und melden kann, dass sie zu kompliziert ist.
80 | Kapitel 2: Grundlagen regulärer Ausdrücke
In diesem Fall lässt sich der nicht sehr sinnhafte reguläre Ausdruck leicht umschreiben zu ‹xx+y›, womit genau die gleichen Übereinstimmungen mit linearem Zeitaufwand gefunden werden. In der Praxis ist die Lösung aber nicht so augenscheinlich, wenn die Regexes komplizierter sind. Im Prinzip müssen Sie aufpassen, ob zwei oder mehr Teile des regulären Ausdrucks auf den gleichen Text passen können. In diesen Fällen brauchen Sie eventuell atomare Gruppen, um sicherzustellen, dass die Regex-Engine nicht alle Möglichkeiten ausprobiert, den Text zwischen den beiden Teilen der Regex aufzuteilen. Testen Sie Ihre Regex immer mit (langen) Testtexten, in denen sich Abschnitte befinden, die zwar teilweise zur Regex passen, aber eben nicht ganz.
Siehe auch Rezepte 2.13 und 2.14.
2.16 Etwas auf Übereinstimmung prüfen, ohne es dem Gesamtergebnis hinzuzufügen Problem Finden eines beliebigen Worts, das zwischen einem Paar HTML-Bold-Tags steht, ohne die Tags in das Regex-Suchergebnis mit aufzunehmen. Wenn der Text zum Beispiel Meine Katze ist flauschig lautet, soll als Ergebnis nur Katze herauskommen.
Lösung (? Den gleichen Text zwei Mal finden Wenn Sie am Anfang der Regex ein Lookbehind oder am Ende ein Lookahead verwenden, wollen Sie schlussendlich, dass vor oder nach der Regex-Übereinstimmung etwas steht, ohne dass es im gefundenen Text enthalten ist. Wenn Sie ein Lookaround mitten in Ihrem regulären Ausdruck nutzen, können Sie mehrere Überprüfungen des gleichen Texts durchführen. In „Variantenspezifische Features“ auf Seite 36 (ein Abschnitt aus Rezept 2.3) haben wir gezeigt, wie man Zeichenklassen voneinander subtrahieren kann, um eine Thai-Ziffer zu finden. Nur .NET und Java unterstützen das Subtrahieren von Zeichenklassen. Ein Zeichen ist eine Thai-Ziffer, wenn es sich sowohl um ein Thai-Zeichen (beliebiger Art) als auch um eine Ziffer (aus einem beliebigen Schriftsystem) handelt. Mit einem Lookahead können Sie beide Anforderungen für das gleiche Zeichen prüfen: (?=\p{Thai})\p{N}
Regex-Optionen: Keine Regex-Varianten: PCRE, Perl, Ruby 1.9 Diese Regex funktioniert nur mit den drei Varianten, die Unicode-Schriftsysteme unterstützen, wie wir in Rezept 2.7 erklärt haben. Aber ein Lookahead zu nutzen, um das gleiche Zeichen mehr als einmal zu prüfen, funktioniert mit allen in diesem Buch behandelten Varianten.
2 Die Regex-Engine des RegexBuddy erlaubt ebenfalls eine vollständige Regex innerhalb eines Lookbehind, aber es gibt (noch) kein Feature, das RegexOptions.RightToLeft von .NET gleicht, um den gesamten regulären Ausdruck umzukehren.
84 | Kapitel 2: Grundlagen regulärer Ausdrücke
Sucht die Regex-Engine nach ‹(?=\p{Thai})\p{N}›, nutzt sie das Lookahead an jeder Position im String, an der sie eine Überprüfung beginnt. Wenn das Zeichen an der Position nicht aus dem Thai-Schriftsystem stammt (‹\p{Thai}› also keine Übereinstimmung zeigt), schlägt das Lookahead fehl. Damit ist die Suche an dieser Stelle komplett erfolglos, und die Regex-Engine muss beim nächsten Zeichen weitermachen. Wenn die Regex ein Thai-Zeichen erreicht, passt ‹\p{Thai}›. Damit passt auch das Lookaround ‹(?=\p{Thai})›. Verlässt die Engine das Lookaround, greift sie wieder auf den vorherigen Status der gefundenen Zeichen zurück. In diesem Fall sind noch keine Zeichen gefunden. Als Nächstes kommt ‹\p{N}› dran. Da das Lookahead seine gefundenen Zeichen verworfen hat, wird ‹\p{N}› mit dem gleichen Zeichen verglichen, für das ‹\p{Thai}› bereits gültig war. Wenn dieses Zeichen die Unicode-Eigenschaft Number besitzt, passt auch ‹\p{N}›. Da sich ‹\p{N}› nicht innerhalb eines Lookaround befindet, konsumiert es dieses Zeichen, und wir haben eine Thai-Ziffer gefunden.
Ein Lookaround ist atomar Wenn die Regex-Engine eine Lookaround-Gruppe verlässt, verwirft sie den Text, der durch das Lookaround gefunden wurde. Damit werden auch alle Backtracking-Positionen, die durch Alternation oder Quantoren innerhalb des Lookaround entstanden, gelöscht. Damit ist ein Lookahead oder Lookbehind im Endeffekt atomar. Rezept 2.15 erläutert, was atomare Gruppen sind. In den meisten Situationen ist die atomare Natur von Lookarounds uninteressant. Ein Lookaround ist nicht mehr als eine Zusicherung, dass geprüft wird, ob die Regex innerhalb des Lookaround passt. Auf wie vielen verschiedenen Wegen sie passen könnte, ist unwichtig, und sie konsumiert auch keine Teile des Ausgangstexts. Aber die atomare Natur kommt dann ins Spiel, wenn Sie einfangende Gruppen innerhalb von Lookaheads (und Lookbehinds, wenn Ihre Regex-Variante das zulässt) nutzen. Obwohl ein Lookahead keinen Text konsumiert, wird sich die Regex-Engine merken, welcher Teil des Texts durch eine einfangende Gruppe innerhalb des Lookahead passte. Wenn sich ein Lookahead am Ende der Regex befindet, haben Sie doch einfangende Gruppen mit gefundenem Text, der durch den regulären Ausdruck selbst gar nicht gefunden wurde. Befindet sich das Lookahead in der Mitte der Regex, kann es sein, dass Sie einfangende Gruppen haben, die überlappende Teile des Texts gefunden haben. Die einzige Situation, in der die atomare Natur eines Lookaround das abschließende Suchergebnis einer Regex verändern kann, ergibt sich durch das Verwenden einer Rückwärtsreferenz auf eine einfangende Gruppe, die innerhalb des Lookaround „definiert“, aber außerhalb verwendet wird. Schauen Sie sich diesen regulären Ausdruck an: (?=(\d+))\w+\1
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
2.16 Etwas auf Übereinstimmung prüfen, ohne es dem Gesamtergebnis hinzuzufügen | 85
Auf den ersten Blick würden Sie vielleicht vermuten, dass diese Regex auf 123x12 passt. ‹\d+› würde die 12 in die erste einfangende Gruppe übernehmen, dann würde ‹\w+› auf 3x passen, und schließlich würde ‹\1› wieder zu 12 passen. Aber das wird so nie passieren. Der reguläre Ausdruck betritt das Lookaround und die einfangende Gruppe. Das gierige ‹\d+› passt auf 123. Diese Übereinstimmung wird für die erste einfangende Gruppe gespeichert. Dann verlässt die Engine das Lookahead, setzt den gefundenen Text zurück auf den Wert vor der Gruppe – also auf den Anfang des Strings – und verwirft die Backtracking-Positionen, die sie sich für das gierige Plus gemerkt hat, aber sie merkt sich weiterhin die 123, die für die erste einfangende Gruppe gespeichert wurden. Jetzt wird das gierige ‹\w+› auf den Anfang des Strings angesetzt und frisst gleich den ganzen Wert 123x12 auf. ‹\1› bezieht sich auf 123, kann das aber nun am Ende des Strings nicht mehr finden. ‹\w+› geht ein Zeichen zurück. ‹\1› findet wieder nichts. ‹\w+› geht noch mehr Schritte zurück, bis es alles außer der ersten 1 im Text wieder hergegeben hat. Aber auch jetzt noch findet ‹\1› keine Übereinstimmung nach der ersten 1. Die abschließende 12 könnte zu ‹\1› passen, wenn die Regex-Engine zum Lookahead zurückkehren und den Wert 123 für 12 aufgeben könnte. Das tut sie aber nicht. Die Regex-Engine hat nun keine weiteren Backtracking-Positionen mehr zur Verfügung. ‹\w+› ist ganz zurückgegangen, und das Lookaround hat ‹\d+› dazu gezwungen, seine
Backtracking-Positionen aufzugeben. Damit schlägt die Suche fehl.
Lösung ohne Lookbehind All die schönen Erklärungen und Hinweise helfen Ihnen leider nicht, wenn Sie Python oder JavaScript nutzen, da es dort kein Lookbehind gibt. Es gibt keine Möglichkeit, das Problem so, wie es beschrieben wurde, mit diesen Regex-Varianten zu lösen. Aber es gibt einen anderen Weg: die Verwendung von einfangenden Gruppen. Diese Version funktioniert auch mit den anderen Regex-Varianten: ()(\w+)(?=)
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Statt eines Lookbehind haben wir eine einfangende Gruppe für das öffnende Tag ‹› genutzt. Zudem haben wir den Teil der Übereinstimmung, an dem wir interessiert sind – das ‹\w+› – ebenfalls in eine einfangende Gruppe gesteckt. Wenn Sie diesen regulären Ausdruck auf Meine Katze ist flauschig anwenden, wird das komplette Suchergebnis Katze sein. Die erste einfangende Gruppe wird das enthalten, die zweite das Wort Katze. Wenn es darum geht, nur Katze zu erhalten (das Wort zwischen den -Tags), können Sie nun einfach den von der zweiten einfangenden Gruppe ermittelten Text verwenden, statt auf das vollständige Suchergebnis zuzugreifen.
86 | Kapitel 2: Grundlagen regulärer Ausdrücke
Möchten Sie eigentlich nur das Wort zwischen den Tags per Suchen und Ersetzen austauschen, verwenden Sie einfach eine Rückwärtsreferenz auf die erste einfangende Gruppe, um das öffnende Tag in den Ersetzungstext einzufügen. In dem hier genutzten Beispiel brauchen Sie die erste einfangende Gruppe eigentlich nicht, da das öffnende Tag immer das gleiche ist. Aber wenn es variabel ist, fügt die einfangende Gruppe genau das ein, das gefunden wurde. Rezept 2.21 erklärt das genauer. Wenn Sie schließlich wirklich ein Lookbehind simulieren wollen, können Sie das über zwei reguläre Ausdrücke machen. Zunächst suchen Sie nach Ihrer Regex ohne Lookbehind. Wenn sie passt, kopieren Sie den Teil des Ausgangstexts vor der Übereinstimmung in eine neue String-Variable. Führen Sie nun den Test, den Sie eigentlich innerhalb des Lookbehind machen wollten, mit einer zweiten Regex durch, wobei Sie einen EndeAnker anfügen (‹\z› oder ‹$›). Der Anker stellt sicher, dass die Übereinstimmung der zweiten Regex am Ende des Strings liegt. Da Sie den String dort abgeschnitten haben, wo die erste Regex passte, ist auf diese Weise dafür gesorgt, dass die zweite Übereinstimmung direkt links von der ersten liegt. In JavaScript könnten Sie das mit folgendem Code erreichen: var mainregexp = /\w+(?=)/; var lookbehind = /$/; if (match = mainregexp.exec("Meine Katze ist flauschig")) { // Wort vor einem schließenden Tag gefunden var potentialmatch = match[0]; var leftContext = match.input.substring(0, match.index); if (lookbehind.exec(leftContext)) { // Lookbehind passte: // potentialmatch steht zwischen -Tags } else { // Lookbehind passte nicht: potentialmatch ist nicht das Gewünschte } } else { // Kein Wort vor einem schließenden Tag gefunden }
Siehe auch Rezepte 5.5 und 5.6.
2.17 Abhängig von einer Bedingung eine von zwei Alternativen finden Problem Erstellen eines regulären Ausdrucks, der eine durch Kommata getrennte Liste mit den Worten eins, zwei und drei findet. Jedes Wort kann beliebig häufig vorkommen, muss aber mindestens ein Mal vorhanden sein.
2.17 Abhängig von einer Bedingung eine von zwei Alternativen finden | 87
Regex-Optionen: Keine Regex-Varianten: .NET, JavaScript, PCRE, Perl, Python Java und Ruby unterstützen keine bedingten Ausdrücke. Wenn Sie in Java oder Ruby programmieren (oder in einer anderen Sprache), können Sie den regulären Ausdruck ohne die Bedingungen nutzen und mit zusätzlichem Code prüfen, ob jede der drei einfangenden Gruppen etwas enthält. \b(?:(?:(eins)|(zwei)|(drei))(?:,|\b)){3,}
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion .NET, JavaScript, PCRE, Perl und Python unterstützen bedingte Ausdrücke mithilfe nummerierter einfangender Gruppen. ‹(?(1)then|else)› ist ein bedingter Ausdruck, der prüft, ob die erste einfangende Gruppe schon etwas gefunden hat. Wenn dem so ist, versucht die Regex-Engine, das ‹then› zu finden. Enthält die einfangende Gruppe bisher noch keine Übereinstimmung, wird versucht, den ‹else›-Teil zu finden. Die Klammern, das Fragezeichen und der vertikale Balken gehören alle zur Syntax für bedingte Ausdrücke. Sie haben an dieser Stelle nicht ihre normale Bedeutung. Sie können für die ‹then›- und ‹else›-Teile beliebige Regex-Ausdrücke nutzen. Die einzige Einschränkung besteht darin, dass Sie bei der Verwendung von Alternationen eine Gruppe nutzen müssen, um sie zusammenzuhalten. Im bedingten Ausdruck selbst ist nur ein einziger vertikaler Balken direkt erlaubt. Wenn Sie möchten, können Sie einen der beiden Teile ‹then› oder ‹else› auch weglassen. Die leere Regex findet immer eine Übereinstimmung der Länge null. Die Lösung für dieses Rezept nutzt drei bedingte Ausdrücke, die alle einen leeren ‹then›-Teil haben. Wenn die einfangende Gruppe etwas enthält, ist der bedingte Ausdruck direkt erfolgreich. Im ‹else›-Teil steht ein leeres negatives Lookahead ‹(?!)›. Da die leere Regex immer passt, schlägt ein negatives Lookahead mit einer leeren Regex immer fehl. Damit schlägt auch der bedingte Ausdruck ‹(?(1)|(?!))› immer fehl, wenn die erste einfangende Gruppe nichts Passendes enthält. Indem jede der drei erforderlichen Alternativen in ihrer eigenen einfangenden Gruppe untergebracht ist, können wir am Ende der Regex drei bedingte Ausdrücke nutzen, um zu prüfen, ob alle einfangenden Gruppen etwas enthalten. .NET unterstützt auch benannte bedingte Ausdrücke. ‹(?(Name)then|else)› prüft, ob die benannte einfangende Gruppe Name schon etwas enthält.
88 | Kapitel 2: Grundlagen regulärer Ausdrücke
Um besser verstehen zu können, wie bedingte Ausdrücke funktionieren, wollen wir uns den regulären Ausdruck ‹(a)?b(?(1)c|d)› anschauen. Dieser ist im Prinzip nichts anderes als eine komplizierte Form von ‹abc|bd›. Beginnt der Ausgangstext mit einem a, wird dieses in der ersten einfangenden Gruppe gesichert. Wenn nicht, enthält diese Gruppe auch nichts. Es ist wichtig, dass sich das Fragezeichen außerhalb der einfangenden Gruppe befindet, da damit die gesamte Gruppe optional wird. Wenn es kein a gibt, wird die Gruppe null Mal wiederholt und erhält auch keine Chance, etwas anderes einzufangen. Sie kann keinen String der Länge null abspeichern. Wenn Sie ‹(a?)› nutzen, nimmt die Gruppe immer an den Übereinstimmungsversuchen teil. Es gibt dann nach der Gruppe keinen Quantor, daher wird sie genau ein Mal ausgeführt. Die Gruppe wird entweder ein a oder gar nichts einfangen. Unabhängig davon, ob ein ‹a› gefunden wurde oder nicht, ist das nächste Token ein ‹b›. Dann kommt der bedingte Ausdruck. Wenn die einfangende Gruppe etwas einfangen konnte – selbst einen String der Länge null (was hier nicht geht) –, wird mit ‹c› weitergemacht. Wenn nicht, nutzt die Regex-Engine stattdessen ‹d›. Einfach gesagt, passt ‹(a)?b(?(1)c|d)› entweder zu ab, gefolgt von c, oder zu b, gefolgt von d. Bei .NET, PCRE und Perl, aber nicht bei Python, können bedingte Ausdrücke auch Lookarounds nutzen. ‹(?(?=if)then|else)› prüft zunächst ‹(?=if)› als normales Lookahead. Rezept 2.16 beschreibt dazu die Details. Wenn das Lookaround erfolgreich war, wird mit dem ‹then›-Teil weitergemacht, ansonsten mit dem ‹else›-Teil. Da ein Lookaround die Länge null hat, werden die ‹then›- und ‹else›-Regexes an der gleichen Position im Ausgangstext gesucht – egal ob ‹if› erfolgreich war oder fehlschlug. Sie können im bedingten Ausdruck Lookbehinds statt Lookaheads nutzen. Negative Lookarounds sind ebenfalls möglich, auch wenn wir das eher nicht empfehlen, da man dann nur mit den verschiedenen Zweigen der bedingten Ausführung durcheinandergerät. Ein bedingter Ausdruck, der Lookarounds nutzt, kann auch ohne den bedingten Ausdruck mittels ‹(?=if)then|(?!if)else› geschrieben werden. Wenn das positive Lookahead erfolgreich ist, wird ‹then› getestet. Wenn es keinen Erfolg hatte, springt die Alternation ein. Das negative Lookahead führt den gleichen Test durch. Wenn ‹if› erfolglos geprüft wird – was sichergestellt ist, weil ‹(?=if)› fehlschlug –, ist es erfolgreich. Daher wird der ‹else›-Zweig getestet. Setzt man das Lookahead in einen bedingten Ausdruck, spart man allerdings Zeit, weil ‹if› damit nur ein Mal ausgetestet wird.
Siehe auch Rezepte 2.9 und 2.16.
2.17 Abhängig von einer Bedingung eine von zwei Alternativen finden | 89
2.18 Kommentare für einen regulären Ausdruck Problem ‹\d{4}-\d{2}-\d{2}› passt zu einem Datum im Format yyyy-mm-dd, allerdings ohne eine Überprüfung der Zahlen auf Sinnhaftigkeit. Solch ein einfacher regulärer Ausdruck ist dann passend, wenn Sie wissen, dass Ihre Daten keine ungültigen Werte enthalten. Durch Hinzufügen von Kommentaren zum regulären Ausdruck soll deutlich gemacht werden, was jeder Teil des Ausdrucks tut.
Diskussion Freiform-Modus Reguläre Ausdrücke können schnell unübersichtlich und schwer verständlich werden. Analog zum Kommentieren von Quellcode sollten Sie auch alle regulären Ausdrücke kommentieren, die nicht ganz trivial sind. Alle in diesem Buch behandelten Regex-Varianten, mit Ausnahme von JavaScript, bieten eine alternative Syntax für reguläre Ausdrücke an, die es sehr leicht macht, Ihre regulären Ausdrücke sauber zu kommentieren. Sie können diese Syntax aktivieren, indem Sie die Freiform-Option einschalten. In den verschiedenen Programmiersprachen hat sie allerdings unterschiedliche Namen. In .NET setzen Sie die Option RegexOptions.IgnorePatternWhitespace. In Java übergeben Sie die Option Pattern.COMMENTS. Python erwartet re.VERBOSE. PHP, Perl und Ruby nutzen die Option /x. Schaltet man den Freiform-Modus ein, hat das zwei Effekte. Das Hash-Symbol (#) wird (außerhalb von Zeichenklassen) zu einem Metazeichen. Mit dem Hash wird ein Kommentar eingeleitet, der bis zum Ende der Zeile oder bis zum Ende der Regex läuft (je nachdem, was zuerst eintritt). Das Hash und alles danach wird von der Regex-Engine schlicht ignoriert. Um ein literales Hash-Zeichen zu finden, stecken Sie es entweder in eine Zeichenklasse ‹[#]› oder maskieren es per ‹\#›.
90 | Kapitel 2: Grundlagen regulärer Ausdrücke
Der andere Effekt ist, dass Leerraum – also Leerzeichen, Tabs und Zeilenumbrüche – ebenfalls außerhalb von Zeichenklassen ignoriert werden. Um ein literales Leerzeichen zu finden, müssen Sie es entweder innerhalb einer Zeichenklasse unterbringen ‹[z]› oder es maskieren ‹\z›. Wenn Sie sich Sorgen um die Lesbarkeit machen, können Sie stattdessen auch die hexadezimale Markierung ‹\x20› oder die Unicode-Maskierung ‹\u0020› bzw. ‹\x{0020}› nutzen. Um ein Tab-Zeichen zu finden, nutzen Sie ‹\t›, für Zeilenumbrüche ‹\r\n› (Windows) oder ‹\n› (Unix/Linux/OS X). Innerhalb von Zeichenklassen ändert sich durch den Freiform-Modus nichts. Eine Zeichenklasse ist ein einzelnes Token. Jegliche Whitespace-Zeichen oder Hashes innerhalb von Zeichenklassen sind literale Zeichen und Bestandteile der Zeichenklasse. Sie können diese Klassen nicht aufbrechen, um Kommentare darin unterzubringen.
Java hat Freiform-Zeichenklassen Reguläre Ausdrücke würden ihrem Ruf nicht gerecht, wenn nicht wenigstens eine Variante inkompatibel zu den anderen wäre. In diesem Fall ist Java der Ausreißer. In Java werden Zeichenklassen nicht als einzelnes Token verarbeitet. Wenn Sie den Freiform-Modus aktivieren, ignoriert Java Leerraum in Zeichenklassen, und Hashes beginnen auch dort einen Kommentar. Das bedeutet, dass Sie ‹[z]› und ‹[#]› nicht nutzen können, um diese literalen Zeichen zu finden. Greifen Sie stattdessen auf ‹\u0020› und ‹\#› zurück.
Regex-Optionen: Keine Regex-Varianten: .NET, PCRE, Perl, Python, Ruby Wenn Sie aus irgendeinem Grund keine Freiformsyntax nutzen können oder wollen, haben Sie immer noch die Möglichkeit, per ‹(?#Kommentar)› Kommentare einzufügen. Alle Zeichen zwischen ‹(?#› und ‹)› werden ignoriert. Leider ist JavaScript die einzige Variante in diesem Buch, die weder den Freiform-Modus noch die hier beschriebene Kommentarsyntax kennt. Java unterstützt Letztere ebenfalls nicht. (?x)\d{4} \d{2} \d{2}
# # # # #
Jahr Trennzeichen Monat Trennzeichen Tag
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Wenn Sie den Freiform-Modus außerhalb des regulären Ausdrucks nicht einschalten können, haben Sie die Möglichkeit, den Modus-Modifikator ‹(?x)› an den Anfang des
2.18 Kommentare für einen regulären Ausdruck | 91
regulären Ausdrucks zu setzen. Stellen Sie sicher, dass es vor dem ‹(?x)› keinen Leerraum gibt. Der Freiform-Modus beginnt erst mit dem Modus-Modifikator, jeglicher Leerraum vorher wird für die Regex berücksichtigt. Modus-Modifikatoren werden detailliert in „Übereinstimmungen unabhängig von Großund Kleinschreibung“ auf Seite 29, einem Abschnitt von Rezept 2.1, erläutert.
2.19 Literalen Text im Ersetzungstext nutzen Problem Suchen und Ersetzen des Suchergebnisses eines regulären Ausdrucks durch die acht literalen Zeichen $%\*$1\1.
Diskussion Welche Zeichen wo im Ersetzungstext zu maskieren sind Dieses Rezept zeigt Ihnen die verschiedenen Maskierungsregeln, die von den unterschiedlichen Ersetzungstextvarianten genutzt werden. Die einzigen beiden Zeichen, die Sie im Ersetzungstext je maskieren müssen, sind das Dollarzeichen und der Backslash. Die Maskierungszeichen sind ebenfalls das Dollarzeichen und der Backslash. Das in diesem Beispiel genutzte Prozentzeichen und der Stern sind immer literale Zeichen, auch wenn ein davor gesetzter Backslash als Maskierungszeichen statt als literaler Backslash behandelt werden kann. «$1» und/oder «\1» sind Rückwärtsreferenzen auf eine einfangende Gruppe. Rezept 2.21 erklärt, welche Variante welche Syntax für Rückwärtsreferenzen nutzt.
92 | Kapitel 2: Grundlagen regulärer Ausdrücke
Anhand der Tatsache, dass dieses Problem fünf verschiedene Lösungen für sieben Ersetzungstextvarianten hat, sieht man, dass es wirklich keinen Standard für die Ersetzungstextsyntax gibt.
.NET und JavaScript .NET und JavaScript behandeln einen Backslash immer als literales Zeichen. Maskieren Sie ihn niemals mit einem anderen Backslash, da Sie ansonsten im Ersetzungstext zwei Backslashs vorfinden werden. Ein allein stehendes Dollarzeichen ist ein literales Zeichen. Dollarzeichen müssen nur dann maskiert werden, wenn ihnen eine Ziffer, ein Kaufmanns-Und, ein Gravis (Backtick), ein einfaches Anführungszeichen, ein Unterstrich, ein Pluszeichen oder ein weiteres Dollarzeichen folgt. Um ein Dollarzeichen zu maskieren, nutzen Sie ein zweites Dollarzeichen. Sie können alle Dollarzeichen verdoppeln, wenn Sie das Gefühl haben, dass Ihr Ersetzungstext dadurch lesbarer wird. Diese Lösung ist daher gleichwertig: $$%\*$$1\1
Ersetzungstextvarianten: .NET, JavaScript .NET fordert zudem, dass Dollarzeichen, auf die eine öffnende geschweifte Klammer folgt, maskiert werden. «${Gruppe}» ist in .NET ein benannter Rückwärtsverweis. JavaScript unterstützt diese nicht.
Java In Java wird der Backslash genutzt, um Backslashs und Dollarzeichen im Ersetzungstext zu maskieren. Alle literalen Backslashs und Dollarzeichen müssen maskiert werden. Wenn Sie das nicht tun, wirft Java eine Exception.
PHP PHP braucht für Backslashs, auf die eine Ziffer folgt, und für Dollarzeichen, auf die eine Ziffer oder eine öffnende geschweifte Klammer folgt, eine Maskierung durch einen Backslash. Ein Backslash maskiert auch andere Backslashs. So müssen Sie also «\\\\» schreiben, um das Suchergebnis durch zwei literale Backslashs zu ersetzen. Alle andere Backslashs werden als literale Backslashs behandelt.
Perl Perl unterscheidet sich ein wenig von den anderen Ersetzungstextvarianten. Es hat eigentlich gar keine Ersetzungstextvariante. Wo andere Programmiersprachen eine besondere Logik haben, mit der die Routinen zum Suchen und Ersetzen Elemente substituieren, wie zum Beispiel «$1», gibt es in Perl einfach die normale Variablenersetzung. Im Ersetzungstext müssen Sie alle literalen Dollarzeichen mit einem Backslash maskieren, so wie Sie es in jedem normalen String in doppelten Anführungszeichen machen würden.
2.19 Literalen Text im Ersetzungstext nutzen | 93
Eine Ausnahme ist bei Perl, dass es die Syntax «\1» für Rückwärtsreferenzen unterstützt. Daher müssen Sie einen Backslash, auf den eine Ziffer folgt, maskieren, wenn der Backslash ein Literal sein soll. Ein Backslash, dem ein Dollarzeichen folgt, muss auch maskiert werden, um zu verhindern, dass der Backslash das Dollarzeichen maskiert. Ein Backslash maskiert zudem einen anderen Backslash. So müssen Sie also «\\\\» schreiben, um das Suchergebnis durch zwei literale Backslashs zu ersetzen. Alle anderen Backslashs werden als literale Backslashs behandelt.
Python und Ruby Das Dollarzeichen hat bei Python und Ruby keine besondere Bedeutung im Ersetzungstext. Backslashs müssen mit einem anderen Backslash maskiert werden, wenn ihnen ein Zeichen folgt, das dem Backslash eine besondere Bedeutung zuweist. Bei Python erzeugen «\1» bis «\9» und «\g\g
Ersetzungstextvariante: Python
Diskussion Durch das Einfügen des vollständigen Suchergebnisses der Regex in den Ersetzungstext ist es einfach, vor und/oder hinter dem gefundenen Text zusätzlichen Text einzufügen. Es lassen sich sogar mehrere Kopien des gefundenen Texts erzeugen. Sofern Sie nicht Python nutzen, müssen Sie auch keine einfangenden Gruppen in Ihrem regulären Ausdruck nutzen, um das gesamte Suchergebnis zu verwenden. In Perl ist «$&» sogar eine Variable. Perl speichert darin nach jeder erfolgreichen Anwendung einer Regex das Suchergebnis.
2.20 Einfügen des Suchergebnisses in den Ersetzungstext | 95
.NET und JavaScript haben die Perl-Syntax «$&» übernommen, um das Suchergebnis im Ersetzungstext einzufügen. Ruby nutzt statt der Dollarzeichen Backslashs, daher wird dort für das Suchergebnis «\&» verwendet. Java, PHP und Python haben kein spezielles Token, mit dem das gesamte Suchergebnis eingefügt werden kann, aber man kann einfangende Gruppen nutzen, um deren Inhalte in den Ersetzungstext einzufügen. Das Gesamtsuchergebnis ist dabei eine implizite einfangende Gruppe mit der Nummer 0. Bei Python müssen wir die Syntax für benannte Captures nutzen, um auf die Gruppe null zu verweisen. Dort wird «\0» nicht unterstützt. .NET und Ruby unterstützen ebenfalls eine einfangende Gruppe mit der Nummer 0. Das Ergebnis ist dort unabhängig von der Syntax.
Siehe auch „Suchen und Ersetzen mit regulären Ausdrücken“ in Kapitel 1 und Rezept 3.15.
2.21 Teile des gefundenen Texts in den Ersetzungstext einfügen Problem Finden einer zusammenhängende Folge von zehn Ziffern, wie zum Beispiel 1234567890. Umwandeln der Zahl in eine hübsch formatierte (amerikanische) Telefonnummer, zum Beispiel (123) 456-7890.
Diskussion Verwendung einfangender Gruppen in Ersetzungstexten Rezept 2.10 erklärt, wie Sie einfangende Gruppen in Ihrem regulären Ausdruck nutzen können, um den gleichen Text mehr als ein Mal zu finden. Der von jeder einfangenden Gruppe gefundene Text ist aber auch nach jeder erfolgreichen Übereinstimmung verfügbar. Sie können ihn – in beliebiger Reihenfolge und auch mehr als ein Mal – in den Ersetzungstext einfügen. Manche Varianten, wie zum Beispiel Python und Ruby, nutzen die gleiche Syntax für Rückwärtsreferenzen («\1») sowohl im regulären Ausdruck als auch im Ersetzungstext. Andere Varianten greifen auf die Perl-Syntax «$1» zurück, bei der ein Dollarzeichen statt eines Backslashs verwendet wird. PHP unterstützt beide Formen. In Perl ist «$1», auch mit höheren Ziffern, eine echte Variable. Diese Variablen werden nach jeder erfolgreichen Suche per Regex gesetzt. Sie können sie bis zur nächsten RegexSuche beliebig im Code nutzen. .NET, Java, JavaScript und PHP lassen «$1» nur im Ersetzungstext selbst zu. Diese Programmiersprachen bieten andere Wege an, um im Code auf den Inhalt von einfangenden Gruppen zuzugreifen. Kapitel 3 beschreibt das detailliert.
$10 und größer Alle in diesem Buch behandelten Regex-Varianten unterstützen in ihren regulären Ausdrücken bis zu 99 einfangende Gruppen. Im Ersetzungstext kann es dabei schwierig sein, bei «$10» oder «\10» und noch höheren Gruppennummern zu entscheiden, was gemeint ist. Man kann dies entweder als die zehnte einfangende Gruppe betrachten oder als erste einfangende Gruppe, auf die eine literale Null folgt. .NET, PHP und Perl ermöglichen es Ihnen, um die Zahl eine geschweifte Klammer zu legen, um Ihr Anliegen deutlicher zu machen. «${10}» ist immer die zehnte einfangende Gruppe, und «${1}0» ist immer die erste einfangende Gruppe, gefolgt von einer literalen Null. Java und JavaScript versuchen, bei «$10» schlau zu sein. Wenn in Ihrem regulären Ausdruck eine einfangende Gruppe mit der angegebenen zweistelligen Zahl vorhanden ist, werden beide Ziffern für die Gruppe genutzt. Gibt es weniger einfangende Gruppen, wird nur die erste Ziffer für die Gruppe und die zweite als literale Ziffer genutzt. Damit ist «$23» die 23. einfangende Gruppe, wenn es sie denn gibt. Wenn nicht, handelt es sich um die zweite einfangende Gruppe, gefolgt von einer literalen «3». .NET, PHP, Perl, Python und Ruby behandeln «$10» und «\10» immer als zehnte einfangende Gruppe, egal ob sie existiert oder nicht. Wenn sie nicht vorhanden ist, kommt das Verhalten bei nicht-existierenden Gruppen ins Spiel.
2.21 Teile des gefundenen Texts in den Ersetzungstext einfügen | 97
Referenzen auf nicht-existierende Gruppen Der reguläre Ausdruck in der Lösung für dieses Rezept enthält drei einfangende Gruppen. Wenn Sie im Ersetzungstext «$4» oder «\4» einfügen, ergänzen Sie damit eine Referenz auf eine einfangende Gruppe, die gar nicht vorhanden ist. Damit wird eine von drei verschiedenen Verhaltensweisen ausgelöst. Java und Python werden laut aufschreien und eine Exception werfen oder eine Fehlermeldung liefern. Bei diesen Varianten sollten Sie also auf keinen Fall ungültige Rückwärtsverweise nutzen. (Eigentlich sollten Sie bei keiner Variante ungültige Rückwärtsverweise verwenden.) Wenn Sie «$4» oder «\4» als Literal einfügen wollen, müssen Sie das Dollarzeichen oder den Backslash maskieren. Rezept 2.19 beschreibt das im Detail. PHP, Perl und Ruby ersetzen alle Rückwärtsreferenzen im Ersetzungstext, auch solche, die auf nicht vorhandene Gruppen zeigen. Solche Gruppen enthalten natürlich keinen Text, daher werden die Referenzen auf diese Gruppen einfach durch nichts ersetzt. .NET und JavaScript belassen schließlich Rückwärtsverweise auf Gruppen, die nicht vorhanden sind, als literalen Text im Ersetzungstext. Alle Varianten ersetzen Gruppen, die zwar im regulären Ausdruck vorhanden sind, aber nichts gefunden haben. Dabei wird leerer Text eingefügt.
Lösung mit benannten Captures Regulärer Ausdruck \b(?<area>\d{3})(?<exchange>\d{3})(?\d{4})\b
Varianten, die benannte Captures unterstützen .NET, Python und Ruby 1.9 ermöglichen es Ihnen, benannte Rückwärtsreferenzen im Ersetzungstext zu verwenden, wenn Sie benannte einfangende Gruppen in Ihrem regulären Ausdruck eingebaut haben. Bei .NET und Python funktioniert die Syntax für benannte Rückwärtsreferenzen genau so wie die von benannten und nummerierten einfangenden Gruppen. Geben Sie einfach den Namen oder die Nummer der Gruppe zwischen den geschweiften oder spitzen Klammern an. Ruby nutzt für Rückwärtsreferenzen die gleiche Syntax im Ersetzungstext wie im regulären Ausdruck. Bei benannten einfangenden Gruppen ist die Syntax in Ruby 1.9 «\k» oder «\k'group'». Die Wahl zwischen spitzen Klammern und einfachen Anführungszeichen ist schlicht Geschmackssache. Perl 5.10 und PHP (mit PCRE) unterstützen benannte einfangende Gruppen in regulären Ausdrücken, aber nicht im Ersetzungstext. Dort können Sie nummerierte Rückwärtsreferenzen verwenden, um auf die benannten einfangenden Gruppen aus dem regulären Ausdruck zuzugreifen. Perl 5.10 und PCRE weisen auch benannten Gruppen Nummern zu – von links nach rechts. .NET, Python und Ruby 1.9 lassen ebenfalls nummerierte Referenzen auf benannte Gruppen zu. Allerdings verwendet .NET ein anderes Nummerierungsschema für benannte Gruppen, wie in Rezept 2.11 erläutert. Ein Mischen von Namen und Nummern ist in .NET, Python oder Ruby nicht empfehlenswert. Entweder geben Sie all Ihren Gruppen Namen, oder Sie lassen es ganz. Nutzen Sie aber vor allem immer benannte Rückwärtsreferenzen für benannte Gruppen.
Siehe auch „Suchen und Ersetzen mit regulären Ausdrücken“ in Kapitel 1 und die Rezepte 2.9, 2.10, 2.11 und 3.15.
2.21 Teile des gefundenen Texts in den Ersetzungstext einfügen | 99
2.22 Suchergebniskontext in den Ersetzungstext einfügen Problem Erstellen eines Ersetzungstexts, der das Suchergebnis durch den Text vor der Übereinstimmung ersetzt, gefolgt vom kompletten Ausgangstext und dem Text nach der Übereinstimmung. Wenn zum Beispiel das Wort Treffer in VorherTrefferNachher gefunden wird, soll die Übereinstimmung durch VorherVorherTrefferNachherNachher ersetzt werden, was zum Ergebnis VorherVorherVorherTrefferNachherNachherNachher führt.
Lösung $`$_$'
Ersetzungstextvarianten: .NET, Perl \`\`\&\'\'
Ersetzungstextvariante: Ruby $`$`$&$'$'
Ersetzungstextvariante: JavaScript
Diskussion Der Begriff Kontext bezieht sich auf den Ausgangstext, auf den der reguläre Ausdruck angewandt wurde. Es gibt drei Elemente des Kontexts: den Ausgangstext vor dem Übereinstimmungsbereich, den Ausgangstext nach dem Übereinstimmungsbereich und den gesamten Ausgangstext. Der Text vor dem Übereinstimmungsbereich wird manchmal als linker Kontext und der danach dementsprechend als rechter Kontext bezeichnet. Der gesamte Ausgangstext setzt sich aus dem linken Kontext, dem Übereinstimmungsbereich und dem rechten Kontext zusammen. .NET und Perl unterstützen «$`», «$'» und «$_», um alle drei Kontextelemente im Ersetzungstext einzufügen. In Perl handelt es sich sogar um Variablen, die nach einer erfolgreichen Regex-Suche gefüllt werden und bis zum nächsten Suchvorgang im Code zur Verfügung stehen. Dollar plus Gravis (Backtick) ist der linke Kontext. Auf einer deutschen Tastatur erreichen Sie den Gravis, indem Sie die Umschalttaste zusammen mit der Taste rechts neben dem scharfen s (ß) und danach eventuell noch einmal die Leertaste drücken. Dollar plus einfaches Anführungszeichen ist der rechte Kontext. Das einfache Anführungszeichen erreicht man auf einer deutschen Tastatur üblicherweise über die Umschalttaste zusammen mit der Taste rechts neben dem Ä. Dollar plus Unterstrich enthält den gesamten Ausgangstext. Wie .NET und Perl nutzt JavaScript «$`» und «$'» für den linken und rechten Kontext. Allerdings gibt es in JavaScript kein Token für das Einfügen des gesamten Ausgangstexts. Sie können ihn zusammensetzen, indem Sie das Suchergebnis «$&» mit dem linken und rechten Kontext verknüpfen.
100 | Kapitel 2: Grundlagen regulärer Ausdrücke
In Ruby kann man auf den linken und rechten Kontext über «\`» und «\'» zugreifen. «\&» fügt das vollständige Suchergebnis ein. Wie bei JavaScript gibt es kein Token für den gesamten Ausgangstext.
Siehe auch „Suchen und Ersetzen mit regulären Ausdrücken“ in Kapitel 1 und Rezept 3.15.
2.22 Suchergebniskontext in den Ersetzungstext einfügen | 101
KAPITEL 3
Mit regulären Ausdrücken programmieren
Programmiersprachen und Regex-Varianten Dieses Kapitel beschreibt, wie man reguläre Ausdrücke mit der Programmiersprache Ihrer Wahl implementiert. Die Rezepte in diesem Kapitel gehen davon aus, dass Sie schon einen regulären Ausdruck haben, der funktioniert. Das vorhergehende Kapitel kann Ihnen da eventuell weiterhelfen. Jetzt haben Sie die Aufgabe, einen regulären Ausdruck in Quellcode umzuwandeln und auch dafür zu sorgen, dass er tatsächlich etwas tut. Wir geben in diesem Kapitel unser Bestes, um genau zu erklären, wie und warum jeder Codeabschnitt so arbeitet, wie er arbeitet. Aufgrund der vielen Details in diesem Kapitel kann es etwas ermüdend sein, es vom Anfang bis zum Ende durchzulesen. Wenn Sie das Reguläre Ausdrücke Kochbuch das erste Mal lesen, empfehlen wir Ihnen, dieses Kapitel zunächst nur zu überfliegen, um zu verstehen, was möglich ist und was nicht. Wollen Sie dann später einen der regulären Ausdrücke aus den folgenden Kapiteln implementieren, kommen Sie hierhin zurück, um zu erfahren, wie man genau die Regexes in Ihre Programmiersprache integriert. Die Kapitel 4 bis 8 nutzen reguläre Ausdrücke, um reale Probleme zu lösen. Diese Kapitel konzentrieren sich auf die regulären Ausdrücke selbst, und viele Rezepte enthalten überhaupt keinen Quellcode. Damit die dort gefundenen regulären Ausdrücke auch wirklich funktionieren, fügen Sie sie einfach in einen der Codeabschnitte aus diesem Kapitel ein. Da sich die anderen Kapitel auf die regulären Ausdrücke konzentrieren, werden die Lösungen dort für bestimmte Regex-Varianten vorgestellt und nicht für bestimmte Programmiersprachen. Regex-Varianten entsprechen nicht eins zu eins Programmiersprachen. Skriptsprachen haben meist schon ihre eigene Regex-Variante eingebaut, während andere Programmiersprachen auf Bibliotheken zurückgreifen. Einige der Bibliotheken gibt es für mehrere Sprachen, während manche Sprache auch mehrere Bibliotheken im Angebot hat. „Viele Varianten regulärer Ausdrücke“ auf Seite 2, beschreibt alle in diesem Buch behandelten Regex-Varianten, „Viele Varianten des Ersetzungstexts“ auf Seite 6, zählt die Vari| 103
anten für die Ersetzungstexte auf, die beim Suchen und Ersetzen mithilfe regulärer Ausdrücke genutzt werden. Alle in diesem Kapitel besprochenen Programmiersprachen nutzen eine dieser Varianten.
In diesem Kapitel behandelte Sprachen Dieses Kapitel behandelt acht Programmiersprachen. Jedes Rezept enthält eigene Lösungen für alle acht Sprachen, und viele Rezepte enthalten auch getrennte Diskussionsabschnitte für alle acht Sprachen. Wenn sich eine Technik mit mehr als einer Sprache einsetzen lässt, wiederholen wir sie für jede der Sprachen. Das haben wir gemacht, damit Sie ohne Gefahr die Diskussionen zu Programmiersprachen überspringen können, an denen Sie nicht interessiert sind. C# C# nutzt das .NET Framework von Microsoft. Die Klassen in System.Text.RegularExpressions nutzen die .NET-Variante für reguläre Ausdrücke und Ersetzungstexte. Dieses Buch behandelt C# in den Versionen 1.0 bis 3.5 oder Visual Studio 2002 bis 2008. VB.NET Dieses Buch nutzt VB.NET und Visual Basic.NET, um auf Visual Basic 2002 und neuer zu verweisen und diese Versionen von Visual Basic 6 und älter zu unterscheiden. Visual Basic nutzt mittlerweile das .NET Framework von Microsoft. Die Klassen in System.Text.RegularExpressions nutzen die .NET-Variante für reguläre Ausdrücke und Ersetzungstexte. Dieses Buch behandelt Visual Basic 2002 bis 2008. Java Java 4 ist das erste Java-Release, das reguläre Ausdrücke auch ohne externe Anbieter unterstützt. Dazu dient das Paket java.util.regex. Es verwendet die Java-Variante für reguläre Ausdrücke und Ersetzungstexte. Dieses Buch behandelt Java 4, 5 und 6. JavaScript Dies ist die Regex-Variante, die in der Programmiersprache genutzt wird, die allgemein als JavaScript bekannt ist. Alle modernen Webbrowser haben sie implementiert: Internet Explorer (ab Version 5.5), Firefox, Opera, Safari und Chrome. Viele andere Anwendungen nutzen ebenfalls JavaScript als Skriptsprache. Streng genommen verwenden wir den Begriff JavaScript in diesem Buch, um uns auf die Programmiersprache zu beziehen, die in Version 3 des ECMA-262-Standards definiert ist. Dieser Standard definiert die Programmiersprache ECMAScript, die eher durch ihre Implementierungen JavaScript und JScript in den verschiedenen Webbrowsern bekannt ist. ECMA-262v3 definiert auch die Variante für reguläre Ausdrücke und Ersetzungstexte, die von JavaScript genutzt wird. In diesem Buch bezeichnen wir diese Varianten als „JavaScript“.
104 | Kapitel 3: Mit regulären Ausdrücken programmieren
PHP PHP bietet drei verschiedene Arten von Regex-Funktionen an. Wir empfehlen dringend, die preg-Funktionen zu nutzen. Daher geht es in diesem Buch nur um die preg-Funktionen, die es in PHP seit Version 4.2.0 gibt. Dieses Buch behandelt PHP 4 und 5. Die preg-Funktionen sind PHP-Wrapper um die PCRE-Bibliothek. Die PCRE-Regex-Variante wird in diesem Buch als „PCRE“ bezeichnet. Da PCRE keine Funktionalität zum Suchen und Ersetzen enthält, haben die PHP-Entwickler ihre eigene Ersetzungstextsyntax für preg_replace entwickelt. Diese Ersetzungstextvariante wird im Weiteren als „PHP“ bezeichnet. Die mb_ereg-Funktionen sind Teil von PHPs „Multibyte“-Funktionen, die dafür gedacht sind, gut mit Sprachen zusammenzuarbeiten, die traditionell mit MehrbyteZeichensätzen kodiert sind, wie zum Beispiel Japanisch und Chinesisch. In PHP 5 nutzen die mb_ereg-Funktionen die Regex-Bibliothek Oniguruma, die ursprünglich für Ruby entwickelt wurde. Die Oniguruma-Regex-Variante wird hier als „Ruby 1.9“ bezeichnet. Sie sollten die mb_ereg-Funktionen nur dann nutzen, wenn Sie auf jeden Fall mit Mehrbyte-Codepages arbeiten müssen und schon mit den mb_-Funktionen in PHP vertraut sind. Die Gruppe der ereg-Funktionen enthält die ältesten Regex-Funktionen in PHP und gilt offiziell seit PHP 5.3.0 als veraltet. Diese Funktionen benötigen keine externen Bibliotheken, und sie implementieren die POSIX ERE-Variante. Diese Variante stellt nur einen eingeschränkten Satz an Features zur Verfügung und wird auch nicht in diesem Buch behandelt. POSIX ERE ist eine echte Untermenge der Varianten Ruby 1.9 und PCRE. Sie können jede Regex aus einem ereg-Funktionsaufruf nehmen und mit mb_ereg oder preg aufrufen. Bei preg müssen Sie noch Begrenzer im Perl-Stil hinzufügen (Rezept 3.1). Perl Die in Perl eingebaute Unterstützung für reguläre Ausdrücke ist der Hauptgrund für deren aktuelle Beliebtheit. Die von den Perl-Operatoren m// und s/// genutzten Varianten für Regexes und Ersetzungstexte werden in diesem Buch als „Perl“ bezeichnet. Behandelt werden Perl 5.6, 5.8 und 5.10. Python Python unterstützt reguläre Ausdrücke über sein Modul re. Die Varianten für reguläre Ausdrücke und Ersetzungstexte, die dieses Modul nutzt, werden in diesem Buch als „Python“ bezeichnet. Behandelt werden Python 2.4 and 2.5. Ruby Ruby enthält bereits in der Sprache eine Unterstützung für reguläre Ausdrücke. In diesem Buch werden Ruby 1.8 und Ruby 1.9 behandelt. Diese zwei Versionen von Ruby nutzen unterschiedliche Regex-Engines. Ruby 1.9 verwendet die OnigurumaEngine, die mehr Features enthält als die klassische Engine aus Ruby 1.8. „RegexVarianten in diesem Buch“ auf Seite 3 geht darauf detaillierter ein. In diesem Kapitel geht es uns nicht so sehr um die Unterschiede zwischen Ruby 1.8 und 1.9. Die regulären Ausdrücke in diesem Kapitel sind sehr einfach und verwen-
Programmiersprachen und Regex-Varianten | 105
den keine der neuen Features aus Ruby 1.9. Da die Unterstützung für reguläre Ausdrücke in die Sprache selbst hineinkompiliert ist, ist der Ruby-Code, den Sie zum Implementieren Ihrer regulären Ausdrücke nutzen, unabhängig von der verwendeten Engine – egal ob Sie Ruby mit der klassischen Regex-Engine oder mit der Oniguruma-Engine kompiliert haben. Sie können Ruby 1.8 neu kompilieren und dabei die Oniguruma-Engine nutzen, wenn Sie deren Features benötigen.
Mehr Programmiersprachen Die Programmiersprachen in der folgenden Liste werden in diesem Buch nicht explizit behandelt, aber sie nutzen eine der hier vorgestellten Regex-Varianten. Sollten Sie mit einer der folgenden Sprachen arbeiten, können Sie dieses Kapitel überspringen, aber alle weiteren werden trotzdem nützlich für Sie sein: ActionScript ActionScript ist Adobes Implementierung des ECMA-262-Standards. Seit Version 3.0 enthält ActionScript eine vollständige Unterstützung der in ECMA-262v3 definierten regulären Ausdrücke. Diese Regex-Variante wird in diesem Buch als „JavaScript“ bezeichnet. Die Sprache ActionScript ist JavaScript sehr ähnlich. Sie sollten die in diesem Kapitel vorgestellten JavaScript-Beispiele an ActionScript anpassen können. C C kann eine große Zahl an Bibliotheken für reguläre Ausdrücke nutzen. Die Open Source-Bibliothek PCRE ist vermutlich die beste Wahl aus den hier vorgestellten Varianten. Sie können den vollständigen C-Quellcode unter http://www.pcre.org herunterladen. Der Code ist so geschrieben, dass er sich mit einer Vielzahl von Compilern auf vielen Plattformen kompilieren lässt. C++ C++ kann ebenfalls diverse Bibliotheken für reguläre Ausdrücke nutzen. Die Open Source-Bibliothek PCRE ist vermutlich die beste Wahl aus den hier vorgestellten Varianten. Sie können entweder direkt die C-API oder die C++-Wrapper-Klassen nutzen, die in der PCRE bereits enthalten sind (siehe http://www.pcre.org). Unter Windows können Sie das COM-Objekt VBScript 5.5 RegExp COM importieren, wie es später für Visual Basic 6 erläutert wird. Das kann nützlich sein, um in einem C++-Backend und einem JavaScript-Frontend die gleichen Regexes nutzen zu können. Delphi for Win32 Während der Entstehung dieses Texts enthält die Win32-Version von Delphi keine eingebaute Unterstützung für reguläre Ausdrücke. Es gibt viele VCL-Komponenten, mit denen reguläre Ausdrücke genutzt werden können. Ich empfehle, eine zu verwenden, die auf PCRE basiert. Delphi kann C-Objektdateien in Ihre Anwendungen einbinden, und viele VCL-Wrapper für PCRE nutzen solche Objektdateien. Damit können Sie Ihre Anwendung als EXE-Datei ausliefern.
106 | Kapitel 3: Mit regulären Ausdrücken programmieren
Sie können die von mir (Jan Goyvaerts) entwickelte TPerlRegEx-Komponente unter http://www.regexp.info/delphi.html herunterladen. Dabei handelt es sich um eine VCL-Komponente, die sich selbst in der Komponentenpalette installiert, sodass sie sich leicht auf eine Form ziehen lässt. Ein weiterer beliebter PCRE-Wrapper für Delphi ist die Klasse TJclRegEx, die Teil der JCL-Bibliothek ist (siehe http://www.delphijedi.org). TJclRegEx leitet sich von TObject ab, daher lässt sie sich nicht auf eine Form ziehen. Beide Bibliotheken sind Open Source und befinden sich unter der Mozilla Public License. Delphi Prism In Delphi Prism können Sie die vom .NET Framework angebotene Unterstützung für reguläre Ausdrücke nutzen. Fügen Sie einfach der uses-Klausel einer Delphi Prism Unit, in der Sie reguläre Ausdrücke nutzen wollen, System.Text.RegularExpressions hinzu. Wenn Sie das erledigt haben, können Sie die gleichen Techniken nutzen, die in den Codeschnipseln für C# und VB.NET in diesem Kapitel demonstriert werden. Groovy Sie können in Groovy reguläre Ausdrücke genau wie in Java mit dem Paket java.util.regex nutzen. Tatsächlich müssten alle in diesem Kapitel vorgestellten Lösungen für Java auch mit Groovy funktionieren. Die Groovy-eigene Syntax für reguläre Ausdrücke stellt eigentlich nur Notationsabkürzungen dar. Eine literale Regex, die durch Schrägstriche begrenzt ist, ist eine Instanz von java.lang.String, und der Operator =~ instantiiert java.util.regex.Matcher. Sie können die GroovySyntax mit der normalen Java-Syntax beliebig mischen – die Klassen und Objekte sind überall gleich. PowerShell PowerShell ist Microsofts eigene Shell-Skriptsprache. Sie basiert auf dem .NET Framework. Die in die PowerShell eingebauten Operatoren -match und -replace nutzen die in diesem Buch vorgestellten .NET-Varianten für reguläre Ausdrücke und Ersetzungstexte. R Das Projekt R unterstützt reguläre Ausdrücke über die Funktionen grep, sub und regexpr aus dem Paket base. Alle diese Funktionen erwarten ein Argument namens perl, das auf FALSE gesetzt wird, wenn Sie es weglassen. Setzen Sie es auf TRUE, um die in diesem Buch beschriebene PCRE-Variante zu verwenden. Die für PCRE 7 gezeigten Ausdrücke arbeiten mit R 2.5.0 und neuer. Frühere Versionen von R nutzen reguläre Ausdrücke, die in diesem Buch mit „PCRE 4 und neuer“ gekennzeichnet sind. Die von R unterstützten Varianten „basic“ und „extended“ sind älter und eingeschränkter. In diesem Buch werden sie nicht behandelt.
Programmiersprachen und Regex-Varianten | 107
REALbasic REALbasic hat eine eingebaute Klasse RegEx. Intern nutzt diese Klasse die UTF-8Version der PCRE-Bibliothek. Das bedeutet, dass Sie die Unicode-Unterstützung der PCRE nutzen können, aber die REALbasic-Klasse TextConverter verwenden müssen, um Nicht-ASCII-Texte nach UTF-8 zu konvertieren, bevor Sie sie an die Klasse RegEx übergeben. Alle in diesem Buch für PCRE 6 gezeigten regulären Ausdrücke funktionieren in REALbasic. Allerdings sind in REALbasic die Optionen Groß-/Kleinschreibung ignorieren und Zirkumflex und Dollar passen zu Zeilenumbrüchen (“Multiline”) standardmäßig eingeschaltet. Wenn Sie einen regulären Ausdruck aus diesem Buch nutzen wollen, der nicht explizit darauf hinweist, diese Optionen einzuschalten, müssen Sie sie in REALbasic deaktivieren. Scala Scala stellt eine eingebaute Unterstützung durch das Paket scala.util.matching bereit. Dabei wird die Regex-Engine aus dem Java-Paket java.util.regex genutzt. Die von Java und Scala genutzten Varianten für reguläre Ausdrücke und Ersetzungstexte werden in diesem Buch als „Java“ bezeichnet. Visual Basic 6 Visual Basic 6 ist die letzte Version von Visual Basic, die nicht das .NET Framework benötigt. Das bedeutet aber auch, dass Visual Basic 6 die ausgezeichnete Unterstützung von regulären Ausdrücken durch das .NET Framework nicht nutzen kann. Die Codebeispiele in diesem Kapitel für VB.NET funktionieren nicht mit VB 6. Mit Visual Basic 6 kann man aber sehr leicht Funktionalität aus ActiveX- und COMBibliotheken integrieren. Eine solche Bibliothek ist die VBScript-Skripting-Bibliothek von Microsoft, die ab Version 5.5 eine halbwegs anständige Unterstützung für reguläre Ausdrücke anbietet. Die Skripting-Bibliothek implementiert die gleiche Regex-Variante, die auch in JavaScript genutzt wird und die in ECMA-262v3 standardisiert ist. Diese Bibliothek ist Teil des Internet Explorer 5.5 und neuer. Sie steht auf allen Rechnern zur Verfügung, die unter Windows XP oder Vista laufen, aber auch unter älteren Windows-Versionen, wenn der Benutzer den IE auf Version 5.5 oder neuer aktualisiert hat. Das bedeutet, nahezu jeder Windows-PC, der für das Surfen im Internet verwendet wird, bietet diese Bibliothek an. Um diese Bibliothek in Ihrer Visual Basic-Anwendung zu nutzen, wählen Sie im VBIDE-Menü Project/References. Blättern Sie durch die Liste, bis Sie den Eintrag Microsoft VBScript Regular Expressions 5.5 finden, der direkt unter Microsoft VBScript Regular Expressions 1.0 liegt. Stellen Sie sicher, dass Sie Version 5.5 markiert haben und nicht Version 1.0. Letztere ist nur aus Gründen der Abwärtskompatibilität verfügbar, und ihre Fähigkeiten sind nicht sehr umfangreich. Nach dem Hinzufügen der Referenz können Sie sehen, welche Klassen und KlassenMember durch die Bibliothek bereitgestellt werden. Wählen Sie im Menü View/ Object Browser aus. Im Object Browser wählen Sie aus der Auswahlliste in der linken oberen Ecke die Bibliothek VBScript_RegExp_55 aus.
108 | Kapitel 3: Mit regulären Ausdrücken programmieren
3.1
Literale reguläre Ausdrücke im Quellcode
Problem Sie haben den regulären Ausdruck ‹[$"'\n\d/\\]› als Lösung für ein Problem erhalten. Dieser reguläre Ausdruck besteht nur aus einer Zeichenklasse, mit der ein Dollarzeichen, ein doppeltes Anführungszeichen, ein einfaches Anführungszeichen, ein Line-Feed, eine Ziffer von 0 bis 9, ein Schrägstrich oder ein Backslash gefunden werden kann. Sie wollen diesen regulären Ausdruck hartkodiert in Ihrem Quellcode als String-Konstante oder als Regex-Operator einbauen.
Python String mit dreifachem Anführungszeichen: r"""[$"'\n\d/\\]"""
Normaler String: "[$\"'\n\\d/\\\\]"
Ruby Literale Regex, begrenzt durch Schrägstriche: /[$"'\n\d\/\\]/
Literale Regex, begrenzt durch ein Zeichen Ihrer Wahl: %r![$"'\n\d/\\]!
Diskussion Wenn in diesem Buch ein regulärer Ausdruck allein vorgestellt wird (und nicht als Teil eines längeren Quellcodeabschnitts), wird er immer ganz schmucklos präsentiert. Dieses Rezept ist die einzige Ausnahme dazu. Wollen Sie ein Programm zum Testen regulärer Ausdrücke nutzen, wie zum Beispiel RegexBuddy oder RegexPal, würden Sie die Regex genau so eingeben. Erwartet Ihre Anwendung einen regulären Ausdruck als Benutzereingabe, würde der Anwender sie ebenfalls so eingeben. Möchten Sie jedoch den regulären Ausdruck hartkodiert in Ihrem Quellcode unterbringen, müssen Sie ein bisschen mehr tun. Wenn Sie einfach einen regulären Ausdruck aus einem Testprogramm in Ihren Quellcode kopieren – oder umgekehrt –, wird das häufig dazu führen, dass Sie sich am Kopf kratzen und sich fragen, warum der Ausdruck in Ihrem Tool funktioniert, aber nicht in Ihrem Quellcode, oder warum das Testprogramm nichts mit einer Regex anfangen kann, die Sie aus dem Quellcode von jemand anderem kopiert haben. Bei allen in diesem Buch besprochenen Programmiersprachen müssen literale reguläre Ausdrücke auf eine bestimmte Art und Weise begrenzt sein, wobei manche Sprachen Strings nutzen und andere dagegen eine spezielle Regex-Konstante verwenden. Wenn Ihre Regex eines der Begrenzungszeichen Ihrer Sprache enthält oder andere Zeichen mit besonderen Bedeutungen, müssen Sie sie maskieren. Der Backslash ist das am häufigsten genutzte Maskierungszeichen. Das ist der Grund dafür, dass die meisten Lösungen für dieses Problem deutlich mehr Backslashs enthalten als die im ursprünglichen regulären Ausdruck vorhandenen vier.
C# In C# können Sie literale reguläre Ausdrücke an den Konstruktor Regex() und eine ganze Reihe von Member-Funktionen der Klasse Regex übergeben. Der Parameter, der den regulären Ausdruck aufnimmt, ist immer als String deklariert.
110 | Kapitel 3: Mit regulären Ausdrücken programmieren
C# unterstützt zwei Arten von String-Literalen. Die gebräuchlichste ist der String in doppelten Anführungszeichen, der auch aus Sprachen wie C++ und Java bekannt ist. Bei solchen Strings müssen doppelte Anführungszeichen und Backslashs durch einen Backslash maskiert werden. Maskierte Zeichen für nicht druckbare Zeichen, wie zum Beispiel ‹\n›, sind in Strings ebenfalls möglich. Es gibt aber einen Unterschied zwischen "\n" und "\\n", wenn RegexOptions.IgnorePatternWhitespace genutzt wird (siehe Rezept 3.4), um den Freiform-Modus einzuschalten (erklärt in Rezept 2.18). "\n" ist ein String mit einem literalen Zeilenumbruch, was als Whitespace ignoriert wird, "\\n" ist dagegen ein String mit dem Regex-Token ‹\n›, das zu einem Newline-Zeichen passt. Verbatim-Strings beginnen mit einem At-Zeichen und einem doppelten Anführungszeichen, und sie enden mit einem doppelten Anführungszeichen. Um ein doppeltes Anführungszeichen in einem Verbatim-String zu nutzen, müssen Sie es verdoppeln. Backslashs werden zum Maskieren nicht benötigt, wodurch sich die Lesbarkeit eines regulären Ausdrucks deutlich verbessert. @"\n" ist immer das Regex-Token ‹\n›, mit dem ein Zeilenumbruch gefunden wird, selbst im Freiform-Modus. Verbatim-Strings unterstützen kein ‹\n› auf String-Ebene, sie können jedoch mehrere Zeilen umfassen. Damit sind sie ideale Kandidaten für reguläre Ausdrücke im Freiform-Modus. Die Wahl ist klar: Für reguläre Ausdrücke sollte man im C#-Quellcode Verbatim-Strings nutzen.
VB.NET In VB.NET können Sie literale reguläre Ausdrücke an den Konstruktor Regex() und verschiedene Member-Funktionen der Klasse Regex übergeben. Die dafür genutzten Parameter sind immer als String deklariert. Visual Basic nutzt Strings zwischen doppelten Anführungszeichen. Innerhalb solcher Strings müssen doppelte Anführungszeichen verdoppelt werden. Andere Zeichen sind nicht zu maskieren.
Java In Java können Sie literale reguläre Ausdrücke an die Klassenfabrik Pattern.compile() und an eine Reihe von Funktionen der Klasse String übergeben. Die entsprechenden Parameter für die regulären Ausdrücke sind immer als Strings deklariert. Java nutzt Strings zwischen doppelten Anführungszeichen. In solchen Strings müssen doppelte Anführungszeichen und Backslashs durch einen Backslash maskiert werden. Maskierte Zeichen für nicht druckbare Zeichen, wie zum Beispiel ‹\n›, und UnicodeMaskierungen wie ‹\uFFFF› werden in Strings ebenfalls unterstützt. Es gibt einen Unterschied zwischen "\n" und "\\n", wenn man Pattern.COMMENTS nutzt (siehe Rezept 3.4), um den Freiform-Modus einzuschalten (erklärt in Rezept 2.18). "\n" ist ein String mit einem literalen Zeilenumbruch, was als Whitespace ignoriert wird, "\\n" ist dagegen ein String mit dem Regex-Token ‹\n›, das zu einem Newline-Zeichen passt.
3.1 Literale reguläre Ausdrücke im Quellcode |
111
JavaScript In JavaScript werden reguläre Ausdrücke am besten erstellt, indem man die spezielle Syntax für das Deklarieren literaler regulärer Ausdrücke nutzt. Fassen Sie Ihren regulären Ausdruck einfach in Schrägstriche ein. Wenn innerhalb des regulären Ausdrucks selbst ein Schrägstrich vorkommt, maskieren Sie ihn mit einem Backslash. Obwohl es möglich ist, ein RegExp-Objekt aus einem String zu erstellen, ist es nicht so sinnvoll, die String-Notation für literale reguläre Ausdrücke in Ihrem Code zu nutzen. Sie müssten Anführungszeichen und Backslashs maskieren, was im Allgemeinen zu einem ganzen Wald von Backslashs führt.
PHP Literale reguläre Ausdrücke für die preg-Funktionen von PHP nutzen eine interessante Syntax. Anders als in JavaScript oder Perl gibt es in PHP keinen eingebauten Typ für reguläre Ausdrücke. Sie müssen immer als Strings eingegeben werden. Das gilt auch für die ereg- und mb_ereg-Funktionen. Aber bei dem Versuch, sich so weit wie möglich an Perl zu orientieren, haben die Entwickler der PHP-Wrapper-Funktionen für die PCRE noch eine weitere Anforderung hinzugefügt. Innerhalb des Strings muss der reguläre Ausdruck wie eine literale Regex in Perl notiert werden. Würden Sie also in Perl /Regex/ schreiben, bräuchten die preg-Funktionen in PHP für den String den Wert '/Regex/'. Wie in Perl können Sie beliebige Begrenzungszeichen nutzen. Wenn das Regex-Begrenzungszeichen innerhalb der Regex auftaucht, muss es durch einen Backslash maskiert werden. Um das zu verhindern, sollten Sie ein Begrenzungszeichen wählen, das nicht in der Regex vorkommt. Für dieses Rezept nutze ich das Prozentzeichen, weil der Schrägstrich Teil der Regex ist. Ist er jedoch nicht Teil der Regex, sollten Sie ihn als Begrenzungszeichen nutzen, da er in Perl das am häufigsten dafür genutzte Zeichen ist. In JavaScript und Ruby ist er sogar verpflichtend. PHP unterstützt Strings sowohl mit einfachen als auch mit doppelten Anführungszeichen. Bei beiden müssen die Anführungszeichen (einfache und doppelte) sowie der Backslash innerhalb einer Regex mit einem Backslash maskiert werden. In Strings mit doppelten Anführungszeichen muss auch das Dollarzeichen maskiert werden. Für reguläre Ausdrücke sollten Sie Strings in einfachen Anführungszeichen nutzen, sofern Sie nicht tatsächlich Variablen innerhalb Ihrer Regex auflösen wollen.
Perl In Perl werden literale reguläre Ausdrücke zusammen mit dem Operator zur Musterübereinstimmung und zum Ersetzen genutzt. Der Operator zur Musterübereinstimmung besteht aus zwei Schrägstrichen, zwischen denen sich die Regex befindet. Schrägstriche innerhalb des regulären Ausdrucks müssen durch einen Backslash maskiert werden. Andere Zeichen benötigen keine Maskierung, außer vielleicht $ und @, wie am Ende dieses Abschnitts noch erläutert wird.
112 | Kapitel 3: Mit regulären Ausdrücken programmieren
Eine alternative Notation für den Musterübereinstimmungsoperator steckt den regulären Ausdruck zwischen zwei beliebige Satzzeichen, vor denen der Buchstabe m steht. Wenn Sie beliebige Zeichen für den öffnenden und schließenden Begrenzer nutzen (runde, geschweifte oder eckige Klammern), müssen diese zueinander passen, zum Beispiel bei m{Regex}. Verwenden Sie andere Satzzeichen, können Sie das Zeichen doppelt nutzen. Die Lösung für dieses Rezept verwendet das Ausrufezeichen. Damit müssen wir den literalen Schrägstrich im regulären Ausdruck nicht maskieren. Wenn das öffnende und das schließende Zeichen unterschiedlich sind, muss nur das schließende Zeichen mit einem Backslash maskiert werden, wenn es als Literal im regulären Ausdruck vorkommt. Der Ersetzungsoperator sieht dem Musterübereinstimmungsoperator sehr ähnlich. Er beginnt mit einem s statt mit einem m, und am Ende findet sich noch der Ersetzungstext. Wenn Sie Klammern als Begrenzer nutzen, brauchen Sie zwei Paare: s[Regex][Ersetzung]. Alle anderen Satzzeichen nutzen Sie drei Mal: s/Regex/Ersetzung/. Perl parst die beiden Operatoren als Strings in doppelten Anführungszeichen. Wenn Sie m/Ich heiße $name/ schreiben und $name den Wert "Jan" enthält, führt das im Endeffekt zum regulären Ausdruck ‹IchzheißezJan›. $" ist in Perl ebenfalls eine Variable, daher müssen wir das literale Dollarzeichen in unserer Zeichenklasse hier im Rezept maskieren. Maskieren Sie niemals ein Dollarzeichen, das Sie als Anker nutzen wollen (siehe Rezept 2.5). Ein maskiertes Dollarzeichen ist immer ein Literal. Perl ist schlau genug, zwischen Dollarzeichen als Anker und Dollarzeichen für das Auswerten von Variablen zu unterscheiden. Denn Anker können sich sinnvollerweise nur am Ende einer Gruppe, einer ganzen Regex oder vor einem Newline befinden. Sie dürfen das Dollarzeichen in ‹m/^Regex$/› nicht maskieren, wenn Sie herausfinden wollen, ob „Regex“ dem ganzen Ausgangstext entspricht. Das At-Zeichen (der Klammeraffe) hat in regulären Ausdrücken keine besondere Bedeutung, es wird aber beim Auswerten von Variablen in Perl verwendet. Daher müssen Sie es in literalen regulären Ausdrücken im Perl-Code maskieren, so wie Sie es auch in Strings mit doppelten Anführungszeichen tun.
Python Die Funktionen im re-Modul von Python erwarten die ihnen übergebenen regulären Ausdrücke als Strings. Sie können selbst entscheiden, welchen der von Python angebotenen Wege Sie nutzen wollen, um die Strings im Quelltext zu nutzen. Je nachdem, welche Zeichen Sie in Ihrem regulären Ausdruck verwenden, werden durch die unterschiedlichen Begrenzungszeichen weniger Backslashs zum Maskieren notwendig sein. Im Allgemeinen sind Raw-Strings die beste Wahl. In solchen Strings müssen in Python keine Zeichen maskiert werden. Wenn Sie einen Raw-String verwenden, brauchen Sie die Backslashs in Ihrem regulären Ausdruck nicht zu verdoppeln. r"\d+" lässt sich einfacher lesen als "\\d+", insbesondere wenn Ihre Regex länger wird.
3.1 Literale reguläre Ausdrücke im Quellcode |
113
Raw-Strings sind allerdings nicht so ideal, wenn Ihr regulärer Ausdruck sowohl einzelne als auch doppelte Anführungszeichen enthält. Dann können Sie keinen Raw-String nutzen, der von einzelnen oder doppelten Anführungszeichen umschlossen ist, da sich diese Zeichen innerhalb des Ausdrucks nicht maskieren lassen. In diesem Fall können Sie drei Anführungszeichen als Begrenzer nutzen, wie wir es auch in der Python-Lösung für dieses Rezept getan haben. Der normale String ist zum Vergleich mit angegeben. Wenn Sie das in Rezept 2.7 beschriebene Unicode-Feature in Ihrem regulären Ausdruck nutzen wollen, müssen Sie Unicode-Strings verwenden. Ein String lässt sich in einen Unicode-String verwandeln, indem Sie ihm ein u voranstellen. Raw-Strings unterstützen keine nicht druckbaren Zeichen wie zum Beispiel \n. Dort werden solche Maskierungssequenzen als literaler Text betrachtet. Das ist für das Modul re allerdings kein Problem. Diese Maskierungen werden dort als Teil der Regex-Syntax und der Ersetzungstextsyntax unterstützt. Ein literales \n in einem Raw-String wird in Ihren regulären Ausdrücken und Ersetzungstexten trotzdem als Newline interpretiert werden. Es gibt einen Unterschied zwischen dem String "\n" einerseits und dem String "\\n" und dem Raw-String r"\n" andererseits, wenn man mit re.VERBOSE (siehe Rezept 3.4) den Freiform-Modus aktiviert (erläutert in Rezept 2.18). "\n" ist ein String mit einem literalen Zeilenumbruch, der als Whitespace ignoriert wird. "\\n" und r"\n" sind Strings mit dem Regex-Token ‹\n›, das zu einem Newline passt. Im Freiform-Modus sind Raw-Strings mit drei Anführungszeichen als Begrenzer (zum Beispiel r"""\n""") die beste Wahl, da sie über mehrere Zeilen reichen können. Zudem wird ‹\n› nicht auf String-Ebene interpretiert, sodass es von der Regex-Engine genutzt werden kann, um einen Zeilenumbruch zu finden.
Ruby In Ruby werden reguläre Ausdrücke am besten mit der speziellen Syntax für das Deklarieren literaler regulärer Ausdrücke erstellt. Setzen Sie Ihren regulären Ausdruck einfach zwischen zwei Schrägstriche. Wenn die Regex selbst einen Schrägstrich enthält, maskieren Sie ihn mit einem Backslash. Wenn Sie keine Schrägstriche in Ihrer Regex maskieren wollen, können Sie Ihrem regulären Ausdruck ein %r voranstellen und dann ein beliebiges Satzzeichen als Begrenzer nutzen. Es ist zwar möglich, ein Regexp aus einem String zu erstellen, aber das ist bei literalen Regexes nicht sehr sinnvoll. Sie müssten dann Anführungszeichen und Backslashs maskieren, was im Allgemeinen zu einem ganzen Wald von Backslashs führt. Ruby ähnelt hier sehr stark JavaScript, nur dass die Klasse in Ruby Regexp heißt, während sie in JavaScript den Namen RegExp (in CamelCase) trägt.
114 | Kapitel 3: Mit regulären Ausdrücken programmieren
Siehe auch Rezept 2.3 erklärt, wie Zeichenklassen funktionieren und warum im regulären Ausdruck zwei Backslashs erforderlich sind, um in der Zeichenklasse nur einen zu nutzen. Rezept 3.4 erklärt, wie man die Optionen für reguläre Ausdrücke setzt. In manchen Programmiersprachen ist das Teil des literalen regulären Ausdrucks.
3.2
Importieren der Regex-Bibliothek
Problem Um reguläre Ausdrücke in Ihrer Anwendung nutzen zu können, wollen Sie die RegexBibliothek oder den entsprechenden Namensraum in Ihren Quellcode importieren. Bei den weiteren Quellcodeschnipseln, die in diesem Buch verwendet werden, wird davon ausgegangen, dass das schon geschehen ist (sofern nötig).
Lösung C# using System.Text.RegularExpressions;
VB.NET Imports System.Text.RegularExpressions
Java import java.util.regex.*;
Python import re
Diskussion In manche Programmiersprachen wurde die Unterstützung regulärer Ausdrücke bereits eingebaut. Dort müssen Sie nichts tun, um Regexes nutzen zu können. Bei anderen Sprachen wird die Funktionalität über eine Bibliothek bereitgestellt, die mit einer entsprechenden Anweisung in Ihrem Quellcode importiert werden muss. Einige Sprachen bieten gar keine Unterstützung regulärer Ausdrücke an. Dort müssen Sie selbst eine entsprechende Funktionalität kompilieren und linken.
3.2 Importieren der Regex-Bibliothek | 115
C# Wenn Sie die using-Anweisung am Anfang Ihrer C#-Quellcodedatei eintragen, können Sie direkt auf die Klassen mit der Regex-Funktionalität verweisen, ohne jedes Mal ihren vollständig qualifizierten Namen angeben zu müssen. So können Sie zum Beispiel Regex() statt System.Text.RegularExpressions.Regex() schreiben.
VB.NET Tragen Sie die Imports-Anweisung am Anfang Ihrer VB.NET-Quellcodedatei ein, können Sie direkt auf die Klassen mit der Regex-Funktionalität verweisen, ohne jedes Mal ihren vollständig qualifizierten Namen angeben zu müssen. So können Sie zum Beispiel Regex() statt System.Text.RegularExpressions.Regex() schreiben.
Java Sie müssen das Paket java.util.regex in Ihre Anwendung importieren, um die bei Java mitgelieferte Bibliothek für reguläre Ausdrücke nutzen zu können.
JavaScript In JavaScript ist die Unterstützung regulärer Ausdrücke schon eingebaut und direkt verfügbar.
PHP Die preg-Funktionen sind bereits eingebaut und ab PHP 4.2.0 immer verfügbar.
Perl Die Unterstützung regulärer Ausdrücke ist in Perl eingebaut und immer verfügbar.
Python Sie müssen das Modul re in Ihr Skript importieren, um die Regex-Funktionen von Python nutzen zu können.
Ruby Die Unterstützung regulärer Ausdrücke ist in Ruby eingebaut und immer verfügbar.
116 | Kapitel 3: Mit regulären Ausdrücken programmieren
3.3
Erstellen eines Regex-Objekts
Problem Sie wollen ein Objekt für einen regulären Ausdruck instantiieren oder stattdessen einen regulären Ausdruck so kompilieren, dass er in Ihrer ganzen Anwendung effizient genutzt werden kann.
Lösung C# Wenn Sie wissen, dass die Regex korrekt ist: Regex regexObj = new Regex("Regex-Muster");
Wenn die Regex vom Endanwender angegeben wird (UserInput sei eine String-Variable): try { Regex regexObj = new Regex(UserInput); } catch (ArgumentException ex) { // Syntaxfehler im regulären Ausdruck }
VB.NET Wenn Sie wissen, dass die Regex korrekt ist: Dim RegexObj As New Regex("Regex-Muster")
Wenn die Regex vom Endanwender angegeben wird (UserInput sei eine String-Variable): Try Dim RegexObj As New Regex(UserInput) Catch ex As ArgumentException 'Syntaxfehler im regulären Ausdruck End Try
Java Wenn Sie wissen, dass die Regex korrekt ist: Pattern regex = Pattern.compile("Regex-Muster");
Wenn die Regex vom Endanwender angegeben wird (userInput sei eine String-Variable): try { Pattern regex = Pattern.compile(userInput); } catch (PatternSyntaxException ex) { // Syntaxfehler im regulären Ausdruck }
3.3 Erstellen eines Regex-Objekts | 117
Um die Regex auf einen String anzuwenden, erzeugen Sie einen Matcher: Matcher regexMatcher = regex.matcher(subjectString);
Um die Regex auf einen anderen String anzuwenden, können Sie, wie eben gezeigt, einen neuen Matcher erstellen oder einen bestehenden wiederverwenden: regexMatcher.reset(anotherSubjectString);
JavaScript Literaler regulärer Ausdruck in Ihrem Code: var myregexp = /Regex-Muster/;
Regulärer Ausdruck, der von einem Benutzer angegeben wurde und der als String in der Variablen userinput abgelegt ist: var myregexp = new RegExp(userinput);
Perl $myregex = qr/Regex-Muster/
Regulärer Ausdruck, der von einem Benutzer angegeben wurde und der als String in der Variablen $userinput abgelegt ist: $myregex = qr/$userinput/
Python reobj = re.compile("Regex-Muster")
Regulärer Ausdruck, der von einem Benutzer angegeben wurde und der als String in der Variablen userinput abgelegt ist: reobj = re.compile(userinput)
Ruby Literaler regulärer Ausdruck in Ihrem Code: myregexp = /Regex-Muster/;
Regulärer Ausdruck, der von einem Benutzer angegeben wurde und der als String in der Variablen userinput abgelegt ist: myregexp = Regexp.new(userinput);
Diskussion Bevor die Regex-Engine einen regulären Ausdruck auf einen String anwenden kann, muss der Ausdruck kompiliert werden. Das geschieht, während Ihre Anwendung ausgeführt wird. Der Regex-Konstruktor oder die Kompilierungsfunktion parst den String, der Ihren regulären Ausdruck enthält, und wandelt ihn in eine Baumstruktur oder einen endlichen
118 | Kapitel 3: Mit regulären Ausdrücken programmieren
Automaten um. Bei der eigentlichen Musterüberprüfung wird dieser Baum beziehungsweise dieser Automat durchlaufen, während der String überprüft wird. Programmiersprachen, die literale reguläre Ausdrücke unterstützen, kompilieren den regulären Ausdruck, wenn die Ausführungsabfolge den entsprechenden Regex-Operator erreicht.
.NET In C# und VB.NET hält die .NET-Klasse System.Text.RegularExpressions.Regex einen kompilierten regulären Ausdruck. Der einfachste Konstruktor erwartet nur einen Parameter: einen String mit Ihrem regulären Ausdruck. Wenn es einen Syntaxfehler im regulären Ausdruck gibt, wirft der Regex()-Konstruktor eine ArgumentException. Der Exception-Text gibt dann genauer Auskunft darüber, was für ein Fehler aufgetreten ist. Es ist wichtig, diese Exception abzufangen, wenn der reguläre Ausdruck vom Anwender angegeben wird. Zeigen Sie dann den Exception-Text an und bitten Sie den Anwender, den Ausdruck zu korrigieren. Ist Ihr regulärer Ausdruck als String-Literal hartkodiert, können Sie sich das Fangen der Exception sparen, falls Sie ein Tool nutzen, durch das sichergestellt ist, dass diese Codezeile keinen Fehler werfen wird. Der Zustand oder Modus kann sich nicht so ändern, dass die gleiche literale Regex einmal kompiliert werden kann und einmal nicht. Beachten Sie, dass die Exception bei einer fehlerhaften Regex beim Ausführen Ihres Programms auftreten wird, nicht beim Kompilieren. Sie sollten ein Regex-Objekt erstellen, wenn Sie den regulären Ausdruck in einer Schleife oder zumindest wiederholt in Ihrer Anwendung nutzen wollen. Durch das Erstellen des Regex-Objekts gibt es keinen zusätzlichen Overhead. Der statische Member der RegexKlasse, der die Regex als String-Parameter übernimmt, erzeugt so oder so intern ein Regex-Objekt, daher können Sie das auch in Ihrem Code machen und sich eine Referenz auf das Objekt merken. Wenn Sie vorhaben, die Regex nur ein paar wenige Male zu nutzen, können Sie stattdessen auch die statischen Member der Regex-Klasse nutzen, um sich eine Codezeile zu sparen. Die statischen Regex-Member werfen das intern erzeugte Regex-Objekt nicht sofort wieder weg, stattdessen verwalten sie einen Cache mit den 15 am häufigsten genutzten regulären Ausdrücken. Sie können die Größe des Caches durch das Setzen der Eigenschaft Regex.CacheSize anpassen. Im Cache wird nach der Regex gesucht, indem der Regex-String verglichen wird. Aber verlassen Sie sich nicht komplett auf den Cache. Wenn Sie viele Regex-Objekte immer wieder benötigen, bauen Sie lieber einen eigenen Cache auf, den Sie dann auch effizienter durchsuchen können.
Java In Java verwaltet die Pattern-Klasse einen kompilierten regulären Ausdruck. Sie können Objekte dieser Klasse mit der Klassenfabrik Pattern.compile() erstellen. Dafür ist nur ein Parameter notwendig: ein String mit Ihrem regulären Ausdruck.
3.3 Erstellen eines Regex-Objekts | 119
Wenn es im regulären Ausdruck einen Syntaxfehler gibt, wirft die Pattern.compile()Fabrik eine PatternSyntaxException. Der Exception-Text beschreibt genauer, was für ein Fehler aufgetreten ist. Es ist wichtig, diese Exception abzufangen, wenn der reguläre Ausdruck aus einer Benutzereingabe stammt. Zeigen Sie dann die Fehlermeldung an und bitten Sie den Anwender, den regulären Ausdruck zu korrigieren. Ist Ihr regulärer Ausdruck als String-Literal hartkodiert, können Sie sich das Fangen der Exception sparen, falls Sie ein Tool nutzen, durch das sichergestellt ist, dass diese Codezeile keinen Fehler werfen wird. Der Zustand oder Modus kann sich nicht so ändern, dass die gleiche literale Regex einmal kompiliert werden kann und einmal nicht. Beachten Sie, dass die Exception bei einer fehlerhaften Regex beim Ausführen Ihres Programms auftreten wird, nicht beim Kompilieren. Sofern Sie eine Regex nicht nur einmal nutzen wollen, sollten Sie ein Pattern-Objekt erstellen, statt die statischen Member der Klasse String zu verwenden. Das erfordert zwar ein paar zusätzliche Codezeilen, aber der Code wird effizienter ablaufen. Die statischen Aufrufe kompilieren Ihre Regex jedes Mal neu. Tatsächlich bietet Java statische Aufrufe nur für ein paar wenige, sehr einfache Regex-Aufgaben an. Ein Pattern-Objekt speichert nur einen kompilierten regulären Ausdruck und führt keinerlei echte Aufgaben aus. Die eigentliche Musterfindung wird durch die Klasse Matcher ausgeführt. Um einen Matcher zu erstellen, rufen Sie die Methode matcher() für Ihren kompilierten regulären Ausdruck auf. Übergeben Sie dabei den Ausgangstext als Argument an matcher(). Rufen matcher() so oft auf, wie Sie den gleichen regulären Ausdruck auf unterschiedliche Strings anwenden wollen. Sie können mit mehreren Matchern für die gleiche Regex arbeiten, solange Sie alles in einem einzelnen Thread halten. Die Klassen Pattern und Matcher sind nicht Thread-sicher. Wenn Sie die gleiche Regex in mehreren Threads nutzen wollen, rufen Sie in jedem Pattern.compile() auf. Haben Sie eine Regex auf einen String angewendet und möchten die gleiche Regex auf einen anderen String anwenden, können Sie das Matcher-Objekt wiederverwenden, indem Sie reset() aufrufen. Übergeben Sie den neuen Ausgangstext als Argument. Das ist effizienter als das Erstellen eines neuen Matcher-Objekts. reset() liefert den gleichen Matcher zurück, für den Sie es aufgerufen haben, sodass Sie in einer Zeile einen Matcher zurücksetzen und wieder anwenden können, zum Beispiel regexMatcher.reset(nextString).find().
JavaScript Die in Rezept 3.2 gezeigte Notation für literale reguläre Ausdrücke erstellt immer ein neues Regex-Objekt. Um dasselbe Objekt mehrfach zu verwenden, weisen Sie es einfach einer Variablen zu. Wenn Sie einen regulären Ausdruck in einer String-Variablen abgelegt haben (weil Sie zum Beispiel den Anwender gebeten haben, einen regulären Ausdruck einzugeben), verwenden Sie den Konstruktor RegExp(), um den regulären Ausdruck zu kompilieren. 120 | Kapitel 3: Mit regulären Ausdrücken programmieren
Beachten Sie, dass der reguläre Ausdruck innerhalb des Strings nicht durch Schrägstriche begrenzt wird. Diese Schrägstriche sind Teil der Notation für literale RegExp-Objekte in JavaScript und gehören nicht zum regulären Ausdruck selbst. Da das Zuweisen einer literalen Regex zu einer Variablen so einfach ist, lassen die meisten JavaScript-Lösungen in diesem Kapitel diese Codezeile weg und verwenden den literalen regulären Ausdruck direkt. In Ihrem eigenen Code sollten Sie die Regex einer Variablen zuweisen und diese dann verwenden, falls Sie sie mehrfach benötigen. Dadurch verbessert sich die Performance, und Ihr Code lässt sich leichter warten.
PHP PHP bietet keine Möglichkeit, einen kompilierten regulären Ausdruck in einer Variablen abzulegen. Immer wenn Sie etwas mit einer Regex tun wollen, müssen Sie sie als String an eine der preg-Funktionen übergeben. Die preg-Funktionen verwalten intern einen Cache von bis zu 4.096 kompilierten regulären Ausdrücken. Auch wenn die Hash-basierte Cache-Suche nicht so schnell wie eine Referenz auf eine Variable ist, ist der Performanceverlust dennoch weit geringer als bei einem regelmäßigen Neukompilieren des gleichen regulären Ausdrucks. Wenn der Cache voll ist, wird die Regex zuerst entfernt, deren Kompilierungszeitpunkt am weitesten zurückliegt.
Perl Sie können den „Quote-Regex“-Operator nutzen, um einen regulären Ausdruck zu kompilieren und einer Variablen zuzuweisen. Er nutzt die gleiche Syntax wie der in Rezept 3.1 beschriebene Mustererkennungsoperator, nur dass er mit den Buchstaben qr und nicht mit m beginnt. Perl ist im Allgemeinen beim erneuten Verwenden vorher kompilierter regulärer Ausdrücke ziemlich effizient. Daher verwenden wir in diesem Kapitel in den Codebeispielen den Operator qr// nicht. Nur in Rezept 3.5 wird gezeigt, wie er funktioniert. qr// ist dann nützlich, wenn Sie Variablen im regulären Ausdruck auswerten oder wenn Sie den kompletten regulären Ausdruck als String erhalten (zum Beispiel aus einer Benutzereingabe). Mit qr/$regexstring/ können Sie Einfluss darauf nehmen, wann die Regex neu kompiliert wird, um den neuen Inhalt von $regexstring widerzuspiegeln. m/$regexstring/ würde die Regex jedes Mal neu kompilieren, während sie mit m/$regexstring/o niemals neu kompiliert würde. Rezept 3.4 beschreibt die Option /o.
Python Die Funktion compile() aus Pythons Modul re erwartet einen String mit Ihrem regulären Ausdruck und liefert ein Objekt mit der kompilierten Version zurück.
3.3 Erstellen eines Regex-Objekts | 121
Sie sollten compile() explizit aufrufen, wenn Sie die gleiche Regex mehrfach nutzen wollen. Alle Funktionen im Modul re rufen zunächst compile() und dann die von Ihnen gewünschte Funktion mit dem kompilierten Regex-Objekt auf. Die Funktion compile() merkt sich die letzten 100 regulären Ausdrücke, die sie kompiliert hat. Das erübrigt eine erneute Dictionary-Suche nach Ausdrücken, die in den 100 zuletzt genutzten regulären Ausdrücken enthalten waren. Wenn der Cache voll ist, wird er komplett geleert. Wenn die Performance kein Thema ist, ist der Cache völlig ausreichend, und Sie können die Funktionen im Modul re direkt nutzen. Aber wenn etwas zeitkritisch ist, ist ein Aufruf von compile() anzuraten.
Ruby Die in Rezept 3.2 gezeigte Notation für literale reguläre Ausdrücke erzeugt immer ein neues Regex-Objekt. Um dasselbe Objekt mehrfach verwenden zu können, weisen Sie es einfach einer Variablen zu. Wenn Sie einen regulären Ausdruck haben, der in einer String-Variablen gespeichert ist (weil Sie zum Beispiel den Anwender gebeten haben, einen regulären Ausdruck einzugeben), nutzen Sie die Regexp.new()-Fabrik oder ihr Synonym Regexp.compile(), um den regulären Ausdruck zu kompilieren. Beachten Sie, dass der reguläre Ausdruck innerhalb des Strings nicht durch Schrägstriche eingefasst ist. Diese Schrägstriche sind Teil der Notation für literale Regexp-Objekte in Ruby und gehören nicht zum regulären Ausdruck selbst. Da das Zuweisen einer literalen Regex zu einer Variablen so einfach ist, lassen die meisten Ruby-Lösungen in diesem Kapitel diese Codezeile weg und verwenden den literalen regulären Ausdruck direkt. In Ihrem eigenen Code sollten Sie die Regex einer Variablen zuweisen und diese dann verwenden, falls Sie sie mehrfach benötigen. Dadurch verbessert sich die Performance, und Ihr Code lässt sich leichter warten.
Einen regulären Ausdruck in CIL kompilieren C# Regex regexObj = new Regex("Regex-Muster", RegexOptions.Compiled);
VB.NET Dim RegexObj As New Regex("Regex-Muster", RegexOptions.Compiled)
122 | Kapitel 3: Mit regulären Ausdrücken programmieren
Diskussion Wenn Sie in .NET ein Regex-Objekt erstellen, ohne Optionen zu übergeben, wird der reguläre Ausdruck so kompiliert, wie wir es in „Diskussion“ auf Seite 118 beschrieben haben. Geben Sie als zweiten Parameter für den Regex()-Konstruktor RegexOptions.Compiled mit, verhält sich die Regex-Klasse etwas anders. Sie kompiliert dann Ihren regulären Ausdruck bis hinunter in die CIL, auch bekannt als MSIL. CIL steht für Common Intermediate Language, eine Programmiersprache, die deutlich enger an Assembler ausgerichtet ist als an C# oder Visual Basic. Alle .NET-Compiler erzeugen CIL. Wenn Ihre Anwendung das erste Mal ausgeführt wird, kompiliert das .NET Framework die CIL dann zu Maschinencode, der vom Computer direkt nutzbar ist. Der Vorteil, einen regulären Ausdruck mit RegexOptions.Compiled zu kompilieren, ist seine bis zu zehnfach höhere Ausführungsgeschwindigkeit, verglichen mit einem normal kompilierten regulären Ausdruck. Nachteil ist, dass das Kompilieren bis zu zwei Größenordnungen langsamer sein kann als das reine Parsen des Regex-Strings in einen Baum. Der CIL-Code wird zudem Teil Ihrer Anwendung, bis sie beendet ist. CIL-Code nutzt keine Garbage Collection. Verwenden Sie RegexOptions.Compiled nur dann, wenn ein regulärer Ausdruck entweder so komplex ist oder so umfangreiche Textmengen bearbeiten muss, dass der Anwender eine deutliche Wartezeit bemerkt. Der Overhead durch das Kompilieren und Assemblieren lohnt sich nicht für Regexes, die in Sekundenbruchteilen ausgeführt werden.
Siehe auch Rezepte 3.1, 3.2 und 3.4.
3.4
Optionen für reguläre Ausdrücke setzen
Problem Sie wollen einen regulären Ausdruck mit allen verfügbaren Modi kompilieren – FreiformModus sowie die Modi Groß-/Kleinschreibung ignorieren, Punkt passt zu Zeilenumbruch und Zirkumflex und Dollar passen zu Zeilenumbruch.
VB.NET Dim RegexObj As New Regex("Regex-Muster", RegexOptions.IgnorePatternWhitespace Or RegexOptions.IgnoreCase Or RegexOptions.Singleline Or RegexOptions.Multiline)
Ruby Literaler regulärer Ausdruck in Ihrem Code: myregexp = /Regex-Muster/mix;
Regulärer Ausdruck aus einer Benutzereingabe als String: myregexp = Regexp.new(userinput, Regexp::EXTENDED or Regexp::IGNORECASE or Regexp::MULTILINE);
Diskussion Viele der regulären Ausdrücke in diesem Buch und auch solche, die Sie woanders finden, sind dafür geschrieben, mit bestimmten Regex-Modi zu funktionieren. Es gibt vier grundlegende Basismodi, die nahezu alle modernen Regex-Varianten unterstützen. Lei-
124 | Kapitel 3: Mit regulären Ausdrücken programmieren
der nutzen einige Varianten inkonsistente und verwirrende Namen für die Optionen, durch die die Modi implementiert werden. Nutzt man einen falschen Modus, funktioniert der reguläre Ausdruck im Allgemeinen nicht mehr so, wie man sich das vorgestellt hat. Alle Lösungen in diesem Rezept nutzen Schalter oder Optionen, die von der Programmiersprache oder der Regex-Klasse zum Setzen der Modi verwendet werden. Eine andere Möglichkeit, Modi zu setzen, ist die Verwendung von Modus-Modifikatoren innerhalb der regulären Ausdrücke. Modus-Modifikatoren in der Regex überschreiben immer die Optionen oder Schalter, die außerhalb des regulären Ausdrucks gesetzt wurden.
.NET Der Konstruktor Regex() besitzt einen zweiten, optionalen Parameter für die RegexOptionen. Sie finden die verfügbaren Optionen in der Enumeration RegexOptions. Freiform: RegexOptions.IgnorePatternWhitespace Groß-/Kleinschreibung ignorieren: RegexOptions.IgnoreCase Punkt passt zu Zeilenumbruch: RegexOptions.Singleline Zirkumflex und Dollar passen zu Zeilenumbruch: RegexOptions.Multiline
Java Der Klassenfabrik Pattern.compile() kann man einen optionalen zweiten Parameter für die Regex-Optionen mitgeben. Die Klasse Pattern definiert eine Reihe von Konstanten für die verschiedenen Optionen. Sie können mehrere Optionen gleichzeitig setzen, indem Sie sie durch den bitweisen Oder-Operator | verbinden. Freiform: Pattern.COMMENTS Groß-/Kleinschreibung ignorieren: Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE Punkt passt zu Zeilenumbruch: Pattern.DOTALL Zirkumflex und Dollar passen zu Zeilenumbruch: Pattern.MULTILINE Für das Ignorieren von Groß- und Kleinschreibung gibt es tatsächlich zwei Optionen, die Sie beide setzen müssen, wenn Sie die Schreibweise komplett ignorieren wollen. Setzen Sie nur Pattern.CASE_INSENSITIVE, werden lediglich die Buchstaben A bis Z unabhängig von der Schreibweise gefunden. Setzen Sie beide Optionen, werden alle Zeichen aus allen Schriftsystemen unabhängig von der Groß-/Kleinschreibung gefunden. Der einzige Grund, Pattern.UNICODE_CASE nicht zu nutzen, ist der Performanceaspekt, wenn Sie schon im Voraus wissen, dass Sie nur mit ASCII-Text arbeiten. Wenn Sie innerhalb Ihres regulären Ausdrucks Modus-Modifikatoren verwenden, nutzen Sie ‹(?i)› für das Ignorieren von Groß- und Kleinschreibung nur für ASCII-Zeichen und ‹(?iu)› für ein vollständiges Ignorieren.
3.4 Optionen für reguläre Ausdrücke setzen | 125
JavaScript In JavaScript können Sie Optionen festlegen, indem Sie ein oder mehrere Zeichen nach dem zweiten Schrägstrich an das RegExp-Literal anhängen. Wenn in Büchern oder auf Webseiten über diese Schalter geschrieben wird, geschieht das meist in der Form /i und /m, obwohl die Schalter eigentlich nur aus dem Zeichen selbst bestehen. Es sind keine zusätzlichen Schrägstriche anzugeben, wenn man die Modusschalter nutzt. Verwenden Sie den Konstruktor RegExp(), um einen String zu einem regulären Ausdruck zu kompilieren, können Sie einen optionalen zweiten Parameter mit den Schaltern übergeben. Dabei sollte es sich um einen String mit den Buchstaben der Optionen handeln, die Sie setzen wollen. Fügen Sie hier keine Schrägstriche ein. Freiform: nicht durch JavaScript unterstützt Groß-/Kleinschreibung ignorieren: /i Punkt passt zu Zeilenumbruch: nicht durch JavaScript unterstützt Zirkumflex und Dollar passen zu Zeilenumbruch: /m
PHP In Rezept 3.1 ist beschrieben, dass literale reguläre Ausdrücke bei den preg-Funktionen von PHP durch zwei Satzzeichen begrenzt sein müssen – im Allgemeinen Schrägstriche – und dass alles als String zu formatieren ist. Sie können dabei Optionen angeben, indem Sie ein oder mehrere Zeichen als Modifikatoren an das Ende des Strings stellen. Das heißt, der Modifikator-Buchstabe kommt nach dem schließenden Regex-Begrenzer, aber er muss sich immer noch innerhalb der Anführungszeichen des Strings befinden. Wenn in Büchern oder auf Webseiten über diese Schalter geschrieben wird, geschieht das meist in der Form /x, obwohl die Schalter eigentlich nur aus dem Zeichen selbst bestehen und der Begrenzer zwischen Regex und Modifikator gar kein Schrägstrich sein muss. Freiform: /x Groß-/Kleinschreibung ignorieren: /i Punkt passt zu Zeilenumbruch: /s Zirkumflex und Dollar passen zu Zeilenumbruch: /m
Perl Sie können Optionen für reguläre Ausdrücke festlegen, indem Sie einen oder mehrere aus einem Zeichen bestehende Modifikatoren an das Ende des Operators zur Mustererkennung oder zum Ersetzen anhängen. Wenn in Büchern oder auf Webseiten über diese Schalter geschrieben wird, geschieht das meist in der Form /x, obwohl die Schalter eigentlich nur aus dem Zeichen selbst bestehen und der Begrenzer zwischen Regex und Modifikator gar kein Schrägstrich sein muss. Freiform: /x Groß-/Kleinschreibung ignorieren: /i
126 | Kapitel 3: Mit regulären Ausdrücken programmieren
Punkt passt zu Zeilenumbruch: /s Zirkumflex und Dollar passen zu Zeilenumbruch: /m
Python Der (im obigen Rezept erläuterten) Funktion compile() kann ein optionaler zweiter Parameter für die Optionen für den regulären Ausdruck mitgegeben werden. Sie können diesen Parameter aufbauen, indem Sie die im Modul re definierten Konstanten mit dem Operator | kombinieren. Viele der anderen Funktionen im Modul re, die einen literalen regulären Ausdruck als Parameter nutzen, bieten auch einen (letzten) optionalen Parameter an, mit dem die Optionen übergeben werden können. Die Konstanten für die Regex-Optionen gibt es immer paarweise. Jede Option kann entweder als Konstante mit einem vollständigen Namen oder mit einem einfachen Buchstaben genutzt werden. Die Funktionalität ist die gleiche. Einziger Unterschied ist, dass der vollständige Name Ihren Code für Entwickler leichter lesbar macht, die mit der Buchstabensuppe der Regex-Optionen nicht so vertraut sind. Die aufgeführten Ein-BuchstabenOptionen entsprechen denen von Perl. Freiform: re.VERBOSE oder re.X Groß-/Kleinschreibung ignorieren: re.IGNORECASE oder re.I Punkt passt zu Zeilenumbruch: re.DOTALL oder re.S Zirkumflex und Dollar passen zu Zeilenumbruch: re.MULTILINE oder re.M
Ruby In Ruby können Sie die Optionen angeben, indem Sie dem Regexp-Literal einen oder mehrere Buchstaben als Schalter anhängen. Wenn in Büchern oder auf Webseiten über diese Schalter geschrieben wird, geschieht das meist in der Form /i und /m, obwohl die Schalter eigentlich nur aus dem Zeichen selbst bestehen. Es sind keine zusätzlichen Schrägstriche anzugeben, wenn man die Modusschalter nutzt. Verwenden Sie die Regexp.new()-Fabrik, um einen String in einen regulären Ausdruck zu kompilieren, können Sie einen optionalen zweiten Parameter mit Schaltern an den Konstruktor übergeben. Der zweite Parameter sollte entweder den Wert nil haben, um alle Optionen abzuschalten, oder aus einer Kombination der Konstanten aus der Klasse Regexp bestehen, die mit dem Operator or kombiniert wurden. Freiform: /r oder Regexp::EXTENDED Groß-/Kleinschreibung ignorieren: /i oder Regexp::IGNORECASE Punkt passt zu Zeilenumbruch : /m oder Regexp::MULTILINE. Ruby nutzt hier tatsächlich „m“ und „Multiline“, obwohl alle anderen Varianten „s“ oder „Single Line“ für Punkt passt zu Zeilenumbruch verwenden. Zirkumflex und Dollar passen zu Zeilenumbruch: Zirkumflex und Dollar passen in Ruby immer auf Zeilenumbrüche. Sie können dieses Verhalten nicht abschalten. Mit ‹\A› und ‹\Z› finden Sie den Anfang oder das Ende des Ausgangstexts.
3.4 Optionen für reguläre Ausdrücke setzen | 127
Weitere sprachspezifische Optionen .NET RegexOptions.ExplicitCapture sorgt dafür, dass alle Gruppen, mit Ausnahme der benannten Gruppen, nicht-einfangend sind. Mit dieser Option ist ‹(Gruppe)› das Gleiche wie ‹(?:Gruppe)›. Wenn Sie Ihre einfangenden Gruppen immer benennen, schalten Sie diese Option ein, damit Ihr regulärer Ausdruck effizienter wird, ohne die ‹(?:Gruppe)›-Syntax nutzen zu müssen. Anstatt RegexOptions.ExplicitCapture zu verwenden, können Sie diese Option auch aktivieren, indem Sie ‹(?n)› an den Anfang Ihres regulären Ausdrucks
stellen. In Rezept 2.9 erfahren Sie mehr über Gruppen. Rezept 2.11 erklärt benannte Gruppen. Wenn Sie den gleichen regulären Ausdruck in Ihrem .NET-Code und in JavaScript-Code nutzen und Sie sicherstellen wollen, dass er sich in beiden Umgebungen gleich verhält, können Sie RegexOptions.ECMAScript nutzen. Das ist insbesondere dann nützlich, wenn Sie die Clientseite einer Webanwendung in JavaScript und die Serverseite in ASP.NET entwickeln. Der wichtigste Effekt, der durch diese Option eintritt, ist die Einschränkung von \w und \d auf ASCII-Zeichen, so wie es in JavaScript auch der Fall ist.
Java Eine in Java einmalige Option ist Pattern.CANON_EQ, mit der eine „kanonische Äquivalenz“ ermöglicht wird. Wie in „Unicode-Graphem“ auf Seite 56 beschrieben, gibt es in Unicode unterschiedliche Wege, diakritische Zeichen zu repräsentieren. Wenn Sie diese Option aktivieren, wird Ihre Regex ein Zeichen finden, auch wenn es im Ausgangstext anders kodiert wurde. So wird zum Beispiel die Regex ‹\u00E0› sowohl auf "\u00E0" als auch auf "\u0061\u0300" passen, weil beide kanonisch äquivalent sind. Beide zeigen auf dem Bildschirm ein „à“ an, sodass der Endanwender keinen Unterschied bemerkt. Ohne die kanonische Äquivalenz passt die Regex ‹\u00E0› nicht zum String "\u0061\u0300". In der Weise verhalten sich jedoch alle anderen Regex-Varianten aus diesem Buch. Schließlich teilt Pattern.UNIX_LINES Java noch mit, nur ‹\n› von Punkt, Zirkumflex und Dollarzeichen als echten Zeilenumbruch behandeln zu lassen. Standardmäßig werden alle Unicode-Zeilenumbrüche als solche Zeichen angesehen.
JavaScript Wenn Sie einen regulären Ausdruck wiederholt auf den gleichen String anwenden wollen – zum Beispiel um alle Fundstellen zu durchlaufen oder um nicht nur die erste, sondern alle Übereinstimmungen zu ersetzen –, geben Sie den „Global“-Schalter /g mit.
PHP /u weist die PCRE an, sowohl den regulären Ausdruck als auch den Ausgangstext als UTF-8-Strings zu interpretieren. Dieser Modifikator ermöglicht es zudem, Unicode-
128 | Kapitel 3: Mit regulären Ausdrücken programmieren
Regex-Tokens zu nutzen, wie zum Beispiel ‹\p{FFFF}› und ‹\p{L}›. Diese werden in Rezept 2.7 erklärt. Ohne den Modifikator behandelt die PCRE jedes Byte als eigenes Zeichen, und die Unicode-Regex-Tokens führen zu einem Fehler. /U vertauscht das „genügsame“ und das „gierige“ Verhalten von Quantoren mit und ohne Fragezeichen. Normalerweise ist ‹.*› gierig und ‹.*?› genügsam. Mit der Option /U ist ‹.*› genügsam und ‹.*?› gierig. Ich empfehle dringend, diese Option niemals zu verwenden, da Programmierer, die später Ihren Code lesen und den Modifikator /U übersehen, komplett verwirrt werden können. Verwechseln Sie auch nicht /U mit /u, wenn Sie
diese Optionen im Code von anderen Leuten vorfinden. Regex-Modifikatoren reagieren durchaus empfindlich auf eine unterschiedliche Schreibweise.
Perl Wenn Sie einen regulären Ausdruck wiederholt auf den gleichen String anwenden wollen – zum Beispiel um alle Fundstellen zu durchlaufen oder um nicht nur die erste, sondern alle Übereinstimmungen zu ersetzen –, geben Sie den „Global“-Schalter /g mit. Lassen Sie eine Variable in einer Regex auswerten – zum Beispiel m/Ich heiße $name/ –, wird Perl den regulären Ausdruck jedes Mal neu kompilieren, wenn er angewendet werden soll, da sich der Inhalt von $name geändert haben könnte. Sie können dieses Verhalten mit dem Modifikator /o unterbinden. m/Ich heiße $name/o wird von Perl nur kompiliert, wenn der Ausdruck das erste Mal benötigt wird, und dann immer wieder angewandt. Wenn sich der Inhalt von $name ändert, wird die Regex diese Änderung nicht widerspiegeln. In Rezept 3.3 finden Sie Informationen zum neuen Kompilieren der Regex.
Python Python bietet zwei zusätzliche Optionen an, die die Bedeutung der Wortgrenzen (siehe Rezept 2.6), der Kurzzeichenklassen ‹\w›, ‹\d› und ‹\s› und ihrer negierten Gegenspieler (siehe Rezept 2.3) ändern. Standardmäßig nutzen diese Tokens nur ASCII-Buchstaben, Ziffern und Whitespace. Die Option re.LOCALE oder re.L lässt diese Tokens abhängig vom aktuellen Locale agieren. Das Locale bestimmt dann, welche Zeichen von den Regex-Tokens als Buchstaben, Ziffern und Whitespace behandelt werden. Sie sollten diese Option angeben, wenn der Ausgangstext kein Unicode-String ist und Sie zum Beispiel diakritische Zeichen als Buchstaben behandelt wissen wollen. re.UNICODE oder re.U lässt diese Tokens auf den Unicode-Standard Rücksicht nehmen.
Alle Zeichen, die von Unicode als Buchstaben, Ziffern und Whitespace gekennzeichnet sind, werden dann von den Regex-Tokens auch als solche behandelt. Sie sollten diese Option nutzen, wenn es sich beim Ausgangstext um einen Unicode-String handelt.
3.4 Optionen für reguläre Ausdrücke setzen | 129
Ruby Der Regexp.new()-Fabrik kann ein optionaler dritter Parameter übergeben werden, um die String-Kodierung auszuwählen, die Ihr regulärer Ausdruck unterstützt. Wenn Sie keine Kodierung für Ihren regulären Ausdruck angeben, wird die genommen, die auch Ihre Quellcodedatei nutzt. Meist ist die von der Quellcodedatei genutzte Kodierung schon die richtige. Um eine Kodierung explizit auszuwählen, übergeben Sie für diesen Parameter ein einzelnes Zeichen. Groß- und Kleinschreibung ist nicht wichtig. Mögliche Werte sind: n
Das steht für „None“. Jedes Byte in Ihrem String wird als einzelnes Zeichen behandelt. Nutzen Sie diese Kodierung für ASCII-Texte. e
Verwendet die „EUC“-Kodierung für Sprachen aus dem ostasiatischen Raum. s
Verwendet die japanische „Shift-JIS“-Kodierung. u
Verwendet UTF-8, das ein bis vier Byte pro Zeichen nutzt und alle Sprachen im Unicode-Standard unterstützt (wozu alle wichtigen lebenden Sprachen gehören). Wenn Sie einen literalen regulären Ausdruck verwenden, können Sie die Kodierung durch die Modifikatoren /n, /e, /s und /u setzen. Dabei kann nur einer dieser Modifikatoren für einen einzelnen regulären Ausdruck genutzt werden. Man kann sie aber mit den Modifikatoren /x, /i und /m kombinieren. Verwechseln Sie Rubys /s nicht mit dem von Perl, Java oder .NET. In Ruby sorgt /s für die Anwendung der Shift-JIS-Kodierung. In Perl und den meisten anderen Regex-Varianten wird damit der Modus „Punkt passt zu Zeilenumbruch“ aktiviert. In Ruby erreichen Sie das wiederum mit /m.
Siehe auch Die Auswirkungen der Modi werden detailliert in Kapitel 2 erläutert. Dieser Abschnitt zeigt auch, wie man die Modus-Modifikatoren innerhalb von regulären Ausdrücken nutzt: Freiform: Rezept 2.18 Groß-/Kleinschreibung ignorieren: „Übereinstimmungen unabhängig von Groß- und Kleinschreibung“ auf Seite 29 in Rezept 2.1 Punkt passt zu Zeilenumbruch: Rezept 2.4 Zirkumflex und Dollar passen zu Zeilenumbruch: Rezept 2.5
130 | Kapitel 3: Mit regulären Ausdrücken programmieren
Die Rezepte 3.1 und 3.3 beschreiben, wie Sie literale reguläre Ausdrücke in Ihrem Quellcode verwenden und wie Sie Regex-Objekte erstellen. Die Regex-Optionen werden dabei während des Erzeugens eines regulären Ausdrucks gesetzt.
3.5
Auf eine Übereinstimmung in einem Text prüfen
Problem Sie wollen prüfen, ob für einen bestimmten regulären Ausdruck in einem bestimmten String eine Übereinstimmung gefunden werden kann. Eine teilweise Übereinstimmung ist ausreichend, zum Beispiel stimmt die Regex ‹RegexzMuster› teilweise mit Das RegexMuster kann gefunden werden überein. Sie kümmern sich nicht um die Details der Übereinstimmung, Sie wollen bloß wissen, ob die Regex im String gefunden wird.
Lösung C# Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie diesen statischen Aufruf verwenden: bool foundMatch = Regex.IsMatch(subjectString, "Regex-Muster");
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie den statischen Aufruf per Exception Handling absichern: bool foundMatch = false; try { foundMatch = Regex.IsMatch(subjectString, UserInput); } catch (ArgumentNullException ex) { // Regex und Text müssen vorhanden sein } catch (ArgumentException ex) { // Syntaxfehler im regulären Ausdruck }
Um die gleiche Regex mehrfach einzusetzen, erstellen Sie ein Regex-Objekt: Regex regexObj = new Regex("Regex-Muster"); bool foundMatch = regexObj.IsMatch(subjectString);
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie das Regex-Objekt per Exception Handling absichern: bool foundMatch = false; try { Regex regexObj = new Regex(UserInput); try { foundMatch = regexObj.IsMatch(subjectString); } catch (ArgumentNullException ex) { // Regex und Text müssen vorhanden sein
3.5 Auf eine Übereinstimmung in einem Text prüfen | 131
VB.NET Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie diesen statischen Aufruf verwenden: Dim FoundMatch = Regex.IsMatch(SubjectString, "Regex-Muster")
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie den statischen Aufruf per Exception Handling absichern: Dim FoundMatch As Boolean Try FoundMatch = Regex.IsMatch(SubjectString, UserInput) Catch ex As ArgumentNullException 'Regex und Text müssen vorhanden sein Catch ex As ArgumentException 'Syntaxfehler im regulären Ausdruck End Try
Um die gleiche Regex mehrfach einzusetzen, erstellen Sie ein Regex-Objekt: Dim RegexObj As New Regex("Regex-Muster") Dim FoundMatch = RegexObj.IsMatch(SubjectString)
Der Aufruf von IsMatch() sollte als Parameter nur SubjectString nutzen. Achten Sie darauf, den Aufruf für die Instanz RegexObj durchzuführen, nicht für die Klasse Regex: Dim FoundMatch = RegexObj.IsMatch(SubjectString)
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie das Regex-Objekt per Exception Handling absichern: Dim FoundMatch As Boolean Try Dim RegexObj As New Regex(UserInput) Try FoundMatch = Regex.IsMatch(SubjectString) Catch ex As ArgumentNullException 'Regex und Text müssen vorhanden sein End Try Catch ex As ArgumentException 'Syntaxfehler im regulären Ausdruck End Try
Java Um auf eine teilweise Übereinstimmung zu testen, muss man einen Matcher erzeugen: Pattern regex = Pattern.compile("Regex-Muster"); Matcher regexMatcher = regex.matcher(subjectString); boolean foundMatch = regexMatcher.find();
132 | Kapitel 3: Mit regulären Ausdrücken programmieren
Wenn die Regex vom Benutzer eingegeben wird, sollten Sie Exception Handling berücksichtigen: boolean foundMatch = false; try { Pattern regex = Pattern.compile(UserInput); Matcher regexMatcher = regex.matcher(subjectString); foundMatch = regexMatcher.find(); } catch (PatternSyntaxException ex) { // Syntaxfehler im regulären Ausdruck }
JavaScript if (/Regex-Muster/.test(subject)) { // Übereinstimmung gefunden } else { // Keine Übereinstimmung }
PHP if (preg_match('/Regex-Muster/', $subject)) { # Übereinstimmung gefunden } else { # Keine Übereinstimmung }
Perl Wenn sich der Text in der Spezial-Variablen $_ befindet: if (m/Regex-Muster/) { # Übereinstimmung gefunden } else { # Keine Übereinstimmung }
Wenn sich der Text in der Variablen $subject befindet: if ($subject =~ m/Regex-Muster/) { # Übereinstimmung gefunden } else { # Keine Übereinstimmung }
Beim Verwenden eines vorkompilierten regulären Ausdrucks: $regex = qr/Regex-Muster/; if ($subject =~ $regex) { # Übereinstimmung gefunden } else { # Keine Übereinstimmung }
3.5 Auf eine Übereinstimmung in einem Text prüfen | 133
Python Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie diese globale Funktion verwenden: if re.search("Regex-Muster", subject): # Übereinstimmung gefunden else: # Keine Übereinstimmung
Um die gleiche Regex mehrfach einzusetzen, verwenden Sie ein kompiliertes Objekt: reobj = re.compile("Regex-Muster") if reobj.search(subject): # Übereinstimmung gefunden else: # Keine Übereinstimmung
Ruby if subject =~ /Regex-Muster/ # Übereinstimmung gefunden else # Keine Übereinstimmung end
Dieser Code macht genau das Gleiche: if /Regex-Muster/ =~ subject # Übereinstimmung gefunden else # Keine Übereinstimmung end
Diskussion Die grundlegendste Aufgabe eines regulären Ausdrucks ist das Prüfen, ob in einem String eine Übereinstimmung gefunden werden kann. In den meisten Programmiersprachen reicht eine teilweise Übereinstimmung aus, damit die entsprechende Funktion einen Erfolg meldet. Die Übereinstimmungsfunktion durchläuft den gesamten Ausgangstext, um herauszufinden, ob der reguläre Ausdruck einen Teil davon findet. Sobald es eine Übereinstimmung gibt, liefert die Funktion true zurück. Den Wert false gibt sie nur dann zurück, wenn sie das Ende des Strings erreicht, ohne eine Übereinstimmung gefunden zu haben. Die Codebeispiele in diesem Rezept sind nützlich, wenn man prüfen will, ob ein String bestimmte Daten enthält. Wollen Sie jedoch testen, ob ein String in seiner Gesamtheit einem bestimmten Muster entspricht (zum Beispiel bei der Eingabekontrolle), ist das nächste Rezept das richtige für Sie.
134 | Kapitel 3: Mit regulären Ausdrücken programmieren
C# und VB.NET Die Klasse Regex stellt vier überladene Versionen der Methode IsMatch() bereit, von denen zwei statisch sind. Damit ist es möglich, IsMatch() mit unterschiedlichen Parametern aufzurufen. Der Ausgangstext ist immer der erste Parameter. In diesem String versucht der reguläre Ausdruck, eine Übereinstimmung zu finden. Dieser erste Parameter darf nicht null sein, da IsMatch() ansonsten eine ArgumentNullException wirft. Sie können den Test in einer einzelnen Codezeile durchführen, indem Sie Regex.IsMatch() aufrufen, ohne ein Regex-Objekt zu erzeugen. Übergeben Sie einfach den regulären Ausdruck als zweiten Parameter und die Regex-Optionen optional als dritten Parameter. Wenn Ihr regulärer Ausdruck einen Syntaxfehler enthält, wird eine ArgumentException von IsMatch() geworfen. Ist Ihre Regex gültig, wird der Aufruf bei einer Übereinstimmung mit einem Teil des Strings true zurückgeben. Wenn keine Übereinstimmung gefunden werden konnte, liefert die Funktion stattdessen false zurück. Möchten Sie den gleichen regulären Ausdruck auf mehrere Strings anwenden, können Sie Ihren Code effizienter machen, indem Sie zunächst ein Regex-Objekt erzeugen und dann für dieses Objekt IsMatch() aufrufen. Der erste Parameter mit dem Ausgangstext ist der einzig notwendige Parameter. Sie können optional einen zweiten Parameter angeben, in dem die Position im String angegeben wird, ab der die Suche beginnen soll. Im Prinzip handelt es sich bei diesem Wert um die Anzahl der Zeichen, die der reguläre Ausdruck am Anfang des Ausgangstexts ignorieren soll. Das kann nützlich sein, wenn Sie den String schon bis zu einer gewissen Position verarbeitet haben und nun prüfen wollen, ob der Rest auch noch zu bearbeiten ist. Wenn Sie diese Zahl angeben, muss sie größer oder gleich null und kleiner oder gleich der Länge des Strings sein. Ansonsten wirft IsMatch() eine ArgumentOutOfRangeException. Die statisch überladenen Varianten ermöglichen keine Angabe der Position, ab der im String gesucht werden soll. Es gibt auch keine Version von IsMatch(), der mitgeteilt werden kann, dass die Funktion nicht bis zum Ende des Strings zu suchen braucht. Wenn Sie das erreichen wollen, können Sie Regex.Match("subject", start, stop) aufrufen und dann die Eigenschaft Success des zurückgelieferten Match-Objekts auswerten. In Rezept 3.8 finden Sie weitere Details dazu.
Java Um zu prüfen, ob eine Regex einen String teilweise oder vollständig abbildet, instantiieren Sie einen Matcher, wie in Rezept 3.3 beschrieben. Dann rufen Sie die Methode find() für Ihren neu erstellten oder frisch zurückgesetzten Matcher auf. Verwenden Sie nicht die Methoden String.matches(), Pattern.matches() oder Matcher.matches(). Bei all diesen Methoden muss die Regex auf den gesamten String passen.
3.5 Auf eine Übereinstimmung in einem Text prüfen | 135
JavaScript Um zu prüfen, ob ein regulärer Ausdruck auf einen Teil des Strings passt, rufen Sie die Methode test() für Ihren regulären Ausdruck auf. Übergeben Sie den Ausgangs-String als einzigen Parameter. regexp.test() gibt true zurück, wenn der reguläre Ausdruck zu einem Teil oder der Gesamtheit des Strings passt. Ansonsten gibt die Funktion false zurück.
PHP Die Funktion preg_match() kann für eine ganze Reihe von Aufgaben genutzt werden. Die einfachste Variante ist der Aufruf mit den beiden notwendigen Parametern: dem String mit Ihrem regulären Ausdruck und dem String mit dem Text, auf den die Regex angewendet werden soll. preg_match() liefert 1 zurück, wenn es eine Übereinstimmung gab, und 0, wenn nichts gefunden wurde. Weitere Rezepte weiter unten in diesem Kapitel erklären die optionalen Parameter, die Sie an preg_match() übergeben können.
Perl In Perl ist m// ein echter Regex-Operator, nicht nur ein Regex-Container. Wenn Sie m// allein verwenden, greift er auf die Variable $_ als Ausgangstext zurück. Wollen Sie den Mustererkennungsoperator auf den Inhalt einer anderen Variablen anwenden, nutzen Sie den Bindungsoperator =~, um den Regex-Operator mit Ihrer Variablen zu verknüpfen. Durch das Binden der Regex an einen String wird die Regex direkt ausgeführt. Der Mustererkennungsoperator liefert true zurück, wenn die Regex zu einem Teil des Ausgangstexts passt, und false, wenn keine Übereinstimmung gefunden wurde. Wenn Sie prüfen wollen, ob ein regulärer Ausdruck nicht auf einen String passt, können Sie !~ nutzen, die negierte Version von =~.
Python Die Funktion search() aus dem Modul re durchsucht einen String, um herauszufinden, ob der reguläre Ausdruck auf einen Teil davon passt. Übergeben Sie Ihren regulären Ausdruck als ersten Parameter und den Ausgangstext als zweiten Parameter. Optional können Sie noch die Regex-Optionen als dritten Parameter mitgeben. Die Funktion re.search() ruft re.compile() und dann die Methode search() für das kompilierte Regex-Objekt auf. Diese Methode erhält nur einen einzigen Parameter: den Ausgangstext. Wenn der reguläre Ausdruck eine Übereinstimmung findet, liefert search() eine Instanz eines MatchObject zurück. Findet die Regex keine Übereinstimmung, liefert search() den Wert None zurück. Wenn Sie den zurückgegebenen Wert in einer if-Anweisung auswerten,
136 | Kapitel 3: Mit regulären Ausdrücken programmieren
führt MatchObject zu True, während None zu False wird. Weitere Rezepte in diesem Kapitel werden zeigen, wie Sie die Informationen in einem MatchObject verwenden können. Bringen Sie search() nicht mit match() durcheinander. Sie können match() nicht nutzen, um eine Übereinstimmung mitten in einem String zu finden. Das nächste Rezept verwendet match().
Ruby Der Operator =~ ist der Mustererkennungsoperator. Wenn Sie ihn zwischen einem regulären Ausdruck und einem String einsetzen, wird damit die erste Übereinstimmung durch die Regex gefunden. Der Operator gibt einen Integer-Wert mit der Position der Übereinstimmung zurück. Wenn keine Übereinstimmung gefunden wurde, liefert er stattdessen nil. Dieser Operator ist in den beiden Klassen Regexp und String implementiert. In Ruby 1.8 ist es egal, welche Klasse Sie links und welche Sie rechts des Operators verwenden. In Ruby 1.9 gibt es spezielle Nebeneffekte, die benannte einfangende Gruppen betreffen. Rezept 3.9 geht darauf näher ein. In allen anderen Ruby-Codeschnipseln in diesem Buch haben wir den Ausgangstext links vom =~-Operator und den regulären Ausdruck rechts davon platziert. Das entspricht der Vorgehensweise in Perl, aus der Ruby sich die =~-Syntax ausgeliehen hat. Zudem werden damit die Besonderheiten in Ruby 1.9 bezüglich der benannten einfangenden Gruppen vermieden, die eventuell nicht berücksichtigt weredn.
Siehe auch Rezepte 3.6 und 3.7.
3.6
Auf eine vollständige Übereinstimmung einer Regex mit einem Text prüfen
Problem Sie wollen prüfen, ob ein String komplett zu einem bestimmten Muster passt. Das heißt, Sie wollen sicherstellen, dass der reguläre Ausdruck, der das Muster enthält, den String vom Anfang bis zum Ende abdecken kann. Wenn Ihre Regex zum Beispiel ‹RegexzMuster› ist, wird der Text Regex-Muster gefunden, aber nicht der längere String Das Regex-Muster kann gefunden werden.
3.6 Auf eine vollständige Übereinstimmung einer Regex mit einem Text prüfen | 137
Lösung C# Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie den statischen Aufruf verwenden: bool foundMatch = Regex.IsMatch(subjectString, @"\ARegex-Muster\Z");
Um die gleiche Regex mehrfach einzusetzen, erstellen Sie ein Regex-Objekt: Regex regexObj = new Regex(@"\ARegex-Muster\Z"); bool foundMatch = regexObj.IsMatch(subjectString);
VB.NET Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie den statischen Aufruf verwenden: Dim FoundMatch = Regex.IsMatch(SubjectString, "\ARegex-Muster\Z")
Um die gleiche Regex mehrfach einzusetzen, erstellen Sie ein Regex-Objekt: Dim RegexObj As New Regex("\ARegex-Muster\Z") Dim FoundMatch = RegexObj.IsMatch(SubjectString)
Der Aufruf von IsMatch() sollte als Parameter nur SubjectString nutzen. Achten Sie darauf, den Aufruf für die Instanz RegexObj durchzuführen, nicht für die Klasse Regex: Dim FoundMatch = RegexObj.IsMatch(SubjectString)
Java Wenn Sie nur einen String prüfen wollen, können Sie den statischen Aufruf nutzen: boolean foundMatch = subjectString.matches("Regex-Muster");
Wollen Sie die gleiche Regex auf mehrere Strings anwenden, kompilieren Sie Ihre Regex und erstellen einen Matcher: Pattern regex = Pattern.compile("Regex-Muster"); Matcher regexMatcher = regex.matcher(subjectString); boolean foundMatch = regexMatcher.matches(subjectString);
JavaScript if (/^Regex-Muster$/.test(subject)) { // Übereinstimmung gefunden } else { // Keine Übereinstimmung }
138 | Kapitel 3: Mit regulären Ausdrücken programmieren
PHP if (preg_match('/\ARegex-Muster\Z/', $subject)) { # Übereinstimmung gefunden } else { # Keine Übereinstimmung }
Perl if ($subject =~ m/\ARegex-Muster\Z/) { # Übereinstimmung gefunden } else { # Keine Übereinstimmung }
Python Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie die globale Funktion verwenden: if re.match(r"Regex-Muster\Z", subject): # Übereinstimmung gefunden else: # Keine Übereinstimmung
Um die gleiche Regex mehrfach einzusetzen, verwenden Sie ein kompiliertes Objekt: reobj = re.compile(r"Regex-Muster\Z") if reobj.match(subject): # Übereinstimmung gefunden else: # Keine Übereinstimmung
Ruby if subject =~ /\ARegex-Muster\Z/ # Übereinstimmung gefunden else # Keine Übereinstimmung end
Diskussion Normalerweise erfahren Sie durch eine erfolgreiche Übereinstimmung eines regulären Ausdrucks nur, dass das Muster irgendwo innerhalb des Texts vorhanden ist. In vielen Situationen wollen Sie aber auch sicherstellen, dass der Text vollständig abgedeckt ist und nichts enthält, was nicht mit dem Muster übereinstimmt. Das kommt vor allem in Situationen vor, in denen man Eingaben von Benutzern auf Gültigkeit prüfen will. Wenn ein Anwender eine Telefonnummer oder eine IP-Adresse eingibt, aber darüber hinaus zusätzliche Zeichen hinzufügt, wollen Sie die Angaben zurückweisen.
3.6 Auf eine vollständige Übereinstimmung einer Regex mit einem Text prüfen | 139
Die Lösungen, die die Anker ‹$› und ‹\Z› nutzen, funktionieren auch, wenn Sie eine Datei Zeile für Zeile verarbeiten (Rezept 3.21) und beim Einlesen der Zeilen den Zeilenumbruch am Ende belassen. Wie in Rezept 2.5 erläutert, passen diese Anker auch vor einem abschließenden Zeilenumbruch, womit sich dieser Zeilenumbruch ignorieren lässt. In den folgenden Abschnitten erläutern wir die Lösungen für die verschiedenen Sprachen.
C# und VB.NET Die Klasse Regex() im .NET Framework bietet keine Funktion an, mit der sich testen lässt, ob eine Regex einen String komplett abdeckt. Daher muss man hier den Textanfangs-Anker ‹\A› an den Anfang der Regex und den Textende-Anker ‹\Z› an das Ende setzen. So passt der reguläre Ausdruck nur auf den vollständigen String – oder gar nicht. Wenn Ihr regulärer Ausdruck Alternationen verwendet, zum Beispiel ‹eins|zwei|drei›, müssen Sie sicherstellen, dass die Alternation in eine Gruppe gesteckt wird, bevor Sie die Anker hinzufügen: ‹\A(?:eins|zwei|drei)\Z›. Ist Ihr regulärer Ausdruck so angepasst, können Sie die gleiche Methode IsMatch() benutzen, die schon im vorhergehenden Rezept beschrieben wurde.
Java In Java gibt es drei Methoden mit dem Namen matches(). Alle drei prüfen, ob eine Regex einen String vollständig abdecken kann. Diese Methoden stellen eine einfache Möglichkeit zur Eingabeüberprüfung dar, da Sie Ihre Regex nicht extra mit Ankern am Anfang und am Ende versehen müssen. Die Klasse String enthält eine Methode matches(), die als einzigen Parameter einen regulären Ausdruck erwartet. Sie liefert true oder false zurück, um anzugeben, ob die Regex den ganzen String abdecken konnte. Die Klasse Pattern besitzt eine statische Methode matches(), die zwei Strings erwartet. Der erste ist der reguläre Ausdruck, der zweite der fragliche Text. Sie können als Text sogar eine beliebige CharSequence an Pattern. matches() übergeben. Das ist der einzige Grund dafür, Pattern.matches() statt String. matches() zu verwenden. Sowohl String.matches() als auch Pattern.matches() kompilieren den regulären Ausdruck jedes Mal, indem sie Pattern.compile("Regex").matcher(subjectString).matches() aufrufen. Da die Regex immer wieder neu kompiliert wird, sollten Sie diese Methoden nur verwenden, wenn Sie die Regex lediglich ein Mal nutzen wollen (um zum Beispiel ein Feld in einem Eingabefenster auszuwerten) oder wenn Effizienz kein Thema ist. Bei diesen Methoden kann man außerhalb des regulären Ausdrucks keine Regex-Optionen mitgeben. Wenn Ihr regulärer Ausdruck einen Syntaxfehler enthält, wird eine PatternSyntaxException geworfen.
140 | Kapitel 3: Mit regulären Ausdrücken programmieren
Möchten Sie die gleiche Regex effizient auf mehrere Strings anwenden, sollten Sie Ihre Regex kompilieren, dann einen Matcher erstellen und diesen wiederholt nutzen, wie in Rezept 3.3 beschrieben. Anschließend rufen Sie für Ihre Matcher-Instanz die Methode matches() auf. Diese Funktion erwartet keine Parameter, da Sie den Ausgangstext schon beim Erstellen oder Zurücksetzen des Matchers angegeben haben.
JavaScript JavaScript bietet keine Funktion an, mit der man testen kann, ob eine Regex einen String vollständig abdeckt. Stattdessen fügen Sie am Anfang Ihres regulären Ausdrucks ein ‹^› und am Ende ein ‹$› an. Stellen Sie sicher, dass Sie nicht die Option /m nutzen. Denn nur ohne diese Option passen Zirkumflex und Dollarzeichen lediglich am Anfang und am Ende des Ausgangstexts. Wenn Sie /m setzen, passen sie auch auf Zeilenumbrüche mitten im Text. Wenn Sie Ihren regulären Ausdruck um die Anker ergänzt haben, können Sie die gleiche Methode regexp.test() nutzen, die im vorhergehenden Rezept beschrieben wurde.
PHP PHP bietet keine Funktion an, mit der sich testen lässt, ob eine Regex einen String komplett abdeckt. Daher muss man hier den Textanfangs-Anker ‹\A› an den Anfang der Regex und den Textende-Anker ‹\Z› an das Ende setzen. So passt der reguläre Ausdruck nur auf den vollständigen String – oder gar nicht. Wenn Ihr regulärer Ausdruck Alternationen verwendet, zum Beispiel ‹eins|zwei|drei›, müssen Sie sicherstellen, dass die Alternation in eine Gruppe gesteckt wird, bevor Sie die Anker hinzufügen: ‹\A(?:eins|zwei|drei)\Z›. Wenn Sie Ihren regulären Ausdruck um die Anker ergänzt haben, können Sie die gleiche Funktion preg_match() nutzen, die im vorhergehenden Rezept beschrieben wurde.
Perl Perl hat nur einen Mustererkennungsoperator, der schon bei Übereinstimmung mit einem Teil des Texts zufrieden ist. Um also zu prüfen, ob Ihre Regex den gesamten Text abdeckt, fügen Sie den Textanfangs-Anker ‹\A› an den Anfang der Regex und den Textende-Anker ‹\Z› an das Ende ein. So passt der reguläre Ausdruck nur auf den vollständigen String – oder gar nicht. Wenn Ihr regulärer Ausdruck Alternationen verwendet, zum Beispiel ‹eins|zwei|drei›, müssen Sie sicherstellen, dass die Alternation in eine Gruppe gesteckt wird, bevor Sie die Anker hinzufügen: ‹\A(?:eins|zwei|drei)\Z›. Wenn Sie Ihren regulären Ausdruck um die Anker ergänzt haben, können Sie ihn genau so wie im vorhergehenden Rezept beschrieben anwenden.
3.6 Auf eine vollständige Übereinstimmung einer Regex mit einem Text prüfen | 141
Python Die Funktion match() ähnelt sehr der im vorigen Rezept beschriebenen Funktion search(). Hauptunterschied ist, dass match() den regulären Ausdruck nur am Anfang des Ausgangstexts auswertet. Wenn die Regex nicht am Anfang des Strings passt, liefert match() direkt den Wert None zurück. Die Funktion search() versucht hingegen, die Regex an jeder möglichen Position im String anzuwenden, bis sie entweder eine Übereinstimmung findet oder das Ende des Strings erreicht. Bei der Funktion match() ist es nicht erforderlich, dass der reguläre Ausdruck den gesamten String abdeckt. Es reicht eine Übereinstimmung mit einem Teil des Texts, solange diese am Anfang des Strings beginnt. Wenn Sie prüfen wollen, ob Ihre Regex den gesamten String abdeckt, müssen Sie einen Textende-Anker ‹\Z› an Ihren regulären Ausdruck anhängen.
Ruby Die Ruby-Klasse Regexp enthält keine Funktion, mit der sich testen lässt, ob eine Regex einen String komplett abdeckt. Daher müssen Sie hier den Textanfangs-Anker ‹\A› an den Anfang der Regex und den Textende-Anker ‹\Z› an das Ende setzen. So passt der reguläre Ausdruck nur auf den vollständigen String – oder gar nicht. Wenn Ihr regulärer Ausdruck Alternationen verwendet, zum Beispiel ‹eins|zwei|drei›, müssen Sie sicherstellen, dass die Alternation in eine Gruppe gesteckt wird, bevor Sie die Anker hinzufügen: ‹\A(?:eins|zwei|drei)\Z›. Haben Sie Ihren regulären Ausdruck um die Anker ergänzt, können Sie den gleichen Operator =~ nutzen, der schon im vorhergehenden Rezept beschrieben wurde.
Siehe auch Rezept 2.5 erklärt genauer, wie Anker funktionieren. Die Rezepte 2.8 und 2.9 erklären die Alternation und das Gruppieren. Wenn Ihre Regex eine Alternation außerhalb von Gruppen verwendet, müssen Sie sie gruppieren, bevor Sie die Anker hinzufügen. Nutzen Sie keine Alternation oder verwenden Sie sie nur innerhalb von Gruppen, brauchen Sie keine zusätzliche Gruppe, damit die Anker wie gewünscht funktionieren. Schauen Sie sich Rezept 3.5 an, wenn eine teilweise Übereinstimmung ausreichend ist.
3.7
Auslesen des übereinstimmenden Texts
Problem Sie haben einen regulären Ausdruck, der zu einem Teil des Ausgangstexts passt. Sie wollen den übereinstimmenden Text auslesen. Findet der reguläre Ausdruck im String mehr
142 | Kapitel 3: Mit regulären Ausdrücken programmieren
als eine Übereinstimmung, wollen Sie lediglich die erste auslesen. Wenn Sie zum Beispiel die Regex ‹\d+› auf den String Mögen Sie 13 oder 42? anwenden, sollte 13 zurückgegeben werden.
Lösung C# Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie den statischen Aufruf verwenden: string resultString = Regex.Match(subjectString, @"\d+").Value;
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie den statischen Aufruf per Exception Handling absichern: string resultString = null; try { resultString = Regex.Match(subjectString, @"\d+").Value; } catch (ArgumentNullException ex) { // Regex und Text müssen vorhanden sein } catch (ArgumentException ex) { // Syntaxfehler im regulären Ausdruck }
Um die gleiche Regex mehrfach einzusetzen, erstellen Sie ein Regex-Objekt: Regex regexObj = new Regex(@"\d+"); string resultString = regexObj.Match(subjectString).Value;
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie das Regex-Objekt per Exception Handling absichern: string resultString = null; try { Regex regexObj = new Regex(@"\d+"); try { resultString = regexObj.Match(subjectString).Value; } catch (ArgumentNullException ex) { // Regex und Text müssen vorhanden sein } } catch (ArgumentException ex) { // Syntaxfehler im regulären Ausdruck }
VB.NET Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie den statischen Aufruf verwenden: Dim ResultString = Regex.Match(SubjectString, "\d+").Value
3.7 Auslesen des übereinstimmenden Texts | 143
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie den statischen Aufruf per Exception Handling absichern: Dim ResultString As String = Nothing Try ResultString = Regex.Match(SubjectString, "\d+").Value Catch ex As ArgumentNullException 'Regex und Text dürfen nicht Nothing sein Catch ex As ArgumentException 'Syntaxfehler im regulären Ausdruck End Try
Um die gleiche Regex mehrfach einzusetzen, erstellen Sie ein Regex-Objekt: Dim RegexObj As New Regex("\d+") Dim ResultString = RegexObj.Match(SubjectString).Value
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie das Regex-Objekt per Exception Handling absichern: Dim ResultString As String = Nothing Try Dim RegexObj As New Regex("\d+") Try ResultString = RegexObj.Match(SubjectString).Value Catch ex As ArgumentNullException 'Text darf nicht Nothing sein End Try Catch ex As ArgumentException 'Syntaxfehler im regulären Ausdruck End Try
Java Erstellen Sie einen Matcher, um die Suche auszuführen und das Ergebnis zu sichern: String resultString = null; Pattern regex = Pattern.compile("\\d+"); Matcher regexMatcher = regex.matcher(subjectString); if (regexMatcher.find()) { resultString = regexMatcher.group(); }
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie das Ganze mit einem kompletten Exception Handling versehen: String resultString = null; try { Pattern regex = Pattern.compile("\\d+"); Matcher regexMatcher = regex.matcher(subjectString); if (regexMatcher.find()) { resultString = regexMatcher.group(); } } catch (PatternSyntaxException ex) { // Syntaxfehler im regulären Ausdruck }
144 | Kapitel 3: Mit regulären Ausdrücken programmieren
JavaScript var result = subject.match(/\d+/); if (result) { result = result[0]; } else { result = ''; }
Python Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie die globale Funktion verwenden: matchobj = re.search("Regex-Muster", subject) if matchobj: result = matchobj.group() else: result = ""
Um die gleiche Regex mehrfach einzusetzen, verwenden Sie ein kompiliertes Objekt: reobj = re.compile("Regex-Muster") matchobj = reobj.search(subject) if match: result = matchobj.group() else: result = ""
Ruby Sie können den Operator =~ und seine magische Variable $& nutzen: if subject =~ /Regex-Muster/ result = $& else result = "" end
3.7 Auslesen des übereinstimmenden Texts | 145
Alternativ können Sie auch die Methode match für ein Regexp-Objekt aufrufen: matchobj = /Regex-Muster/.match(subject) if matchobj result = matchobj[0] else result = "" end
Diskussion Das Extrahieren eines Teils eines längeren Strings, der zu einem bestimmten Muster passt, ist ein weiteres Haupteinsatzgebiet regulärer Ausdrücke. Alle in diesem Buch behandelten Programmiersprachen bieten eine einfache Möglichkeit, die erste Übereinstimmung eines regulären Ausdrucks in einem String auszulesen. Die Funktion versucht dabei, die Regex am Anfang des Strings anzuwenden, und durchläuft ihn so lange, bis der Ausdruck passt.
.NET Die .NET-Klasse Regex hat keinen Member, der den von einem regulären Ausdruck gefundenen String zurückgibt. Aber es gibt eine Methode Match(), die eine Instanz der Klasse Match zurückgibt. Dieses Match-Objekt hat eine Eigenschaft namens Value, in der der vom regulären Ausdruck gefundene Text zu finden ist. Wenn der reguläre Ausdruck nichts findet, gibt er trotzdem ein Match-Objekt zurück, aber die Eigenschaft Value enthält dann einen leeren String. Fünf verschieden überladene Versionen ermöglichen es Ihnen, die Methode Match() auf unterschiedlichste Art und Weise aufzurufen. Der erste Parameter ist immer der String, in dem sich der Ausgangstext befindet, auf den der reguläre Ausdruck angewendet werden soll. Dieser Parameter sollte nicht null sein, da Match() ansonsten eine ArgumentNullException werfen wird. Wenn Sie den regulären Ausdruck nur ein paar Mal nutzen wollen, können Sie einen statischen Aufruf durchführen. Der zweite Parameter ist dann der reguläre Ausdruck, den Sie nutzen wollen. Sie können als optionalen dritten Parameter noch Regex-Optionen angeben. Enthält Ihr regulärer Ausdruck einen Syntaxfehler, wird eine ArgumentException geworfen. Möchten Sie den gleichen regulären Ausdruck auf viele Strings anwenden, können Sie Ihren Code effizienter gestalten, indem Sie zunächst ein Regex-Objekt erstellen und dann für dieses Objekt Match() aufrufen. Der erste Parameter mit dem Text ist der einzig notwendige. Sie können einen optionalen zweiten Parameter mit der Zeichenposition angeben, an der der reguläre Ausdruck mit der Suche beginnen soll. Im Prinzip handelt es sich bei diesem Wert um die Anzahl der Zeichen, die der reguläre Ausdruck am Anfang des Ausgangstexts ignorieren soll. Das kann nützlich sein, wenn Sie den String schon bis zu einer gewissen Position verarbeitet haben und nun noch den Rest durchsuchen wollen.
146 | Kapitel 3: Mit regulären Ausdrücken programmieren
Diese Zahl muss größer oder gleich null und kleiner oder gleich der Länge des Strings sein. Ansonsten wirft IsMatch() eine ArgumentOutOfRangeException. Wenn Sie den zweiten Parameter mit der Startposition übergeben, können Sie auch einen dritten Parameter angeben, der die Länge des Substrings festlegt, in der die Regex suchen darf. Diese Zahl muss größer oder gleich null sein und darf die Länge des restlichen Strings nicht überschreiten. So versucht zum Beispiel regexObj.Match("123456", 3, 2), eine Übereinstimmung in "45" zu finden. Wenn der dritte Parameter größer als die Länge des Texts ist, wirft Match() eine ArgumentOutOfRangeException. Ist er nicht größer als die Länge des Texts, aber größer als der verbleibende Text, wird stattdessen eine IndexOutOfRangeException geworfen. Erlauben Sie dem Anwender, Anfangs- und Endpositionen anzugeben, müssen Sie sie entweder vor dem Aufruf von Match() prüfen oder zumindest sicherstellen, dass Sie beide Exceptions abfangen. Bei den statischen Methoden können keine Parameter für die Definition des zu durchsuchenden Abschnitts mitgegeben werden.
Java Um den Teil eines Strings zu erhalten, der von einem regulären Ausdruck gefunden wurde, müssen Sie einen Matcher erstellen, wie in Rezept 3.3 beschrieben. Dann rufen Sie dessen Methode find() ohne weitere Parameter auf. Wenn find() den Wert true zurückgibt, rufen Sie group() ohne weitere Parameter auf, um den von Ihrem regulären Ausdruck gefundenen Text zu erhalten. Liefert find() den Wert false, sollten Sie group() nicht aufrufen, da Sie dann nur eine IllegalStateException erhalten würden. Matcher.find() hat einen optionalen Parameter, mit dem die Startposition im Text angegeben werden kann. Sie können ihn dazu verwenden, die Suche an einer bestimmten Stelle im String beginnen zu lassen. Wenn Sie 0 angeben, wird am Anfang des Texts begonnen. Es wird eine IndexOutOfBoundsException geworfen, wenn Sie die Startposition auf einen negativen Wert setzen oder auf einen Wert, der größer ist als die Länge des Texts.
Lassen Sie den Parameter weg, beginnt find() mit dem Zeichen nach der Position, an der die letzte Übereinstimmung durch find() gefunden wurde. Wenn Sie find() das erste Mal nach Pattern.matcher() oder Matcher.reset() aufrufen, beginnt find() mit der Suche am Anfang des Strings.
JavaScript Die Methode string.match() erwartet einen regulären Ausdruck als Parameter. Sie können einen regulären Ausdruck als literale Regex, als Regex-Objekt oder als String übergeben. Wenn Sie einen String mitgeben, erzeugt string.match() ein temporäres regexpObjekt. Ist die Suche erfolglos, liefert string.match() den Wert null zurück. Damit können Sie unterscheiden zwischen einer Regex, die keine Übereinstimmungen gefunden hat, und
3.7 Auslesen des übereinstimmenden Texts | 147
einer, die eine Übereinstimmung der Länge null enthält. Das bedeutet, dass Sie das Ergebnis nicht direkt anzeigen können, da „null“ oder ein Fehler über ein Null-Objekt erscheinen kann. Wenn die Suche erfolgreich war, liefert string.match() ein Array mit den Details der Übereinstimmung zurück. Das nullte Element im Array ist ein String mit dem vom regulären Ausdruck gefundenen Text. Achten Sie darauf, nicht die Option /g zu nutzen. Denn dann verhält sich string.match() anders, wie in Rezept 3.10 geschildert wird.
PHP Der in den vorletzten beiden Rezepten besprochenen Funktion preg_match() kann ein optionaler dritter Parameter mitgegeben werden, in dem der gefundene Text und die eingefangenen Gruppen abgelegt werden. Wenn preg_match() den Wert 1 zurückgibt, enthält die Variable ein Array mit Strings. Das nullte Element des Arrays enthält den vom regulären Ausdruck gefundenen Teil des Texts. Die anderen Elemente werden in Rezept 3.9 beschrieben.
Perl Wenn der Mustererkennungsoperator m// eine Übereinstimmung findet, setzt er eine Reihe von speziellen Variablen. Eine davon ist die Variable $&, die den Teil des Strings enthält, der durch den regulären Ausdruck gefunden wurde. Die anderen speziellen Variablen werden in späteren Rezepten erläutert.
Python In Rezept 3.5 wird die Funktion search() beschrieben. Hier speichern wir die von search() zurückgegebene Instanz von MatchObject in einer Variablen. Um an den vom regulären Ausdruck gefundenen Teil des Strings zu gelangen, rufen wir die Methode group() für das Match-Objekt ohne Parameter auf.
Ruby In Rezept 3.8 sind die Variable $~ und das Objekt MatchData beschrieben. In einem String-Kontext wird aus diesem Objekt der Text, der durch den regulären Ausdruck gefunden wurde. In einem Array-Kontext wird es zu einem Array, dessen nulltes Element den von der Regex gefundenen Text enthält. $& ist eine spezielle, nur lesbare Variable. Sie ist ein Alias für $~[0], in dem ein String mit
dem vom regulären Ausdruck gefundenen Text zu finden ist.
Siehe auch Rezepte 3.5, 3.8, 3.9, 3.10 und 3.11.
148 | Kapitel 3: Mit regulären Ausdrücken programmieren
3.8
Position und Länge der Übereinstimmung ermitteln
Problem Statt den Substring auszulesen, der von einem regulären Ausdruck gefunden wurde (wie im vorherigen Rezept), wollen Sie Startposition und Länge der Übereinstimmung wissen. Mit dieser Information können Sie die Übereinstimmung in Ihrem eigenen Code ermitteln oder irgendwelche spannenden Verarbeitungsschritte mit dem ursprünglichen String anstellen.
Lösung C# Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie den statischen Aufruf verwenden: int matchstart, matchlength = -1; Match matchResult = Regex.Match(subjectString, @"\d+"); if (matchResult.Success) { matchstart = matchResult.Index; matchlength = matchResult.Length; }
Um die gleiche Regex mehrfach einzusetzen, erstellen Sie ein Regex-Objekt: int matchstart, matchlength = -1; Regex regexObj = new Regex(@"\d+"); Match matchResult = regexObj.Match(subjectString).Value; if (matchResult.Success) { matchstart = matchResult.Index; matchlength = matchResult.Length; }
VB.NET Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie den statischen Aufruf verwenden: Dim MatchStart = -1 Dim MatchLength = -1 Dim MatchResult = Regex.Match(SubjectString, "\d+") If MatchResult.Success Then MatchStart = MatchResult.Index MatchLength = MatchResult.Length End If
Um die gleiche Regex mehrfach einzusetzen, erstellen Sie ein Regex-Objekt: Dim MatchStart = -1 Dim MatchLength = -1
3.8 Position und Länge der Übereinstimmung ermitteln | 149
Dim RegexObj As New Regex("\d+") Dim MatchResult = Regex.Match(SubjectString, "\d+") If MatchResult.Success Then MatchStart = MatchResult.Index MatchLength = MatchResult.Length End If
JavaScript var matchstart = -1; var matchlength = -1; var match = /\d+/.exec(subject); if (match) { matchstart = match.index; matchlength = match[0].length; }
Python Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie die globale Funktion verwenden: matchobj = re.search(r"\d+", subject) if matchobj: matchstart = matchobj.start() matchlength = matchobj.end() - matchstart
150 | Kapitel 3: Mit regulären Ausdrücken programmieren
Um die gleiche Regex mehrfach einzusetzen, verwenden Sie ein kompiliertes Objekt: reobj = re.compile(r"\d+") matchobj = reobj.search(subject) if matchobj: matchstart = matchobj.start() matchlength = matchobj.end() - matchstart
Ruby Sie können den Operator =~ und seine magische Variable $& nutzen: if subject =~ /Regex-Muster/ matchstart = $~.begin() matchlength = $~.end() - matchstart end
Alternativ können Sie auch die Methode match für ein Regexp-Objekt aufrufen: matchobj = /Regex-Muster/.match(subject) if matchobj matchstart = matchobj.begin() matchlength = matchobj.end() - matchstart end
Diskussion .NET Um die Startposition und die Länge des Übereinstimmungsbereichs zu erhalten, nutzen wir die gleiche Methode Regex.Match(), die schon im vorhergehenden Rezept beschrieben wurde. Dieses Mal verwenden wir die Eigenschaften Index und Length des von Regex.Match() zurückgegebenen Match-Objekts. Index ist die Position im Text, an der die Regex-Übereinstimmung beginnt. Wenn dies gleich am Anfang des Strings der Fall ist, hat Index den Wert 0. Beginnt die Übereinstimmung mit dem zweiten Zeichen im String, hat Index den Wert 1. Der maximale Wert für Index ist die Länge des Strings. Das kann dann passieren, wenn die Regex eine Übereinstimmung der Länge null am Ende des Strings findet. Besteht die Regex zum Beispiel nur aus dem Textende-Anker ‹\Z›, findet sich die Übereinstimmung immer am Ende des Strings. Length gibt die Zahl der Zeichen an, für die eine Übereinstimmung besteht. Es ist mög-
lich, dass eine Übereinstimmung null Zeichen lang ist. Besteht zum Beispiel die Regex nur aus der Wortgrenze ‹\b›, wird eine Übereinstimmung der Länge null am Anfang des ersten Worts im String gefunden. Wenn es keine Übereinstimmung gibt, liefert Regex.Match() trotzdem ein Match-Objekt zurück. Dessen Eigenschaften Index und Length haben dann beide den Wert null. Diese Werte können aber auch bei einer erfolgreichen Suche vorkommen. Besteht die Regex zum Beispiel aus dem Textanfangs-Anker ‹\A›, wird eine Übereinstimmung der Länge 3.8 Position und Länge der Übereinstimmung ermitteln | 151
null am Anfang des Strings gefunden. Daher können Sie sich nicht auf Match.Index oder Match.Length verlassen, um herauszufinden, ob die Suche erfolgreich war. Verwenden Sie stattdessen besser Match.Success.
Java Um die Position und die Länge der Übereinstimmung zu finden, rufen Sie zunächst, wie schon im vorhergehenden Rezept beschrieben, Matcher.find() auf. Wenn find() true zurückgibt, rufen Sie als Nächstes Matcher.start() ohne Parameter auf, um den Index des ersten Zeichens zu erhalten, das Teil der Regex-Übereinstimmung ist. Ein Aufruf von end() ohne Parameter liefert den Index des ersten Zeichens nach der Übereinstimmung zurück. Subtrahieren Sie den Anfang vom Ende, um die Länge der Übereinstimmung zu erhalten. Diese kann durchaus null sein. Wenn Sie start() oder end() ohne einen vorherigen Aufruf von find() aufrufen, erhalten Sie eine IllegalStateException.
JavaScript Rufen Sie die Methode exec() für ein regexp-Objekt auf, um ein Array mit den Details des Suchergebnisses zu erhalten. Dieses Array besitzt ein paar zusätzliche Eigenschaften. In der Eigenschaft index ist die Position im Text abgelegt, an der die Regex-Übereinstimmung beginnt. Wenn dies der Anfang des Strings ist, hat index den Wert null. Das nullte Element des Arrays enthält einen String mit dem gesamten Suchergebnis. Die Eigenschaft length dieses Strings ist die Länge der Übereinstimmung. Konnte der reguläre Ausdruck keine Übereinstimmung finden, liefert regexp.exec() den Wert null zurück. Verwenden Sie nicht die Eigenschaft lastIndex des von exec() zurückgegebenen Arrays, um das Ende der Übereinstimmung zu erhalten. In einer strikten JavaScript-Implementierung existiert lastIndex gar nicht im zurückgegebenen Array, sondern nur im regexpObjekt selbst. Sie sollten aber ebenfalls nicht regexp.lastIndex verwenden. Es ist aufgrund von Unterschieden zwischen den Browsern nicht verlässlich nutzbar (siehe Rezept 3.11). Stattdessen addieren Sie match.index und match[0].length einfach, um herauszufinden, wo die Regex-Übereinstimmung endet.
PHP Das vorhergehende Rezept hat erläutert, wie Sie den von einem regulären Ausdruck gefundenen Text erhalten können, indem Sie preg_match() einen dritten Parameter übergeben. Sie können die Position der Übereinstimmung ermitteln, indem Sie die Konstante PREG_OFFSET_CAPTURE als vierten Parameter übergeben. Dieser Parameter beeinflusst das, was preg_match() im dritten Parameter ablegt, wenn es 1 zurückliefert. Wenn Sie den vierten Parameter weglassen oder auf null setzen, enthält die als dritter Parameter übergebene Variable ein Array mit Strings. Übergeben Sie PREG_OFFSET_CAPTURE als vierten Parameter, enthält die Variable ein Array aus Arrays. Das nullte Element im
152 | Kapitel 3: Mit regulären Ausdrücken programmieren
Hauptarray enthält weiterhin das Suchergebnis (siehe dazu das obige Rezept), während die darauffolgenden Elemente immer noch die Ergebnisse der einfangenden Gruppen enthalten. Aber statt eines Strings mit dem von der Regex oder einer einfangenden Gruppe gefundenen Text enthält das Element nun ein Array mit zwei Werten: dem Text, der gefunden wurde, und der Position im String, an der er gefunden wurde. Um die Details des gesamten Suchergebnisses zu bekommen, liefert uns das nullte Unterelement des nullten Elements den Text, der von der Regex gefunden wurde. Diesen übergeben wir an die Funktion strlen(), um seine Länge zu ermitteln. Das erste Unterelement des nullten Elements enthält einen Integer-Wert mit der Position im Text, an der die Übereinstimmung beginnt.
Perl Um die Länge der Übereinstimmung zu ermitteln, berechnen wir einfach die Länge der Variablen $&, die das vollständige Suchergebnis enthält. Um den Anfang des Übereinstimmungsbereichs herauszufinden, berechnen wir die Länge der Variablen $`, in der der Text des Strings vor dem Regex-Übereinstimmungsbereich zu finden ist.
Python Die Methode start() von MatchObject gibt die Position im String zurück, an der die Regex-Übereinstimmung beginnt. Die Methode end() gibt die Position des ersten Zeichens nach dem Übereinstimmungsbereich zurück. Beide Methoden liefern den gleichen Wert, wenn eine Übereinstimmung der Länge null gefunden wurde. Sie können an die Methoden start() und end() einen Parameter übergeben, um den Textbereich einer der einfangenden Gruppen des regulären Ausdrucks zu erhalten. Mit start(1) sprechen Sie die erste einfangende Gruppe an, mit end(2) die zweite und so weiter. Python unterstützt bis zu 99 einfangende Gruppen. Die Gruppe mit der Nummer 0 ist das gesamte Suchergebnis. Jede Zahl außerhalb des Bereichs von 0 bis zur Anzahl der einfangenden Gruppen (mit 99 als absoluter Obergrenze) führt zu einem IndexError. Wenn die Gruppennummer gültig ist, die Gruppe aber an der Regex-Übereinstimmung nicht beteiligt war, liefern sowohl start() als auch end() für diese Gruppe den Wert -1 zurück. Wollen Sie Start- und Endposition in einem Tupel speichern, rufen Sie die Methode span() für das Match-Objekt auf.
Ruby In Rezept 3.5 wird der Operator =~ genutzt, um die erste Regex-Übereinstimmung in einem String zu finden. Ein Nebeneffekt dieses Operators ist, dass er die spezielle Variable $~ mit einer Instanz der Klasse MatchData bestückt. Diese Variable ist Thread- und Methoden-lokal. Das bedeutet, Sie können den Inhalt dieser Variablen nutzen, bis Ihre Methode beendet ist oder bis Sie das nächste Mal den Operator =~ in Ihrer Methode nut-
3.8 Position und Länge der Übereinstimmung ermitteln | 153
zen. Sie müssen sich nicht darum sorgen, dass ein anderer Thread oder eine andere Methode in Ihrem Thread den Inhalt überschreibt. Wenn Sie die Details mehrerer Regex-Suchen sichern wollen, rufen Sie die Methode match() für ein Regexp-Objekt auf. Diese Methode erwartet den Text als einzigen Parameter. Sie liefert eine Instanz von MatchData zurück, wenn es eine Übereinstimmung gab, ansonsten den Wert nil. Auch sie setzt die Variable $~ auf die gleiche Instanz von MatchObject, überschreibt aber keine anderen MatchObject-Instanzen, die in anderen Variablen gespeichert waren. Das Objekt MatchData speichert alle Details zu einer Regex-Übereinstimmung. Die Rezepte 3.7 und 3.9 beschreiben, wie man an den Text gelangt, der vom regulären Ausdruck und von den einfangenden Gruppen gefunden wurde. Die Methode begin() liefert die Position im Text zurück, an der die Regex-Übereinstimmung beginnt. end() liefert die Position des ersten Zeichens nach dem Regex-Übereinstimmungsbereich zurück. offset() gibt ein Array zurück, in dem sich die Anfangs- und Endpositionen befinden. Diese drei Methoden erwarten jeweils einen Parameter. Mit 0 erhalten Sie die Positionen des gesamten Suchergebnisses. Übergeben Sie eine positive Zahl, erhalten Sie die Daten für die entsprechende einfangende Gruppe. So liefert begin(1) zum Beispiel den Anfang der ersten einfangenden Gruppe. Verwenden Sie nicht length() oder size(), um die Länge der Übereinstimmung zu ermitteln. Beide Methoden geben die Anzahl der Elemente im Array zurück, das MatchData im Array-Kontext liefert (erklärt in Rezept 3.9).
Siehe auch Rezepte 3.5 und 3.9.
3.9
Teile des übereinstimmenden Texts auslesen
Problem Wie in Rezept 3.7 haben Sie einen regulären Ausdruck, der auf einen Substring des Texts passt. Dieses Mal wollen Sie aber nur einen Teil dieses Substrings nutzen. Um den gewünschten Teil abzugrenzen, haben Sie Ihrem regulären Ausdruck eine einfangende Gruppe hinzugefügt (wie bereits in Rezept 2.9 beschrieben). So passt der reguläre Ausdruck ‹http://([a-z0-9.-]+)› zum Beispiel auf http://www. regexcookbook.com im String Auf http://www.regexcookbook.com finden Sie mehr Informationen. Der Teil der Regex innerhalb der ersten einfangenden Gruppe passt auf www.regexcookbook.com, und Sie wollen den Domainnamen aus der ersten einfangenden Gruppe in eine String-Variable auslesen.
154 | Kapitel 3: Mit regulären Ausdrücken programmieren
Wir verwenden diese einfache Regex, um das Konzept von einfangenden Gruppen deutlich zu machen. In Kapitel 7 finden Sie exaktere Regexes, die auf URLs passen.
Lösung C# Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie den statischen Aufruf verwenden: string resultString = Regex.Match(subjectString, "http://([a-z0-9.-]+)").Groups[1].Value;
Um die gleiche Regex mehrfach einzusetzen, erstellen Sie ein Regex-Objekt: Regex regexObj = new Regex("http://([a-z0-9.-]+)"); string resultString = regexObj.Match(subjectString).Groups[1].Value;
VB.NET Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie den statischen Aufruf verwenden: Dim ResultString = Regex.Match(SubjectString, "http://([a-z0-9.-]+)").Groups(1).Value
Um die gleiche Regex mehrfach einzusetzen, erstellen Sie ein Regex-Objekt: Dim RegexObj As New Regex("http://([a-z0-9.-]+)") Dim ResultString = RegexObj.Match(SubjectString).Groups(1).Value
Python Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie die globale Funktion verwenden: matchobj = re.search("http://([a-z0-9.-]+)", subject) if matchobj: result = matchobj.group(1) else: result = ""
Um die gleiche Regex mehrfach einzusetzen, verwenden Sie ein kompiliertes Objekt: reobj = re.compile("http://([a-z0-9.-]+)") matchobj = reobj.search(subject) if match: result = matchobj.group(1) else: result = ""
Ruby Sie können den Operator =~ und seine magischen nummerierten Variablen (wie zum Beispiel $1) nutzen: if subject =~ %r!http://([a-z0-9.-]+)! result = $1 else result = "" end
Alternativ können Sie auch die Methode match für ein Regexp-Objekt aufrufen: matchobj = %r!http://([a-z0-9.-]+)!.match(subject) if matchobj result = matchobj[1] else result = "" end
156 | Kapitel 3: Mit regulären Ausdrücken programmieren
Diskussion In Rezept 2.10 und 2.21 haben wir erklärt, wie Sie nummerierte Rückwärtsreferenzen im regulären Ausdruck und im Ersetzungstext nutzen können, um denselben Text erneut zu suchen oder um Teile der Regex-Übereinstimmung in den Ersetzungstext einzufügen. Sie können die gleichen Referenzzahlen nutzen, um den Text in Ihrem Code auszulesen, der von einer oder mehreren einfangenden Gruppen gefunden wurde. Bei regulären Ausdrücken werden einfangende Gruppen beginnend mit eins nummeriert. Programmiersprachen nummerieren Arrays und Listen im Allgemeinen bei null beginnend. Alle in diesem Buch behandelten Programmiersprachen speichern die einfangenden Gruppen in einem Array oder in einer Liste, wobei sie aber die gleiche Nummerierung wie bei den einfangenden Gruppen im regulären Ausdruck verwenden, also mit eins beginnen. Das nullte Element im Array oder in der Liste wird dafür genutzt, das gesamte Suchergebnis zu speichern. Wenn also Ihr regulärer Ausdruck drei einfangende Gruppen hat, finden sich im Array mit den Übereinstimmungen vier Elemente. Das Element null enthält das gesamte Suchergebnis, während die Elemente eins, zwei und drei den Text enthalten, der durch die drei einfangenden Gruppen gefunden wurde.
.NET Um die Details zu einfangenden Gruppen zu erhalten, greifen wir erneut auf die Member-Funktion Regex.Match() zurück, die erstmals in Rezept 3.7 beschrieben wurde. Das zurückgegebene Objekt vom Typ Match besitzt eine Eigenschaft namens Groups. Dabei handelt es sich um eine Collection des Typs GroupCollection. Die Collection enthält die Details aller einfangenden Gruppen in Ihrem regulären Ausdruck. In Groups[1] stehen die Details der ersten einfangenden Gruppe, in Groups[2] die der zweiten und so weiter. Die Collection Groups enthält für jede einfangende Gruppe ein Objekt des Typs Group. Die Klasse Group besitzt die gleichen Eigenschaften wie die Klasse Match, abgesehen von der Eigenschaft Groups. Match.Groups[1].Value liefert den von der ersten einfangenden Gruppe gefundenen Text zurück – genauso wie Match.Value das gesamte Suchergebnis liefert. Match.Groups[1].Index und Match.Groups[1].Length geben die Startposition und die Länge des von der Gruppe gefundenen Texts zurück. In Rezept 3.8, finden Sie mehr Informationen zu Index und Length. Groups[0] enthält die Details des gesamten Suchergebnisses, die auch direkt im MatchObjekt gefunden werden können. Match.Value und Match.Groups[0].Value sind äquivalent.
Die Collection Groups wirft keine Exception, wenn Sie eine ungültige Gruppennummer angeben. So liefert Groups[-1] zum Beispiel trotzdem ein Group-Objekt zurück, aber die Eigenschaften dieses Objekts zeigen dann, dass die fiktive einfangende Gruppe -1 nichts gefunden hat. Die beste Möglichkeit, das zu prüfen, ist die Eigenschaft Success. Groups [-1].Success wird den Wert false zurückgeben.
3.9 Teile des übereinstimmenden Texts auslesen | 157
Um herauszubekommen, wie viele einfangende Gruppen es gibt, schauen Sie sich Match.Groups.Count an. Die Eigenschaft Count folgt den gleichen Konventionen wie die Eigenschaft Count aller anderen Collection-Objekte in .NET: Sie gibt die Anzahl an Elementen in der Collection zurück – also den größten zulässigen Index plus eins. In unserem Beispiel gibt es in der Collection Groups die Elemente Groups[0] und Groups[1].Groups.Count ergibt damit 2.
Java Um den von einer einfangenden Gruppe gefundenen Text oder die Details der Übereinstimmung zu ermitteln, braucht man praktisch den gleichen Code wie für das gesamte Suchergebnis, der in den vorhergehenden beiden Rezepten zu finden ist. Die Methoden group(), start() und end() der Klasse Matcher besitzen alle einen optionalen Parameter. Ohne diesen Parameter oder mit dem Wert null erhalten Sie die Übereinstimmung beziehungsweise die Positionen des gesamten Suchergebnisses. Wenn Sie eine positive Zahl übergeben, erhalten Sie die Details der einfangenden Gruppe. Gruppen werden mit eins beginnend nummeriert, so wie die Rückwärtsreferenzen im regulären Ausdruck selbst. Geben Sie eine Zahl an, die größer ist als die Anzahl der einfangenden Gruppen in Ihrem regulären Ausdruck, werfen diese drei Funktionen eine IndexOutOfBoundsException. Wenn die einfangende Gruppe vorhanden ist, es aber keine Übereinstimmung für sie gibt, liefert group(n) den Wert null, während start(n) und end(n) als Ergebnis -1 ausgeben.
JavaScript Wie im vorhergehenden Rezept beschrieben, liefert die Methode exec() eines regulären Ausdrucks ein Array mit den Details über das Suchergebnis zurück. In Element null steht der gesamte Suchausdruck. Element eins enthält den Text, der durch die erste einfangende Gruppe gefunden wurde, Element zwei den der zweiten Gruppe und so weiter. Wenn ein regulärer Ausdruck gar nichts findet, liefert regexp.exec() den Wert null zurück.
PHP In Rezept 3.7 wird beschrieben, wie Sie den vom regulären Ausdruck gefundenen Text erhalten können, indem Sie preg_match() einen dritten Parameter übergeben. Wenn preg_match() den Wert 1 zurückgibt, wird der Parameter mit einem Array gefüllt. Das nullte Element enthält einen String mit dem gesamten Suchergebnis. Das erste Element enthält den Text der ersten einfangenden Gruppe, das zweite den Text der zweiten Gruppe und so weiter. Die Länge des Arrays entspricht der Anzahl der einfangenden Gruppen plus eins. Array-Indexe entsprechen den Rückwärtsverweisnummern im regulären Ausdruck.
158 | Kapitel 3: Mit regulären Ausdrücken programmieren
Wenn Sie die Konstante PREG_OFFSET_CAPTURE als vierten Parameter angeben, wie es in obigem Rezept beschrieben wurde, entspricht die Länge des Arrays immer noch der Anzahl der einfangenden Gruppen plus eins. Aber statt an jedem Index einen String zu enthalten, findet sich dort ein Unterarray mit zwei Elementen. Das nullte Unterelement ist der String mit dem von der Regex oder einfangenden Gruppe gefundenen Text. Das erste Unterelement ist eine Integer-Zahl, die die Position des gefundenen Texts im Ausgangstext angibt.
Perl Wenn der Operator m// eine Übereinstimmung findet, wird eine Reihe von speziellen Variablen gesetzt. Dazu gehören auch die nummerierten Variablen $1, $2, $3 und so weiter, die den Teil des Strings enthalten, der von der entsprechenden einfangenden Gruppe im regulären Ausdruck gefunden wurden.
Python Die Lösung für dieses Problem ist nahezu identisch mit der aus Rezept 3.7. Statt group() ohne Parameter aufzurufen, geben wir die Nummer der einfangenden Gruppe an, an der wir interessiert sind. Mit group(1) erhalten Sie den Text, der durch die erste einfangende Gruppe gefunden wurde, mit group(2) den der zweiten Gruppe und so weiter. Python unterstützt bis zu 99 einfangende Gruppen. Die Gruppe mit der Nummer 0 enthält das gesamte Suchergebnis. Übergeben Sie eine Zahl, die größer als die Anzahl der einfangenden Gruppen ist, wirft group() eine IndexError-Exception. Wenn die Gruppennummer gültig ist, die Gruppe aber an der Regex-Übereinstimmung nicht beteiligt war, liefert group() den Wert None. Sie können group() mehrere Gruppennummern übergeben, um den von mehreren einfangenden Gruppen gefundenen Text mit einem Aufruf zu erhalten. Das Ergebnis ist dann eine Liste mit Strings. Wenn Sie ein Tupel mit den von allen einfangenden Gruppen gefundenen Texten erhalten wollen, können Sie die Methode groups() von MatchObject aufrufen. Das Tupel enthält für Gruppen, die an der Übereinstimmung nicht beteiligt sind, den Wert None. Wenn Sie groups() einen Parameter mitgeben, wird für Gruppen ohne Suchergebnis dieser Wert anstelle von None genommen. Wenn Sie ein Dictionary statt eines Tupels haben wollen, rufen Sie groupdict() statt groups() auf. Sie können auch hier einen Parameter übergeben, dessen Inhalt anstelle von None für Gruppen genommen wird, die kein Suchergebnis enthalten.
Ruby In Rezept 3.8 werden die Variable $~ und das Objekt MatchData beschrieben. In einem ArrayKontext liefert dieses Objekt ein Array mit den Texten, die durch die einfangenden Gruppen gefunden wurden. Die Gruppen werden dabei wie die Rückwärtsreferenzen mit 1 beginnend nummeriert, während sich in Element 0 im Array das gesamte Suchergebnis befindet.
3.9 Teile des übereinstimmenden Texts auslesen | 159
$1, $2 und folgende sind spezielle, nur lesbare Variablen. $1 ist eine Kurzform von $~[1], in dem sich der von der ersten einfangenden Gruppe gefundene Text befindet. $2 liefert
den Text aus der zweiten Gruppe und so weiter.
Benannte Captures Wenn Ihr regulärer Ausdruck benannte einfangende Gruppen nutzt, können Sie den Namen der Gruppe verwenden, um den von ihr gefundenen Text in Ihrem Code zu verwenden.
C# Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie den statischen Aufruf verwenden: string resultString = Regex.Match(subjectString, "http://(?<domain>[a-z0-9.-]+)").Groups["domain"].Value;
Um die gleiche Regex mehrfach einzusetzen, erstellen Sie ein Regex-Objekt: Regex regexObj = new Regex("http://(?<domain>[a-z0-9.-]+)"); string resultString = regexObj.Match(subjectString).Groups["domain"].Value;
In C# sieht der Code für das Ermitteln des Group-Objekts einer benannten Gruppe und einer nummerierten Gruppe gar nicht so unterschiedlich aus. Statt die Groups-Collection mit einem Integer-Wert als Index anzusprechen, nutzen Sie einen String. Auch dann wird .NET keine Exception werfen, wenn die Gruppe nicht existiert. Match.Groups ["keinegruppe"].Success liefert in diesem Fall schlicht false zurück.
VB.NET Für schnelle Tests, die nur einmal durchgeführt werden sollen, können Sie den statischen Aufruf verwenden: Dim ResultString = Regex.Match(SubjectString, "http://(?<domain>[a-z0-9.-]+)").Groups("domain").Value
Um die gleiche Regex mehrfach einzusetzen, erstellen Sie ein Regex-Objekt: Dim RegexObj As New Regex("http://(?<domain>[a-z0-9.-]+)") Dim ResultString = RegexObj.Match(SubjectString).Groups("domain").Value
In VB.NET fällt der Code für das Ermitteln des Group-Objekts einer benannten Gruppe und einer nummerierten Gruppe gar nicht so unterschiedlich aus. Statt die GroupsCollection mit einem Integer-Wert als Index anzusprechen, nutzen Sie einen String. Auch dann wird .NET keine Exception werfen, wenn die Gruppe nicht existiert. Match.Groups("keinegruppe").Success liefert in diesem Fall schlicht False zurück.
160 | Kapitel 3: Mit regulären Ausdrücken programmieren
Wenn Ihr regulärer Ausdruck benannte einfangende Gruppen besitzt, ist das $groups zugewiesene Array ein assoziatives Array. Der Text, der von jeder benannten einfangenden Gruppe gefunden wird, steht im Array zwei Mal zur Verfügung. Sie können den Text auslesen, indem Sie das Array entweder über die Gruppennummer oder über den Gruppennamen ansprechen. Im Codebeispiel speichert $groups[0] das gesamte Suchergebnis der Regex, während sowohl $groups[1] als auch $groups['domain'] den Text enthalten, der von der einen einfangenden Gruppe gefunden wurde, die im regulären Ausdruck enthalten ist.
Perl unterstützt benannte einfangende Gruppen seit Version 5.10. Der Hash $+ enthält den Text, der von allen einfangenden Gruppen gefunden wurde. Perl nummeriert benannte Gruppen zusammen mit den nummerierten Gruppen durch. In diesem Beispiel findet sich sowohl in $1 als auch in $+{'domain'} der Text, der von der einen einfangenden Gruppe gefunden wurde, die im regulären Ausdruck enthalten ist.
Python matchobj = re.search("http://(?P<domain>[a-z0-9.-]+)", subject) if matchobj: result = matchobj.group("domain") else: result = ""
Wenn Ihr regulärer Ausdruck benannte Gruppen besitzt, können Sie der Methode group() statt der Nummer auch den Gruppennamen übergeben.
3.9 Teile des übereinstimmenden Texts auslesen | 161
3.10 Eine Liste aller Übereinstimmungen erhalten Problem Alle bisherigen Rezepte in diesem Kapitel drehen sich nur darum, die erste Übereinstimmung zu finden, die ein regulärer Ausdruck im Text ermittelt. Aber in vielen Fällen kann ein regulärer Ausdruck, der einen String nur teilweise abdeckt, auch noch eine weitere Übereinstimmung im restlichen Text ermitteln ... und vielleicht noch eine dritte und so weiter. So kann zum Beispiel die Regex ‹\d+› sechs Übereinstimmungen im Text Die Gewinnzahlen sind 7, 13, 16, 42, 65 und 99 finden: 7, 13, 16, 42, 65 und 99. Sie wollen die Liste aller Substrings ermitteln, die der reguläre Ausdruck findet, wenn er nach jeder Übereinstimmung erneut auf den Rest des Strings angewendet wird.
Lösung C# Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck verarbeiten: MatchCollection matchlist = Regex.Matches(subjectString, @"\d+");
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck mit einer großen Zahl an Strings verarbeiten wollen: Regex regexObj = new Regex(@"\d+"); MatchCollection matchlist = regexObj.Matches(subjectString);
VB.NET Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck verarbeiten: Dim matchlist = Regex.Matches(SubjectString, "\d+")
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck mit einer großen Zahl an Strings verarbeiten wollen: Dim RegexObj As New Regex("\d+") Dim MatchList = RegexObj.Matches(SubjectString)
Java List<String> resultList = new ArrayList<String>(); Pattern regex = Pattern.compile("\\d+"); Matcher regexMatcher = regex.matcher(subjectString); while (regexMatcher.find()) { resultList.add(regexMatcher.group()); }
162 | Kapitel 3: Mit regulären Ausdrücken programmieren
Das funktioniert nur bei regulären Ausdrücken, die keine einfangenden Gruppen enthalten. Greifen Sie daher auf nicht-einfangende Gruppen zurück. Details dazu finden Sie in Rezept 2.9.
Python Wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck verarbeiten wollen, können Sie die globale Funktion nutzen: result = re.findall(r"\d+", subject)
Um die gleiche Regex mehrfach zu verwenden, nutzen Sie ein kompiliertes Objekt: reobj = re.compile(r"\d+") result = reobj.findall(subject)
Ruby result = subject.scan(/\d+/)
Diskussion .NET Die Methode Matches() der Klasse Regex wendet den regulären Ausdruck wiederholt auf den String an, bis alle Übereinstimmungen gefunden wurden. Sie liefert ein Objekt vom Typ MatchCollection zurück, in der sich alle Übereinstimmungen finden. Der Ausgangstext ist immer der erste Parameter. In diesem String versucht der reguläre Ausdruck, eine Übereinstimmung zu finden. Dieser Parameter darf nicht null sein, da Matches() sonst eine ArgumentNullException wirft. Wenn Sie die Regex-Treffer nur in ein paar wenigen Strings erhalten wollen, können Sie die statische Version von Matches() nutzen. Übergeben Sie Ihren Text als ersten und den regulären Ausdruck als zweiten Parameter. Optionen für den regulären Ausdruck können Sie als optionalen dritten Parameter mitgeben. Wenn Sie viele Strings verarbeiten, erstellen Sie zunächst ein Regex-Objekt und rufen dann dafür Matches() auf. Der Ausgangstext ist dabei der einzige notwendige Parameter. 3.10 Eine Liste aller Übereinstimmungen erhalten | 163
Mit einem optionalen zweiten Parameter können Sie festlegen, ab welcher Zeichenposition der reguläre Ausdruck seine Überprüfung beginnen soll. Im Prinzip handelt es sich bei diesem Wert um die Anzahl der Zeichen, die der reguläre Ausdruck am Anfang des Ausgangstexts ignorieren soll. Das kann nützlich sein, wenn Sie den String schon bis zu einer gewissen Position verarbeitet haben und nun noch den Rest durchsuchen wollen. Wenn Sie diese Zahl angeben, muss sie größer oder gleich null und kleiner oder gleich der Länge des Strings sein. Ansonsten wirft IsMatch() eine ArgumentOutOfRangeException. Die statische Version der Methode ermöglicht keine Angabe, ab welcher Position die Regex ihre Suche beginnen soll. Es gibt auch keine Version von Matches(), der Sie sagen können, wo die Suche vor Ende des Strings abbrechen soll. Wenn Sie das wollen, können Sie Regex.Match("Ausgangstext", start, stop) in einer Schleife aufrufen, wie dies im nächsten Rezept gezeigt wird, und alle Übereinstimmungen selbst in einer Liste sammeln.
Java Java bietet keine Funktion an, mit der Sie eine Liste von Übereinstimmungen erhalten können. Aber das lässt sich leicht mit eigenem Code erreichen, indem man Rezept 3.7 anpasst. Anstatt find() in einer if-Anweisung aufzurufen, nutzen Sie eine while-Schleife. Für die im Beispiel verwendeten Klassen List und ArrayList müssen Sie an den Anfang Ihres Codes import java.util.*; setzen.
JavaScript Dieser Code ruft genauso wie in Rezept 3.7 die Funktion string.match() auf. Es gibt jedoch einen kleinen, aber entscheidenden Unterschied – die Option /g. Regex-Optionen werden in Rezept 3.4 beschrieben. Die Option /g teilt der Funktion match() mit, über alle Übereinstimmungen im String zu iterieren und sie in einem Array abzulegen. Im Codebeispiel enthält list[0] dann die erste Regex-Übereinstimmung, list[1] die zweite und so weiter. Über list.length bekommen Sie heraus, wie viele Übereinstimmungen es gibt. Wenn es gar keine Übereinstimmungen gab, liefert string.match wie üblich null zurück. Die Elemente im Array sind Strings. Wenn Sie eine Regex mit der Option /g verwenden, liefert string.match() keine weiteren Details zu den Übereinstimmungen. Benötigen Sie weitere Informationen, müssen Sie, wie in Rezept 3.11 beschrieben, über die einzelnen Ergebnisse iterieren.
PHP In allen bisherigen Rezepten wurde bei PHP die Funktion preg_match() genutzt, die die erste Regex-Übereinstimmung in einem String findet. preg_match_all() ist eine sehr ähnliche Funktion. Der Hauptunterschied ist, dass sie alle Übereinstimmungen findet. Sie liefert eine Integer-Zahl zurück, die angibt, wie oft die Regex gefunden werden konnte.
164 | Kapitel 3: Mit regulären Ausdrücken programmieren
Die ersten drei Parameter von preg_match_all() sind die gleichen wie die ersten drei von preg_match(): ein String mit Ihrem regulären Ausdruck, der zu durchsuchende String und eine Variable, in der ein Array mit den Ergebnissen abgelegt wird. Nur ist dieses Mal der dritte Parameter nicht optional, und das Array ist immer mehrdimensional. Für den vierten Parameter geben Sie entweder die Konstante PREG_PATTERN_ORDER oder PREG_SET_ORDER an. Wenn Sie den Parameter weglassen, wird PREG_PATTERN_ORDER als Standardwert genutzt. Mit PREG_PATTERN_ORDER erhalten Sie ein Array, in dem sich die Details des gesamten Suchergebnisses im nullten Element befinden, während die Details der einfangenden Gruppen in den darauffolgenden Elementen zu finden sind. Die Länge des Arrays entspricht der Anzahl der einfangenden Gruppen plus eins. Das ist die gleiche Reihenfolge, die bei preg_match() genutzt wird. Der Unterschied liegt darin, dass jedes Element nun nicht nur einen String mit dem Suchergebnis enthält – wie es bei preg_match() der Fall ist –, sondern ein Unterarray mit allen Übereinstimmungen, die von preg_match_all() gefunden wurden. Die Länge jedes Unterarrays entspricht dem Wert, der von preg_match_all() zurückgegeben wurde. Um eine Liste aller Regex-Übereinstimmungen im String zu erhalten, ohne sich für die Texte zu interessieren, die von einfangenden Gruppen gefunden wurden, geben Sie PREG_PATTERN_ORDER an und nutzen das nullte Element im Array. Wenn Sie nur an den Texten interessiert sind, die von einer bestimmten einfangenden Gruppe gefunden wurden, nutzen Sie PREG_PATTERN_ORDER und die Gruppennummer der einfangenden Gruppe. So erhalten Sie zum Beispiel nach dem Aufruf von preg_match('%http://([a-z0-9.-]+)%', $subject, $result) in $result[1] die Liste der Domainnamen aller URLs in Ihrem Ausgangstext. PREG_SET_ORDER füllt das Array mit den gleichen Strings, aber in einer anderen Kombination. Die Länge des Arrays entspricht dem Wert, der von preg_match_all() zurückgege-
ben wird. Jedes Element in diesem Array ist ein Unterarray, in dem sich an Position null das Suchergebnis befindet und die Ergebnisse der einfangenden Gruppen an darauffolgenden Positionen. Wenn Sie PREG_SET_ORDER angeben, enthält $result[0] das gleiche Array wie bei einem Aufruf von preg_match(). Sie können PREG_OFFSET_CAPTURE mit PREG_PATTERN_ORDER oder PREG_SET_ORDER kombinieren. Das hat den gleichen Effekt wie die Übergabe von PREG_OFFSET_CAPTURE als vierten Parameter an preg_match(). Statt in den Elementen des Arrays Strings abzulegen, finden sich dort Unterarrays mit zwei Elementen – dem String und der Position, an der der String im Ausgangstext vorkommt.
Perl In Rezept 3.4 wird erklärt, dass Sie Ihre Regex um den Modifikator /g ergänzen müssen, um mehr als eine Übereinstimmung im Ausgangstext zu finden. Wenn Sie eine globale Regex in einem Listenkontext verwenden, wird sie alle Übereinstimmungen finden und zurückliefern. In diesem Rezept sorgt die List-Variable auf der linken Seite des Zuweisungsoperators für den entsprechenden Kontext. 3.10 Eine Liste aller Übereinstimmungen erhalten | 165
Wenn der reguläre Ausdruck keine einfangenden Gruppen enthält, wird die Liste alle Suchergebnisse enthalten. Sind aber einfangende Gruppen vorhanden, wird die Liste die von allen einfangenden Gruppen gefundenen Texte für alle Regex-Übereinstimmungen enthalten. Das gesamte Suchergebnis ist dann nicht enthalten, sofern Sie nicht noch eine einfangende Gruppe um die gesamte Regex legen. Möchten Sie nur eine Liste aller Suchergebnisse bekommen, ersetzen Sie alle einfangenden durch nicht-einfangende Gruppen. In Rezept 2.9 werden beide Gruppenarten beschrieben.
Python Die Funktion findall() im Modul re durchsucht wiederholt einen String, um alle Übereinstimmungen zum regulären Ausdruck zu finden. Übergeben Sie Ihren regulären Ausdruck als ersten und den Ausgangstext als zweiten Parameter. Optionen für den regulären Ausdruck können Sie optional im dritten Parameter mitliefern. Die Funktion re.findall() ruft re.compile() und dann die Methode findall() für das kompilierte Regex-Objekt auf. Diese Methode hat nur einen notwendigen Parameter – den Ausgangstext. Die Methode findall() besitzt zwei optionale Parameter, die von der globalen Funktion re.findall() nicht unterstützt werden. Nach dem Ausgangstext können Sie die Zeichenposition im String angeben, ab der findall() mit ihrer Suche beginnen soll. Lassen Sie diesen Parameter weg, verarbeitet findall() den gesamten Text. Wenn Sie eine Startposition angeben, können Sie auch eine Endposition festlegen. Geben Sie keine Endposition mit, wird die Suche bis zum Ende des Strings durchgeführt. Egal wie Sie findall() aufrufen – das Ergebnis ist immer eine Liste mit allen Übereinstimmungen, die gefunden wurden. Wenn die Regex keine einfangenden Gruppen besitzt, erhalten Sie eine Liste mit Strings. Sind welche vorhanden, bekommen Sie eine Liste mit Tupeln, in denen sich der Text aller einfangenden Gruppen für jede Regex-Übereinstimmung findet.
Ruby Die Methode scan() der Klasse String erwartet einen regulären Ausdruck als einzigen Parameter. Sie iteriert über alle Regex-Übereinstimmungen im String. Ruft man sie ohne einen Block auf, liefert scan() ein Array mit allen Regex-Übereinstimmungen zurück. Wenn Ihr regulärer Ausdruck keine einfangenden Gruppen besitzt, gibt scan() ein StringArray zurück. In diesem Array gibt es für jede Regex-Übereinstimmung ein Element mit dem gefundenen Text. Gibt es einfangende Gruppen, liefert scan() ein Array aus Arrays zurück. Das Array enthält ein Element für jede Regex-Übereinstimmung. Jedes dieser Elemente ist ein Array mit den bei jeder Regex-Übereinstimmung gefundenen Texten. Unterelement null enthält den Text, der von der ersten einfangenden Gruppe gefunden wurde, Unterelement
166 | Kapitel 3: Mit regulären Ausdrücken programmieren
eins enthält den Text der zweiten einfangenden Gruppe und so weiter. Das gesamte Suchergebnis ist nicht im Array enthalten. Wenn Sie auch dieses brauchen, müssen Sie Ihren gesamten regulären Ausdruck mit einer zusätzlichen einfangenden Gruppe umschließen. Ruby bietet keine Möglichkeit an, sich durch scan() ein Array mit Strings zurückgeben zu lassen, wenn die Regex einfangende Gruppen besitzt. Sie können dann nur alle benannten und nummerierten Gruppen durch nicht-einfangende Gruppen ersetzen.
Siehe auch Rezepte 3.7, 3.11 und 3.12.
3.11 Durch alle Übereinstimmungen iterieren Problem Das vorhergehende Rezept hat gezeigt, wie eine Regex wiederholt auf einen String angewendet werden kann, um eine Liste mit Übereinstimmungen zu erhalten. Jetzt wollen Sie über alle diese Übereinstimmungen in Ihrem eigenen Code iterieren.
Lösung C# Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck verarbeiten: Match matchResult = Regex.Match(subjectString, @"\d+"); while (matchResult.Success) { // Hier können Sie die in matchResult abgelegten Übereinstimmungen bearbeiten matchResult = matchResult.NextMatch(); }
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck mit einer großen Zahl an Strings verarbeiten wollen: Regex regexObj = new Regex(@"\d+"); matchResult = regexObj.Match(subjectString); while (matchResult.Success) { // Hier können Sie die in matchResult abgelegten Übereinstimmungen bearbeiten matchResult = matchResult.NextMatch(); }
3.11 Durch alle Übereinstimmungen iterieren | 167
VB.NET Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck verarbeiten: Dim MatchResult = Regex.Match(SubjectString, "\d+") While MatchResult.Success 'Hier können Sie die in MatchResult abgelegten Übereinstimmungen bearbeiten MatchResult = MatchResult.NextMatch End While
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck mit einer großen Zahl an Strings verarbeiten wollen: Dim RegexObj As New Regex("\d+") Dim MatchResult = RegexObj.Match(SubjectString) While MatchResult.Success 'Hier können Sie die in MatchResult abgelegten Übereinstimmungen bearbeiten MatchResult = MatchResult.NextMatch End While
Java Pattern regex = Pattern.compile("\\d+"); Matcher regexMatcher = regex.matcher(subjectString); while (regexMatcher.find()) { // Hier können Sie die in regexMatcher abgelegten Übereinstimmungen bearbeiten }
JavaScript Wenn Ihr regulärer Ausdruck eine Übereinstimmung der Länge null enthalten kann oder Sie sich da einfach nicht sicher sind, sollten Sie nach Möglichkeit versuchen, Probleme zwischen den verschiedenen Browsern bezüglich solcher Übereinstimmungen und exec() zu umgehen: var regex = /\d+/g; var match = null; while (match = regex.exec(subject)) { // Browser wie Firefox sollen nicht in einer Endlosschleife festsitzen if (match.index == regex.lastIndex) regex.lastIndex++; // Hier können Sie die in match abgelegten Übereinstimmungen bearbeiten }
Wenn Sie sicher sind, dass Ihre Regex niemals eine Übereinstimmung der Länge null finden wird, können Sie direkt über die Regex iterieren: var regex = /\d+/g; var match = null; while (match = regex.exec(subject)) { // Hier können Sie die in match abgelegten Übereinstimmungen bearbeiten }
168 | Kapitel 3: Mit regulären Ausdrücken programmieren
PHP preg_match_all('/\d+/', $subject, $result, PREG_PATTERN_ORDER); for ($i = 0; $i < count($result[0]); $i++) { # Gefundener Text = $result[0][$i]; }
Perl while ($subject =~ m/\d+/g) { # Gefundener Text = $& }
Python Wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck verarbeiten wollen, können Sie die globale Funktion nutzen: for matchobj in re.finditer(r"\d+", subject): # Hier können Sie die in matchobj abgelegten Übereinstimmungen bearbeiten
Um die gleiche Regex mehrfach zu verwenden, nutzen Sie ein kompiliertes Objekt: reobj = re.compile(r"\d+") for matchobj in reobj.finditer(subject): # Hier können Sie die in matchobj abgelegten Übereinstimmungen bearbeiten
Ruby subject.scan(/\d+/) {|match| # Hier können Sie die in match abgelegten Übereinstimmungen bearbeiten }
Diskussion .NET In Rezept 3.7 wird beschrieben, wie man die Member-Funktion Match() der Klasse Regex nutzt, um die erste Regex-Übereinstimmung im String zu finden. Um nach und nach alle Übereinstimmungen im String zu erhalten, rufen wir wieder die Funktion Match() auf, um die Details der ersten Übereinstimmung zu ermitteln. Diese Funktion liefert eine Instanz der Klasse Match zurück, die wir in der Variablen matchResult ablegen. Wenn die Eigenschaft Success des Objekts matchResult den Wert true besitzt, können wir mit der Schleife beginnen. Am Anfang der Schleife können Sie die Eigenschaften der Klasse Match verwenden, um die Details der ersten Übereinstimmung zu nutzen. In Rezept 3.7 wird die Eigenschaft Value beschrieben, in Rezept 3.8 die Eigenschaften Index und Length und in Rezept 3.9 die Collection Groups.
3.11 Durch alle Übereinstimmungen iterieren | 169
Wenn Sie mit der ersten Übereinstimmung fertig sind, rufen Sie die Member-Funktion NextMatch() der Variablen matchResult auf. Match.NextMatch() liefert eine Instanz der Klasse Match zurück – so wie es auch Regex.Match() tut. Die neue Instanz enthält die Details der zweiten Übereinstimmung. Durch das Zuweisen des Ergebnisses von matchResult.NextMatch() an die gleiche Variable matchResult ist es einfach, über alle Übereinstimmungen zu iterieren. Wir müssen immer nur matchResult.Success prüfen, um herauszufinden, ob NextMatch() tatsächlich noch eine Übereinstimmung gefunden hat. Wenn NextMatch() fehlschlägt, gibt es dennoch ein Match-Objekt zurück, aber dessen Eigenschaft Success ist dann auf false gesetzt. Indem Sie nur eine Variable matchResult nutzen, kann der erste Test und der Test nach dem Aufruf von NextMatch() in einer einzelnen while-Anweisung kombiniert werden. Der Aufruf von NextMatch() macht das Match-Objekt, für das Sie es aufgerufen haben, nicht ungültig. Wenn Sie möchten, können Sie das komplette Match-Objekt für jede Übereinstimmung aufheben. Der Methode NextMatch() können keine weitere Parameter übergeben werden. Sie nutzt den gleichen regulären Ausdruck und den gleichen Ausgangstext, den Sie der Methode Regex.Match() übergeben haben. Das Match-Objekt hält die Verweise auf Ihren regulären Ausdruck und Ausgangstext. Sie können die statische Version von Regex.Match() nutzen, auch wenn Ihr Ausgangstext eine große Zahl von Übereinstimmungen enthält. Regex.Match() kompiliert Ihren regulären Ausdruck einmal, und das zurückgegebene Match-Objekt merkt sich eine Referenz auf diese kompilierte Regex. Match.MatchAgain() verwendet den vorher kompilierten regulären Ausdruck, der vom Match-Objekt referenziert wurde, auch wenn Sie den statischen Aufruf von Regex.Match() genutzt haben. Sie müssen die Klasse Regex nur dann instantiieren, wenn Sie Regex.Match() wiederholt aufrufen wollen, also die gleiche Regex für verschiedene Strings verwenden.
Java Das Iterieren über alle Übereinstimmungen in einem String ist in Java sehr einfach. Rufen Sie die in Rezept 3.7 vorgestellte Methode find() in einer while-Schleife auf. Jeder Aufruf von find() aktualisiert das Matcher-Objekt mit der nächsten Übereinstimmung und der Startposition für den folgenden Versuch.
JavaScript Bevor Sie beginnen, sollten Sie sicherstellen, dass Sie die Option /g gesetzt haben, wenn Sie Ihre Regex in einer Schleife nutzen wollen. Diese Option wird in Rezept 3.4 beschrieben. while (regexp.exec()) findet alle Zahlen im Ausgangstext, wenn regexp = /\d+/g gilt. Ist regexp = /\d+/, findet while (regexp.exec()) nur die erste Zahl – und zwar wieder und wieder, bis Ihr Skript abstürzt oder vom Browser zum Aufgeben gezwungen wird.
170 | Kapitel 3: Mit regulären Ausdrücken programmieren
Beachten Sie, dass while (/\d+/g.exec()) (eine Schleife über eine literale Regex mit /g) ebenfalls in einer Endlosschleife stecken bleiben kann, zumindest bei bestimmten JavaScript-Implementierungen. Denn der reguläre Ausdruck wird mit jeder Iteration der while-Schleife neu kompiliert. Dabei wird auch die Startposition für die Übereinstimmungssuchen auf den Anfang des Strings zurückgesetzt. Weisen Sie daher den regulären Ausdruck außerhalb der Schleife einer Variablen zu, um sicherzustellen, dass er nur einmal kompiliert wird. Die Rezepte 3.8 und 3.9 beschreiben das von regexp.exec() zurückgelieferte Objekt. Hier ist es das gleiche Objekt, auch wenn Sie exec() in einer Schleife verwenden. Sie können mit diesem Objekt machen, was Sie wollen. Einziger Effekt von /g ist das Aktualisieren der Eigenschaft lastIndex des regexp-Objekts, für das Sie exec() aufrufen. Das funktioniert auch, wenn Sie einen literalen regulären Ausdruck verwenden, wie in der zweiten JavaScript-Lösung für dieses Rezept gezeigt wird. Wenn Sie exec() das nächste Mal aufrufen, wird die Übereinstimmungssuche bei lastIndex beginnen. Weisen Sie lastIndex einen neuen Wert zu, wird die Suche dort weitergehen. Es gibt bei lastIndex allerdings ein größeres Problem. Wenn Sie den ECMA-262v3-Standard für JavaScript wörtlich nehmen, sollte exec() den Wert von lastIndex auf das erste Zeichen nach der Übereinstimmung setzen. Wenn die Übereinstimmung die Länge null hat, bedeutet das, dass die nächste Suche an der gerade gefundenen Position starten wird – das führt zu einer Endlosschleife. Alle Regex-Engines, die in diesem Buch behandelt werden (mit Ausnahme von JavaScript), umgehen dieses Problem, indem sie den nächsten Suchvorgang automatisch ein Zeichen weiter beginnen, wenn die vorherige Übereinstimmung die Länge null hat. Das ist der Grund dafür, dass Rezept 3.7 beschreibt, dass Sie mit lastIndex nicht das Ende der Übereinstimmung finden können, weil Sie im Internet Explorer falsche Werte erhalten. Die Firefox-Entwickler waren bei der Implementierung des ECMA-262v3-Standards allerdings gnadenlos, obwohl das bedeutet, dass regexp.exec() damit in einer Endlosschleife landen kann. Und das ist gar nicht so unwahrscheinlich. So können Sie zum Beispiel mit re = /^.*$/gm; while (re.exec()) über alle Zeilen eines mehrzeiligen Strings iterieren – wenn der String Leerzeilen enthält, wird Firefox dort hängen bleiben. Sie können das umgehen, indem Sie lastIndex in Ihrem Code um eins erhöhen, wenn die Funktion exec() das noch nicht selbst getan hat. Die erste JavaScript-Lösung in diesem Rezept zeigt, wie’s geht. Wenn Sie unsicher sind, kopieren Sie diese Codezeile einfach in Ihren Code und verschwenden danach keine Gedanken mehr daran. Bei string.replace() (Rezept 3.14) oder beim Finden aller Übereinstimmungen mit string.match() (Rezept 3.10) gibt es dieses Problem nicht. Sie nutzen lastIndex zwar intern, aber der ECMA-262v3-Standard gibt hier an, dass lastIndex für Übereinstimmungen der Länge null erhöht werden muss.
3.11 Durch alle Übereinstimmungen iterieren | 171
PHP Der Funktion preg_match() kann ein optionaler fünfter Parameter mitgegeben werden. Dieser gibt die Position im String an, an der die Suche beginnen soll. Sie könnten Rezept 3.8 anpassen, indem Sie $matchstart + $matchlength beim zweiten Aufruf von preg_match() als fünften Parameter übergeben, um die zweite Übereinstimmung im String zu finden, und das für die weiteren Übereinstimmungen wiederholen, bis preg_match() den Wert 0 zurückgibt. Rezept 3.18 nutzt diese Methode. Neben dem zusätzlichen Code zum Berechnen der Startposition für jeden Suchvorgang ist ein wiederholtes Aufrufen von preg_match() ineffizient, da es keine Möglichkeit gibt, einen kompilierten regulären Ausdruck in einer Variablen zu speichern. preg_match() muss jedes Mal in seinem Cache nach dem kompilierten regulären Ausdruck suchen, wenn Sie es aufrufen. Einfacher und effizienter ist es, preg_match_all() aufzurufen, wie es schon im vorhergehenden Rezept beschrieben wurde, und über das Array mit den Suchergebnissen zu iterieren.
Perl In Rezept 3.4 ist beschrieben, dass Sie den Modifikator /g nutzen müssen, um mehr als eine Übereinstimmung im Ausgangstext zu finden. Wenn Sie eine globale Regex in einem skalaren Kontext verwenden, wird es die nächste Übereinstimmung suchen. In diesem Rezept sorgt die while-Anweisung für den skalaren Kontext. Alle speziellen Variablen, wie zum Beispiel $& (beschrieben in Rezept 3.7), stehen innerhalb der while-Schleife zur Verfügung.
Python Die Funktion finditer() aus re liefert einen Iterator zurück, den Sie nutzen können, um alle Übereinstimmungen des regulären Ausdrucks zu finden. Übergeben Sie Ihren regulären Ausdruck als ersten und den Ausgangstext als zweiten Parameter. Optionen für den regulären Ausdruck können Sie optional als dritten Parameter übergeben. Die Funktion re.finditer() ruft re.compile() und dann die Methode finditer() für das kompilierte Regex-Objekt auf. Diese Methode hat nur einen Pflichtparameter: den Ausgangstext. Der Methode finditer() können zwei optionale Parameter übergeben werden, die die globale Funktion re.finditer() nicht unterstützt. Nach dem Ausgangstext können Sie die Zeichenposition angeben, an der finditer() mit ihrer Suche beginnen soll. Lassen Sie diesen Parameter weg, wird der Iterator den gesamten Text verarbeiten. Wenn Sie eine Startposition angeben, können Sie auch eine Endposition festlegen. Geben Sie keine Endposition an, läuft die Suche bis zum Ende des Strings.
172 | Kapitel 3: Mit regulären Ausdrücken programmieren
Ruby Die Methode scan() der Klasse String erwartet als einzigen Parameter einen regulären Ausdruck. Dann iteriert sie über alle Übereinstimmungen im String. Wenn sie mit einem Block aufgerufen wird, können Sie jede Übereinstimmung direkt verarbeiten. Enthält Ihr regulärer Ausdruck keine einfangenden Gruppen, geben Sie im Block eine Iterator-Variable an. In dieser Variablen findet sich dann ein String mit dem vom regulären Ausdruck gefundenen Text. Wenn sich in Ihrer Regex eine oder mehrere einfangende Gruppen befinden, geben Sie eine Variable für jede Gruppe an. In der ersten Variablen steht der String mit dem von der ersten Gruppe gefundenen Text, in der zweiten der von der zweiten Gruppe und so weiter. Es wird aber keine Variable mit dem gesamten Suchergebnis gefüllt. Brauchen Sie aber das gesamte Suchergebnis, müssen Sie Ihren kompletten regulären Ausdruck mit einer zusätzlichen einfangenden Gruppe umschließen. subject.scan(/(a)(b)(c)/) {|a, b, c| # a, b und c enthalten den Text, der von den # drei einfangenden Gruppen gefunden wurde }
Geben Sie weniger Variablen an, als es einfangenden Gruppen gibt, werden Sie nur auf die Gruppen zugreifen können, für die Sie Variablen angegeben haben. Wenn Sie mehr Variablen angeben, als Gruppen vorhanden sind, werden die zusätzlichen Variablen auf nil gesetzt. Wenn Sie nur eine Iterator-Variable angeben und Ihre Regex eine oder mehrere einfangende Gruppen besitzt, wird die Variable mit einem Array aus Strings gefüllt. Das Array enthält dann einen String für jede einfangende Gruppe. Gibt es lediglich eine Gruppe, wird das Array auch nur ein Element enthalten: subject.scan(/(a)(b)(c)/) {|abc| # abc[0], abc[1] und abc[2] enthalten den von den drei # einfangenden Gruppen gefundenen Text }
Siehe auch Rezepte 3.7, 3.8, 3.10 und 3.12.
3.12 Übereinstimmungen in prozeduralem Code überprüfen Problem In Rezept 3.10 wird gezeigt, wie Sie eine Liste aller Übereinstimmungen eines regulären Ausdrucks in einem String finden können, wenn die Regex wiederholt auf den jeweils verbleibenden Rest angewendet wird. Jetzt wollen Sie eine Liste mit Übereinstimmungen
3.12 Übereinstimmungen in prozeduralem Code überprüfen | 173
haben, die bestimmte Bedingungen erfüllen sollen. Diese Bedingungen lassen sich aber nicht (einfach) mit einem regulären Ausdruck beschreiben. Wenn Sie zum Beispiel eine Liste mit Gewinnzahlen auslesen, möchten Sie nur diejenigen behalten, die sich (ohne Rest) durch 13 teilen lassen.
Lösung C# Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck verarbeiten: StringCollection resultList = new StringCollection(); Match matchResult = Regex.Match(subjectString, @"\d+"); while (matchResult.Success) { if (int.Parse(matchResult.Value) % 13 == 0) { resultList.Add(matchResult.Value); } matchResult = matchResult.NextMatch(); }
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck mit einer großen Zahl an Strings verarbeiten wollen: StringCollection resultList = new StringCollection(); Regex regexObj = new Regex(@"\d+"); matchResult = regexObj.Match(subjectString); while (matchResult.Success) { if (int.Parse(matchResult.Value) % 13 == 0) { resultList.Add(matchResult.Value); } matchResult = matchResult.NextMatch(); }
VB.NET Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck verarbeiten: Dim ResultList = New StringCollection Dim MatchResult = Regex.Match(SubjectString, "\d+") While MatchResult.Success If Integer.Parse(MatchResult.Value) Mod 13 = 0 Then ResultList.Add(MatchResult.Value) End If MatchResult = MatchResult.NextMatch End While
174 | Kapitel 3: Mit regulären Ausdrücken programmieren
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck mit einer großen Zahl an Strings verarbeiten wollen: Dim ResultList = New StringCollection Dim RegexObj As New Regex("\d+") Dim MatchResult = RegexObj.Match(SubjectString) While MatchResult.Success If Integer.Parse(MatchResult.Value) Mod 13 = 0 Then ResultList.Add(MatchResult.Value) End If MatchResult = MatchResult.NextMatch End While
Java List<String> resultList = new ArrayList<String>(); Pattern regex = Pattern.compile("\\d+"); Matcher regexMatcher = regex.matcher(subjectString); while (regexMatcher.find()) { if (Integer.parseInt(regexMatcher.group()) % 13 == 0) { resultList.add(regexMatcher.group()); } }
JavaScript var list = []; var regex = /\d+/g; var match = null; while (match = regex.exec(subject)) { // Browser wie Firefox sollen nicht in einer Endlosschleife hängen bleiben if (match.index == regex.lastIndex) regex.lastIndex++; // Hier können Sie die in match abgelegten Übereinstimmungen bearbeiten if (match[0] % 13 == 0) { list.push(match[0]); } }
Perl while ($subject =~ m/\d+/g) { if ($& % 13 == 0) { push(@list, $&); } }
3.12 Übereinstimmungen in prozeduralem Code überprüfen | 175
Python Wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck verarbeiten wollen, können Sie die globale Funktion nutzen: list = [] for matchobj in re.finditer(r"\d+", subject): if int(matchobj.group()) % 13 == 0: list.append(matchobj.group())
Um die gleiche Regex mehrfach zu verwenden, nutzen Sie ein kompiliertes Objekt: list = [] reobj = re.compile(r"\d+") for matchobj in reobj.finditer(subject): if int(matchobj.group()) % 13 == 0: list.append(matchobj.group())
Ruby list = [] subject.scan(/\d+/) {|match| list resultList = new ArrayList<String>(); Pattern outerRegex = Pattern.compile("(.*?)", Pattern.DOTALL); Pattern innerRegex = Pattern.compile("\\d+"); Matcher outerMatcher = outerRegex.matcher(subjectString); while (outerMatcher.find()) { Matcher innerMatcher = innerRegex.matcher(outerMatcher.group()); while (innerMatcher.find()) { resultList.add(innerMatcher.group()); } }
Der folgende Code ist effizienter (weil innerMatcher nur einmal erzeugt wird), aber man benötigt mindestens Java 5: List<String> resultList = new ArrayList<String>(); Pattern outerRegex = Pattern.compile("(.*?)", Pattern.DOTALL); Pattern innerRegex = Pattern.compile("\\d+"); Matcher outerMatcher = outerRegex.matcher(subjectString); Matcher innerMatcher = innerRegex.matcher(subjectString); while (outerMatcher.find()) { innerMatcher.region(outerMatcher.start(), outerMatcher.end()); while (innerMatcher.find()) { resultList.add(innerMatcher.group()); } }
JavaScript var result = []; var outerRegex = /([\s\S]*?)/g; var innerRegex = /\d+/g; var outerMatch = null; while (outerMatch = outerRegex.exec(subject)) { if (outerMatch.index == outerRegex.lastIndex) outerRegex.lastIndex++; var innerSubject = subject.substr(outerMatch.index, outerMatch[0].length); var innerMatch = null; while (innerMatch = innerRegex.exec(innerSubject)) { if (innerMatch.index == innerRegex.lastIndex) innerRegex.lastIndex++; result.push(innerMatch[0]); } }
178 | Kapitel 3: Mit regulären Ausdrücken programmieren
Das funktioniert nur, wenn der innere reguläre Ausdruck (in diesem Beispiel ‹\d+›) keine einfangenden Gruppen enthält, daher sollten Sie dort nicht-einfangende Gruppen verwenden. Details dazu finden Sie in Rezept 2.9.
Python list = [] innerre = re.compile(r"\d+") for outermatch in re.finditer("(?s)(.*?)", subject): list.extend(innerre.findall(outermatch.group(1)))
Ruby list = [] subject.scan(/(.*?)/m) {|outergroups| list += outergroups[0].scan(/\d+/) }
Diskussion Reguläre Ausdrücke sind wunderbar dazu geeignet, Eingaben in Tokens aufzuteilen, aber weniger dazu, Eingaben zu parsen. Aufteilen in Tokens bedeutet, verschiedene Teile eines Strings zu erkennen, wie zum Beispiel Zahlen, Wörter, Symbole, Tags, Kommentare und so weiter. Dazu muss der Text von links nach rechts gescannt und unterschiedliche Alternativen und Mengen von Buchstaben gefunden werden. Reguläre Ausdrücke können damit sehr gut umgehen. Parsen bedeutet, die Beziehungen zwischen diesen Tokens herzustellen. So bilden zum Beispiel in einer Programmiersprache die Kombinationen solcher Tokens Anweisungen, Funktionen, Klassen, Namensräume und so weiter. Es ist am einfachsten, das Erfassen der Bedeutung von Tokens innerhalb des größeren Eingabekontexts prozeduralem Code
3.13 Eine Übereinstimmung in einer anderen Übereinstimmung finden | 179
zu überlassen. Insbesondere können reguläre Ausdrücke keine nicht linearen Kontexte verfolgen, wie zum Beispiel bei verschachtelten Konstrukten.1 Das Finden einer Art von Token innerhalb einer anderen Art von Token ist eine Aufgabe, die man gern mit regulären Ausdrücken angeht. Ein Paar HTML-Tags für Fettschrift lässt sich leicht durch den regulären Ausdruck ‹(.*?)› finden.2 Eine Zahl ist noch einfacher durch die Regex ‹\d+› gefunden. Aber wenn Sie versuchen, dies beides zu einer einzelnen Regex zu kombinieren, landen Sie bei etwas deutlich anderem: \d+(?=(?:.(?!).)*)
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Dieser reguläre Ausdruck ist zwar eine Lösung für das Problem dieses Rezepts, aber er ist nicht wirklich intuitiv. Selbst ein Experte für reguläre Ausdrücke wird die Regex sorgfältig analysieren müssen, um herauszufinden, was sie tut, oder ein Tool nutzen, um die Fundstellen hervorzuheben. Und das ist die Kombination aus nur zwei einfachen Regexes. Es ist besser, die beiden regulären Ausdrücke so zu belassen, wie sie sind, und sie nur mit prozeduralem Code zu kombinieren. Der sich daraus ergebende Code ist zwar ein bisschen länger, aber viel einfacher zu verstehen und zu warten. Und schließlich ist ein Hauptgrund für die Verwendung regulärer Ausdrücke ja schließlich, einfachen Code erstellen zu können. Eine Regex wie ‹(.*?)› lässt sich von jedermann leicht verstehen, der ein bisschen Erfahrungen mit regulären Ausdrücken besitzt. Zudem würde man ohne sie viel mehr Zeilen Code brauchen, der sich auch noch schlechter warten ließe. Obwohl die Lösungen für diese Rezepte zu den komplexesten in diesem Kapitel gehören, sind sie doch immer noch recht einfach. Es werden zwei reguläre Ausdrücke verwendet. Der „äußere“ reguläre Ausdruck passt zu den HTML-Bold-Tags, und dem Text zwischen dem Start- und dem End-Tag wird durch die erste einfangende Gruppe erfasst. Dieser reguläre Ausdruck ist mit dem gleichen Code implementiert, der auch in Rezept 3.11 genutzt wurde. Den Unterschied bildet nur der Platzhalter-Kommentar, der nun durch Code ersetzt wurde, der mit dem „inneren“ regulären Ausdruck arbeitet. Der zweite reguläre Ausdruck passt zu einer Ziffer. Diese Regex ist mit dem gleichen Code implementiert, der in Rezept 3.10 vorgestellt wurde. Nur wird dieses Mal der Ausgangstext nicht komplett verarbeitet, sondern die zweite Regex wird auf den Teil des Texts angewandt, der von der ersten einfangenden Gruppe des äußeren regulären Ausdrucks gefunden wurde.
1 Ein paar moderne Regex-Varianten haben versucht, Features für ein balanciertes oder rekursives Finden einzuführen. Diese Features führen aber zu so komplexen regulären Ausdrücken, dass sie nur unsere These unterstützen, das Parsen am besten prozeduralem Code zu überlassen. 2 Damit das Tag auch mehrere Zeilen umfassen kann, müssen Sie den „Punkt passt zu Zeilenumbruch“-Modus aktivieren. In JavaScript verwenden Sie ‹([\s\S]*?)›.
180 | Kapitel 3: Mit regulären Ausdrücken programmieren
Es gibt zwei Möglichkeiten, den inneren regulären Ausdruck auf den Text einzuschränken, der vom äußeren regulären Ausdruck oder einer einfangenden Gruppe des äußeren regulären Ausdrucks gefunden wurde. Manche Sprachen bieten eine Funktion an, mit der der reguläre Ausdruck nur auf einen Teil eines Strings angewandt werden kann. Damit erspart man sich eine zusätzliche String-Kopie, wenn die Match-Funktion nicht sowieso eine Struktur mit dem gefundenen Text aus den einfangenden Gruppen füllt. Wir können auf jeden Fall immer den Substring auslesen, der von der einfangenden Gruppe gefunden wurde, und die innere Regex auf ihn anwenden. Bei beiden Varianten ist es schneller, beide reguläre Ausdrücke zusammen in einer Schleife zu nutzen als den einen regulären Ausdruck mit seinen verschachtelten Lookahead-Gruppen. Bei Letzterem muss die Regex-Engine eine ganze Menge Backtracking vornehmen. Bei großen Dateien wird die Verwendung dieser einen Regex viel langsamer sein, da sie die Abschnittsgrenzen (die HTML-Bold-Tags) bei allen Zahlen im Ausgangstext finden muss – auch bei solchen, die sich gar nicht zwischen -Tags befinden. Die Lösung mit den zwei regulären Ausdrücken sucht erst nach Zahlen, wenn sie die Abschnittsgrenzen gefunden hat. Und das geschieht mit linearem Zeitaufwand.
Siehe auch Rezepte 3.8, 3.10 und 3.11.
3.14 Alle Übereinstimmungen ersetzen Problem Sie wollen alle Übereinstimmungen des regulären Ausdrucks ‹vorher› durch den Text «danach» ersetzen.
Lösung C# Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck bearbeiten wollen: string resultString = Regex.Replace(subjectString, "vorher", "danach");
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie den statischen Aufruf per Exception Handling absichern: string resultString = null; try { resultString = Regex.Replace(subjectString, "vorher", "danach"); } catch (ArgumentNullException ex) { // Regulärer Ausdruck, Ausgangstext oder Ersetzungstext
3.14 Alle Übereinstimmungen ersetzen | 181
// ist null } catch (ArgumentException ex) { // Syntaxfehler im regulären Ausdruck }
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings nutzen wollen: Regex regexObj = new Regex("vorher"); string resultString = regexObj.Replace(subjectString, "danach");
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie die Verwendung des Regex-Objekts mit einem kompletten Exception Handling versehen: string resultString = null; try { Regex regexObj = new Regex("vorher"); try { resultString = regexObj.Replace(subjectString, "danach"); } catch (ArgumentNullException ex) { // Ausgangstext oder Ersetzungstext ist null } } catch (ArgumentException ex) { // Syntaxfehler im regulären Ausdruck }
VB.NET Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck bearbeiten wollen: Dim ResultString = Regex.Replace(SubjectString, "vorher", "danach")
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie den statischen Aufruf per Exception Handling absichern: Dim ResultString As String = Nothing Try ResultString = Regex.Replace(SubjectString, "before", "after") Catch ex As ArgumentNullException 'Regulärer Ausdruck, Ausgangstext oder Ersetzungstext 'ist null Catch ex As ArgumentException 'Syntaxfehler im regulären Ausdruck End Try
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings nutzen wollen: Dim RegexObj As New Regex("vorher") Dim ResultString = RegexObj.Replace(SubjectString, "danach")
182 | Kapitel 3: Mit regulären Ausdrücken programmieren
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie die Verwendung des Regex-Objekts mit einem kompletten Exception Handling versehen: Dim ResultString As String = Nothing Try Dim RegexObj As New Regex("vorher") Try ResultString = RegexObj.Replace(SubjectString, "danach") Catch ex As ArgumentNullException 'Ausgangstext oder Ersetzungstext ist null End Try Catch ex As ArgumentException 'Syntaxfehler im regulären Ausdruck End Try
Java Sie können den statischen Aufruf nutzen, wenn Sie nur einen String mit dem gleichen regulären Ausdruck bearbeiten wollen: String resultString = subjectString.replaceAll("vorher", "danach");
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie den statischen Aufruf per Exception Handling absichern: try { String resultString = subjectString.replaceAll("vorher", "danach"); } catch (PatternSyntaxException ex) { // Syntaxfehler im regulären Ausdruck } catch (IllegalArgumentException ex) { // Syntaxfehler im Ersetzungstext (unmaskierte $-Zeichen?) } catch (IndexOutOfBoundsException ex) { // Nicht vorhandene Rückwärtsreferenzen im Ersetzungstext verwendet }
Erstellen Sie ein Matcher-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings verwenden wollen: Pattern regex = Pattern.compile("vorher"); Matcher regexMatcher = regex.matcher(subjectString); String resultString = regexMatcher.replaceAll("danach");
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie die Verwendung der Pattern- und Matcher-Objekte mit einem kompletten Exception Handling versehen: String resultString = null; try { Pattern regex = Pattern.compile("vorher"); Matcher regexMatcher = regex.matcher(subjectString); try { resultString = regexMatcher.replaceAll("danach"); } catch (IllegalArgumentException ex) { // Syntaxfehler im Ersetzungstext (unmaskierte $-Zeichen?) } catch (IndexOutOfBoundsException ex) { // Nicht vorhandene Rückwärtsreferenzen im Ersetzungstext verwendet
Perl Befindet sich der Ausgangstext in der speziellen Variablen $_ und kann das Ergebnis in $_ genutzt werden: s/vorher/danach/g;
Befindet sich der Ausgangstext in der Variablen $subject und soll das Ergebnis dort auch wieder abgelegt werden: $subject =~ s/vorher/danach/g;
Befindet sich der Ausgangstext in der Variablen $subject und soll das Ergebnis in $result abgelegt werden: ($result = $subject) =~ s/vorher/danach/g;
Python Wenn Sie nur ein paar Strings verarbeiten müssen, können Sie die globale Funktion verwenden: result = re.sub("vorher", "danach", subject)
Um die gleiche Regex wiederholt zu nutzen, verwenden Sie ein kompiliertes Objekt: reobj = re.compile("vorher") result = reobj.sub("danach", subject)
Ruby result = subject.gsub(/vorher/, 'danach')
Diskussion .NET In .NET werden Sie immer die Methode Regex.Replace() nutzen, um mithilfe eines regulären Ausdrucks zu suchen und zu ersetzen. Die Methode Replace() ist zehnfach überladen. Die Hälfte – die hier besprochenen Versionen – erwartet einen String als
184 | Kapitel 3: Mit regulären Ausdrücken programmieren
Ersetzungstext. Die andere Hälfte erwartet ein MatchEvaluator-Delegate als Ersetzungsobjekt. Diese werden in Rezept 3.16 besprochen. Der erste Parameter, der an Replace() übergeben werden muss, ist immer der String mit dem Ausgangstext, in dem Sie suchen und ersetzen wollen. Dieser Parameter sollte niemals null sein. Ansonsten wird Replace() eine ArgumentNullException werfen. Der Rückgabewert von Replace() ist immer der String mit den angewandten Ersetzungen. Wenn Sie den regulären Ausdruck nur ein paar Mal nutzen wollen, können Sie einen statischen Aufruf einsetzen. Der zweite Parameter ist dann der reguläre Ausdruck, der anzuwenden ist. Der Ersetzungstext wird als dritter Parameter übergeben. Optionen für die Regex lassen sich als optionaler vierter Parameter definieren. Wenn Ihr regulärer Ausdruck einen Syntaxfehler enthält, wird eine ArgumentException geworfen. Möchten Sie den gleichen regulären Ausdruck auf viele Strings anwenden, können Sie Ihren Code effizienter machen, indem Sie zunächst ein Regex-Objekt erstellen und dann für dieses Objekt Replace() aufrufen. Übergeben Sie dabei den Ausgangstext als ersten und den Ersetzungstext als zweiten Parameter. Das sind die einzigen erforderlichen Parameter. Wenn Sie Replace() für eine Instanz der Regex-Klasse aufrufen, können Sie zusätzliche Parameter übergeben, um den Bereich zum Suchen und Ersetzen einzuschränken. Lassen Sie diese Parameter weg, werden alle Übereinstimmungen des regulären Ausdrucks im Ausgangstext ersetzt. Die statisch überladenen Versionen von Replace() ermöglichen diese zusätzlichen Parameter nicht, sie ersetzen immer alle Übereinstimmungen. Als optionalen dritten Parameter nach dem Text und dem Ersetzungstext können Sie die Anzahl der vorzunehmenden Ersetzungen angeben. Wenn Sie eine Zahl größer als eins übergeben, ist das die maximale Anzahl an zu ersetzenden Übereinstimmungen. So ersetzt zum Beispiel Replace(subject, replacement, 3) nur die ersten drei Übereinstimmungen der Regex. Weitere Übereinstimmungen werden ignoriert. Wenn es weniger als drei mögliche Übereinstimmungen im String gibt, werden alle Übereinstimmungen ersetzt. Sie erhalten aber keine Information darüber, dass weniger ersetzt wurde, als Sie angefordert haben. Wenn Sie als dritten Parameter den Wert 0 übergeben, werden keinerlei Ersetzungen vorgenommen, und der Ausgangstext wird unverändert zurückgegeben. Geben Sie -1 an, werden alle Regex-Übereinstimmungen ersetzt. Eine Zahl kleiner als -1 führt dazu, dass Replace() eine ArgumentOutOfRangeException wirft. Geben Sie über den dritten Parameter die Anzahl der Ersetzungen an, können Sie einen optionalen vierten Parameter festlegen, der bestimmt, an welcher Zeichenposition mit der Suche begonnen werden soll. Im Prinzip ist das die Anzahl der Zeichen am Anfang des Strings, die der reguläre Ausdruck ignorieren soll. Das kann nützlich sein, wenn Sie den String schon bis zu einer bestimmten Position verarbeitet haben und nun nur noch den Rest des Strings durchsuchen und ersetzen lassen wollen. Geben Sie diese Zahl an, muss sie zwischen null und der Länge des Ausgangstexts liegen. Ansonsten wirft Replace() eine ArgumentOutOfRangeException. Anders als Match() ermöglicht Replace() nicht die Angabe eines Parameters, der die Länge des zu durchsuchenden Substrings definiert.
3.14 Alle Übereinstimmungen ersetzen | 185
Java Wenn Sie nur einen String mit einer Regex durchsuchen und ersetzen lassen wollen, können Sie entweder die Methode replaceFirst() oder replaceAll() für Ihren String direkt aufrufen. Beide Methoden erwarten zwei Parameter: einen String mit Ihrem regulären Ausdruck und einen mit Ihrem Ersetzungstext. Das sind sehr praktische Funktionen, die intern Pattern.compile("vorher").matcher(subjectString).replaceFirst("danach") und Pattern.compile("vorher").matcher(subjectString).replaceAll("danach") aufrufen. Möchten Sie die gleiche Regex für mehrere Strings aufrufen, sollten Sie das MatcherObjekt wie in Rezept 3.3 beschrieben erstellen. Dann rufen Sie für Ihren Matcher replaceFirst() oder replaceAll() auf und übergeben den Ersetzungstext als einzigen Parameter. Es gibt drei verschiedene Exception-Klassen, die Sie berücksichtigen müssen, wenn die Regex und der Ersetzungstext vom Endanwender bereitgestellt werden. Die ExceptionKlasse PatternSyntaxException wird von Pattern.compile(), String.replaceFirst() und String.replaceAll() geworfen, wenn der reguläre Ausdruck einen Syntaxfehler enthält. IllegalArgumentException wird von replaceFirst() und replaceAll() geworfen, wenn es einen Syntaxfehler im Ersetzungstext gibt. Ist der Ersetzungstext syntaktisch korrekt, verweist aber auf eine einfangende Gruppe, die nicht vorhanden ist, wird stattdessen IndexOutOfBoundsException geworfen.
JavaScript Um einen String mit einem regulären Ausdruck zu durchsuchen und Ersetzungen vorzunehmen, rufen Sie für den String die Funktion replace() auf. Übergeben Sie Ihren regulären Ausdruck als ersten und Ihren String mit dem Ersetzungstext als zweiten Parameter. Die Funktion replace() gibt einen neuen String zurück, in dem die Ersetzungen angewendet wurden. Wenn Sie alle Regex-Übereinstimmungen im String ersetzen wollen, setzen Sie die Option /g beim Erzeugen Ihres Regex-Objekts. In Rezept 3.4 wird beschrieben, wie das funktioniert. Nutzen Sie die Option /g nicht, wird nur die erste Übereinstimmung ersetzt.
PHP Sie können in einem String ganz einfach suchen und ersetzen, indem Sie die Funktion preg_replace() aufrufen. Übergeben Sie Ihren regulären Ausdruck als ersten, den Ersetzungstext als zweiten und den Ausgangstext als dritten Parameter. Der zurückgegebene Wert ist ein String mit den angewendeten Ersetzungen. Der optionale vierte Parameter ermöglicht es Ihnen, die Anzahl der Ersetzungen zu beschränken. Lassen Sie den Parameter weg oder geben -1 an, werden alle RegexÜbereinstimmungen ersetzt. Geben Sie 0 an, werden keine Ersetzungen vorgenommen. Nutzen Sie eine positive Zahl, wird preg_replace() nur höchstens so viele Regex-Übereinstimmungen ersetzen, wie Sie angegeben haben. Gibt es weniger Übereinstimmungen, werden alle ohne Fehlermeldung ersetzt.
186 | Kapitel 3: Mit regulären Ausdrücken programmieren
Möchten Sie wissen, wie viele Ersetzungen vorgenommen wurden, können Sie dem Aufruf einen fünften Parameter mitgeben. In dieser Variablen wird eine Integer-Zahl zurückgeliefert, die die Anzahl der tatsächlich vorgenommenen Ersetzungen angibt. Ein besonderes Feature von preg_replace() ist, dass Sie für die ersten drei Parameter auch Arrays statt Strings übergeben können. Übergeben Sie als dritten Parameter ein Array aus Strings statt einen einzelnen String, liefert preg_replace() ein Array mit allen Strings zurück, in denen Ersetzungen vorgenommen wurden. Übergeben Sie als ersten Parameter ein Array mit Regex-Strings, nutzt preg_replace() die regulären Ausdrücke nacheinander, um im Ausgangstext zu suchen und zu ersetzen. Übergeben Sie ein Array mit Ausgangs-Strings, werden alle regulären Ausdrücke auf alle Ausgangs-Strings angewendet. Suchen Sie mit einem Array mit mehreren regulären Ausdrücken, können Sie entweder einen einzelnen String als Ersetzungstext angeben (der dann von allen Regexes genutzt wird) oder ein Array mit Ersetzungs-Strings übergeben. Nutzen Sie zwei Arrays, arbeitet sich preg_replace() sowohl durch das Regex- als auch durch das Ersetzungstext-Array und nutzt für jede Regex einen anderen Ersetzungstext. preg_replace() geht dabei nach der Reihenfolge im Speicher vor, die nicht notwendigerweise der numerischen Reihenfolge der Indexe im Array entsprechen muss. Wenn Sie das Array nicht in numerischer Reihenfolge aufgebaut haben, rufen Sie für die Arrays mit den regulären Ausdrücken und den Ersetzungstexten die Funktion ksort() auf, bevor Sie sie an preg_replace() übergeben. Dieses Beispiel baut das Array $replace in umgekehrter Reihenfolge auf: $regex[0] = $regex[1] = $regex[2] = $replace[2] $replace[1] $replace[0]
Der erste Aufruf von preg_replace() gibt 321 aus, was vermutlich nicht Ihr Wunschergebnis ist. Nach dem Anwenden von ksort() liefert die Ersetzung wie gewünscht 123. ksort() verändert die ihr übergebene Variable. Übergeben Sie nicht ihren Rückgabewert (true oder false) an preg_replace.
Perl In Perl ist s/// der Substitutionsoperator. Nutzen Sie s/// allein, wird es die Variable $_ durchsuchen und ersetzen und das Ergebnis wieder in $_ ablegen. Wenn Sie den Substitutionsoperator für eine andere Variable nutzen wollen, verwenden Sie den Bindungsoperator =~, um den Substitutionsoperator mit Ihrer Variablen zu verknüpfen. Binden Sie den Substitutionsoperator an einen String, wird das Suchen und 3.14 Alle Übereinstimmungen ersetzen | 187
Ersetzen direkt ausgeführt. Das Ergebnis wird wieder in der Variablen abgelegt, in der sich der Ausgangstext befindet. Der Operator s/// verändert immer die Variable, an den Sie ihn binden. Wenn Sie das Ergebnis eines Such- und Ersetzungsvorgangs in einer neuen Variablen ablegen wollen, ohne das Original zu verändern, weisen Sie zunächst den ursprünglichen String der Ergebnisvariablen zu und binden dann den Substitutionsoperator an diese Variable. Die Perl-Lösung für dieses Rezept zeigt, wie Sie diese beiden Schritte in einer Codezeile zusammenfassen können. Mit dem Modifikator /g, der in Rezept 3.4 erklärt wurde, ersetzen Sie alle Regex-Übereinstimmungen. Ohne ihn ersetzt Perl nur die erste Übereinstimmung.
Python Die Funktion sub() im Modul re sucht und ersetzt mithilfe eines regulären Ausdrucks. Übergeben Sie Ihren regulären Ausdruck als ersten Parameter, den Ersetzungstext als zweiten und den Ausgangstext als dritten Parameter. Der globalen Funktion sub() kann kein Parameter mit Regex-Optionen übergeben werden. Die Funktion re.sub() ruft re.compile() und dann die Methode sub() des kompilierten Regex-Objekts auf. Diese Methode besitzt zwei Parameter, die übergeben werden müssen: den Ersetzungstext und den Ausgangstext. Beide Formen von sub() geben einen String zurück, in dem alle Übereinstimmungen ersetzt wurden. Sie können einen optionalen Parameter übergeben, der die Anzahl der Ersetzungen begrenzt. Wenn Sie ihn weglassen oder auf null setzen, werden alle RegexÜbereinstimmungen ersetzt. Übergeben Sie eine positive Zahl, ist dies die maximale Anzahl an Ersetzungen, die vorgenommen werden. Werden weniger Übereinstimmungen gefunden, werden alle Übereinstimmungen ohne Fehlermeldung ersetzt.
Ruby Die Methode gsub() der Klasse String sucht und ersetzt mithilfe eines regulären Ausdrucks. Übergeben Sie den regulären Ausdruck als ersten Parameter und einen String mit dem Ersetzungstext als zweiten Parameter. Der Rückgabewert ist ein neuer String mit den durchgeführten Ersetzungen. Konnten keine Regex-Übereinstimmungen gefunden werden, gibt gsub() den ursprünglichen String zurück. gsub() verändert nicht den String, für den Sie sie aufgerufen haben. Soll der ursprüngliche String angepasst werden, nutzen Sie stattdessen gsub!(). Wird keine Regex-Übereinstimmung gefunden, liefert gsub!() den Wert nil zurück. Ansonsten wird der veränderte
String zurückgegeben.
Siehe auch „Suchen und Ersetzen mit regulären Ausdrücken“ in Kapitel 1 und die Rezepte 3.15 und 3.16.
188 | Kapitel 3: Mit regulären Ausdrücken programmieren
3.15 Übereinstimmungen durch Teile des gefundenen Texts ersetzen Problem Sie wollen einen Text durchsuchen und Teile der gefundenen Texte wiederum beim Ersetzen verwenden. Die erneut einzusetzenden Teile sind in Ihrem regulären Ausdruck durch einfangende Gruppen definiert, die in Rezept 2.9 beschrieben wurden. So wollen Sie in diesem Beispiel Wortpaare finden, die durch ein Gleichheitszeichen getrennt sind, und diese Wörter beim Ersetzen austauschen.
Lösung C# Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck bearbeiten wollen: string resultString = Regex.Replace(subjectString, @"(\w+)=(\w+)", "$2=$1");
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings nutzen wollen: Regex regexObj = new Regex(@"(\w+)=(\w+)"); string resultString = regexObj.Replace(subjectString, "$2=$1");
VB.NET Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck bearbeiten wollen: Dim ResultString = Regex.Replace(SubjectString, "(\w+)=(\w+)", "$2=$1")
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings nutzen wollen: Dim RegexObj As New Regex("(\w+)=(\w+)") Dim ResultString = RegexObj.Replace(SubjectString, "$2=$1")
Java Sie können String.replaceAll() aufrufen, wenn Sie nur einen String mit dem gleichen regulären Ausdruck bearbeiten wollen: String resultString = subjectString.replaceAll("(\\w+)=(\\w+)", "$2=$1");
3.15 Übereinstimmungen durch Teile des gefundenen Texts ersetzen | 189
Erstellen Sie ein Matcher-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings verwenden wollen: Pattern regex = Pattern.compile("(\\w+)=(\\w+)"); Matcher regexMatcher = regex.matcher(subjectString); String resultString = regexMatcher.replaceAll("$2=$1");
JavaScript result = subject.replace(/(\w+)=(\w+)/g, "$2=$1");
Python Wenn Sie nur ein paar Strings verarbeiten müssen, können Sie die globale Funktion verwenden: result = re.sub(r"(\w+)=(\w+)", r"\2=\1", subject)
Um die gleiche Regex mehrfach zu nutzen, verwenden Sie ein kompiliertes Objekt: reobj = re.compile(r"(\w+)=(\w+)") result = reobj.sub(r"\2=\1", subject)
Ruby result = subject.gsub(/(\w+)=(\w+)/, '\2=\1')
Diskussion Der reguläre Ausdruck ‹(\w+)=(\w+)› passt auf ein Wortpaar und „fängt“ jedes Wort in seiner eigenen einfangenden Gruppe. Das Wort vor dem Gleichheitszeichen wird durch die erste Gruppe eingefangen, das Wort nach dem Zeichen durch die zweite Gruppe. Beim Ersetzen müssen Sie angeben, dass Sie den von der zweiten einfangenden Gruppe gefundenen Text nutzen wollen, gefolgt von einem Gleichheitszeichen und dem Text, der von der ersten einfangenden Gruppe gefunden wurde. Das erreichen Sie mit speziellen Platzhaltern im Ersetzungstext. Die Syntax für den Ersetzungstext ist in den verschiedenen Programmiersprachen unterschiedlich. In „Suchen und Ersetzen mit regulären Ausdrücken“ in Kapitel 1 werden die Ersetzungstextvarianten beschrieben, und in Rezept 2.21 wird erklärt, wie im Ersetzungstext auf einfangende Gruppen verwiesen wird.
190 | Kapitel 3: Mit regulären Ausdrücken programmieren
.NET In .NET können Sie die gleiche Methode Regex.Replace() verwenden, die schon im vorhergehenden Rezept mit einem String als Ersatztext verwendet wurde. Die Syntax für die Verwendung von Rückwärtsreferenzen im Ersetzungstext entspricht der .NET-Variante für Ersetzungstexte. Diese wird in Rezept 2.21 beschrieben.
Java In Java können Sie die gleichen Methoden replaceFirst() und replaceAll() verwenden, die schon im vorhergehenden Rezept beschrieben wurden. Die Syntax für das Hinzufügen von Rückwärtsreferenzen entspricht der hier im Buch beschriebenen Java-Variante für Ersetzungstexte.
JavaScript In JavaScript können Sie die gleiche Methode string.replace() verwenden, die schon im vorhergehenden Rezept beschrieben wurde. Die Syntax für das Hinzufügen von Rückwärtsreferenzen entspricht der hier im Buch beschriebenen JavaScript-Variante für Ersetzungstexte.
PHP In PHP können Sie die gleiche Funktion preg_replace() verwenden, die schon im vorhergehenden Rezept beschrieben wurde. Die Syntax für das Hinzufügen von Rückwärtsreferenzen entspricht der hier im Buch beschriebenen PHP-Variante für Ersetzungstexte.
Perl In Perl wird der replace-Teil in s/regex/replace/ einfach als String in doppelten Anführungszeichen interpretiert. Sie können die speziellen Variablen $&, $1, $2 und so weiter verwenden, die in Rezept 3.7 und Rezept 3.9 für den Ersetzungstext beschrieben wurden. Die Variablen werden gesetzt, nachdem die Regex-Übereinstimmung gefunden wurde und bevor der Text ersetzt wird. Sie können diese Variablen auch an beliebigen anderen Stellen im Perl-Code nutzen. Ihr Wert bleibt bestehen, bis Sie Perl anweisen, eine weitere Regex-Suche durchzuführen. Alle anderen Programmiersprachen in diesem Buch stellen eine Funktion bereit, die den Ersetzungstext als String übernimmt. Dabei wird dieser String geparst, um Rückwärtsreferenzen wie $1 oder \1 zu verarbeiten. Aber außerhalb des Ersetzungstexts hat $1 in diesen Sprachen keinerlei Bedeutung.
Python In Python können Sie die gleiche Funktion sub() verwenden, die schon im vorhergehenden Rezept beschrieben wurde. Die Syntax für das Hinzufügen von Rückwärtsreferenzen entspricht der hier im Buch beschriebenen Python-Variante für Ersetzungstexte.
3.15 Übereinstimmungen durch Teile des gefundenen Texts ersetzen | 191
Ruby In Ruby können Sie die gleiche Methode String.gsub() verwenden, die schon im vorhergehenden Rezept beschrieben wurde. Die Syntax für das Hinzufügen von Rückwärtsreferenzen entspricht der hier im Buch beschriebenen Ruby-Variante für Ersetzungstexte. Sie können im Ersetzungstext Variablen wie $1 nicht auswerten. Das liegt daran, dass Ruby die Variablenauswertung durchführt, bevor gsub() aufgerufen wird. Zu diesem Zeitpunkt hat gsub() aber noch keine Übereinstimmungen gefunden, daher können Rückwärtsreferenzen nicht ersetzt werden. Versuchen Sie, $1 auszuwerten, erhalten Sie den Text, der von der ersten einfangenden Gruppe in der letzten Regex-Übereinstimmung gefunden wurde, noch bevor gsub() aufgerufen wird. Stattdessen müssen Sie Ersetzungstext-Tokens wie «\1» verwenden. Die Funktion gsub() ersetzt diese Tokens im Ersetzungstext für jede Regex-Übereinstimmung. Ich empfehle, für den Ersetzungstext Strings mit einfachen Anführungszeichen zu verwenden. Bei Strings mit doppelten Anführungszeichen wird der Backslash als Maskierungszeichen verwendet, und maskierte Ziffern stehen für oktale Werte. '\1' und "\\1" verwenden den von der ersten einfangenden Gruppe gefundenen Text zum Ersetzen, während "\1" mit dem einzelnen literalen Zeichen 0x01 ersetzt wird.
Benannte Captures Wenn Sie in Ihrem regulären Ausdruck benannte einfangende Gruppen verwenden, können Sie die Gruppen in Ihrem Ersetzungstext über den Namen ansprechen.
C# Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck bearbeiten wollen: string resultString = Regex.Replace(subjectString, @"(?\w+)=(?\w+)", "${right}=${left}");
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings nutzen wollen: Regex regexObj = new Regex(@"(?\w+)=(?\w+)"); string resultString = regexObj.Replace(subjectString, "${right}=${left}");
VB.NET Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck bearbeiten wollen: Dim ResultString = Regex.Replace(SubjectString, "(?\w+)=(?\w+)", "${right}=${left}")
192 | Kapitel 3: Mit regulären Ausdrücken programmieren
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings nutzen wollen: Dim RegexObj As New Regex("(?\w+)=(?\w+)") Dim ResultString = RegexObj.Replace(SubjectString, "${right}=${left}")
Die PHP-preg-Funktionen verwenden die PCRE-Bibliothek, die benannte einfangende Captures unterstützt. Die Funktionen preg_match() und preg_match_all() ergänzen das Array mit den gefundenen Ergebnissen um benannte einfangende Gruppen. Leider stellt preg_replace() keine Möglichkeit bereit, benannte Rückwärtsreferenzen im Ersetzungstext zu verwenden. Wenn Ihre Regex benannte einfangende Gruppen nutzt, müssen Sie sowohl die benannten als auch die nummerierten einfangenden Gruppen von links nach rechts durchzählen, um die Nummer der Rückwärtsreferenz zu ermitteln, die Sie dann auch im Ersetzungstext verwenden können.
Perl unterstützt benannte einfangende Gruppen seit Version 5.10. Der Hash $+ speichert den von allen benannten einfangenden Gruppen gefundenen Text ab, die im letzten regulären Ausdruck verwendet wurden. Sie können diesen Hash im Ersetzungstext nutzen, aber auch an anderen Stellen im Quellcode.
Python Wenn Sie nur ein paar Strings verarbeiten müssen, können Sie die globale Funktion verwenden: result = re.sub(r"(?P\w+)=(?P\w+)", r"\g=\g", subject)
Um die gleiche Regex mehrfach zu nutzen, verwenden Sie ein kompiliertes Objekt: reobj = re.compile(r"(?P\w+)=(?P\w+)") result = reobj.sub(r"\g=\g", subject)
Ruby result = subject.gsub(/(?\w+)=(?\w+)/, '\k=\k')
Siehe auch „Suchen und Ersetzen mit regulären Ausdrücken“ in Kapitel 1 beschreibt die Ersetzungstextvarianten. Rezept 2.21 erklärt, wie man im Ersetzungstext auf einfangende Gruppen zugreifen kann. 3.15 Übereinstimmungen durch Teile des gefundenen Texts ersetzen | 193
3.16 Übereinstimmungen durch Text ersetzen, der im Code erzeugt wurde Problem Sie wollen alle Übereinstimmungen eines regulären Ausdrucks durch einen neuen String ersetzen, den Sie in Ihrem prozeduralen Code erzeugt haben. Sie wollen jede Übereinstimmung durch einen anderen String ersetzen können, der davon abhängen soll, was tatsächlich gefunden wurde. Stellen Sie sich zum Beispiel vor, Sie wollen alle Zahlen in einem String mit zwei multiplizieren und wieder einsetzen.
Lösung C# Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck bearbeiten wollen: string resultString = Regex.Replace(subjectString, @"\d+", new MatchEvaluator(ComputeReplacement));
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings nutzen wollen: Regex regexObj = new Regex(@"\d+"); string resultString = regexObj.Replace(subjectString, new MatchEvaluator(ComputeReplacement));
Beide Codeschnipsel nutzen die Funktion ComputeReplacement. Sie sollten diese Methode der Klasse hinzufügen, in der Sie diese Lösung implementieren: public String ComputeReplacement(Match matchResult) { int twiceasmuch = int.Parse(matchResult.Value) * 2; return twiceasmuch.ToString(); }
VB.NET Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck bearbeiten wollen: Dim MyMatchEvaluator As New MatchEvaluator(AddressOf ComputeReplacement) Dim ResultString = Regex.Replace(SubjectString, "\d+", MyMatchEvaluator)
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings nutzen wollen: Dim RegexObj As New Regex("\d+") Dim MyMatchEvaluator As New MatchEvaluator(AddressOf ComputeReplacement) Dim ResultString = RegexObj.Replace(SubjectString, MyMatchEvaluator)
194 | Kapitel 3: Mit regulären Ausdrücken programmieren
Beide Codeschnipsel nutzen die Funktion ComputeReplacement. Sie sollten diese Methode der Klasse hinzufügen, in der Sie diese Lösung implementieren: Public Function ComputeReplacement(ByVal MatchResult As Match) As String Dim TwiceAsMuch = Int.Parse(MatchResult.Value) * 2; Return TwiceAsMuch.ToString(); End Function
JavaScript var result = subject.replace(/\d+/g, function(match) { return match * 2; } );
PHP Mit einer deklarierten Callback-Funktion: $result = preg_replace_callback('/\d+/', compute_replacement, $subject); function compute_replacement($groups) { return $groups[0] * 2; }
Mit einer anonymen Callback-Funktion: $result = preg_replace_callback( '/\d+/', create_function( '$groups', 'return $groups[0] * 2;' ), $subject );
Perl $subject =~ s/\d+/$& * 2/eg;
3.16 Übereinstimmungen durch Text ersetzen, der im Code erzeugt wurde | 195
Python Wenn Sie nur ein paar Strings verarbeiten müssen, können Sie die globale Funktion verwenden: result = re.sub(r"\d+", computereplacement, subject)
Um die gleiche Regex wiederholt zu nutzen, verwenden Sie ein kompiliertes Objekt: reobj = re.compile(r"\d+") result = reobj.sub(computereplacement, subject))
Beide Codeschnipsel rufen die Funktion computereplacement auf. Diese Funktion muss deklariert werden, bevor Sie sie an sub() übergeben können. def computereplacement(matchobj): return str(int(matchobj.group()) * 2)
Ruby result = subject.gsub(/\d+/) {|match| Integer(match) * 2 }
Diskussion Wenn Sie einen String als Ersetzungstext verwenden, können Sie auch nur einfache Textersetzungen vornehmen. Um jede Übereinstimmung mit etwas anderem zu ersetzen, das auch noch vom gefundenen Text abhängt, müssen Sie den Ersetzungstext in Ihrem eigenen Code erstellen.
C# In Rezept 3.14 werden die verschiedenen Möglichkeiten besprochen, die Methode Regex.Replace() aufzurufen und einen String als Ersetzungstext zu übergeben. Wenn ein statischer Aufruf genutzt wird, ist der Ersetzungstext der dritte Parameter – nach dem Ausgangstext und dem regulären Ausdruck. Haben Sie den regulären Ausdruck dem Konstruktor Regex() übergeben, können Sie Replace() mit dem Ersetzungstext als zweitem Parameter für dieses Objekt aufrufen. Statt einen String als zweiten oder dritten Parameter zu verwenden, können Sie auch ein MatchEvaluator-Delegate übergeben. Dieses Delegate ist eine Referenz auf eine MemberFunktion, die Sie in der Klasse definieren können, in der Sie das Suchen und Ersetzen durchführen wollen. Um das Delegate zu erstellen, verwenden Sie das Schlüsselwort new, um den Konstruktor MatchEvaluator() aufzufrufen. Übergeben Sie dabei Ihre MemberFunktion als einzigen Parameter. Die Funktion, die Sie für das Delegate nutzen wollen, sollte einen String zurückgeben und einen Parameter der Klasse System.Text.RegularExpressions.Match erwarten. Das ist die gleiche Klasse Match, die vom Member Regex.Match() in nahezu allen bisherigen Rezepten in diesem Kapitel genutzt wurde.
196 | Kapitel 3: Mit regulären Ausdrücken programmieren
Wenn Sie Replace() mit einem MatchEvaluator als Ersetzung aufrufen, wird Ihre Funktion für jede Regex-Übereinstimmung aufgerufen, die ersetzt werden muss. Ihre Funktion muss dann den Ersetzungstext liefern. Sie können beliebige Eigenschaften des MatchObjekts nutzen, um Ihren Ersetzungstext zu erzeugen. Das weiter oben gezeigte Beispiel hat matchResult.Value verwendet, um den String mit der vollständigen Regex-Übereinstimmung zu erhalten. Häufig werden Sie matchResult.Groups[] nutzen, um Ihren eigenen Ersetzungstext aus den einfangenden Gruppen in Ihrem regulären Ausdruck zu erstellen. Wollen Sie bestimmte Regex-Übereinstimmungen nicht ersetzen, sollte Ihre Funktion matchResult.Value zurückgeben. Liefern Sie null oder einen leeren String, wird die RegexÜbereinstimmung nur entfernt (also durch einen leeren String ersetzt).
VB.NET In Rezept 3.14 werden die verschiedenen Möglichkeiten besprochen, die Methode Regex.Replace() aufzurufen und einen String als Ersetzungstext zu übergeben. Wenn ein statischer Aufruf genutzt wird, ist der Ersetzungstext der dritte Parameter – nach dem Ausgangstext und dem regulären Ausdruck. Haben Sie das Schlüsselwort Dim genutzt, um eine Variable mit Ihrem regulären Ausdruck zu erstellen, können Sie Replace() mit dem Ersetzungstext als zweitem Parameter für dieses Objekt aufrufen. Statt einen String als zweiten oder dritten Parameter zu verwenden, können Sie auch ein MatchEvaluator-Objekt übergeben. Dieses Objekt ist eine Referenz auf eine Funktion, die
Sie in der Klasse definieren können, in der Sie das Suchen und Ersetzen durchführen wollen. Mit dem Schlüsselwort Dim erstellen Sie eine neue Variable des Typs MatchEvaluator. Der Operator AddressOf gibt eine Referenz auf Ihre Funktion zurück, ohne sie selbst in diesem Moment aufzurufen. Die Funktion, die Sie für MatchEvaluator nutzen wollen, sollte einen String zurückgeben und einen Parameter der Klasse System.Text.RegularExpressions.Match erwarten. Das ist die gleiche Klasse Match, die vom Member Regex.Match() in nahezu allen bisherigen Rezepten in diesem Kapitel genutzt wurde. Der Parameter wird als Wert übergeben, daher sollten Sie ihn mit ByVal definieren. Wenn Sie Replace() mit einem MatchEvaluator als Ersetzung aufrufen, wird Ihre Funktion für jede Regex-Übereinstimmung aufgerufen, die ersetzt werden muss. Ihre Funktion muss dann den Ersetzungstext liefern. Sie können beliebige Eigenschaften des MatchObjekts nutzen, um Ihren Ersetzungstext zu erzeugen. Das weiter oben gezeigte Beispiel hat MatchResult.Value verwendet, um den String mit der vollständigen Regex-Übereinstimmung zu erhalten. Häufig werden Sie MatchResult.Groups() nutzen, um Ihren eigenen Ersetzungstext aus den einfangenden Gruppen in Ihrem regulären Ausdruck zu erstellen. Wenn Sie bestimmte Regex-Übereinstimmungen nicht ersetzen wollen, sollte Ihre Funktion MatchResult.Value zurückgeben. Liefern Sie Nothing oder einen leeren String, wird die Regex-Übereinstimmung nur entfernt (also durch einen leeren String ersetzt). 3.16 Übereinstimmungen durch Text ersetzen, der im Code erzeugt wurde | 197
Java Die Lösung in Java ist sehr geradlinig. Wir iterieren, wie in Rezept 3.11 beschrieben, über alle Regex-Übereinstimmungen. Innerhalb der Schleife rufen wir für das Matcher-Objekt die Funktion appendReplacement() auf. Hat find() keinen weiteren Erfolg mehr, rufen wir appendTail() auf. Die beiden Methoden appendReplacement() und appendTail() machen es sehr leicht, für jede Regex-Übereinstimmung einen anderen Ersetzungstext zu verwenden. appendReplacement() erwartet zwei Parameter. Der erste ist der StringBuffer, in dem Sie
(temporär) das Ergebnis des aktuellen Such- und Ersetzungsvorgangs abspeichern. Der zweite ist der Ersetzungstext, der für die Übereinstimmung genutzt werden soll, die von find() gefunden wurde. Dieser Ersetzungstext kann Verweise auf einfangende Gruppen enthalten, wie zum Beispiel "$1". Gibt es in diesem Text einen Syntaxfehler, wird eine IllegalArgumentException geworfen. Verweist der Ersetzungstext auf eine einfangende Gruppe, die nicht existiert, wird stattdessen eine IndexOutOfBoundsException geworfen. Rufen Sie appendReplacement() ohne einen vorherigen erfolgreichen Aufruf von find() auf, führt das zu einer IllegalStateException. Wird appendReplacement() korrekt aufgerufen, geschehen zwei Dinge. Zuerst wird der Text in den aktuellen String-Buffer kopiert, der sich zwischen der vorherigen und der aktuellen Regex-Übereinstimmung befindet, ohne ihn zu verändern. Danach wird Ihr Ersetzungstext angehängt, wobei alle Rückwärtsreferenzen durch den Text der entsprechenden einfangenden Gruppen ersetzt werden. Möchten Sie eine bestimmte Übereinstimmung löschen, ersetzen Sie sie einfach durch einen leeren String. Wollen Sie eine Übereinstimmung nicht ändern, können Sie für diese eine den Aufruf von appendReplacement() weglassen. Wenn ich „vorherige Regex-Übereinstimmung“ sage, meine ich die vorherige Übereinstimmung, für die Sie appendReplacement() aufgerufen haben. Nutzen Sie appendReplacement() nicht für bestimmte Übereinstimmungen, werden diese Teil des Texts zwischen den Übereinstimmungen, die Sie ersetzen. Dieser Text wird dann unverändert in den Ziel-String-Buffer kopiert. Wenn Sie Ihre Ersetzungen durchgeführt haben, rufen Sie appendTail() auf. Damit wird der Text am Ende des Strings hinter der letzten Regex-Übereinstimmung kopiert, für die Sie appendReplacement() aufgerufen haben.
JavaScript In JavaScript ist eine Funktion einfach ein weiteres Objekt, das Sie einer Variablen zuweisen können. Statt einen literalen String oder eine Variable mit einem String an die Funktion string.replace() zu übergeben, nutzen wir eine Funktion, die einen String zurückgibt. Diese Funktion wird dann jedes Mal aufgerufen, wenn eine Ersetzung ansteht. Sie können Ihre Ersetzungsfunktion mehr als einen Parameter akzeptieren lassen. Wenn Sie das tun, wird im ersten Parameter der Text übergeben, der vom regulären Ausdruck gefunden wurde. Enthält Ihr regulärer Ausdruck einfangende Gruppen, finden Sie im zweiten Parameter den Text, der von der ersten einfangenden Gruppe gefunden wurde,
198 | Kapitel 3: Mit regulären Ausdrücken programmieren
im dritten Parameter den Text der zweiten einfangenden Gruppe und so weiter. Sie können diese Parameter nutzen, wenn Sie diese Teile des regulären Ausdrucks zum Aufbau Ihres Ersetzungstexts verwenden wollen. Die Ersetzungsfunktion in der JavaScript-Lösung für dieses Rezept übernimmt einfach den Text, der vom regulären Ausdruck gefunden wurde, und liefert ihn mit zwei multipliziert zurück. JavaScript kümmert sich implizit um das Umwandeln des Strings in eine Zahl und umgekehrt.
PHP Die Funktion preg_replace_callback() funktioniert genau so wie die in Rezept 3.14 beschriebene Funktion preg_replace(). Sie erwartet einen regulären Ausdruck, eine Ersetzung, den Ausgangstext, eine optionale maximale Ersetzungsanzahl und eine optionale Variable, in der die Anzahl der vorgenommenen Ersetzungen abgelegt wird. Der reguläre Ausdruck und der Ausgangstext können wieder einfache Strings oder Arrays sein. Der Unterschied liegt darin, dass man preg_replace_callback() keinen String und auch kein Array mit Strings als Ersetzung mitgeben kann, sondern nur eine Funktion. Sie können diese Funktion in Ihrem Code deklarieren oder create_function() nutzen, um eine anonyme Funktion zu erzeugen. Die Funktion sollte einen Parameter übernehmen und einen String zurückgeben (oder etwas, das in einen String umgewandelt werden kann). Jedes Mal, wenn preg_replace_callback() eine Regex-Übereinstimmung findet, wird Ihre Callback-Funktion aufgerufen. Der Parameter wird mit einem String-Array gefüllt. Im nullten Element findet sich das gesamte Regex-Suchergebnis. Die folgenden Elemente enthalten den Text, der durch die einfangenden Gruppen gefunden wurde. Sie können dieses Array nutzen, um Ihren Ersetzungstext aus dem Suchergebnis oder aus den einfangenden Gruppen aufzubauen.
Perl Der Operator s/// unterstützt einen zusätzlichen Modifikator, der vom Operator m// ignoriert wird: /e. Dieser Modifikator, der auch für „Execute“ steht, weist den Substitutionsoperator an, den Ersetzungsteil als Perl-Code zu interpretieren statt als Inhalt eines Strings in doppelten Anführungszeichen. Mit diesem Modifikator können wir den gefundenen Text einfach über die Variable $& ansprechen und mit zwei multiplizieren. Das Ergebnis des Codes wird dann als Ersetzungstext genutzt.
Python Die Python-Funktion sub() ermöglicht es Ihnen, den Namen einer Funktion statt einen String als Ersetzungstext zu übergeben. Diese Funktion wird dann für jede Regex-Übereinstimmung aufgerufen, die ersetzt werden soll.
3.16 Übereinstimmungen durch Text ersetzen, der im Code erzeugt wurde | 199
Sie müssen diese Funktion deklarieren, bevor Sie auf sie verweisen können. Sie sollte einen Parameter haben, um eine Instanz eines MatchObject zu erhalten. Das ist das gleiche Objekt, das auch von der Funktion search() zurückgegeben wird. Sie können es nutzen, um mit der Regex-Übereinstimmung oder Teilen davon einen Ersetzungstext zu bauen. In Rezept 3.7 und Rezept 3.9 finden Sie Details dazu. Ihre Funktion soll einen String mit dem Ersetzungstext zurückgeben.
Ruby Die vorhergehenden zwei Rezepte haben die Methode gsub() der Klasse String mit zwei Parametern aufgerufen – der Regex und dem Ersetzungstext. Diese Methode gibt es auch in einer Blockform. In der Blockform erwartet gsub() nur Ihren regulären Ausdruck als einzigen Parameter. Sie füllt dann eine Iterator-Variable mit einem String, der den vom regulären Ausdruck gefundenen Text enthält. Geben Sie zusätzliche Iterator-Variablen an, werden diese auf nil gesetzt, auch wenn Ihr regulärer Ausdruck einfangende Gruppen enthält. Innerhalb des Blocks setzen Sie einen Ausdruck, der zu dem String wird, den Sie als Ersetzungstext verwenden wollen. Sie können innerhalb des Blocks die speziellen RegexVariablen nutzen, wie zum Beispiel $~, $& und $1. Deren Wert ändert sich bei jeder Auswertung des Blocks. In den Rezepten 3.7, 3.8 und 3.9 finden Sie die Details dazu. Sie können keine Ersetzungstext-Tokens nutzen, wie zum Beispiel «\1». Diese werden als literaler Text behandelt.
Siehe auch Rezepte 3.9 und 3.15.
3.17 Alle Übereinstimmungen innerhalb der Übereinstimmungen einer anderen Regex ersetzen Problem Sie wollen alle Übereinstimmungen eines bestimmten regulären Ausdrucks ersetzen, aber nur in bestimmten Bereichen des Ausgangstexts. Ein weiterer regulärer Ausdruck findet jeden dieser Bereiche im String. Stellen Sie sich beispielsweise vor, Sie haben eine HTML-Datei, in der eine Reihe von Abschnitten mit -Tags als fett markiert ist. Zwischen jedem Paar Bold-Tags wollen Sie alle Übereinstimmungen des regulären Ausdrucks ‹vorher› durch den Ersetzungstext ‹danach› austauschen. Wenn Sie zum Beispiel den String vorher erster vorher vorher vorher vorher bearbeiten, erwarten Sie als Ergebnis vorher erster danach vorher danach danach.
200 | Kapitel 3: Mit regulären Ausdrücken programmieren
Lösung C# Regex outerRegex = new Regex(".*?", RegexOptions.Singleline); Regex innerRegex = new Regex("vorher"); string resultString = outerRegex.Replace(subjectString, new MatchEvaluator(ComputeReplacement)); public String ComputeReplacement(Match matchResult) { // Inneres Suchen und Ersetzen für jede Übereinstimmung // der äußeren Regex ausführen return innerRegex.Replace(matchResult.Value, "danach"); }
VB.NET Dim Dim Dim Dim
OuterRegex As New Regex(".*?", RegexOptions.Singleline) InnerRegex As New Regex("vorher") MyMatchEvaluator As New MatchEvaluator(AddressOf ComputeReplacement) ResultString = OuterRegex.Replace(SubjectString, MyMatchEvaluator)
Public Function ComputeReplacement(ByVal MatchResult As Match) As String 'Inneres Suchen und Ersetzen für jede Übereinstimmung 'der äußeren Regex ausführen Return InnerRegex.Replace(MatchResult.Value, "danach"); End Function
Diskussion Diese Lösung ist wieder eine Kombination aus zwei schon besprochenen Lösungen, wobei zwei reguläre Ausdrücke verwendet werden. Der „äußere“ reguläre Ausdruck ‹.*?› passt zu den HTML-Bold-Tags und dem Text dazwischen, der „innere“ reguläre Ausdruck passt zu „vorher”, was wir durch „danach“ ersetzen werden. In Rezept 3.16 wird erläutert, wie Sie suchen und dabei jede Regex-Übereinstimmung durch Text ersetzen können, den Sie selbst im Code zusammengestellt haben. Hier machen wir das mit dem äußeren regulären Ausdruck. Jedes Mal, wenn er ein Paar öffnender und schließender -Tags findet, suchen und ersetzen wir mit der inneren Regex – genau so, wie wir es in Rezept 3.14 gemacht haben. Der Ausgangstext für das Suchen und Ersetzen mit der inneren Regex ist der Text, der von der äußeren Regex gefunden wurde.
Siehe auch Rezepte 3.11, 3.13 und 3.16.
202 | Kapitel 3: Mit regulären Ausdrücken programmieren
3.18 Alle Übereinstimmungen zwischen den Übereinstimmungen einer anderen Regex ersetzen Problem Sie wollen alle Übereinstimmungen eines bestimmten regulären Ausdrucks ersetzen – aber nur in bestimmten Bereichen des Ausgangstexts. Ein anderer regulärer Ausdruck findet den Text zwischen diesen Bereichen. Sie wollen also nur in den Teilen des Ausgangstexts suchen und ersetzen, die nicht von dem anderen regulären Ausdruck gefunden werden. Stellen Sie sich vor, Sie haben eine HTML-Datei, in der Sie gerade doppelte Anführungszeichen durch typografisch korrekte doppelte Anführungszeichen ersetzen wollen – aber nur außerhalb von HTML-Tags. Anführungszeichen innerhalb von HTML-Tags müssen als echte ASCII-Anführungszeichen erhalten bleiben, da Ihr Webbrowser den HTMLCode ansonsten nicht mehr parsen kann. So wollen Sie zum Beispiel "Text" <span >"Text" "Text" in “Text” <span >“Text” “Text” ändern.
Lösung C# string resultString = null; Regex outerRegex = new Regex(""); Regex innerRegex = new Regex("\"([^\"]*)\""); // Ersten Bereich finden int lastIndex = 0; Match outerMatch = outerRegex.Match(subjectString); while (outerMatch.Success) { // Suchen und Ersetzen im Text zwischen dieser // und der vorigen Übereinstimmung string textBetween = subjectString.Substring(lastIndex, outerMatch.Index - lastIndex); resultString = resultString + innerRegex.Replace(textBetween, "\u201E$1\u201C"); lastIndex = outerMatch.Index + outerMatch.Length; // Text im Bereich unverändert kopieren resultString = resultString + outerMatch.Value; // Nächsten Bereich finden outerMatch = outerMatch.NextMatch(); } // Suchen und Ersetzen im Rest nach der letzten Regex-Übereinstimmung string textAfter = subjectString.Substring(lastIndex, subjectString.Length - lastIndex); resultString = resultString + innerRegex.Replace(textAfter, "\u201E$1\u201C");
3.18 Alle Übereinstimmungen zwischen den Übereinstimmungen einer anderen Regex ersetzen | 203
VB.NET Dim ResultString As String = Nothing Dim OuterRegex As New Regex("") Dim InnerRegex As New Regex("""([^""]*)""") 'Ersten Bereich finden Dim LastIndex = 0 Dim OuterMatch = OuterRegex.Match(SubjectString) While OuterMatch.Success 'Suchen und Ersetzen im Text zwischen dieser 'und der vorigen Übereinstimmung Dim TextBetween = SubjectString.Substring(LastIndex, OuterMatch.Index - LastIndex); ResultString = ResultString + InnerRegex.Replace(TextBetween, ChrW(&H201E) + "$1" + ChrW(&H201C)) LastIndex = OuterMatch.Index + OuterMatch.Length 'Text im Bereich unverändert kopieren ResultString = ResultString + OuterMatch.Value 'Nächsten Bereich finden OuterMatch = OuterMatch.NextMatch End While 'Suchen und Ersetzen im Rest nach der letzten Regex-Übereinstimmung Dim TextAfter = SubjectString.Substring(LastIndex, SubjectString.Length - LastIndex); ResultString = ResultString + InnerRegex.Replace(TextAfter, ChrW(&H201E) + "$1" + ChrW(&H201C))
Java StringBuffer resultString = new StringBuffer(); Pattern outerRegex = Pattern.compile(""); Pattern innerRegex = Pattern.compile("\"([^\"]*)\""); Matcher outerMatcher = outerRegex.matcher(subjectString); int lastIndex = 0; while (outerMatcher.find()) { // Suchen und Ersetzen im Text zwischen dieser // und der vorigen Übereinstimmung String textBetween = subjectString.substring(lastIndex, outerMatcher.start()); Matcher innerMatcher = innerRegex.matcher(textBetween); resultString.append(innerMatcher.replaceAll("\u201E$1\u201C")); lastIndex = outerMatcher.end(); // Regex-Übereinstimmung selbst unverändert anhängen resultString.append(outerMatcher.group()); } // Suchen und Ersetzen im Rest nach der letzten Regex-Übereinstimmung String textAfter = subjectString.substring(lastIndex); Matcher innerMatcher = innerRegex.matcher(textAfter); resultString.append(innerMatcher.replaceAll("\u201E$1\u201C"));
JavaScript var result = ""; var outerRegex = //g;
204 | Kapitel 3: Mit regulären Ausdrücken programmieren
var innerRegex = /"([^"]*)"/g; var outerMatch = null; var lastIndex = 0; while (outerMatch = outerRegex.exec(subject)) { if (outerMatch.index == outerRegex.lastIndex) outerRegex.lastIndex++; // Suchen und Ersetzen im Text zwischen dieser // und der vorigen Übereinstimmung var textBetween = subject.substring(lastIndex, outerMatch.index); result = result + textBetween.replace(innerRegex, "\u201E$1\u201C"); lastIndex = outerMatch.index + outerMatch[0].length; // Regex-Übereinstimmung selbst unverändert anhängen result = result + outerMatch[0]; } // Suchen und Ersetzen im Rest nach der letzten Regex-Übereinstimmung var textAfter = subject.substr(lastIndex); result = result + textAfter.replace(innerRegex, "\u201E$1\u201C");
PHP $result = ''; $lastindex = 0; while (preg_match('//', $subject, $groups, PREG_OFFSET_CAPTURE, $lastindex)) { $matchstart = $groups[0][1]; $matchlength = strlen($groups[0][0]); // Suchen und Ersetzen im Text zwischen dieser // und der vorigen Übereinstimmung $textbetween = substr($subject, $lastindex, $matchstart-$lastindex); $result .= preg_replace('/"([^"]*)"/', '“$1”', $textbetween); // Regex-Übereinstimmung selbst unverändert anhängen $result .= $groups[0][0]; // Startposition für die nächste Suche setzen $lastindex = $matchstart + $matchlength; if ($matchlength == 0) { // Nicht in einer Endlosschleife hängen bleiben, // wenn die Regex Übereinstimmungen der Länge null erlaubt $lastindex++; } } // Suchen und Ersetzen im Rest nach der letzten Regex-Übereinstimmung $textafter = substr($subject, $lastindex); $result .= preg_replace('/"([^"]*)"/', '“$1”', $textafter);
3.18 Alle Übereinstimmungen zwischen den Übereinstimmungen einer anderen Regex ersetzen | 205
Python innerre = re.compile('"([^"]*)"') result = ""; lastindex = 0; for outermatch in re.finditer("", subject): # Suchen und Ersetzen im Text zwischen dieser # und der vorigen Übereinstimmung textbetween = subject[lastindex:outermatch.start()] result += innerre.sub(u"\u201E\\1\u201C", textbetween) lastindex = outermatch.end() # Regex-Übereinstimmung selbst unverändert anhängen result += outermatch.group() # Suchen und Ersetzen im Rest nach der letzten Regex-Übereinstimmung textafter = subject[lastindex:] result += innerre.sub(u"\u201E\\1\u201C", textafter)
Ruby result = ''; textafter = '' subject.scan(//) {|match| textafter = $' textbetween = $`.gsub(/"([^"]*)"/, '“\1”') result += textbetween + match } result += textafter.gsub(/"([^"]*)"/, '“\1”')
Diskussion In Rezept 3.13 wird beschrieben, wie man zwei reguläre Ausdrücke nutzt, um Übereinstimmungen (der zweiten Regex) nur in bestimmten Abschnitten der Datei (Übereinstimmungen der ersten Regex) zu finden. Die Lösung für dieses Rezept verwendet die gleiche Technik zum Suchen und Ersetzen in ausgewählten Bereichen des Ausgangstexts. Es ist wichtig, dass der reguläre Ausdruck, den Sie zum Finden der Abschnitte nutzen, immer auf dem ursprünglichen String arbeitet. Wenn Sie den ursprünglichen Ausgangstext verändern und die innere Regex dabei Zeichen ergänzt oder entfernt, müssen Sie die Startposition für die Regex, die die Abschnitte findet, verschieben. Vor allem können die Änderungen unerwünschte Nebeneffekte haben. Wenn Ihre äußere Regex zum Beispiel den Anker ‹^› nutzt, um etwas am Anfang einer Zeile zu finden, Ihre innere Regex aber einen Zeilenumbruch am Ende des von der äußeren Regex gefundenen Abschnitts einfügt, wird ‹^› aufgrund dieses neu eingefügten Zeilenumbruchs direkt nach dem vorigen Abschnitt passen. Auch wenn die Lösungen für dieses Rezept ziemlich lang sind, sind alle doch recht geradlinig angelegt. Es werden zwei reguläre Ausdrücke genutzt. Der „äußere“ reguläre Ausdruck ‹› passt zu einem Paar spitzer Klammern und allem dazwischen – abgesehen von spitzen Klammern. Das ist ein recht kruder Weg, ein HTML-Tag zu finden. Diese Regex funktioniert wunderbar, solange die HTML-Datei keine literalen spit-
206 | Kapitel 3: Mit regulären Ausdrücken programmieren
zen Klammern enthält, die (fälschlicherweise) nicht als Entitäten kodiert sind. Wir implementieren diesen regulären Ausdruck mit dem gleichen Code, der auch schon in Rezept 3.11 genutzt wurde. Der einzige Unterschied ist, dass der Platzhalter-Kommentar im dortigen Code nun durch tatsächlichen Code ersetzt wurde, der sucht und ersetzt. Der Code für das Suchen und Ersetzen innerhalb der Schleife orientiert sich am Code aus Rezept 3.14. Der Ausgangstext für das Suchen und Ersetzen ist der Text zwischen der vorigen Übereinstimmung der äußeren Regex und der aktuellen Übereinstimmung. Wir fügen das Ergebnis des inneren Suchens und Ersetzens an den Gesamtergebnis-String an. Zudem ergänzen wir die aktuelle Übereinstimmung des äußeren regulären Ausdrucks unverändert. Wenn die äußere Regex keine weiteren Übereinstimmungen findet, führen wir das innere Suchen und Ersetzen ein weiteres Mal durch, jetzt aber für den Text nach der letzten Übereinstimmung der äußeren Regex. Die Regex ‹"([^"]*)"›, die für das Suchen und Ersetzen innerhalb der Schleife verwendet wird, findet ein Paar doppelte Anführungszeichen und alles dazwischen, mit Ausnahme der doppelten Anführungszeichen. Der Text zwischen den Anführungszeichen wird durch die erste einfangende Gruppe gesichert. Für den Ersetzungstext nutzen wir eine Referenz auf die erste einfangende Gruppe, die zwischen typografischen (deutschen) Anführungszeichen gesetzt wird. Diese finden sich an den Unicode-Codepoints U+201E und U+201C. Normalerweise können Sie die typografischen Anführungszeichen direkt in Ihren Quellcode einfügen. Visual Studio 2008 versucht allerdings, schlau zu sein, und ersetzt die literalen typografischen Anführungszeichen durch ASCII-Anführungszeichen. In einem regulären Ausdruck können Sie einen Unicode-Codepoint durch ‹\u201E› oder ‹\x{201E}› finden, aber keine der in diesem Buch behandelten Programmiersprachen unterstützt solche Tokens als Teil des Ersetzungstexts. Wenn ein Endanwender typografische Anführungszeichen in ein Eingabefeld eintragen will, wird er sie direkt aus einer Zeichentabelle kopieren. In Ihrem Quellcode können Sie Unicode-Maskierungszeichen im Ersetzungstext verwenden, wenn Ihre Sprache solche Maskierungszeichen als Teil eines literalen Strings unterstützt. So lässt sich zum Beispiel in C# und Java \u201E direkt im String angeben, aber VB.NET bietet keine Möglichkeit, Unicode-Zeichen in Strings zu maskieren. In VB.NET können Sie dafür die Funktion ChrW nutzen, um einen UnicodeCodepoint in ein Zeichen umzuwandeln.
Perl und Ruby Die Lösungen für Perl und Ruby verwenden zwei spezielle Variablen, die in diesen Sprachen zur Verfügung stehen, von uns aber noch nicht erklärt wurden. In $` (Dollar plus Gravis) findet sich der Teil des Texts links von der Übereinstimmung, während $' (Dollar plus einfaches Anführungszeichen) den Teil des Texts rechts von der Übereinstimmung enthält. Anstatt über die Übereinstimmungen im ursprünglichen Ausgangstext zu iterieren, beginnen wir eine neue Suche für den Teil des Strings nach der vorherigen 3.18 Alle Übereinstimmungen zwischen den Übereinstimmungen einer anderen Regex ersetzen | 207
Übereinstimmung. So können wir mit $` bequem auf den Text zwischen der aktuellen und der vorherigen Übereinstimmung zugreifen.
Python Das Ergebnis dieses Codes ist ein Unicode-String, weil der Ersetzungstext als UnicodeString spezifiziert wurde. Es kann sein, dass Sie encode() aufrufen müssen, um den Text anzeigen zu können, zum Beispiel mit: print result.encode('1252')
Siehe auch Rezepte 3.11, 3.13 und 3.16.
3.19 Einen String aufteilen Problem Sie wollen einen String mithilfe eines regulären Ausdrucks aufteilen. Nach dem Aufteilen werden Sie ein Array oder eine Liste mit Strings haben, in denen sich der Text zwischen den Regex-Übereinstimmungen findet. So wollen Sie beispielsweise einen String mit HTML-Tags an den Tags aufteilen. Eine Bearbeitung von IchzmagzfettezundzkursivezFonts sollte zu einem Array von fünf Strings führen: Ichzmagz, fette, zundz, kursive und zFonts.
Lösung C# Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck bearbeiten wollen: string[] splitArray = Regex.Split(subjectString, "");
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie den statischen Aufruf per Exception Handling absichern: string[] splitArray = null; try { splitArray = Regex.Split(subjectString, ""); } catch (ArgumentNullException ex) { // Regulärer Ausdruck und Ausgangstext dürfen nicht null sein } catch (ArgumentException ex) { // Syntaxfehler im regulären Ausdruck }
208 | Kapitel 3: Mit regulären Ausdrücken programmieren
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings nutzen wollen: Regex regexObj = new Regex(""); string[] splitArray = regexObj.Split(subjectString);
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie die Verwendung des Regex-Objekts mit einem kompletten Exception Handling versehen: string[] splitArray = null; try { Regex regexObj = new Regex(""); try { splitArray = regexObj.Split(subjectString); } catch (ArgumentNullException ex) { // Ausgangstext darf nicht null sein } } catch (ArgumentException ex) { // Syntaxfehler im regulären Ausdruck }
VB.NET Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck bearbeiten wollen: Dim SplitArray = Regex.Split(SubjectString, "")
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie den statischen Aufruf per Exception Handling absichern: Dim SplitArray As String() Try SplitArray = Regex.Split(SubjectString, "") Catch ex As ArgumentNullException 'Regulärer Ausdruck und Ausgangstext dürfen nicht null sein Catch ex As ArgumentException 'Syntaxfehler im regulären Ausdruck End Try
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings nutzen wollen: Dim RegexObj As New Regex("") Dim SplitArray = RegexObj.Split(SubjectString)
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie die Verwendung des Regex-Objekts mit einem kompletten Exception Handling versehen: Dim SplitArray As String() Try Dim RegexObj As New Regex("") Try SplitArray = RegexObj.Split(SubjectString) Catch ex As ArgumentNullException 'Ausgangstext darf nicht null sein
3.19 Einen String aufteilen | 209
End Try Catch ex As ArgumentException 'Syntaxfehler im regulären Ausdruck End Try
Java Sie können String.Split() direkt aufrufen, wenn Sie nur einen String mit einem regulären Ausdruck aufteilen wollen: String[] splitArray = subjectString.split("");
Wenn die Regex vom Endanwender bereitgestellt wird, sollten Sie Exception Handling berücksichtigen: try { String[] splitArray = subjectString.split(""); } catch (PatternSyntaxException ex) { // Syntaxfehler im regulären Ausdruck }
Erstellen Sie ein Pattern-Objekt, wenn Sie den gleichen regulären Ausdruck für eine Reihe von Strings nutzen wollen: Pattern regex = Pattern.compile(""); String[] splitArray = regex.split(subjectString);
Wenn die Regex vom Endanwender eingegeben wird, sollten Sie die Verwendung des Pattern-Objekts mit einem kompletten Exception Handling versehen: String[] splitArray = null; try { Pattern regex = Pattern.compile(""); splitArray = regex.split(subjectString); } catch (ArgumentException ex) { // Syntaxfehler im regulären Ausdruck }
JavaScript Die Funktion string.split() kann einen String mit einem regulären Ausdruck aufteilen: result = subject.split(//);
Leider gibt es eine Reihe von Problemen in den verschiedenen Browsern, wenn man string.split() mit einem regulären Ausdruck verwendet. Es ist zuverlässiger, die Liste mit eigenem Code aufzubauen: var list = []; var regex = //g; var match = null; var lastIndex = 0; while (match = regex.exec(subject)) { // Browser wie Firefox sollen nicht in einer Endlosschleife hängen bleiben if (match.index == regex.lastIndex) regex.lastIndex++; // Text vor der Übereinstimmung hinzufügen
210 | Kapitel 3: Mit regulären Ausdrücken programmieren
list.push(subject.substring(lastIndex, match.index)); lastIndex = match.index + match[0].length; } // Rest nach der letzten Übereinstimmung hinzufügen list.push(subject.substr(lastIndex));
PHP $result = preg_split('//', $subject);
Perl @result = split(m//, $subject);
Python Wenn Sie nur ein paar Strings aufteilen wollen, können Sie die globale Funktion verwenden: result = re.split("", subject))
Um die gleiche Regex mehrfach zu verwenden, können Sie ein kompiliertes Objekt nutzen: reobj = re.compile("") result = reobj.split(subject)
Ruby result = subject.split(//)
Diskussion Das Aufteilen eines Strings mithilfe eines regulären Ausdrucks sorgt im Prinzip für ein Ergebnis, das genau das Gegenteil von Rezept 3.10 ist. Statt eine Liste mit allen RegexÜbereinstimmungen zu erhalten, bekommen Sie eine Liste mit dem Text zwischen den Übereinstimmungen, einschließlich des Texts vor der ersten und nach der letzten Übereinstimmung. Die Regex-Übereinstimmungen selbst werden bei der Ausgabe der SplitFunktion weggelassen.
C# und VB.NET In .NET werden Sie stets die Methode Regex.Split() verwenden, um einen String mithilfe eines regulären Ausdrucks aufzuteilen. Der erste Parameter von Split() ist immer der String mit dem Ausgangstext, den Sie aufteilen wollen. Dieser Parameter darf nicht null sein, da Split() ansonsten eine ArgumentNullException wirft. Der Rückgabewert von Split() ist immer ein Array mit Strings. Wenn Sie den regulären Ausdruck nur ein paar Mal nutzen wollen, können Sie einen statischen Aufruf verwenden. Der zweite Parameter ist dann der reguläre Ausdruck, den Sie
3.19 Einen String aufteilen | 211
verwenden wollen. Sie können als dritten, optionalen Parameter Regex-Optionen mitgeben. Wenn Ihr regulärer Ausdruck einen Syntaxfehler enthält, wird eine ArgumentException geworfen. Möchten Sie den gleichen regulären Ausdruck mit mehreren Strings nutzen, können Sie Ihren Code effizienter gestalten, indem Sie zuerst ein Regex-Objekt erzeugen und dann für dieses Objekt die Methode Split() aufrufen. In dem Fall ist der Ausgangstext der einzige erforderliche Parameter. Wenn Sie für eine Instanz der Regex-Klasse die Methode Split() aufrufen, können Sie zusätzliche Parameter angeben, mit denen der Arbeitsbereich der Split-Operation eingeschränkt wird. Lassen Sie diese Parameter weg, wird der String an allen Übereinstimmungen im Ausgangstext aufgeteilt. Die statisch überladenen Versionen von Split() besitzen diese zusätzlichen Parameter nicht. Sie teilen immer den ganzen String an allen Übereinstimmungen auf. Als optionalen zweiten Parameter nach dem Ausgangstext können Sie die maximale Anzahl an aufgeteilten Strings angeben, die Sie haben wollen. Rufen Sie zum Beispiel regexObj.Split(subject, 3) auf, erhalten Sie ein Array mit höchstens drei Strings. Die Funktion Split() wird versuchen, zwei Regex-Übereinstimmungen zu finden und ein Array mit dem Text vor der ersten Übereinstimmung, dem Text zwischen der ersten und der zweiten Übereinstimmung und dem Text nach der zweiten Übereinstimmung zurückzugeben. Alle weiteren möglichen Regex-Übereinstimmungen im Rest des Texts werden ignoriert und verbleiben im letzten String des Arrays. Wenn es nicht genug Regex-Übereinstimmungen gibt, um Ihre Grenze zu erreichen, wird Split() die Aufteilung an allen verfügbaren Regex-Übereinstimmungen vornehmen und ein Array mit weniger Strings als angegeben zurückliefern. regexObj.Split(subject, 1)
teilt den String gar nicht auf und gibt ein Array mit dem ursprünglichen String als einzigem Element zurück. regexObj.Split(subject, 0) teilt den String an allen Regex-Übereinstimmungen auf, so wie es Split() ohne den zweiten Parameter tut. Geben Sie eine negative Zahl an, wirft Split() eine ArgumentOutOfRangeException. Geben Sie den zweiten Parameter mit der maximalen Zahl an Strings für das zurückzugebende Array an, können Sie auch einen optionalen dritten Parameter spezifizieren, mit dem der Zeichenindex festgelegt wird, an dem der reguläre Ausdruck mit seiner Suche beginnen soll. Im Prinzip ist das die Anzahl an Zeichen am Anfang des Strings, die der reguläre Ausdruck ignorieren soll. Das kann nützlich sein, wenn Sie den String schon bis zu einer bestimmten Position verarbeitet haben und nun nur noch den Rest des Strings aufteilen wollen. Die vom regulären Ausdruck übersprungenen Zeichen werden aber dennoch im zurückgelieferten Array enthalten sein. Der erste String im Array ist der gesamte Substring vor der ersten Regex-Übereinstimmung, auch wenn die Suche nicht am Anfang des Strings begann. Geben Sie den dritten Parameter an, muss er zwischen null und der Länge des Ausgangstexts liegen. Sonst wirft Split() eine ArgumentOutOfRangeException. Anders als
212 | Kapitel 3: Mit regulären Ausdrücken programmieren
Match() ermöglicht Split() es nicht, einen Parameter anzugeben, mit dem die Länge des
Substrings definiert wird, in dem der reguläre Ausdruck suchen darf. Wenn es eine Übereinstimmung direkt am Anfang des Ausgangstexts gibt, wird der erste String im zurückgegebenen Array ein leerer String sein. Werden zwei Regex-Übereinstimmungen direkt nebeneinander gefunden, wird dem Array ebenfalls ein leerer String hinzugefügt. Tritt am Ende des Strings eine Übereinstimmung auf, wird das letzte Element des Arrays ein leerer String sein.
Java Haben Sie nur einen String, der aufgeteilt werden soll, können Sie die Methode split() direkt für Ihren Ausgangstext aufrufen. Übergeben Sie den regulären Ausdruck als einzigen Parameter. Diese Methode ruft einfach Pattern.compile("Regex").split(subjectString) auf. Wollen Sie mehrere Strings aufteilen, greifen Sie auf die Fabrikmethode Pattern.compile() zurück, um ein Pattern-Objekt zu erstellen. So muss Ihr regulärer Ausdruck nur einmal kompiliert werden. Danach rufen Sie die Methode split() für Ihre PatternInstanz auf und übergeben den Ausgangstext als Parameter. Sie müssen hier kein Matcher-Objekt erzeugen. Die Klasse Matcher besitzt nicht einmal eine Methode split(). Pattern.split() kann ein optionaler Parameter übergeben werden, was bei String.split() nicht möglich ist. Sie können diesen zweiten Parameter nutzen, um die
maximale Anzahl an aufgeteilten Strings anzugeben, die Sie erhalten wollen. Rufen Sie zum Beispiel Pattern.split(subject, 3) auf, erhalten Sie ein Array mit höchstens drei Strings. Die Funktion split() wird versuchen, zwei Regex-Übereinstimmungen zu finden und ein Array mit dem Text vor der ersten Übereinstimmung, dem Text zwischen der ersten und der zweiten Übereinstimmung und dem Text nach der zweiten Übereinstimmung zurückzugeben. Alle weiteren möglichen Regex-Übereinstimmungen im Rest des Texts werden ignoriert und verbleiben im letzten String des Arrays. Wenn es nicht genug Regex-Übereinstimmungen gibt, um Ihre Grenze zu erreichen, wird split() die Aufteilung an allen verfügbaren Regex-Übereinstimmungen vornehmen und ein Array mit weniger Strings als angegeben zurückliefern. Pattern.split(subject, 1) teilt den String gar nicht auf und gibt ein Array mit dem ursprünglichen String als einzigem Element zurück. Wenn es eine Übereinstimmung direkt am Anfang des Ausgangstexts gibt, wird der erste String im zurückgegebenen Array ein leerer String sein. Werden zwei Regex-Übereinstimmungen direkt nebeneinander gefunden, wird dem Array ebenfalls ein leerer String hinzugefügt. Tritt am Ende des Strings eine Übereinstimmung auf, wird das letzte Element des Arrays ein leerer String sein. Java entfernt allerdings leere Strings am Ende des Arrays. Wollen Sie, dass die leeren Strings trotzdem enthalten sind, übergeben Sie Pattern.split() eine negative Zahl als zweiten Parameter. Damit wird Java angewiesen, den String so oft wie möglich aufzuteilen und leere Strings am Ende des Arrays zu erhalten. Der tatsächliche Wert des zweiten 3.19 Einen String aufteilen | 213
Parameters ist unwichtig, solange er nur negativ ist. Sie können Java nicht anweisen, einen String nur n-mal aufzuteilen und gleichzeitig noch leere Strings am Ende des Arrays zu erhalten.
JavaScript In JavaScript rufen Sie für den String, den Sie aufteilen wollen, die Methode split() auf. Übergeben Sie den regulären Ausdruck als einzigen Parameter, um ein Array zu erhalten, in dem der String so oft wie möglich aufgeteilt wurde. Sie können auch optional einen zweiten Parameter angeben, der die maximale Anzahl an Strings festlegt, die Sie im zurückgegebenen Array haben wollen. Dabei sollte es sich um eine positive Zahl handeln. Übergeben Sie null, erhalten Sie ein leeres Array. Lassen Sie den zweiten Parameter weg oder übergeben eine negative Zahl, wird der String so häufig wie möglich aufgeteilt. Es ist egal, ob Sie die Option /g für die Regex (Rezept 3.4) setzen. Leider implementiert keiner der verbreiteten Webbrowser jeden Aspekt der Methode split() so, wie er im JavaScript-Standard spezifiziert wurde. Beispielsweise übernehmen einige der Browser den von einfangenden Gruppen gefundenen Text, während andere das nicht tun. Diejenigen, die das tun, behandeln nicht teilnehmende Gruppen auch nicht konsistent. Um solche Probleme zu vermeiden, nutzen Sie in regulären Ausdrücken, die Sie an split() übergeben, nur nicht-einfangende Gruppen (Rezept 2.9). Manche JavaScript-Implementierungen übernehmen keine leeren Strings in das Array. Solche Strings sollten dann vorkommen, wenn zwei Regex-Übereinstimmungen direkt nebeneinanderliegen oder wenn die Regex direkt am Anfang oder am Ende des aufzuteilenden Strings gefunden wird. Da Sie das nicht einfach mit einer kleinen Modifizierung Ihres regulären Ausdrucks ändern können, ist es vermutlich sicherer, die längere JavaScript-Lösung in diesem Rezept zu verwenden. Dabei werden alle leeren Strings übernommen, aber Sie können die Lösung leicht so anpassen, dass sie weggelassen werden. Die lange Lösung ist eine Anpassung von Rezept 3.12. Sie fügt dem Array die Texte zwischen den Regex-Übereinstimmungen hinzu. Dazu nutzen wir die in Rezept 3.8 beschriebene Vorgehensweise. Wenn Sie eine Implementierung von String.prototype.split benötigen, die sich an den Standard hält und mit allen Browsern funktioniert, finden Sie bei Steven Levithan eine Lösung unter http://blog.stevenlevithan.com/archives/cross-browser-split.
PHP Rufen Sie preg_split() auf, um einen String in ein Array mit Strings aufzuteilen, die durch die Regex-Übereinstimmungen getrennt sind. Übergeben Sie den regulären Ausdruck als ersten und den Ausgangstext als zweiten Parameter. Lassen Sie den zweiten Parameter weg, wird $_ als Ausgangstext genutzt. Sie können einen optionalen dritten Parameter angeben, um die maximale Anzahl an aufgeteilten Strings festzulegen, die Sie herausbekommen wollen. Rufen Sie zum Beispiel
214 | Kapitel 3: Mit regulären Ausdrücken programmieren
preg_split($regex, $subject, 3) auf, erhalten Sie ein Array mit höchstens drei Strings. Die Funktion preg_split() wird versuchen, zwei Regex-Übereinstimmungen zu finden
und ein Array mit dem Text vor der ersten Übereinstimmung, dem Text zwischen der ersten und der zweiten Übereinstimmung und dem Text nach der zweiten Übereinstimmung zurückzugeben. Alle weiteren möglichen Regex-Übereinstimmungen im Rest des Texts werden ignoriert und verbleiben im letzten String des Arrays. Wenn es nicht genug Regex-Übereinstimmungen gibt, um Ihre Grenze zu erreichen, wird preg_split() die Aufteilung an allen verfügbaren Regex-Übereinstimmungen vornehmen und ein Array mit weniger Strings als angegeben zurückliefern. Lassen Sie den dritten Parameter weg oder setzen ihn auf -1, wird der String so häufig wie möglich aufgeteilt. Wenn es eine Übereinstimmung direkt am Anfang des Ausgangstexts gibt, wird der erste String im zurückgegebenen Array ein leerer String sein. Werden zwei Regex-Übereinstimmungen direkt nebeneinander gefunden, wird dem Array ebenfalls ein leerer String hinzugefügt. Tritt am Ende des Strings eine Übereinstimmung auf, wird das letzte Element des Arrays ein leerer String sein. Standardmäßig belässt preg_split() diese leeren Strings im zurückgegebenen Array. Wollen Sie diese Strings nicht haben, übergeben Sie die Konstante PREG_SPLIT_NO_EMPTY als vierten Parameter.
Perl Rufen Sie die Funktion split() auf, um einen String in ein Array mit Strings zu unterteilen, die durch die Regex-Übereinstimmungen getrennt sind. Übergeben Sie als ersten Parameter einen Regex-Operator und den Ausgangstext als zweiten Parameter. Sie können einen optionalen dritten Parameter übergeben, um die maximale Anzahl an Strings im Array festzulegen, die Sie herausbekommen wollen. Rufen Sie zum Beispiel split(/regex/, subject, 3) auf, erhalten Sie ein Array mit höchstens drei Strings. Die Funktion split() wird versuchen, zwei Regex-Übereinstimmungen zu finden und ein Array mit dem Text vor der ersten Übereinstimmung, dem Text zwischen der ersten und der zweiten Übereinstimmung und dem Text nach der zweiten Übereinstimmung zurückzugeben. Alle weiteren möglichen Regex-Übereinstimmungen im Rest des Texts werden ignoriert und verbleiben im letzten String des Arrays. Wenn es nicht genug Regex-Übereinstimmungen gibt, um Ihre Grenze zu erreichen, wird split() die Aufteilung an allen verfügbaren Regex-Übereinstimmungen vornehmen und ein Array mit weniger Strings als angegeben zurückliefern. Lassen Sie den dritten Parameter weg, wird Perl die passende Grenze bestimmen. Weisen Sie das Ergebnis einer Array-Variablen zu, wie es in der Lösung für dieses Rezept geschieht, wird der String so häufig wie möglich aufgeteilt. Geben Sie als Ziel des Funktionsaufrufs eine Liste von skalaren Variablen an, setzt Perl die Grenze auf die Anzahl der Variablen plus eins. Perl wird also versuchen, alle Variablen zu füllen, und den nicht aufgeteilten Rest verwerfen. So teilt ($one, $two, $three) = split(/,/) die Variable $_ mit einer Grenze von 4.
3.19 Einen String aufteilen | 215
Wenn es eine Übereinstimmung direkt am Anfang des Ausgangstexts gibt, wird der erste String im zurückgegebenen Array ein leerer String sein. Werden zwei Regex-Übereinstimmungen direkt nebeneinander gefunden, wird dem Array ebenfalls ein leerer String hinzugefügt. Tritt am Ende des Strings eine Übereinstimmung auf, wird das letzte Element des Arrays ein leerer String sein.
Python Die Funktion split() im Modul re teilt einen String mithilfe eines regulären Ausdrucks auf. Übergeben Sie Ihren regulären Ausdruck als ersten und den Ausgangstext als zweiten Parameter. Der globalen Funktion split() kann man keinen Parameter mit RegexOptionen übergeben. Die Funktion re.split() ruft re.compile() und dann die Methode split() des kompilierten Regex-Objekts auf. Diese Methode hat nur einen erforderlichen Parameter – den Ausgangstext. Beide Versionen von split() geben eine Liste mit dem Text zwischen allen Regex-Übereinstimmungen zurück. Beiden kann ein optionaler Parameter übergeben werden, mit dem Sie die maximale Anzahl an Übereinstimmungen definieren, an denen der String aufgeteilt werden soll. Lassen Sie ihn weg oder setzen ihn auf null, wird der String so häufig wie möglich geteilt. Übergeben Sie eine positive Zahl, ist das die maximale Anzahl an Regex-Übereinstimmungen. Die Ergebnisliste wird einen String mehr enthalten, als Sie angegeben haben. Der letzte String ist der ungeteilte Rest des Ausgangstexts, der sich hinter der letzten Regex-Übereinstimmung befindet. Gibt es weniger Übereinstimmungen, als Sie angegeben haben, wird der String ohne Fehlermeldung an allen Übereinstimmungen aufgeteilt.
Ruby Rufen Sie die Methode split() für den Ausgangstext auf und übergeben Sie den regulären Ausdruck als ersten Parameter, um den String in ein Array mit Strings aufzuteilen, die durch die Regex-Übereinstimmungen getrennt sind. Der Methode split() kann ein optionaler zweiter Parameter mitgegeben werden, mit dem Sie die maximale Anzahl an aufgeteilten Strings definieren, die Sie herausbekommen wollen. Rufen Sie zum Beispiel subject.split(re, 3) auf, erhalten Sie ein Array mit höchstens drei Strings. Die Funktion split() wird versuchen, zwei Regex-Übereinstimmungen zu finden und ein Array mit dem Text vor der ersten Übereinstimmung, dem Text zwischen der ersten und der zweiten Übereinstimmung und dem Text nach der zweiten Übereinstimmung zurückzugeben. Alle weiteren möglichen Regex-Übereinstimmungen im Rest des Texts werden ignoriert und verbleiben im letzten String des Arrays. Wenn es nicht genug Regex-Übereinstimmungen gibt, um Ihre Grenze zu erreichen, wird split() die Aufteilung an allen verfügbaren Regex-Übereinstimmungen vornehmen und ein Array mit weniger Strings als angegeben zurückliefern. split(re, 1) teilt den String gar nicht auf und gibt ein Array mit dem ursprünglichen String als einzigem Element zurück.
216 | Kapitel 3: Mit regulären Ausdrücken programmieren
Wenn es eine Übereinstimmung direkt am Anfang des Ausgangstexts gibt, wird der erste String im zurückgegebenen Array ein leerer String sein. Werden zwei Regex-Übereinstimmungen direkt nebeneinander gefunden, wird dem Array ebenfalls ein leerer String hinzugefügt. Tritt am Ende des Strings eine Übereinstimmung auf, wird das letzte Element des Arrays ein leerer String sein. Ruby entfernt allerdings leere Strings am Ende des Arrays. Wollen Sie, dass die leeren Strings trotzdem enthalten sind, übergeben Sie split() eine negative Zahl als zweiten Parameter. Damit wird Ruby angewiesen, den String so oft wie möglich aufzuteilen und leere Strings am Ende des Arrays zu erhalten. Der tatsächliche Wert des zweiten Parameters ist unwichtig, solange er nur negativ ist. Sie können Ruby nicht anweisen, einen String nur n-mal aufzuteilen und gleichzeitig noch leere Strings am Ende des Arrays zu erhalten.
Siehe auch Rezept 3.20.
3.20 Einen String aufteilen und die RegexÜbereinstimmungen behalten Problem Sie wollen einen String mithilfe eines regulären Ausdrucks aufteilen. Danach haben Sie ein Array oder eine Liste mit Strings mit dem Text zwischen den Regex-Übereinstimmungen, aber auch mit den Übereinstimmungen selbst. Stellen Sie sich vor, dass Sie zum Beispiel einen String mit HTML-Tags an den Tags aufteilen, aber auch die HTML-Tags selbst behalten wollen. Das Aufteilen von Ichz magzfettezundzkursivezFonts sollte zu einem Array mit neun Strings führen: Ichzmagz, , fette, , zundz, , kursive, und zFonts.
Lösung C# Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck bearbeiten wollen: string[] splitArray = Regex.Split(subjectString, "()");
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings nutzen wollen: Regex regexObj = new Regex("()"); string[] splitArray = regexObj.Split(subjectString);
3.20 Einen String aufteilen und die Regex-Übereinstimmungen behalten | 217
VB.NET Sie können den statischen Aufruf nutzen, wenn Sie nur wenige Strings mit dem gleichen regulären Ausdruck bearbeiten wollen: Dim SplitArray = Regex.Split(SubjectString, "()")
Erstellen Sie ein Regex-Objekt, wenn Sie den gleichen regulären Ausdruck für viele Strings nutzen wollen: Dim RegexObj As New Regex("()") Dim SplitArray = RegexObj.Split(SubjectString)
Java List<String> resultList = new ArrayList<String>(); Pattern regex = Pattern.compile(""); Matcher regexMatcher = regex.matcher(subjectString); int lastIndex = 0; while (regexMatcher.find()) { resultList.add(subjectString.substring(lastIndex, regexMatcher.start())); resultList.add(regexMatcher.group()); lastIndex = regexMatcher.end(); } resultList.add(subjectString.substring(lastIndex));
JavaScript var list = []; var regex = //g; var match = null; var lastIndex = 0; while (match = regex.exec(subject)) { // Browser wie Firefox sollen nicht in einer Endlosschleife hängen bleiben if (match.index == regex.lastIndex) regex.lastIndex++; // Text vor der Übereinstimmung und sie selbst hinzufügen list.push(subject.substring(lastIndex, match.index), match[0]); lastIndex = match.index + match[0].length; } // Rest nach der letzten Übereinstimmung hinzufügen list.push(subject.substr(lastIndex));
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Tag vor Monat: ^(?: # Februar (jedes Jahr 29 Tage) (?[12][0-9]|0?[1-9])\.(?<month>0?2) | # Monate mit 30 Tagen (?30|[12][0-9]|0?[1-9])\.(?<month>0?[469]|11) | # Monate mit 31 Tagen (?3[01]|[12][0-9]|0?[1-9])\.(?<month>0?[13578]|1[02]) ) # Jahr \.(?(?:[0-9]{2})?[0-9]{2})$
Regex-Optionen: Freiform Regex-Varianten: .NET ^(?: # Februar (jedes Jahr 29 Tage) ([12][0-9]|0?[1-9])\.(0?2) | # Monate mit 30 Tagen (30|[12][0-9]|0?[1-9])\.([469]|11) | # Monate mit 31 Tagen (3[01]|[12][0-9]|0?[1-9])\.(0?[13578]|1[02]) ) # Jahr \.((?:[0-9]{2})?[0-9]{2})$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Es gibt im Prinzip zwei Wege, Datumswerte mit einem regulären Ausdruck exakt zu validieren. Der eine ist, eine einfache Regex zu verwenden, die eigentlich nur die Zahlengruppen einfängt, die wie eine Kombination aus Monat/Tag/Jahr beziehungsweise Tag.Monat.Jahr aussehen, und dann mit prozeduralem Code zu prüfen, ob das Datum korrekt ist. Ich habe die erste Regex aus dem vorhergehenden Rezept verwendet, die eine beliebige Zahl zwischen 0 und 39 für Tag und Monat zuließ. Damit lässt sich das Format leicht zwischen mm/dd/yy und dd.mm.yy wechseln, indem man sich aussucht, welche Gruppe den Tag und welche den Monat einfängt (abgesehen vom unterschiedlichen Trennzeichen). Der Hauptvorteil dieser Methode ist, dass Sie einfach zusätzliche Einschränkungen ergänzen können, wie zum Beispiel ein Einschränken auf bestimmte Zeiträume. Viele Programmiersprachen stellen eine ganze Reihe von Möglichkeiten bereit, mit Datumswerten zu arbeiten. Die C#-Lösung verwendet die .NET-Struktur DateTime, um zu prüfen, ob das Datum gültig ist, und es in einem sinnvollen Format zurückzugeben. Der andere Weg ist, alles mit einem regulären Ausdruck abzudecken. Das lässt sich dann erreichen, wenn wir uns die Freiheit nehmen, jedes Jahr als Schaltjahr zu betrachten. Wir können die gleiche Technik für die Alternativen verwenden, die wir schon im letzten Rezept genutzt haben. Mit einem einzelnen regulären Ausdruck haben wir aber nun das Problem, dass Tag und Monat nicht mehr übersichtlich in einer einfangenden Gruppe erfasst werden. Jetzt gibt es drei einfangende Gruppen für den Monat und drei für den Tag. Passt die Regex auf ein Datum, enthalten nur drei der sieben Gruppen in der Regex etwas. Handelt es sich um ein Datum im Februar, fangen die Gruppen 1 und 2 den Monat und den Tag (beziehungsweise umgekehrt). Hat der Monat 30 Tage, enthalten die Gruppen 3 und 4 diese Werte. Bei Monaten mit 31 Tagen muss man sich die Gruppen 5 und 6 anschauen. In Gruppe 7 ist immer das Jahr zu finden. In dieser Situation hilft uns nur die .NET-Variante. Hier können verschiedene benannte einfangende Gruppen (siehe Rezept 2.11) den gleichen Namen tragen und den gleichen Speicherplatz für ihren Wert verwenden. Nutzen Sie die .NET-Lösung mit benannten Captures, können Sie den Text, der durch die Gruppen „month“ und „day“ gefunden wurden, einfach auslesen, ohne sich darum Gedanken machen zu müssen, wie viele Tage der Monat hat. Alle anderen in diesem Buch behandelten Varianten lassen es nicht zu, dass zwei Gruppen den gleichen Namen tragen, oder sie geben nur den Text zurück, der von der letzten einfangenden Gruppe mit einem bestimmten Namen gefunden wurde. Bei diesen Varianten kann man nur mit nummerierten Captures arbeiten.
248 | Kapitel 4: Validierung und Formatierung
Die Lösung, die mit lediglich einer Regex arbeitet, ist nur dann interessant, wenn Sie auch nur eine Regex verwenden können – beispielsweise in dem Fall, dass eine Anwendung nur ein Eingabefeld für eine Regex anbietet. Beim Programmieren wird alles viel einfacher, wenn Sie ein bisschen Code darum herum bauen. Das ist vor allem dann hilfreich, wenn Sie die Datumswerte später noch verarbeiten wollen. Hier finden Sie eine reine Regex-Lösung, die ein Datum zwischen dem 2. Mai 2007 und dem 29. August 2008 im Format d.m.yy oder dd.mm.yyyy findet: # 2. Mai 2007 bis 29. August 2008 ^(?: # 2. Mai 2007 bis 31. Dezember 2007 (?: # 2. Mai bis 31. Mai (?3[01]|[12][0-9]|0?[2-9])\.(?<month>0?5)\.(?2007) | # 1. Juni bis 31. Dezember (?: # Monate mit 30 Tagen (?30|[12][0-9]|0?[1-9])\.(?<month>0?[69]|11) | # Monate mit 31 Tagen (?3[01]|[12][0-9]|0?[1-9])\.(?<month>0?[78]|1[02]) ) \.(?2007) ) | # 1. Januar 2008 bis 29. August 2008 (?: # 1. August bis 29. August (?[12][0-9]|0?[1-9])\.(?<month>0?8)\.(?2008) | # 1. Januar bis 31. Juli (?: # Februar (?[12][0-9]|0?[1-9])\.(?<month>0?2) | # Monate mit 30 Tagen (?30|[12][0-9]|0?[1-9])\.(?<month>0?[46]) | # Monate mit 31 Tagen (?3[01]|[12][0-9]|0?[1-9])\.(?<month>0?[1357]) ) \.(?2008) ) )$
Problem Sie wollen Zeitwerte in klassischen Formaten validieren. Dazu gehören hh:mm und hh:mm:ss – sowohl im 12-Stunden- als auch im 24-Stunden-Format.
Lösung Stunden und Minuten, 12 Stunden: ^(1[0-2]|0?[1-9]):([0-5]?[0-9])$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Stunden und Minuten, 24 Stunden: ^(2[0-3]|[01]?[0-9]):([0-5]?[0-9])$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Stunden, Minuten und Sekunden, 12 Stunden: ^(1[0-2]|0?[1-9]):([0-5]?[0-9]):([0-5]?[0-9])$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Stunden, Minuten und Sekunden, 24 Stunden: ^(2[0-3]|[01]?[0-9]):([0-5]?[0-9]):([0-5]?[0-9])$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Die Fragezeichen in allen angegebenen regulären Ausdrücken sorgen dafür, dass führende Nullen optional werden. Sollen sie immer angegeben werden, entfernen Sie die Fragezeichen.
Diskussion Das Validieren von Uhrzeiten ist deutlich einfacher als das Validieren von Datumswerten. Jede Stunde hat 60 Minuten und jede Minute 60 Sekunden. Somit brauchen wir keine komplizierten Alternationen in der Regex. Für Minuten und Sekunden sind sogar überhaupt keine Alternationen notwendig. ‹[0-5]?[0-9]› passt zu einer Ziffer zwischen 0 und 5, gefolgt von einer Ziffer zwischen 0 und 9. Damit werden alle Zahlen zwischen 0 und 59 gefunden. Durch das Fragezeichen nach der ersten Zeichenklasse wird diese optional. So wird auch eine einzelne Ziffer zwischen 0 und 9 als gültige Minute oder Sekunde
250 | Kapitel 4: Validierung und Formatierung
erkannt. Entfernen Sie das Fragezeichen, wenn die ersten zehn Minuten und Sekunden als 00 bis 09 geschrieben werden sollen. In Rezept 2.3 und Rezept 2.12 finden Sie Details zu Zeichenklassen und Quantoren. Bei den Stunden brauchen wir dann aber eine Alternation (siehe Rezept 2.8). Für die zweite Ziffer sind abhängig von der ersten Ziffer unterschiedliche Bereiche notwendig. Bei einer 12-Stunden-Uhr sind für die zweite Ziffer alle 10 Ziffern erlaubt, wenn die erste Ziffer eine 0 ist. Ist die erste Ziffer dagegen eine 1, muss die zweite Ziffer entweder 0, 1 oder 2 sein. Bei einem regulären Ausdruck schreiben wir das als ‹1[0-2]|0?[1-9]›. Bei einer 24-Stunden-Uhr sind alle zehn Ziffern für die zweite Ziffer erlaubt, wenn die erste Ziffer 0 oder 1 ist. Ist diese aber 2, muss die zweite Ziffer zwischen 0 und 3 liegen. In Regex-Syntax lässt sich das als ‹2[0-3]|[01]?[0-9]› darstellen. Auch hier dient das Fragezeichen wieder dazu, dass die ersten zehn Stunden als einzelne Ziffer geschrieben werden dürfen. Wenn Sie es entfernen, müssen immer zwei Ziffern angegeben werden. Wir haben die Teile der Regex, mit der die Stunden, Minuten und Sekunden gefunden werden, mit Klammern versehen. Dadurch ist es einfach, die Ziffern ohne die Trennzeichen auszulesen. In Rezept 2.9 ist erklärt, wie Klammern für einfangende Gruppen genutzt werden. Und in Rezept 3.9 wird beschrieben, wie Sie den von solchen einfangenden Gruppen gefundenen Text mithilfe von prozeduralem Code auslesen können. Die Klammern um den Stundenteil sorgen dafür, dass die zwei Alternativen für die Stunden zusammengehalten werden. Entfernen Sie die Klammern, wird die Regex nicht mehr richtig funktionieren. Entfernen Sie die Klammern um die Minuten und Sekunden, hat das keine Auswirkungen, außer dass Sie dann die Ziffern nicht mehr einzeln auslesen können.
Variationen Wenn Sie in längeren Texten nach Uhrzeiten suchen wollen, statt zu prüfen, ob eine Eingabe nur aus einer Zeitangabe besteht, können Sie die Anker ‹^› und ‹$› nicht nutzen. Es reicht aber auch nicht aus, die Anker einfach zu entfernen. Damit würden Regexes für Stunden und Minuten den Wert 12:12 innerhalb von 9912:1299 finden. Statt den Anfang und das Ende des Texts mit Anfang und Ende der Regex zu verknüpfen, müssen Sie festlegen, dass die Uhrzeit kein Teil einer längeren Folge von Ziffern sein kann. Das lässt sich leicht mit einem Paar Wortgrenzen erreichen. In regulären Ausdrücken werden Ziffern als Wortzeichen behandelt. Ersetzen Sie also sowohl ‹^› als auch ‹$› durch ‹\b›, beispielsweise: \b(2[0-3]|[01]?[0-9]):([0-5]?[0-9])\b
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Wortgrenzen verhindern nicht alles – nur Buchstaben, Ziffern und den Unterstrich. Die hier gezeigte Regex passt zu Stunden und Minuten auf einer 24-Stunden-Uhr, findet aber auch 16:08 im Text Es ist jetzt genau 16:08:42. Das Leerzeichen ist kein Wortbuch-
4.6 Klassische Zeitformate validieren | 251
stabe, die 1 aber schon, daher passt die Wortgrenze dazwischen. Die 8 ist ein Wortzeichen, der Doppelpunkt aber nicht, also passt ‹\b› auch hier zwischen. Wenn Sie sowohl Doppelpunkte als auch Wortzeichen verhindern wollen, müssen Sie ein Lookaround einsetzen (siehe Rezept 2.16). Die folgende Regex findet den UhrzeitTeil von Es ist jetzt genau 16:08:42 nicht. Allerdings funktioniert sie nur mit Varianten, die Lookbehinds zulassen: (? Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby 1.9
Siehe auch Rezepte 4.4, 4.5 und 4.7.
4.7
Datums- und Uhrzeitwerte im Format ISO 8601 validieren
Problem Sie wollen Datums- und/oder Uhrzeitwerte im offiziellen Format ISO 8601 finden. Dieses Format ist die Grundlage vieler standardisierter Datums- und Zeitformate. So basieren zum Beispiel in XML Schema die eingebauten Typen date, time und dateTime auf ISO 8601.
Lösung Die folgende Regex findet einen Kalendermonat, zum Beispiel 2008-08. Der Bindestrich ist dabei verpflichtend: ^([0-9]{4})-(1[0-2]|0[1-9])$
Regex-Optionen: Keine Regex-Varianten: PCRE, Python Datum, zum Beispiel 2008-08-30. Die Bindestriche sind optional. Diese Regex erlaubt allerdings YYYY-MMDD und YYYYMM-DD, was nicht ISO 8601 entspricht: ^([0-9]{4})-?(1[0-2]|0[1-9])-?(3[0-1]|0[1-9]|[1-2][0-9])$
Regex-Optionen: Keine Regex-Varianten: .NET, PCRE 7, Perl 5.10, Ruby 1.9 Datum, zum Beispiel 2008-08-30. Die Bindestriche sind optional. Diese Regex nutzt eine Bedingung, um YYYY-MMDD und YYYYMM-DD auszuschließen. Es gibt eine zusätzliche einfangende Gruppe für den ersten Bindestrich: ^([0-9]{4})(-)?(1[0-2]|0[1-9])(?(2)-)(3[0-1]|0[1-9]|[1-2][0-9])$
Regex-Optionen: Keine Regex-Varianten: .NET, PCRE, Perl, Python Datum, zum Beispiel 2008-08-30. Die Bindestriche sind optional. Diese Regex nutzt eine Alternation, um YYYY-MMDD und YYYYMM-DD auszuschließen. Es gibt zwei einfangende Gruppen für den Monat: ^([0-9]{4})(?:(1[0-2]|0[1-9])|-?(1[0-2]|0[1-9])-?) (3[0-1]|0[1-9]|[1-2][0-9])$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Kalenderwoche, zum Beispiel 2008-W35. Der Bindestrich ist optional: ^([0-9]{4})-?W(5[0-3]|[1-4][0-9]|0[1-9])$
Regex-Optionen: Keine Regex-Varianten: .NET, PCRE 7, Perl 5.10, Ruby 1.9 Tag einer Woche, zum Beispiel 2008-W35-6. Die Bindestriche sind optional: ^([0-9]{4})-?W(5[0-3]|[1-4][0-9]|0[1-9])-?([1-7])$
Regex-Optionen: Keine Regex-Varianten: .NET, PCRE 7, Perl 5.10, Ruby 1.9 Tag eines Jahres, zum Beispiel 2008-243. Der Bindestrich ist optional: ^([0-9]{4})-?(36[0-6]|3[0-5][0-9]|[12][0-9]{2}|0[1-9][0-9]|00[1-9])$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
4.7 Datums- und Uhrzeitwerte im Format ISO 8601 validieren |
Regex-Optionen: Keine Regex-Varianten: .NET, PCRE 7, Perl 5.10, Ruby 1.9 Stunden und Minuten, zum Beispiel 17:21. Der Doppelpunkt ist optional: ^(2[0-3]|[01]?[0-9]):?([0-5]?[0-9])$
Regex-Optionen: Keine Regex-Varianten: .NET, PCRE 7, Perl 5.10, Ruby 1.9 Stunden, Minuten und Sekunden, zum Beispiel 17:21:59. Die Doppelpunkte sind optional: ^(2[0-3]|[01]?[0-9]):?([0-5]?[0-9]):?([0-5]?[0-9])$
Regex-Optionen: Keine Regex-Varianten: .NET, PCRE 7, Perl 5.10, Ruby 1.9 Zeitzonenangabe, zum Beispiel Z, +07 oder +07:00. Der Doppelpunkt und die Minuten sind optional: ^(Z|[+-](?:2[0-3]|[01]?[0-9])(?::?(?:[0-5]?[0-9]))?)$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Stunden, Minuten und Sekunden mit Zeitzonenangabe, zum Beispiel 17:21:59+07:00. Alle Doppelpunkte sind optional. Die Minuten in der Zeitzonenangabe sind ebenfalls optional: ^(2[0-3]|[01]?[0-9]):?([0-5]?[0-9]):?([0-5]?[0-9]) (Z|[+-](?:2[0-3]|[01]?[0-9])(?::?(?:[0-5]?[0-9]))?)$
Regex-Optionen: Keine Regex-Varianten: .NET, PCRE 7, Perl 5.10, Ruby 1.9
254 | Kapitel 4: Validierung und Formatierung
Datum mit optionaler Zeitzone, zum Beispiel 2008-08-30 oder 2008-08-30+07:00. Bindestriche sind erforderlich. Das ist der XML Schema-Typ date: ^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[0-1]|0[1-9]|[1-2][0-9]) (Z|[+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])?$
Regex-Optionen: Keine Regex-Varianten: .NET, PCRE 7, Perl 5.10, Ruby 1.9 Uhrzeit mit optionalen Sekundenbruchteilen und Zeitzone, zum Beispiel 01:45:36 oder 01:45:36.123+07:00. Das ist der XML Schema-Typ time: ^(2[0-3]|[0-1][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)? (Z|[+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])?$
Regex-Optionen: Keine Regex-Varianten: .NET, PCRE 7, Perl 5.10, Ruby 1.9 Datum und Uhrzeit mit optionalen Sekundenbruchteilen und Zeitzone, zum Beispiel 2008-08-30T01:45:36 oder 2008-08-30T01:45:36.123Z. Das ist der XML Schema-Typ dateTime: ^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-(3[0-1]|0[1-9]|[1-2][0-9]) T(2[0-3]|[0-1][0-9]):([0-5][0-9]):([0-5][0-9])(\.[0-9]+)? (Z|[+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])?$
Regex-Optionen: Keine Regex-Varianten: .NET, PCRE 7, Perl 5.10, Ruby 1.9
Diskussion Die ISO 8601 definiert eine große Anzahl von Datums- und Zeitformaten. Die hier vorgestellten regulären Ausdrücke kümmern sich um die gebräuchlichsten Formate, aber die meisten Systeme, die ISO 8601 verwenden, greifen nur auf eine Untermenge zurück. So sind zum Beispiel bei Datums- und Zeitwerten in XML Schema die Bindestriche und 4.7 Datums- und Uhrzeitwerte im Format ISO 8601 validieren |
255
Doppelpunkte nicht optional. Um diese Zeichen in den regulären Ausdrücken einzufordern, entfernen Sie einfach die Fragezeichen hinter ihnen. Passen Sie aber auf nicht-einfangende Gruppen auf, die die Syntax ‹(?:Gruppe)› verwenden. Wenn ein Fragezeichen und ein Doppelpunkt auf eine öffnende Klammer folgen, sind diese drei Zeichen der Anfang einer nicht-einfangenden Gruppe. Bei den regulären Ausdrücken sind die einzelnen Bindestriche und Doppelpunkte optional, was nicht ganz ISO 8601 entspricht. So ist zum Beispiel 1733:26 nach ISO 8601 keine gültige Zeit, sie wird aber von den Zeit-Regexes akzeptiert. Wenn alle Bindestriche und Doppelpunkte gleichzeitig vorhanden oder eben nicht vorhanden sein sollen, wird Ihre Regex ein ganzes Stück komplexer. Wir haben das als Beispiel für die Datums-Regex gezeigt, aber im Alltag sind die Trennzeichen im Allgemeinen entweder auf jeden Fall erforderlich (wie bei XML Schema) oder immer verboten und nicht optional. Wir haben um alle Zahlenelemente der Regex Klammern gelegt. Damit ist es einfach, die Werte für Jahr, Monat, Tag, Stunden, Minuten, Sekunden und Zeitzonen auszulesen. In Rezept 2.9 wird beschrieben, wie Klammern einfangende Gruppen definieren. Rezept 3.9 zeigt Ihnen, wie Sie den von diesen einfangenden Gruppen gefundenen Text mithilfe von prozeduralem Code auslesen können. Bei den meisten Regexes haben wir auch eine Alternative mit benannten Captures präsentiert. Manche dieser Datums- und Zeitformate sind Ihnen oder Ihren Kollegen vielleicht unbekannt. Benannte Captures erleichtern das Verstehen der Regex. .NET, PCRE 7, Perl 5.10 und Ruby 1.9 unterstützen die Syntax ‹(?‹Name›Gruppe)›. Alle Versionen von PCRE und Python, die in diesem Buch behandelt werden, bieten auch die alternative Syntax ‹(?P‹Name›Gruppe)› an, in der ein zusätzliches ‹P› enthalten ist. In Rezept 2.11 und Rezept 3.9 finden Sie die Details dazu. Die Zahlenbereiche in allen Regexes sind sehr strikt. So ist zum Beispiel der Kalendertag auf Werte zwischen 01 und 31 eingeschränkt. Sie werden nie einen Tag 32 oder einen Monat 13 erhalten. Allerdings versucht keine der vorgestellten Regexes, ungültige Kombinationen aus Tag und Monat auszuschließen, wie zum Beispiel den 31. Februar. In Rezept 4.5 beschreiben wir, wie Sie dieses Problem angehen können. Auch wenn manche dieser Regexes ziemlich lang sind, gibt es in ihnen dennoch keine exotischen Verrenkungen, und alle nutzen die gleichen Techniken, die in Rezept 4.4 und Rezept 4.6 erläutert wurden.
Siehe auch Rezepte 4.4, 4.5, 4.6.
256 | Kapitel 4: Validierung und Formatierung
4.8
Eingabe auf alphanumerische Zeichen beschränken
Problem Bei Ihrer Anwendung soll der Nutzer bei einer Eingabe nur alphanumerische Zeichen aus dem englischen Alphabet eingeben dürfen.
Lösung Mit den Ihnen zur Verfügung stehenden regulären Ausdrücken ist die Lösung ganz einfach. Eine Zeichenklasse kann den erlaubten Bereich mit Zeichen festlegen. Mit einem Quantor, der die Zeichenklasse ein Mal oder mehrfach zulässt, und Ankern, die die Übereinstimmung mit dem Anfang und dem Ende des Strings verbinden, sind Sie schon fertig.
Ruby if subject =~ /^[A-Z0-9]+$/i puts "Text ist alphanumerisch" else puts "Text ist nicht alphanumerisch" end
Andere Programmiersprachen In den Rezepten 3.4 und 3.5 finden Sie Informationen über das Implementieren dieses regulären Ausdrucks in anderen Programmiersprachen.
Diskussion Lassen Sie uns die vier Teile dieses regulären Ausdrucks nacheinander anschauen: ^ [A-Z0-9] + $
# Sicherstellen, dass die Übereinstimmung am Anfang des Texts beginnt. # Ein Zeichen zwischen "A" und "Z" oder zwischen "0" und "9" finden... # einmal bis unbegrenzt oft. # Sicherstellen, dass die Übereinstimmung am Ende des Texts endet.
4.8 Eingabe auf alphanumerische Zeichen beschränken | 257
Die Anker ‹^› und ‹$› am Anfang und Ende des regulären Ausdrucks sorgen dafür, dass die gesamte Eingabe überprüft wird. Ohne sie könnte die Regex auch Teile eines längeren Strings finden, sodass doch ungültige Zeichen übersehen werden. Durch den Quantor ‹+› kann das vorherige Element einmal oder häufiger vorkommen. Wenn Ihre Regex auch einen vollständig leeren String erkennen soll, können Sie das ‹+› durch ‹*› ersetzen. Der Stern-Quantor ‹*› erlaubt, dass ein Element null Mal oder häufiger vorkommt, wodurch dieses Element im Endeffekt optional wird.
Variationen Eingabe auf ASCII-Zeichen einschränken Der folgende reguläre Ausdruck beschränkt die Eingabe auf die 128 Zeichen in der 7-BitASCII-Tabelle. Dazu gehören auch 33 nicht sichtbare Steuerzeichen: ^[\x00-\x7F]+$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Eingabe auf ASCII-Zeichen ohne Steuerzeichen und auf Zeilenumbrüche einschränken Mit dem folgenden regulären Ausdruck beschränken Sie die Eingabe auf sichtbare Zeichen und Whitespace in der ASCII-Tabelle. Steuerzeichen werden damit ausgeschlossen. Die Zeichen für Line Feed und Carriage Return (mit den Werten 0x0A und 0x0D) sind die gebräuchlichsten Steuerzeichen, daher werden sie hier explizit durch ‹\n› (Line Feed) und ‹\r› (Carriage Return) mit aufgenommen: ^[\n\r\x20-\x7E]+$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Eingabe auf die Zeichen begrenzen, die sowohl in ISO-8859-1 als auch in Windows1252 vorkommen ISO-8859-1 und Windows-1252 (häufig als ANSI bezeichnet) sind zwei häufig genutzte Zeichenkodierungen mit 8 Bit Breite, die beide auf dem Latin-1-Standard basieren (genauer gesagt, auf ISO/IEC 8859-1). Allerdings sind die Zeichen an den Positionen von 0x80 und 0x9F inkompatibel. ISO-8859-1 nutzt diese Positionen für Steuerzeichen, während Windows-1252 dort noch mehr Buchstaben und Satzzeichen abgelegt hat. Diese Unterschiede führen manchmal zu Problemen beim Anzeigen von Zeichen, insbesondere bei Dokumenten, die ihre Kodierung nicht angeben, oder bei denen der Empfänger ein Nicht-Windows-System verwendet. Der folgende reguläre Ausdruck kann dazu verwendet werden, die Eingabe auf Zeichen zu beschränken, die in beiden Zeichentabellen ISO8859-1 und Windows-1252 vorhanden sind (einschließlich der gemeinsamen Steuerzeichen):
258 | Kapitel 4: Validierung und Formatierung
^[\x00-\x7F\xA0-\xFF]+$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Durch die hexadezimale Schreibweise ist dieser reguläre Ausdruck vielleicht etwas schlechter zu lesen, aber er arbeitet genau so wie die weiter oben gezeigte Zeichenklasse ‹[A-Z0-9]›. Die Regex passt auf Zeichen in zwei Bereichen: \x00-\x7F und \xA0-\xFF.
Eingabe auf alphanumerische Zeichen in beliebigen Sprachen einschränken Dieser reguläre Ausdruck beschränkt die Eingabe auf Buchstaben und Ziffern aus beliebigen Sprachen oder Schriften. Er verwendet eine Zeichenklasse, die Eigenschaften für alle Codepoints in den Buchstaben- und Ziffernkategorien von Unicode enthält: ^[\p{L}\p{N}]+$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Ruby 1.9 Leider werden Unicode-Eigenschaften nicht von allen in diesem Buch behandelten Regex-Varianten unterstützt. Vor allem funktioniert diese Regex nicht in JavaScript, Python und Ruby 1.8. Zudem muss die PCRE mit UTF-8-Unterstützung kompiliert werden, will man diese Regex dort nutzen. Unicode-Eigenschaften können in den preg-Funktionen von PHP genutzt werden (die auf der PCRE aufbauen), wenn an die Regex die Option /u angehängt wird. Mit der folgenden Regex kann man in Python die fehlende Unterstützung von UnicodeEigenschaften umgehen: ^[^\W_]+$
Regex-Optionen: Unicode Regex-Variante: Python Hier umgehen wir die in Python fehlenden Unicode-Eigenschaften, indem wir den Schalter UNICODE oder U beim Erstellen des regulären Ausdrucks verwenden. Dadurch wird die Bedeutung einiger Regex-Tokens so verändert, dass sie auf die Unicode-Zeichentabelle zugreifen. ‹\w› bringt uns schon recht weit, weil damit alphanumerische Zeichen und der Unterstrich gefunden werden. Durch die inverse Abkürzung ‹\W› in einer negierten Zeichenklasse können wir den Unterstrich aus dieser Menge entfernen. Solche doppelt negierten Elemente sind in regulären Ausdrücken manchmal recht nützlich, auch wenn man eventuell erst einmal seinen Grips dafür anstrengen muss.1
1
Wenn Sie noch mehr Spaß haben wollen (für sehr seltsame Definitionen von „Spaß”), können Sie versuchen, dreifache, vierfache oder noch „höher-fache“ Negierungen zu erzeugen, indem Sie negative Lookarounds (siehe Rezept 2.16) und Zeichenklassen-Subtraktionen (siehe Rezept 2.3) ins Spiel bringen.
4.8 Eingabe auf alphanumerische Zeichen beschränken | 259
Siehe auch In Rezept 4.9 wird gezeigt, wie man die Länge des Texts einschränkt.
4.9
Die Länge des Texts begrenzen
Problem Sie wollen prüfen, ob ein String zwischen einem und zehn Buchstaben von A bis Z enthält.
Lösung Alle Programmiersprachen, die in diesem Buch behandelt werden, stellen eine einfache und effiziente Möglichkeit bereit, die Länge eines Texts zu überprüfen. So haben zum Beispiel JavaScript-Strings eine Eigenschaft length mit einer Integer-Zahl, die die Länge des Strings angibt. Aber manchmal kann es auch sinnvoll sein, die Länge eines Texts mit einem regulären Ausdruck zu prüfen, insbesondere wenn die Länge nur eine von mehreren Regeln ist, die bestimmen, ob der Ausgangstext in das gewünschte Muster passt. Der folgende reguläre Ausdruck stellt sicher, dass der Text zwischen 1 und 10 Zeichen lang ist. Zudem schränkt er den Text auf die Großbuchstaben A bis Z ein. Sie können die regulären Ausdrücke so verändern, dass sie eine minimale oder maximale Textlänge definieren, aber auch andere Zeichen als A bis Z zulassen.
Regulärer Ausdruck ^[A-Z]{1,10}$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Perl if ($ARGV[0] =~ /^[A-Z]{1,10}$/) { print "Eingabe ist gültig\n"; } else { print "Eingabe ist ungültig\n"; }
Andere Programmiersprachen In Rezept 3.5 finden Sie Informationen darüber, wie Sie diesen regulären Ausdruck in anderen Programmiersprachen implementieren können.
260 | Kapitel 4: Validierung und Formatierung
Diskussion Hier ist die Aufteilung dieser sehr einfachen Regex in ihre Bestandteile: ^ [A-Z] {1,10} $
# Sicherstellen, dass die Regex am Textanfang passt. # Einen der Buchstaben von "A" bis "Z" finden ... # zwischen 1 und 10 Mal. # Sicherstellen, dass die Regex am Textende passt.
Regex-Optionen: Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Die Anker ‹^› und ‹$› stellen sicher, dass die Regex den gesamten Ausgangstext umfasst. Ansonsten könnte es sein, dass sie nur zehn Zeichen innerhalb eines längeren Texts findet. Die Zeichenklasse ‹[A-Z]› passt auf einen einzelnen Großbuchstaben von A bis Z. Der Intervall-Quantor ‹{1,10}› sorgt dafür, dass die Zeichenklasse zwischen einem und zehn Mal vorkommen kann. Indem man den Intervall-Quantor mit den Textanfangsund -ende-Ankern kombiniert, wird die Regex fehlschlagen, wenn die Länge des Ausgangstexts außerhalb des gewünschten Bereichs liegt. Beachten Sie, dass die Zeichenklasse ‹[A-Z]› explizit nur Großbuchstaben zulässt. Wenn Sie auch die Kleinbuchstaben a bis z aufnehmen wollen, können Sie entweder als Zeichenklasse ‹[A-Za-z]› verwenden oder die Option zum Ignorieren von Groß- und Kleinschreibung nutzen. In Rezept 3.4 ist nachzulesen, wie man das tut. Ein von Anfängern häufig gemachter Fehler ist, ein paar Tastendrücke dadurch zu sparen, dass man den Zeichenklassenbereich ‹[A-z]› nutzt. Auf den ersten Blick sieht das eigentlich ganz praktisch aus. Aber die ASCII-Zeichentabelle enthält zwischen den Bereichen von A bis Z und a bis z eine Reihe von Satzzeichen. Daher entspricht ‹[A-z]› in Wirklichkeit ‹[A-Z[\]^_`a-z]›.
Variationen Die Länge eines bestimmten Musters begrenzen Da Quantoren wie ‹{1,10}› nur auf das direkt vor ihnen stehende Element wirken, muss man etwas anders vorgehen, wenn man die Zeichenzahl von Mustern begrenzen will, die mehr als ein einzelnes Token enthalten. Wie in Rezept 2.16 beschrieben, sind Lookaheads (und ihre Gegenstücke, die Lookbehinds) besondere Arten von Zusicherungen, die wie ‹^› und ‹$› auf eine Position innerhalb des Ausgangstexts passen, aber keine Zeichen konsumieren. Lookaheads können entweder positiv oder negativ sein. Das bedeutet, man kann mit ihnen prüfen, ob auf die aktuelle Position ein Muster folgt – oder eben nicht. Ein positives Lookahead, das die Syntax ‹(?=...)› hat, kann am Anfang des Musters genutzt werden, um sicherzustellen, dass die Länge des Strings innerhalb des Zielbereichs liegt. Der Rest der Regex kann dann das gewünschte Muster prüfen, ohne sich darum kümmern zu müssen, wie lang der Text ist. Hier ein einfaches Beispiel:
4.9 Die Länge des Texts begrenzen | 261
^(?=.{1,10}$).*
Regex-Optionen: Punkt passt zu Zeilenumbruch Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby ^(?=[\S\s]{1,10}$)[\S\s]*
Regex-Optionen: Keine Regex-Variante: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Es ist wichtig, dass sich der Anker ‹$› innerhalb des Lookaheads befindet, denn das Prüfen der maximalen Länge funktioniert nur, wenn wir sicherstellen, dass es nach dem Erreichen der Grenze keine weiteren Zeichen gibt. Da das Lookahead die Länge des Bereichs am Anfang der Regex sicherstellt, kann das folgende Muster dann beliebige zusätzliche Validierungsregeln umsetzen. In diesem Fall wird das Muster ‹.*› (oder ‹[\S\s]*› für JavaScript) genutzt, um einfach den gesamten Ausgangstext zu finden – ohne zusätzliche Einschränkungen. Die erste Regex verwendet die Option Punkt passt auf Zeilenumbruch, damit der Punkt wirklich alle Zeichen findet – auch Zeilenumbrüche. In Rezept 3.4 finden Sie Details über das Anwenden dieses Modifikators in Ihrer Programmiersprache. Die Regex für JavaScript sieht anders aus, da JavaScript die Option Punkt passt auf Zeilenumbruch nicht besitzt. In „Jedes Zeichen einschließlich Zeilenumbrüchen“ auf Seite 37 in Rezept 2.4 finden Sie weitere Informationen.
Anzahl der Zeichen ohne Whitespace einschränken Die folgende Regex passt auf Strings, die zwischen 10 und 100 Nicht-Whitespace-Zeichen enthalten: ^\s*(?:\S\s*){10,100}$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby In Java, PCRE, Python und Ruby passt ‹\s› nur auf ASCII-Whitespace-Zeichen und ‹\S› auf alles andere. In Python können Sie durch die Option UNICODE oder U beim Erzeugen der Regex dafür sorgen, dass ‹\s› jeden Unicode-Whitespace berücksichtigt. Entwickler, die mit Java, der PCRE oder Ruby 1.9 arbeiten und verhindern wollen, dass UnicodeWhitespace beim Zählen berücksichtigt wird, können die folgende Version nutzen, die von den Unicode-Eigenschaften Gebrauch macht (beschrieben in Rezept 2.7): ^[\p{Z}\s]*(?:[^\p{Z}\s][\p{Z}\s]*){10,100}$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Ruby 1.9 Die PCRE muss mit UTF-8-Unterstützung kompiliert werden, damit diese Regex wie gewünscht funktioniert. In PHP müssen Sie die UTF-8-Unterstützung durch den Modifikator /u aktivieren.
262 | Kapitel 4: Validierung und Formatierung
Diese Regex kombiniert die Unicode-Eigenschaft „Separator“ ‹\p{Z}› mit der Zeichenklassenabkürzung ‹\s› für Whitespace. Das liegt daran, dass Zeichen, die von ‹\p{Z}› und ‹\s› gefunden werden, nicht unbedingt identisch sind. Zu ‹\s› gehören die Zeichen an den Positionen 0x09 bis 0x0D (Tab, Line Feed, vertikaler Tab, Form Feed und Carriage Return), die nicht der Separator-Eigenschaft im Unicode-Standard zugewiesen sind. Indem Sie ‹\p{Z}› und ‹\s› in einer Zeichenklasse kombinieren, stellen Sie sicher, dass alle Whitespace-Zeichen gefunden werden. In beiden Regexes wird der Intervall-Quantor ‹{10,100}› auf die vor ihm stehende nichteinfangende Gruppe angewendet und nicht nur auf ein einzelnes Token. Die Gruppe passt zu jedem einzelnen Nicht-Whitespace-Zeichen, dem null oder mehr WhitespaceZeichen folgen. Der Intervall-Quantor kann zuverlässig erkennen, wie viele NichtWhitespace-Zeichen gefunden wurden, da während jeder Iteration nur genau ein NichtWhitespace-Zeichen passt.
Anzahl der Wörter einschränken Die folgende Regex ähnelt dem vorigen Beispiel, bei dem die Anzahl der NichtWhitespace-Zeichen begrenzt wird. Nur passt hier jede Wiederholung auf ein ganzes Wort und nicht auf ein einzelnes Nicht-Whitespace-Zeichen. Es passt auf 10 bis 100 Wörter, wobei alle Nicht-Wortzeichen ignoriert werden – auch Satzzeichen und Whitespace: ^\W*(?:\w+\b\W*){10,100}$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby In Java, JavaScript, PCRE und Ruby passt das Wortzeichen-Token ‹\w› in dieser Regex nur auf die ASCII-Zeichen A–Z, a–z, 0–9 und _. Daher lassen sich damit Wörter mit Nicht-ASCII-Buchstaben und -Zahlen nicht korrekt zählen. In .NET und Perl basiert ‹\w› auf der Unicode-Tabelle (genauso wie das Gegen-Token ‹\W› und die Wortgrenze ‹\b›) und passt daher auf Buchstaben und Ziffern aus allen Unicode-Schriftsystemen. In Python können Sie selbst wählen, ob diese Tokens auf Unicode basieren sollen oder nicht. Das hängt davon ob, ob Sie die Option UNICODE oder U beim Erstellen der Regex mit angeben. Wenn Sie Wörter zählen wollen, die Nicht-ASCII-Buchstaben und -Zahlen enthalten, können die folgenden Regexes dies für weitere Regex-Varianten ermöglichen: ^[^\p{L}\p{N}_]*(?:[\p{L}\p{N}_]+\b[^\p{L}\p{N}_]*){10,100}$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, Perl ^[^\p{L}\p{N}_]*(?:[\p{L}\p{N}_]+(?:[^\p{L}\p{N}_]+|$)){10,100}$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Ruby 1.9
4.9 Die Länge des Texts begrenzen | 263
Die PCRE muss mit UTF-8-Unterstützung kompiliert werden, damit diese Regex dort funktioniert. In PHP schalten Sie die UTF-8-Unterstützung mit dem Muster-Modifikator /u ein. Wie schon in „Wortzeichen“ auf Seite 46 in Rezept 2.6 erwähnt, ist der Grund für diese verschiedenen (aber gleich funktionierenden) Regexes der unterschiedliche Umgang mit Wortzeichen und Wortgrenzen. Die letzten beiden Regexes nutzen Zeichenklassen, die die Unicode-Eigenschaften für Buchstaben und Zahlen verwenden (‹\p{L}› und ‹\p{N}›). Dazu wurde noch der Unterstrich mit aufgenommen, damit sich die Zeichenklassen genau so verhalten wie bei den anderen Regexes, die auf ‹\w› und ‹\W› aufbauen. Jede Wiederholung der nicht-einfangenden Gruppe in den ersten beiden dieser drei Regexes passt zu einem vollständigen Wort, auf das null oder mehr Nicht-Wortzeichen folgen. Das Token ‹\W› (oder ‹[^\p{L}\p{N}_]›) innerhalb der Gruppe ist optional, falls der String mit einem Wortzeichen endet. Da damit aber die Nicht-Wortzeichen-Folge während des Suchprozesses optional würde, brauchen wir die Wortgrenzenzusicherung ‹\b› zwischen ‹\w› und ‹\W› (oder ‹[\p{L}\p{N}_]› und ‹[^\p{L}\p{N}_]›). Damit ist sichergestellt, dass jede Wiederholung der Gruppe wirklich ein vollständiges Wort findet. Ohne die Wortgrenze würde eine einzelne Wiederholung einen beliebigen Teil eines Worts finden, und die folgenden Wiederholungen könnten dann auf zusätzliche Teile passen. Die dritte Version der Regex (durch die auch PCRE und Ruby 1.9 mitmachen können) funktioniert ein bisschen anders. Sie verwendet einen Plus- (eins oder mehr) statt eines Stern-Quantors (null oder mehr) und lässt explizit nur dann null Zeichen zu, wenn der Suchprozess schon am Ende des Strings angelangt ist. Damit wird das WortgrenzenToken vermieden, was für eine genauere Suche wichtig war, da ‹\b› in der PCRE und in Ruby nicht Unicode-kompatibel ist. In Java ist ‹\b› Unicode-kompatibel, ‹\w› allerdings nicht. Leider ist es mit keiner dieser Optionen möglich, in JavaScript oder Ruby 1.8 korrekt mit Wörtern umzugehen, die Nicht-ASCII-Zeichen verwenden. Eine mögliche Alternative ist, die Regex so umzubauen, dass sie Whitespace-Sequenzen statt Wörter zählt: ^\s*(?:\S+(?:\s+|$)){10,100}$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, Perl, PCRE, Python, Ruby Das funktioniert in vielen Fällen genau so wie die vorherigen Lösungen, ist aber nicht genau das Gleiche. So werden hier zum Beispiel Wörter mit Bindestrichen (wie „SBahn“) als ein Wort gezählt und nicht mehr als zwei Wörter.
Siehe auch Rezepte 4.8 und 4.10.
264 | Kapitel 4: Validierung und Formatierung
4.10 Die Zeilenanzahl eines Texts beschränken Problem Sie müssen prüfen, ob ein String aus fünf oder weniger Zeilen besteht, wobei die Gesamtlänge des Strings unwichtig ist.
Lösung Die Zeichen oder Zeichenfolgen, die als Zeilentrenner genutzt werden, können sehr stark von Ihrem Betriebssystem, der Anwendung oder den Einstellungen des Benutzers abhängig sein. Daher stellt sich beim Schaffen einer idealen Lösung die Frage, welche Konventionen unterstützt werden sollen, um den Anfang einer neuen Zeile zu erkennen. Die folgenden Lösungen unterstützen den Standard von MS-DOS/Windows (‹\r\n›), dem alten Mac OS (‹\r›) und Unix/Linux/OS X (‹\n›).
Regulärer Ausdruck Die folgenden drei variantenspezifischen Regexes besitzen zwei Unterschiede. Die erste Regex verwendet atomare Gruppen, die als ‹(?>...)› geschrieben werden, statt auf nicht-einfangende Gruppen zurückzugreifen (‹(?:...)›), denn dadurch können die Regex-Varianten, die sie unterstützen, eventuell einen kleinen Geschwindigkeitsvorteil erhalten. Python und JavaScript bieten keine atomaren Gruppen, daher werden sie bei diesen Varianten nicht verwendet. Der andere Unterschied sind die Tokens, die genutzt werden, um die Position am Anfang und Ende des Strings sicherzustellen (‹\A› oder ‹^› für den Anfang des Strings und ‹\z›, ‹\Z› oder ‹$› für dessen Ende). Die Gründe für diese Unterschiede werden später noch genauer beleuchtet. Alle drei variantenspezifischen Regexes passen auf genau die gleichen Strings: \A(?>(?>\r\n?|\n)?[^\r\n]*){0,5}\z
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Ruby \A(?:(?:\r\n?|\n)?[^\r\n]*){0,5}\Z
Regex-Optionen: Keine Regex-Variante: Python ^(?:(?:\r\n?|\n)?[^\r\n]*){0,5}$
Regex-Optionen: Keine Regex-Variante: JavaScript
4.10 Die Zeilenanzahl eines Texts beschränken | 265
PHP (PCRE) if (preg_match('/\A(?>(?>\r\n?|\n)?[^\r\n]*){0,5}\z/', $_POST['subject'])) { print 'Text enthält fünf oder weniger Zeilen'; } else { print 'Text enthält mehr als fünf Zeilen'; }
Andere Programmiersprachen In Rezept 3.5 finden Sie Informationen dazu, wie diese regulären Ausdrücke mit anderen Programmiersprachen implementiert werden können.
Diskussion Alle in diesem Rezept bis hierhin gezeigten regulären Ausdrücke nutzen eine Gruppe, die zu einem Zeilenumbruch in MS-DOS/Windows, dem alten Mac OS oder in Unix/Linux/OS X passen, gefolgt von einer beliebigen Zahl von Zeichen, die kein Zeilenumbruch sind. Diese Gruppe wird zwischen null und fünf Mal wiederholt, da wir bis zu fünf Zeilen finden wollen. Im folgenden Beispiel haben wir die JavaScript-Version der Regex in ihre Bestandteile zerlegt. Die JavaScript-Version haben wir hier deshalb verwendet, weil ihre Elemente vermutlich den meisten Lesern bekannt sein dürften. Wir werden die Versionen für andere Regex-Varianten im Anschluss behandeln: ^ (?: (?: \r \n ? | \n ) ? [^\r\n] * ) {0,5} $
# # # # # # # # # # # # # # #
Position am Anfang des Strings sicherstellen. Gruppieren, aber nicht einfangen ... Gruppieren, aber nicht einfangen ... Ein Carriage Return (CR, ASCII-Position 0x0D) finden. Ein Line Feed (LF, ASCII-Position 0x0A) finden ... null oder ein Mal. oder ... Ein Line Feed finden. Ende der nicht-einfangenden Gruppe. Die vorige Gruppe null oder ein Mal wiederholen. Ein beliebiges Zeichen außer CR oder LF finden ... null Mal oder öfter. Ende der nicht-einfangenden Gruppe. Vorherige Gruppe null bis fünf Mal wiederholen. Position am Ende des Strings sicherstellen.
Regex-Optionen: Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Das führende ‹^› stellt sicher, dass sich die Übereinstimmung am Anfang des Strings befindet. Damit wird dafür gesorgt, dass der gesamte String nicht mehr als fünf Zeilen enthält, denn so können sich nicht schon vor dem gefundenen Bereich Zeilen befinden. Als Nächstes umschließt eine nicht-einfangende Gruppe die Kombination einer Zeilenumbruchfolge und einer beliebigen Zahl von Zeichen, die gerade kein Zeilenumbruch
266 | Kapitel 4: Validierung und Formatierung
sind. Der direkt darauffolgende Quantor erlaubt, diese Gruppe zwischen null und fünf Mal zu finden (null Wiederholungen passen zu einem vollständig leeren String). Innerhalb der äußeren Gruppe passt eine optionale Untergruppe zu einer Zeilenumbruchfolge. Danach kommt die Zeichenklasse, die zu einer beliebigen Zahl von Zeichen passt, die kein Zeilenumbruch sind. Schauen Sie sich die Reihenfolge der Elemente der äußeren Gruppe genauer an (zuerst ein Zeilenumbruch, dann anderer Text). Wenn wir die Reihenfolge umkehren, sodass die Gruppe stattdessen als ‹(?:[^\r\n]*(?:\r\n?|\n)?)› geschrieben würde, ließe eine fünfte Wiederholung einen abschließenden Zeilenumbruch zu. Somit würde man eine leere sechste Zeile zulassen. Die Untergruppe erlaubt eine von drei Zeilenumbruchfolgen: • Ein Carriage Return, gefolgt von einem Line Feed (‹\r\n›, der normale Zeilenumbruch im MS-DOS-/Windows-Umfeld). • Ein einzelnes Carriage Return (‹\r›, der Zeilenumbruch im alten Mac OS). • Ein einzelnes Line Feed (‹\n›, der klassische Zeilenumbruch unter Unix/Linux/OS X). Lassen Sie uns nun die Unterschiede bei den Varianten anschauen. Die erste Version der Regex (die für alle Varianten außer Python und JavaScript genutzt werden kann) verwendet atomare Gruppen statt einfache nicht-einfangende Gruppen. Auch wenn die Verwendung von atomaren Gruppen einen deutlich größeren Einfluss auf die Performance haben kann, wird die Regex-Engine in diesem Fall nur vor ein bisschen unnötigem Backtracking bewahrt, das bei einer fehlschlagenden Suche auftreten kann (in Rezept 2.15 erhalten Sie mehr Informationen über atomare Gruppen). Die anderen Unterschiede zwischen den Varianten sind die Tokens, die genutzt werden, um die Position am Anfang und Ende des Strings sicherzustellen. Die auseinandergenommene Regex weiter oben hat dafür ‹^› und ‹$› genutzt. Diese Anker werden zwar von allen hier behandelten Regex-Varianten unterstützt, die anderen Regexes in diesem Abschnitt nutzen aber stattdessen ‹\A›, ‹\Z› und ‹\z›. Kurz gesagt, unterscheidet sich die Bedeutung dieser Metazeichen ein wenig bei den verschiedenen Regex-Varianten. Eine ausführlichere Erklärung lässt uns ein wenig in die Geschichte von Regexes eintauchen … Wenn man Perl nutzt, um eine Zeile aus einer Datei einzulesen, endet der sich so ergebende String mit einem Zeilenumbruch. Daher hat Perl eine „Verbesserung“ für die klassische Bedeutung von ‹$› eingeführt, die seitdem von den meisten Regex-Varianten übernommen wurde. So findet ‹$› nicht nur das absolute Ende des Strings, sondern auch einen Zeilenumbruch direkt vor dem String-Ende. Perl hat zudem zwei weitere Zusicherungen für das Ende eines Strings eingeführt: ‹\Z› und ‹\z›. Der Anker ‹\Z› hat die gleiche eigenartige Bedeutung wie ‹$›, nur dass sich diese nicht ändert, wenn die Option Zirkumflex und Dollar passen auf Zeilenumbruch für ‹^› und ‹$› eingeschaltet wird. ‹\z› passt immer nur auf das absolute Ende eines Strings – ohne Ausnahme. Da dieses Rezept explizit mit Zeilenumbrüchen hantiert, um die Zeilen in einem String zu zählen, nutzt es
4.10 Die Zeilenanzahl eines Texts beschränken | 267
die Zusicherung ‹\z› für die Regex-Varianten, in denen sie angeboten wird. Damit kann sichergestellt werden, dass es keine sechste, leere Zeile gibt. Die meisten anderen Regex-Varianten haben die Zeilenende-/String-Ende-Anker von Perl übernommen. .NET, Java, PCRE und Ruby unterstützen alle sowohl ‹\Z› als auch ‹\z› mit der gleichen Bedeutung wie in Perl. Python bietet nur das ‹\Z› (als Großbuchstabe), wobei es aber verwirrenderweise die Bedeutung ändert. Es passt nur auf das absolute Ende des Strings, so wie das kleine ‹\z› von Perl. JavaScript unterstützt überhaupt keine „z“-Anker, aber anders als die anderen Varianten passt sein Anker ‹$› nur auf das absolute Ende des Strings (wenn die Option Zirkumflex und Dollar passen auf Zeilenumbruch nicht aktiv ist). Bei ‹\A› ist das Ganze etwas übersichtlicher. Dieser Anker passt immer nur auf den Anfang eines Strings und hat überall die gleiche Bedeutung – außer in JavaScript, das ihn nicht unterstützt. Es ist zwar nicht sehr schön, dass es diese verwirrenden Inkonsistenzen gibt, aber einer der Vorteile beim Verwenden regulärer Ausdrücke in diesem Buch ist, dass Sie sich normalerweise keine Sorgen darum machen müssen. Solche unschönen Details werden nur dann relevant, wenn Sie sich doch intensiver mit den Regexes befassen wollen.
Variationen Umgang mit esoterischen Zeilentrennern Die oben gezeigten Regexes unterstützen nur die klassischen Zeilenumbrüche von MSDOS/Windows, Unix/Linux/OS X und dem alten Mac OS. Aber es gibt noch eine Reihe selten genutzter vertikaler Whitespace-Zeichen, die Ihnen gelegentlich über den Weg laufen. Die folgenden Regexes berücksichtigen diese zusätzlichen Zeichen, und schränken dabei die Übereinstimmungen auf maximal fünf Zeilen Text ein. \A(?>\R?\V*){0,5}\z
Regex-Optionen: Keine Regex-Varianten: PCRE 7 (mit der Option PCRE_BSR_UNICODE), Perl 5.10 \A(?>(?>\r\n?|[\n-\f\x85\x{2028}\x{2029}])? [^\n-\r\x85\x{2028}\x{2029}]*){0,5}\z
Regex-Optionen: Keine Regex-Varianten: PCRE, Perl \A(?>(?>\r\n?|[\n-\f\x85\u2028\u2029])?[^\n-\r\x85\u2028\u2029]*){0,5}\z
Regex-Optionen: Keine Regex-Varianten: .NET, Java, Ruby \A(?:(?:\r\n?|[\n-\f\x85\u2028\u2029])?[^\n-\r\x85\u2028\u2029]*){0,5}\Z
Regex-Optionen: Keine Regex-Variante: JavaScript Alle diese Regexes kümmern sich um die Zeilentrenner aus Tabelle 4-1, die zusammen mit ihren Unicode-Positionen und Namen aufgeführt sind. Tabelle 4-1: Zeilentrenner Unicode-Folge
Regex-Äquivalent
Name
Verwendung
U+000D U+000A
‹\r\n›
Carriage Return und Line Feed (CRLF)
Textdateien unter Windows und MS-DOS
U+000A
‹\n›
Line Feed (LF)
Textdateien unter Unix, Linux und OS X
U+000B
‹\v›
Line Tabulation (auch vertikaler Tab oder VT)
(selten)
U+000C
‹\f›
Form Feed (FF)
(selten)
U+000D
‹\r›
Carriage Return (CR)
Textdateien unter Mac OS
U+0085
‹\x85›
Next Line (NEL)
Textdateien auf IBM Mainframes (selten)
U+2028
‹\u2028› oder ‹\x{2028}›
Zeilentrenner (Line Separator)
(selten)
U+2029
‹\u2029› oder ‹\x{2029}›
Absatztrenner (Paragraph Separator)
(selten)
Siehe auch Rezept 4.9.
4.11 Antworten auswerten Problem Sie müssen eine Konfigurationsoption oder eine Eingabe an der Befehlszeile auf einen positiven Wert überprüfen. Sie wollen bei den möglichen Antworten flexibel sein, sodass true, t, yes, y, ja, j, okay, ok und 1 in beliebiger Groß- und Kleinschreibung akzeptiert werden.
Lösung Mit einer Regex, die alle akzeptablen Antworten kombiniert, können Sie die Überprüfung mit einem einfachen Test durchführen.
JavaScript var yes = /^(?:1|t(?:rue)?|y(?:es)?|ja?|ok(?:ay)?)$/i; if (yes.test(subject)) { alert("Ja"); } else { alert("Nein"); }
Andere Programmiersprachen In den Rezepten 3.4 und 3.5 erhalten Sie Hinweise dazu, wie dieser reguläre Ausdruck in anderen Programmiersprachen implementiert werden kann.
Diskussion Die folgende Regex zeigt die einzelnen Elemente im Detail. Kombinationen von Tokens, die leicht zusammen lesbar sind, werden in einer Zeile aufgeführt: ^ (?: 1 | t(?:rue)? | y(?:es)? | ja? | ok(?:ay)? ) $
# # # # # # # # # # # # #
Position am Anfang des Strings sicherstellen. Gruppieren, aber nicht einfangen ... Eine literale "1" finden. oder ... Finde "t", optional gefolgt von "rue". oder ... Finde "y", optional gefolgt von "es". oder ... Finde "j", optional gefolgt von "a". oder ... Finde "ok", optional gefolgt von "ay". Ende der nicht-einfangenden Gruppe. Position am Ende des Strings sicherstellen.
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert, Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Diese Regex ist im Prinzip nur ein einfacher Test auf einen von neun literalen Werten, wobei die Groß-/Kleinschreibung ignoriert wird. Sie könnte auch anders geschrieben werden. So ist zum Beispiel ‹^(?:[1tyj]|true|yes|ja|ok(?:ay)?)$› ein ebenso guter Ansatz. Man könnte auch einfach eine Alternation mit allen neun Werten nutzen, wie zum Beispiel ‹^(?:1|t|true|y|yes|j|ja|ok|okay)$›, aber aus Performancegründen ist es im Allgemeinen besser, die Anzahl der Alternativen mit dem Pipe-Operator ‹|› zu reduzieren und Zeichenklassen und optionale Endungen (mit dem Quantor ‹?›) vorzuziehen. In diesem Fall
270 | Kapitel 4: Validierung und Formatierung
ist der Performanceunterschied vermutlich nur minimal, aber es ist nicht verkehrt, die Performance von Regexes immer im Hinterkopf zu behalten. Manchmal kann der Unterschied zwischen den verschiedenen Vorgehensweisen erstaunlich groß sein. Alle diese Beispiele umgeben die möglichen Werte mit einer nicht-einfangenden Gruppe, um die Reichweite des Alternationsoperators zu begrenzen. Würden wir die Gruppe weglassen und stattdessen so etwas wie ‹^true|yes$› verwenden, würde die RegexEngine nach „dem Anfang des Strings, gefolgt von true’, oder yes’, gefolgt vom Ende des Strings“ suchen. ‹^(?:true|yes)$› weist die Regex-Engine an, den Anfang des Strings zu finden, dann entweder „true“ oder „yes“ und dann das Ende des Strings.
Siehe auch Rezepte 5.2 und 5.3.
4.12 US-Sozialversicherungsnummern validieren Problem Sie müssen prüfen, ob jemand eine gültige US-Sozialversicherungsnummer eingegeben hat.
Lösung Wenn Sie nur sicherstellen wollen, dass sich ein String an das grundlegende Sozialversicherungsnummerformat hält und keine offensichtlich ungültigen Zahlen enthalten sind, stellt die folgende Regex eine einfache Lösung bereit. Brauchen Sie eine strengere Prüfung, die auch bei der Social Security Administration prüft, ob die Nummer zu einer lebenden Person gehört, werfen Sie einen Blick auf die Links im Abschnitt „Siehe auch“ dieses Rezepts.
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Python if re.match(r"^(?!000|666)(?:[0-6][0-9]{2}|7(?:[0-6][0-9]|7[0-2]))(?!00)[0-9]{2}-(?!0000)[0-9]{4}$", sys.argv[1]): print "SSN ist gültig" else: print "SSN ist ungültig"
Andere Programmiersprachen In Rezept 3.5 finden Sie Informationen über das Implementieren dieses regulären Ausdrucks in anderen Programmiersprachen.
Diskussion Sozialversicherungsnummern in den USA sind neunstellige Nummern im Format AAA-GGSSSS: Die ersten drei Ziffern werden anhand der geografischen Region zugewiesen. Dies ist die Area Number. Die Area Number kann nicht den Wert 000 oder 666 haben, zudem gibt es aktuell keine Sozialversicherungsnummer mit einer Area Number größer als 772. Die Ziffern vier und fünf bilden die Group Number und liegen im Bereich 01 bis 99. Die letzten vier Ziffern sind Serial Numbers von 0001 bis 9999. Dieses Rezept nutzt alle diese Regeln. Hier noch einmal der reguläre Ausdruck, dieses Mal Stück für Stück erläutert: ^ (?!000|666) (?: [0-6] [0-9]{2} | 7 (?: [0-6] [0-9] | 7 [0-2] ) ) (?!00) [0-9]{2} (?!0000) [0-9]{4} $
# # # # # # # # # # # # # # # # # # # # # #
Position am Anfang des Strings sicherstellen. Weder "000" noch "666" dürfen hier vorkommen. Gruppieren, aber nicht einfangen ... Ein Zeichen im Bereich von "0" bis "6" finden. Zwei Ziffern finden. oder ... Eine literale "7" finden. Gruppieren, aber nicht einfangen ... Eine Ziffer im Bereich von "0" bis "6" finden. Eine Ziffer finden. oder ... Eine literale "7" finden. Eine Ziffer im Bereich von "0" bis "2" finden. Ende der nicht-einfangenden Gruppe. Ende der nicht-einfangenden Gruppe. Einen literalen "-" finden. Sicherstellen, dass "00" hier nicht vorkommt. Zwei Ziffern finden. Einen literalen "-" finden. Sicherstellen, dass "0000" hier nicht vorkommt. Vier Ziffern finden. Position am Ende des Strings sicherstellen.
Regex-Optionen: Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Abgesehen von den Tokens ‹^› und ‹$›, die sicherstellen, dass die Position am Anfang und Ende des Texts gefunden wird, kann diese Regex in drei Zifferngruppen aufgeteilt werden, die durch Bindestriche getrennt sind. Die erste Gruppe ist die komplexeste. Die zweite und die dritte Gruppe passen einfach auf zwei beziehungsweise vier Ziffern. Dabei
272 | Kapitel 4: Validierung und Formatierung
wird aber vorher ein negatives Lookahead genutzt, um zu verhindern, dass alle Ziffern solch einer Gruppe 0 sind. Die erste Zifferngruppe ist viel komplexer und auch schlechter lesbar, denn sie passt auf einen ganzen Bereich. Zunächst wird das negative Lookahead ‹(?!000|666)› verwendet, um die Werte „000“ und „666“ auszuschließen. Als Nächstes geht es darum, alle Zahlen größer als 772 auszuschließen. Da reguläre Ausdrücke mit Text arbeiten und nicht mit Zahlen, müssen wir den Bereich Zeichen für Zeichen unterteilen. Zunächst einmal wissen wir, dass jede dreistellige Zahl gültig ist, deren erste Ziffern im Bereich von 0 bis 6 liegt. Durch das vorherige negative Lookahead sind die ungültigen Zahlen 000 und 666 schon ausgeschlossen. Dieser erste Teil wird recht einfach durch ein paar Zeichenklassen und einen Quantor umgesetzt: ‹[0-6][0-9]{2}›. Da wir eine Alternative für Zahlen benötigen, die mit 7 beginnen, nutzen wir eine Gruppe wie in ‹(?:[0-6][0-9]{2}|7)›, um die Reichweite des Alternationsoperators einzuschränken. Nummern, die mit 7 beginnen, sind nur zulässig, wenn sie im Bereich von 700 bis 772 liegen, daher müssen wir nun die Nummern in Abhängigkeit von der zweiten Ziffern unterteilen. Liegt sie zwischen 0 und 6, ist eine beliebige dritte Ziffer erlaubt. Ist die zweite Ziffer 7, muss die dritte Ziffer zwischen 0 und 2 liegen. Bringen wir alle diese Regeln zusammen, erhalten wir ‹7(?:[0-6][0-9]|7[0-2])›. Fügen Sie das schließlich in die äußere Gruppe für die restlichen gültigen Nummern ein, erhalten Sie ‹(?:[0-6][0-9]{2}|7(?:[0-6][0-9]|7[0-2]))›. Das ist alles. Sie haben erfolgreich eine Regex erstellt, die eine dreistellige Nummer zwischen 000 und 772 findet.
Variationen Sozialversicherungsnummern in Dokumenten finden Wenn Sie in einem größeren Dokument nach Sozialversicherungsnummern suchen, ersetzen Sie die Anker ‹^› und ‹$› durch Wortgrenzen. Regex-Engines betrachten alle alphanumerischen Zeichen und den Unterstrich als Wortzeichen. \b(?!000|666)(?:[0-6][0-9]{2}|7(?:[0-6][0-9]|7[0-2]))(?!00)[0-9]{2}-(?!0000)[0-9]{4}\b
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Siehe auch Die Website der Social Security Administration (http://www.socialsecurity.gov) beantwortet die am häufigsten gestellten Fragen und führt auch aktuelle Listen mit bisher zugewiesenen Area und Group Numbers.
Der Social Security Number Verification Service (SSNVS) unter http://www.socialsecurity.gov/employer/ssnv.htm stellt zwei Wege bereit, um zu überprüfen, ob Namen und Sozialversicherungsnummern den Daten der Social Security Administration entsprechen. Der Umgang mit Zahlenbereichen, einschließlich Beispielen für das Finden solcher Bereiche, wird in Rezept 6.5 detaillierter erläutert.
4.13 ISBN validieren Problem Sie müssen die Gültigkeit einer International Standard Book Number (ISBN) prüfen. Diese kann entweder im älteren ISBN-10- oder im aktuellen ISBN-13-Format vorliegen. Am Anfang soll optional eine ISBN-Kennung stehen, zudem können die Teile der ISBN optional durch Bindestriche oder Leerzeichen getrennt sein. ISBN 978-0-596-52068-7, ISBN-13: 978-0-596-52068-7, 978 0 596 52068 7, 9780596520687, ISBN-10 0-596-52068-9 und 0-596-52068-9 sind allesamt Beispiele für gültige Eingaben.
Lösung Sie können eine ISBN nicht allein mit einer Regex validieren, da die letzte Ziffer mit einem Prüfsummenalgorithmus berechnet wird. Die regulären Ausdrücke in diesem Abschnitt überprüfen das Format einer ISBN, während die folgenden Codebeispiele auch prüfen, ob die letzte Ziffer korrekt ist.
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby ISBN-10 oder ISBN-13: ^(?:ISBN(?:-1[03])?:?z)?(?=[-0-9z]{17}$|[-0-9Xz]{13}$|[0-9X]{10}$) (?:97[89][-z]?)?[0-9]{1,5}[-z]?(?:[0-9]+[-z]?){2}[0-9X]$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
274 | Kapitel 4: Validierung und Formatierung
JavaScript // `regex` prüft auf die Formate ISBN-10 oder ISBN-13 var regex = /^(?:ISBN(?:-1[03])?:? )?(?=[-0-9 ]{17}$|[-0-9X ]{13}$| [0-9X]{10}$)(?:97[89][- ]?)?[0-9]{1,5}[- ]?(?:[0-9]+[- ]?){2}[0-9X]$/; if (regex.test(subject)) { // Nicht zugehörige ISBN-Elemente entfernen, dann in ein Array aufteilen var chars = subject.replace(/[^0-9X]/g, "").split(""); // Letzte ISBN-Ziffer aus `chars` entfernen und `last` zuweisen var last = chars.pop(); var sum = 0; var digit = 10; var check; if (chars.length == 9) { // ISBN-10-Prüfziffer berechnen for (var i = 0; i < chars.length; i++) { sum += digit * parseInt(chars[i], 10); digit -= 1; } check = 11 - (sum % 11); if (check == 10) { check = "X"; } else if (check == 11) { check = "0"; } } else { // ISBN-13-Prüfziffer berechnen for (var i = 0; i < chars.length; i++) { sum += (i % 2 * 2 + 1) * parseInt(chars[i], 10); } check = 10 - (sum % 10); if (check == 10) { check = "0"; } } if (check == last) { alert("Gültige ISBN"); } else { alert("Ungültige ISBN-Prüfziffer"); } } else { alert("Ungültige ISBN"); }
Python import re import sys # `regex` prüft auf die Formate ISBN-10 oder ISBN-13 regex = re.compile("^(?:ISBN(?:-1[03])?:? )?(?=[-0-9 ]{17}$| [-0-9X ]{13}$|[0-9X]{10}$)(?:97[89][- ]?)?[0-9]{1,5}[- ]?
4.13 ISBN validieren | 275
(?:[0-9]+[- ]?){2}[0-9X]$") subject = sys.argv[1] if regex.search(subject): # Nicht zugehörige ISBN-Elemente entfernen, dann in ein Array aufteilen chars = re.sub("[^0-9X]", "", subject).split("") # Letzte ISBN-Ziffer aus `chars` entfernen und `last` zuweisen last = chars.pop() if len(chars) == 9: # ISBN-10-Prüfziffer berechnen val = sum((x + 2) * int(y) for x,y in enumerate(reversed(chars))) check = 11 - (val % 11) if check == 10: check = "X" elif check == 11: check = "0" else: # ISBN-13-Prüfziffer berechnen val = sum((x % 2 * 2 + 1) * int(y) for x,y in enumerate(chars)) check = 10 - (val % 10) if check == 10: check = "0" if (str(check) == last): print "Gültige ISBN" else: print "Ungültige ISBN-Prüfziffer" else: print "Ungültige ISBN"
Andere Programmiersprachen In Rezept 3.5 wird beschrieben, wie Sie diesen regulären Ausdruck in anderen Programmiersprachen implementieren können.
Diskussion Eine ISBN ist eine eindeutige Kennung für Bücher und bücherähnliche Produkte. Das zehnstellige ISBN-Format wurde als internationaler Standard ISO 2108 im Jahr 1970 veröffentlicht. Alle ISBN, die seit dem 1. Januar 2007 zugewiesen werden, sind 13-stellig. ISBN-10- und ISBN-13-Nummern werden in vier beziehungsweise fünf Elemente aufgeteilt. Drei der Elemente haben eine variable, die verbleibenden ein oder zwei Elemente haben eine feste Länge. Alle fünf Teile werden normalerweise durch Bindestriche oder Leerzeichen getrennt. Dabei haben die Elemente folgende Bedeutung: • 13-stellige ISBN beginnen mit dem Präfix 978 oder 979. • Die Gruppennummer steht für einen geografisch oder sprachlich zusammenhängenden Raum. Sie kann eine bis fünf Ziffern lang sein.
276 | Kapitel 4: Validierung und Formatierung
• Die Verlagsnummer kann unterschiedlich lang sein und wird von der nationalen ISBN-Agentur vergeben. • Die Titelnummer kann auch unterschiedlich lang sein und wird vom Verlag festgelegt. • Das letzte Zeichen ist die Prüfziffer. Sie wird mit einem Prüfsummenalgorithmus ermittelt. Eine ISBN-10-Prüfziffer kann entweder die Werte 0 bis 9 oder den Buchstaben X (für die römische 10) enthalten. Eine ISBN-13-Prüfziffer liegt im Bereich von 0 bis 9. Die hier verwendeten Zeichen sind unterschiedlich, weil auch unterschiedliche Prüfsummenalgorithmen genutzt werden. Die Regex für ISBN-10- und ISBN-13-Nummern wird im folgenden Beispiel in ihre Bestandteile zerlegt. Da sie hier im Freiform-Modus genutzt wird, wurden die literalen Leerzeichen in der Regex durch Backslashs maskiert. Bei Java müssen im FreiformModus Leerzeichen selbst in Zeichenklassen maskiert werden: ^ (?: ISBN (?:-1[03])? :? \ )? (?= [-0-9\ ]{17}$ | [-0-9X\ ]{13}$ | [0-9X]{10}$ ) (?: 97[89] [-\ ]? )? [0-9]{1,5} [-\ ]? (?: [0-9]+ [-\ ]? ){2} [0-9X] $
Position am Anfang des Strings sicherstellen. Gruppieren, aber nicht einfangen ... Den Text "ISBN" finden. Optional den Text "-10" oder "-13" finden. Optional ein literales ":" finden. Ein (maskiertes) Leerzeichen finden. Die Gruppe null oder ein Mal finden. Sicherstellen, dass das Folgende passt ... 17 Bindestriche, Ziffern und Leerzeichen bis zum Ende des Strings finden. Oder ... 13 Bindestriche, Ziffern, X und Leerzeichen bis zum Ende des Strings finden. Oder ... 10 Ziffern und X bis zum Ende finden. Ende des positiven Lookahead. Gruppieren, aber nicht einfangen ... Den Text "978" oder "979" finden. Optional einen Bindestrich oder ein Leerzeichen finden. Die Gruppe null oder ein Mal finden. Eine Ziffer ein bis vier Mal finden. Optional einen Bindestrich oder ein Leerzeichen finden. Gruppieren, aber nicht einfangen ... Eine Ziffer ein Mal oder häufiger finden. Optional einen Bindestrich oder ein Leerzeichen finden. Die Gruppe genau zwei Mal finden. Eine Ziffer oder ein "X" finden. Position am Ende des Strings sicherstellen.
Regex-Optionen: Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Der erste Teil ‹(?:ISBN(?:-1[03])?:?z)?› hat drei optionale Elemente, durch die einer der folgenden sieben Strings passt (mit Ausnahme des leeren Strings enthalten alle ein Leerzeichen am Ende): • ISBNz • ISBN-10z • ISBN-13z
4.13 ISBN validieren | 277
• ISBN:z • ISBN-10:z • ISBN-13:z • Ein leerer String (kein Präfix) Als Nächstes sorgt das positive Lookahead ‹(?=[-0-9z]{17}$|[-0-9Xz]{13}$|[09X]{10}$)› dafür, dass eine der drei Optionen (getrennt durch den Alternationsoperator ‹|›) bezüglich der Länge und erlaubten Zeichen für den Rest der Übereinstimmung gültig ist. Alle drei Optionen enden mit dem Anker ‹$›. Dadurch ist sichergestellt, dass es keinen nachfolgenden Text gibt, der nicht zu einem der Muster passt: ‹[-0-9z]{17}$›
Erlaubt eine ISBN-13 mit vier Trennzeichen (insgesamt 17 Zeichen). ‹[-0-9Xz]{13}$›
Erlaubt eine ISBN-13 ohne Trennzeichen oder eine ISBN-10 mit drei Trennzeichen (insgesamt 13 Zeichen). ‹[0-9X]{10}$›
Erlaubt eine ISBN-10 ohne Trennzeichen (insgesamt 10 Zeichen). Nachdem das positive Lookahead die Länge und die Zeichen geprüft hat, können wir die einzelnen Elemente der ISBN auswerten, ohne uns über ihre Gesamtlänge Gedanken machen zu müssen. ‹(?:97[89][-z]?)?› passt zum Präfix „978“ oder „979“, das von einer ISBN-13 gefordert wird. Die nicht-einfangende Gruppe ist optional, weil sie bei einer ISBN-10 nicht vorkommt. ‹[0-9]{1,5}[-z]?› passt zu einer Zifferngruppe mit ein bis fünf Ziffern, denen optional ein Trennzeichen folgt. ‹(?:[0-9]+[-z]?){2}› passt zur Verlags- und Titelnummer und deren optionalen Separatoren. Schließlich passt ‹[09X]$› auf die Prüfziffer am Ende des Strings. Ein regulärer Ausdruck kann zwar prüfen, ob die letzte Ziffer ein gültiges Zeichen nutzt (eine Ziffer oder ein X), aber nicht, ob es sich dabei um die korrekte Prüfziffer handelt. Einer der beiden Prüfsummenalgorithmen (abhängig davon, ob Sie mit einer ISBN-10oder einer ISBN-13-Nummer arbeiten) wird verwendet, um wenigstens halbwegs sicher zu sein, dass die ISBN-Ziffern nicht unabsichtlich vertauscht oder anders falsch eingegeben wurden. Der weiter oben gezeigte Beispielcode für JavaScript und Python implementiert beide Algorithmen. Der folgende Abschnitt beschreibt die Prüfsummenregeln, damit Sie diese Algorithmen auch in anderen Programmiersprachen implementieren können.
ISBN-10-Prüfsumme Die Prüfziffer einer ISBN-10-Nummer kann den Wert 0 bis 10 haben (wobei die römische Zahl X statt der 10 verwendet wird). Sie wird wie folgt ermittelt: 1. Multipliziere jede der ersten 9 Ziffern mit einer Zahl in der absteigenden Folge von
10 bis 2 und addiere die Ergebnisse. 2. Teile die Summe durch 11.
278 | Kapitel 4: Validierung und Formatierung
3. Ziehe den Rest (nicht den Quotienten) von 11 ab. 4. Wenn das Ergebnis 11 ist, verwende die Ziffer 0; ist es 10, verwende den Buchsta-
ben X. Hier ein Beispiel, wie man die ISBN-10-Prüfziffer für 3-89721-957-? ermittelt: Schritt 1: sum = 10×3 + = 30 + = 305 Schritt 2: 305 ÷ 11 Schritt 3: 11 - 8 = Schritt 4: 3 [keine
Die Prüfziffer ist 3, daher ist die komplette ISBN ISBN 3-89721-957-3.
ISBN-13-Prüfsumme Eine ISBN-13-Prüfziffer liegt im Bereich von 0 bis 9 und wird in ähnlichen Schritten ermittelt. Multipliziere jede der ersten 12 Ziffern mit 1 oder 3 – immer abwechselnd von links nach rechts – und addiere die Ergebnisse. Teile die Summe durch 10. Ziehe den Rest (nicht den Quotienten) von 10 ab. Ist das Ergebnis 10, verwende die Ziffer 0. So wird zum Beispiel die ISBN-13-Prüfziffer für 978-3-89721-957-? wie folgt berechnet: Schritt 1: sum = 1×9 + 3×7 + 1×8 + 3×3 + 1×8 + 3×9 + 1×7 + 3×2 + 1×1 + 3×9 + 1×5 + 3×7 = 9 + 21 + 8 + 9 + 8 + 27 + 7 + 6 + 1 + 27 + 5 + 21 = 149 Schritt 2: 149 ÷ 10 = 14, remainder 9 Schritt 3: 10 - 9 = 1 Schritt 4: 1 [keine Ersetzung notwendig]
Die Prüfziffer ist 1, und die komplette ISBN hat den Wert ISBN 978-3-89721-957-1.
Variationen ISBNs in Dokumenten finden Diese Version der Regex für ISBN-10 und ISBN-13 nutzt statt der Anker Wortgrenzen, um ISBN in längeren Texten zu finden, dabei aber sicherzustellen, dass sie für sich ste-
4.13 ISBN validieren | 279
hen. Der Text „ISBN“ ist in dieser Regex immer erforderlich. Das hat zwei Gründe. Zum einen verhindert man so fälschlicherweise als ISBN erkannte Zahlenfolgen (denn die Regex könnte potenziell beliebige 10- oder 13-stellige Zahlen finden), und zum anderen sollen ISBN diesen Text offiziell enthalten, wenn sie ausgegeben werden: \bISBN(?:-1[03])?:?z(?=[-0-9z]{17}$|[-0-9Xz]{13}$|[0-9X]{10}$) (?:97[89][-z]?)?[0-9]{1,5}[-z]?(?:[0-9]+[-z]?){2}[0-9X]\b
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Falsche ISBN-Kennungen finden Die vorigen Regexes haben ein Problem: Man kann den Text „ISBN-13“ haben, auf den dann aber eine ISBN-10-Nummer folgt und umgekehrt. Die folgende Regex nutzt RegexBedingungen (siehe Rezept 2.17), um sicherzustellen, dass eine Kennung „ISBN-10“ oder „ISBN-13“ immer vom passenden ISBN-Typ begleitet wird. Wenn der Typ nicht explizit angegeben ist, sind beide Nummernarten möglich. Diese Regex ist in den meisten Fällen doch etwas übertrieben, da man das gleiche Ergebnis auch einfacher erreichen kann, wenn man die getrennten ISBN-10- und ISBN-13-Regexes nutzt. Sie soll hier eher gezeigt werden, um eine interessante Anwendung von regulären Ausdrücken zu demonstrieren: ^ (?:ISBN(-1(?:(0)|3))?:?\ )? (?(1) (?(2) (?=[-0-9X ]{13}$|[0-9X]{10}$) [0-9]{1,5}[- ]?(?:[0-9]+[- ]?){2}[0-9X]$ | (?=[-0-9 ]{17}$|[0-9]{13}$) 97[89][- ]?[0-9]{1,5}[- ]?(?:[0-9]+[- ]?){2}[0-9]$ ) | (?=[-0-9 ]{17}$|[-0-9X ]{13}$|[0-9X]{10}$) (?:97[89][- ]?)?[0-9]{1,5}[- ]?(?:[0-9]+[- ]?){2}[0-9X]$ ) $
Siehe auch Die aktuellste Version der ISBN-Dokumente lassen sich auf der Website der International ISBN Agency unter http://www.isbn-international.org finden. Die offizielle Liste mit Gruppennummern findet sich ebenfalls auf der Website der International ISBN Agency. Anhand dieser Liste können Sie das Ursprungsland eines Buchs mithilfe der ersten 1 bis 5 Ziffern der ISBN ermitteln.
280 | Kapitel 4: Validierung und Formatierung
4.14 ZIP-Codes validieren Problem Sie müssen einen ZIP-Code (eine US-Postleitzahl) validieren, wobei sowohl das fünfstellige als auch das neunstellige Format (ZIP + 4) zu erkennen ist. Die Regex sollte auf 12345 und 12345-6789 passen, aber nicht auf 1234, 123456, 123456789 oder 1234-56789.
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
VB.NET If Regex.IsMatch(subjectString, "^[0-9]{5}(?:-[0-9]{4})?$") Then Console.WriteLine("Gültiger ZIP-Code") Else Console.WriteLine("Ungültiger ZIP-Code") End If
Andere Programmiersprachen In Rezept 3.5 finden Sie Informationen über das Implementieren dieses regulären Ausdrucks in anderen Programmiersprachen.
Diskussion Der reguläre Ausdruck für den ZIP-Code sieht im Freiform-Modus so aus: ^ [0-9]{5} (?: [0-9]{4} ) ? $
# # # # # # # #
Position am Anfang des Strings sicherstellen. Fünf Ziffern finden. Gruppieren, aber nicht einfangen ... Einen literalen "-" finden. Vier Ziffern finden. Ende der nicht-einfangenden Gruppe. Die vorige Gruppe null oder ein Mal finden. Position am Ende des Strings sicherstellen.
Regex-Optionen: Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Diese Regex ist ziemlich einfach, daher ist nicht sehr viel dazu zu sagen. Eine simple Änderung ermöglicht es Ihnen, ZIP-Codes in einem längeren String zu finden: Ersetzen Sie die Anker ‹^› und ‹$› durch Wortgrenzen: ‹\b[0-9]{5}(?:-[0-9]{4})?\b›.
4.14 ZIP-Codes validieren | 281
Siehe auch Rezepte 4.15, 4.16 und 4.17.
4.15 Kanadische Postleitzahlen validieren Problem Sie wollen prüfen, ob ein String eine kanadische Postleitzahl enthält.
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Das negative Lookahead am Anfang dieses regulären Ausdrucks verhindert, dass sich irgendwo im Ausgangstext die Buchstaben D, F, I, O, Q oder U befinden. Die Zeichenklasse ‹[A-VXY]› verhindert darüber hinaus, dass W oder Z das erste Zeichen ist. Neben diesen beiden Ausnahmen werden für kanadische Postleitzahlen einfach abwechselnde Folgen von sechs alphanumerischen Zeichen genutzt, wobei in der Mitte ein Leerzeichen steht. So passt diese Regex zum Beispiel auf K1A 0B1. Dabei handelt es sich um die Postleitzahl für die Zentrale der kanadischen Post in Ottawa.
Siehe auch Rezepte 4.14, 4.16 und 4.17.
4.16 Britische Postleitzahlen validieren Problem Sie benötigen einen regulären Ausdruck, der britische Postleitzahlen erkennt.
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
282 | Kapitel 4: Validierung und Formatierung
Diskussion Postleitzahlen in Großbritannien (oder auch Postcodes, wie sie dort genannt werden) bestehen aus fünf bis sieben alphanumerischen Zeichen, die durch ein Leerzeichen unterteilt sind. Die Regeln legen fest, welche Zeichen an welcher Position stehen dürfen. Leider sind sie ziemlich kompliziert und voller Ausnahmen. Daher kümmert sich dieser reguläre Ausdruck nur um die grundlegenden Regeln.
Siehe auch British Standard BS7666, verfügbar unter http://www.govtalk.gov.uk/gdsc/html/frames/ PostCode.htm. Hier werden die Regeln für britische Postleitzahlen beschrieben. Rezepte 4.14, 4.15 und 4.17.
4.17 Deutsche Postleitzahlen validieren Problem Sie benötigen einen regulären Ausdruck, der deutsche Postleitzahlen erkennt.
Lösung ^[0-9]{5}$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Deutsche Postleitzahlen bestehen einfach aus fünf Ziffern ohne weitere Unterteilung. Beachten Sie, dass Postleitzahlen bei einer weiteren Verarbeitung nicht als Zahlen angesehen werden sollten, sondern eher als Zeichenkette. In Deutschland gibt es eine Reihe von Orten, deren Postleitzahl mit einer 0 beginnt. Speichert man Postleitzahlen als Zahl, verschwindet diese 0, was verwirrt und eventuell dafür sorgt, dass die Post nicht (direkt) ankommt.
Variationen Postleitzahlen in anderen europäischen Ländern Belgien ‹^[1-9][0-9]{3}$›
Bulgarien ‹^[1-9][0-9]{3}$›
Dänemark ‹^[1-9][0-9]{3}$›
4.17 Deutsche Postleitzahlen validieren | 283
Finnland ‹^[0-9]{5}$›
Frankreich und Monaco ‹^[0-9]{5}$›
Griechenland ‹^[1-8][0-9]{4}$›
Italien, San Marino und Vatikanstadt ‹^[0-9]{5}$›
Kroatien ‹^(?:[1-4][0-9]|5[1-3])[0-9]{3}$›
Montenegro ‹^8[145][0-9]{3}$›
Niederlande ‹^[1-9][0-9]{3}z?[A-Z]{2}$›
Norwegen ‹^[0-9]{4}$›
Österreich ‹^[1-9][0-9]{3}$›
Polen ‹^[0-9]{2}-[0-9]{3}$›
Portugal ‹^[1-9][0-9]{3}-?[0-9]{2}$›
Rumänien ‹^[0-9]{6}$›
Schweden ‹^[1-9][0-9]{2}z?[0-9]{2}$›
Schweiz und Liechtenstein ‹^[1-9][0-9]{3}$›
Slowakei ‹^[890][0-9]{2}z?[0-9]{2}$›
Spanien ‹^(?:0[1-9]|[1-4][0-9]|5[12])[0-9]{3}$›
Tschechien ‹^[1-7][0-9]{2}z?[0-9]{2}$›
Ungarn ‹^[1-9][0-9]{3}$›
Zypern ‹^[1-9][0-9]{3}$›
Siehe auch Rezepte 4.14, 4.15 und 4.16.
284 | Kapitel 4: Validierung und Formatierung
4.18 Namen von „Vorname Nachname“ nach „Nachname, Vorname“ umwandeln Problem Sie wollen Personennamen von „Vorname Nachname“ umwandeln in „Nachname, Vorname“, um so alphabetisch sortieren zu können. Zudem wollen Sie zusätzlich auf andere Namensbestandteile Rücksicht nehmen, zum Beispiel auf einen zweiten Vornamen.
Lösung Leider ist es nicht möglich, Namen mit einem regulären Ausdruck zuverlässig zu parsen. Reguläre Ausdrücke sind strikt, während Namen so flexibel gehandhabt werden, dass selbst Menschen durcheinanderkommen. Das Bestimmen der Struktur eines Namens und die richtige Einordnung in eine alphabetisch sortierte Liste erfordern häufig das Einbeziehen traditioneller und landesspezifischer Konventionen, und selbst persönliche Vorlieben können eine Rolle spielen. Wenn Sie aber dazu bereit sind, gewissen Annahmen über Ihre Daten zu treffen und auch dann und wann Fehler akzeptieren können, kann ein regulärer Ausdruck eine schnelle Lösung bieten. Der folgende reguläre Ausdruck wird eher einfach gehalten und soll nicht unbedingt alle möglichen Grenzfälle abdecken.
JavaScript function formatName (name) { return name.replace(/^(.+?)z([^\s,]+)$/i, "$2, $1"); }
4.18 Namen von „Vorname Nachname“ nach „Nachname, Vorname“ umwandeln | 285
Andere Programmiersprachen In Rezept 3.15 finden Sie Informationen über das Implementieren dieses regulären Ausdrucks in anderen Programmiersprachen.
Diskussion Lassen Sie uns diesen regulären Ausdruck erst mal Stück für Stück betrachten. Danach erklären wir Ihnen, welche Teile eines Namens von welchen Regex-Elementen gefunden werden. Da die Regex hier im Freiform-Modus geschrieben ist, wurden die literalen Leerzeichen durch Backslashs maskiert: ^ ( .+? ) \ ( [^\s]+ ) $
# # # # # # # # #
Position am Anfang des Strings sicherstellen. Gruppe für Rückwärtsreferenz 1 ... Eines oder mehrere Zeichen finden, aber so wenig wie möglich. Ende der einfangenden Gruppe. Ein Leerzeichen finden. Gruppe für Rückwärtsreferenz 2 ... Eines oder mehrere Zeichen finden, die keine Leerzeichen sind. Ende der einfangenden Gruppe. Position am Ende des Strings sicherstellen.
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert, Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Dieser reguläre Ausdruck geht von folgenden Annahmen aus: • Der Ausgangstext enthält mindestens einen Vornamen und einen Nachnamen (weitere Bestandteile sind optional). • Der Vorname steht vor dem Nachnamen. Ein paar Probleme gibt es aber: • Der reguläre Ausdruck kann keine mehrteiligen Nachnamen erkennen, die nicht per Bindestrich verbunden sind. So würde Sacha Baron Cohen zum Beispiel durch Cohen, Sacha Baron ersetzt werden und nicht durch die korrekte Version Baron Cohen, Sacha. • Namensbestandteile vor dem Familiennamen werden auch nicht dem Nachnamen zugeordnet, obwohl dies aufgrund von Konventionen oder persönlichen Vorlieben teilweise gewünscht wird (so kann „Charles de Gaulle“ entweder als „de Gaulle, Charles“ oder als „Gaulle, Charles de“ aufgeführt sein). • Aufgrund der Anker ‹^› und ‹$›, die die Übereinstimmung mit dem Anfang und Ende des Strings verbinden, kann keine Ersetzung vorgenommen werden, wenn nicht der gesamte Ausgangstext zum Muster passt. Wird keine passende Übereinstimmung gefunden (weil zum Beispiel der Ausgangstext nur einen Namen enthält), bleibt der Name so bestehen. Der reguläre Ausdruck nutzt zwei einfangende Gruppen, um den Namen aufzuteilen. Diese Elemente werden dann über Rückwärtsreferenzen in der gewünschten Reihenfolge
286 | Kapitel 4: Validierung und Formatierung
wieder zusammengesetzt. Die erste einfangende Gruppe nutzt das ausgesprochen flexible Muster ‹.+?›, um den ersten Vornamen zusammen mit allen weiteren Vornamen und den zusätzlichen Bestandteilen des Nachnamens einzufangen, wie zum Beispiel das deutsche „von“ oder das französische, portugiesische und spanische „de“. Diese Namenselemente werden zusammen bearbeitet, da sie in der Ausgabe auch nacheinander erscheinen sollen. Die zweite einfangende Gruppe passt durch ‹[^\s]+› auf den Nachnamen. Wie beim Punkt in der ersten einfangenden Gruppe ermöglicht die Flexibilität dieser Zeichenklasse auch die Verwendung von Umlauten und anderen exotischen Zeichen. In Tabelle 4-2 werden ein paar Beispiele für mit dieser Regex und dem entsprechenden Ersetzungstext umgestellte Namen aufgeführt. Tabelle 4-2: Formatierte Namen Eingabe
Ausgabe
Robert Downey
Downey, Robert
John F. Kennedy
Kennedy, John F.
Scarlett O’Hara
O’Hara, Scarlett
Pepé Le Pew
Pew, Pepé Le
J.R.R. Tolkien
Tolkien, J.R.R.
Catherine Zeta-Jones
Zeta-Jones, Catherine
Variationen Nachnamensbestandteile am Anfang des Namens aufführen Im folgenden regulären Ausdruck haben wir ein Element ergänzt, durch das zusätzliche Bestandteile des Nachnamens bei ihm verbleiben. Diese Regex berücksichtigt „de“, „du“, „la“, „le“, „St“, „St.“, „Ste“, „Ste.“, „van“ und „von“. Dabei sind auch mehrere solcher Bestandteile möglich (zum Beispiel „de la”): ^(.+?)z((?:(?:D[eu]|L[ae]|Ste?\.?|V[ao]n)z)*[^\s]+)$
4.18 Namen von „Vorname Nachname“ nach „Nachname, Vorname“ umwandeln | 287
4.19 Kreditkartennummern validieren Problem Sie sollen für eine Firma ein Bestellformular bauen, das auch eine Bezahlung per Kreditkarte zulässt. Da die Karten-Servicegesellschaft für jeden Transaktionsversuch eine Gebühr erhebt – auch für fehlgeschlagene Versuche –, wollen Sie mit einem regulären Ausdruck die offensichtlich ungültigen Kreditkartennummern ausfiltern. Nebenbei verbessert das auch die Bedienungsfreundlichkeit. Ein regulärer Ausdruck kann offensichtliche Tippfehler sofort erkennen, sobald der Anwender mit dem Ausfüllen der Felder auf der Webseite fertig ist. Eine Anfrage bei der Karten-Servicegesellschaft dauert dagegen leicht einmal 10 bis 30 Sekunden.
Lösung Leerzeichen und Bindestriche entfernen Lesen Sie die vom Kunden eingegebene Kreditkartennummer aus und speichern Sie sie in einer Variablen. Bevor Sie die Gültigkeit der Nummer überprüfen, suchen Sie nach Leerzeichen und Bindestrichen und entfernen sie aus der Nummer. Ersetzen Sie diesen regulären Ausdruck global durch einen leeren Ersetzungstext: [z-]
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby In Rezept 3.14 ist beschrieben, wie Sie diese erste Ersetzung vornehmen.
Validieren der Nummer Nachdem Leerzeichen und Bindestriche aus der Eingabe entfernt wurden, prüft dieser reguläre Ausdruck, ob die Kreditkartennummer dem Format einer der sechs großen Kreditkartenfirmen entspricht. Dabei nutzt die Regex benannte Captures, um herauszufinden, was für eine Kreditkarte der Kunde hat: ^(?: (?4[0-9]{12}(?:[0-9]{3})?) | (?<mastercard>5[1-5][0-9]{14}) | (?6(?:011|5[0-9][0-9])[0-9]{12}) | (?3[47][0-9]{13}) | (?3(?:0[0-5]|[68][0-9])[0-9]{11}) | (?<jcb>(?:2131|1800|35\d{3})\d{11}) )$
Regex-Optionen: Freiform Regex-Varianten: PCRE, Python Java, Perl 5.6, Perl 5.8 und Ruby 1.8 unterstützen keine benannten Captures. Hier können Sie nummerierte Captures verwenden. Gruppe 1 fängt die Visa-Karten, Gruppe 2 die MasterCards und so weiter bis zur Gruppe 6 für JCB: ^(?: (4[0-9]{12}(?:[0-9]{3})?) | (5[1-5][0-9]{14}) | (6(?:011|5[0-9][0-9])[0-9]{12}) | (3[47][0-9]{13}) | (3(?:0[0-5]|[68][0-9])[0-9]{11}) | ((?:2131|1800|35\d{3})\d{11}) )$
# # # # # #
Visa MasterCard Discover American Express Diners Club JCB
Regex-Optionen: Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby JavaScript unterstützt keinen Freiform-Modus. Entfernen wir den Leerraum und die Kommentare, erhalten wir: ^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})| (6(?:011|5[0-9][0-9])[0-9]{12})|(3[47][0-9]{13})| (3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35\d{3})\d{11}))$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Wenn Sie nicht wissen müssen, um welchen Kartentyp es geht, können Sie die unnötigen einfangenden Gruppen entfernen: ^(?: 4[0-9]{12}(?:[0-9]{3})? | 5[1-5][0-9]{14} | 6(?:011|5[0-9][0-9])[0-9]{12} | 3[47][0-9]{13} | 3(?:0[0-5]|[68][0-9])[0-9]{11} | (?:2131|1800|35\d{3})\d{11} )$
# # # # # #
Visa MasterCard Discover American Express Diners Club JCB
Für JavaScript: ^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}| 3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Folgen Sie der Anleitung in Rezept 3.6, um Ihrem Bestellformular diesen regulären Ausdruck hinzuzufügen und die Kreditkartennummer zu überprüfen. Wenn Sie unterschiedliche Serviceunternehmen für verschiedene Karten nutzen oder einfach selbst eine Statistik führen wollen, können Sie wie in Rezept 3.9 prüfen, welche benannten oder nummerierten einfangenden Gruppen die Übereinstimmung enthalten. Damit erfahren Sie, von welcher Firma die Karte Ihres Kunden ist.
Beispiel-Webseite mit JavaScript Kreditkartentest Kreditkartentest
Bitte geben Sie Ihre Kreditkartennummer ein:
(keine Kartennummer eingegeben)
<script> function validatecardnumber(cardnumber) { // Leerzeichen und Bindestriche entfernen cardnumber = cardnumber.replace(/[ -]/g, ''); // Prüfen, ob die Karte gültig ist // Die Regex fängt die Nummer in einer der einfangenden Gruppen var match = /^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})| (6(?:011|5[0-9][0-9])[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9]) [0-9]{11})|((?:2131|1800|35\d{3})\d{11}))$/.exec(cardnumber); if (match) { // Liste der Kartentypen in der gleichen Reihenfolge wie die einfangenden Gruppen var types = ['Visa', 'MasterCard', 'Discover', 'American Express', 'Diners Club', 'JCB']; // Einfangende Gruppe finden, die passt // Das nullte Element des Match-Arrays überspringen (das Gesamtsuchergebnis) for (var i = 1; i < match.length; i++) { if (match[i]) { // Kartentyp für diese Gruppe anzeigen document.getElementById('notice').innerHTML = types[i - 1];
Diskussion Leerzeichen und Bindestriche entfernen Auf Kreditkarten sind die in die Karte eingestanzten Ziffern meist in Vierergruppen unterteilt. So lässt sich die Kartennummer leichter lesen. Natürlich werden viele Leute versuchen, ihre Kartennummer auch genau so auf einer Webseite einzugeben – einschließlich der Leerzeichen. Schreibt man einen regulären Ausdruck, der eine Kartennummer validieren soll und dabei Leerzeichen, Bindestriche und was auch immer berücksichtigen will, ist das deutlich schwieriger als einer, der nur Ziffern zulässt. Um daher den Kunden nicht damit zu nerven, dass er die Kartennummer nochmals ohne Leerzeichen oder Bindestriche eingeben soll, entfernen Sie sie einfach vor dem Überprüfen der Nummer und der Übermittlung an die Karten-Servicegesellschaft. Der reguläre Ausdruck ‹[z-]› passt auf ein Leerzeichen oder einen Bindestrich. Ersetzen Sie alle Übereinstimmungen dieses regulären Ausdrucks durch einen leeren String, werden damit alle Leerzeichen und Bindestriche entfernt. Kreditkartennummern können nur aus Ziffern bestehen. Statt mit ‹[z-]› lediglich Leerzeichen und Bindestriche zu entfernen, können Sie auch die Zeichenklassenabkürzung ‹\D› nutzen, um alles zu entfernen, was keine Ziffer ist.
Validieren der Nummer Jede Kreditkartenfirma verwendet ein anderes Nummernformat. Wir nutzen diese Unterschiede, damit der Anwender eine Nummer eingeben kann, ohne die Kartenfirma angeben zu müssen. Die Firma kann dann aus der Nummer ermittelt werden. Die Formate sind: Visa 13 oder 16 Ziffern, beginnend mit einer 4. MasterCard 16 Ziffern, beginnend mit 51 bis 55. Discover 16 Ziffern, beginnend mit 6011 oder 65.
4.19 Kreditkartennummern validieren | 291
American Express 15 Ziffern, beginnend mit 34 oder 37. Diners Club 14 Ziffern, beginnend mit 300 bis 305, 36 oder 38. JCB 15 Ziffern, beginnend mit 2131 oder 1800, oder 16 Ziffern, beginnend mit 35. Wenn Sie nur bestimmte Kartenfirmen akzeptieren, können Sie die Karten aus der Regex entfernen, die Sie nicht haben wollen. Beim Entfernen von JCB achten Sie darauf, auch das letzte ‹|› zu entfernen. Endet Ihr regulärer Ausdruck mit ‹||› oder ‹|)›, werden auch leere Strings als gültige Kartennummer akzeptiert. Um zum Beispiel nur Visa, MasterCard und American Express zu akzeptieren, nutzen Sie: ^(?: 4[0-9]{12}(?:[0-9]{3})? | 5[1-5][0-9]{14} | 3[47][0-9]{13} )$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Suchen Sie in einem längeren Text nach Kreditkartennummern, ersetzen Sie die Anker durch Wortgrenzen (‹\b›).
Einbau der Lösung in eine Webseite Das Beispiel in „Beispiel-Webseite mit JavaScript“ auf Seite 290 zeigt, wie Sie diese beiden regulären Ausdrücke in Ihr Bestellformular einbauen können. Das Eingabefeld für die Kreditkartennummer hat einen Event-Handler onkeyup, der die Funktion validatecardnumber() aufruft. Diese Funktion liest die Kartennummer aus dem Eingabefeld aus, entfernt Leerzeichen und Bindestriche und führt dann mithilfe des regulären Ausdrucks mit nummerierten einfangenden Gruppen eine Validierung durch. Das Ergebnis dieser Überprüfung wird angezeigt, indem der Text im letzten Absatz auf der Seite ersetzt wird. Hat der reguläre Ausdruck keinen Erfolg, liefert regexp.exec() den Wert null zurück, und es wird (Ungültige Kartennummer) angezeigt. Passt die Regex, liefert regexp.exec() ein String-Array zurück. Das nullte Element dieses Arrays enthält das vollständige Suchergebnis. In den Elementen 1 bis 6 finden sich die Ergebnisse der sechs einfangenden Gruppen.
292 | Kapitel 4: Validierung und Formatierung
Unser regulärer Ausdruck hat sechs einfangende Gruppen, die in den Alternativen einer Alternation untergebracht sind. Das bedeutet, dass immer nur genau eine Gruppe an der Übereinstimmung beteiligt ist und die Kartennummer enthält. Die anderen Gruppen sind dann leer (entweder undefined, oder sie enthalten einen leeren String – das hängt von Ihrem Browser ab). Die Funktion prüft nacheinander die sechs einfangenden Gruppen. Findet sie eine, die nicht leer ist, wird die Kartenfirma erkannt und ausgegeben.
Zusätzliche Validierung mit dem Luhn-Algorithmus Es gibt eine zusätzliche Validierungsmöglichkeit für Kreditkartennummern, bevor die Bestellung wirklich durchgeführt wird. Die letzte Ziffer in der Kartennummer ist eine Prüfsumme, die nach dem Luhn-Algorithmus berechnet wird. Da für diesen Algorithmus ein paar (wenn auch einfache) Kalkulationen notwendig sind, können Sie ihn nicht mit einem regulären Ausdruck implementieren. Sie können Ihr Webseitenbeispiel für dieses Rezept mit dem Luhn-Algorithmus ergänzen, indem Sie vor der else-Zeile in der Funktion validatecardnumber() den Aufruf luhn(cardnumber); einfügen. So wird die Luhn-Prüfung nur dann durchgeführt, wenn der reguläre Ausdruck eine Übereinstimmung gefunden hat und nachdem die Kartenart ermittelt wurde. Allerdings ist das Bestimmen der Kartenart für die Luhn-Prüfung nicht notwendig. Alle Kreditkarten nutzen die gleiche Methode. In JavaScript können Sie den Luhn-Algorithmus wie folgt implementieren: function luhn(cardnumber) { // Aufbau eines Arrays mit den Ziffern der Kartennummer var getdigits = /\d/g; var digits = []; while (match = getdigits.exec(cardnumber)) { digits.push(parseInt(match[0], 10)); } // Luhn-Algorithmus für die Ziffern ausführen var sum = 0; var alt = false; for (var i = digits.length - 1; i >= 0; i--) { if (alt) { digits[i] *= 2; if (digits[i] > 9) { digits[i] -= 9; } } sum += digits[i]; alt = !alt; } // Prüfung der Kartennummer if (sum % 10 == 0) { document.getElementById("notice").innerHTML += '; Luhn-Prüfung erfolgreich'; } else { document.getElementById("notice").innerHTML += '; Luhn-Prüfung nicht erfolgreich'; } }
4.19 Kreditkartennummern validieren | 293
Diese Funktion übernimmt einen String mit der Kreditkartennummer als Parameter. Die Kartennummer sollte nur aus Ziffern bestehen. In unserem Beispiel hat validatecardnumber() schon Leerzeichen und Bindestriche entfernt und ermittelt, ob die Kartennummer die richtige Anzahl an Ziffern besitzt. In der Funktion wird zunächst der reguläre Ausdruck ‹\d› verwendet, um über alle Ziffern im String iterieren zu können. Beachten Sie dabei den Modifikator /g. Innerhalb der Schleife findet sich in match[0] die Ziffer. Da reguläre Ausdrücke nur mit Texten arbeiten, rufen wir parseInt() auf, um sicherzustellen, dass die Variable als Integer und nicht als String gespeichert wird. Tun wir das nicht, findet sich später in der Variablen sum eine String-Verkettung von Ziffern und nicht die aufsummierten Werte. Der eigentliche Algorithmus läuft über das Array und berechnet eine Prüfsumme. Lässt sich diese Summe ohne Rest durch 10 teilen, ist die Kartennummer gültig.
4.20 Europäische Umsatzsteuer-Identifikationsnummern Problem Sie sollen ein Onlinebestellformular für eine Firma in der Europäischen Union erstellen. Kauft eine für die Umsatzsteuer registrierte Firma (Ihr Kunde), die in einem EU-Land sitzt, von einem Verkäufer (Ihre Firma) in einem anderen EU-Land etwas, muss der Verkäufer nach EU-Steuerrecht keine Umsatzsteuer berechnen. Hat der Käufer keine Umsatzsteuer-Identifikationsnummer (USt-IdNr.) angegeben, muss der Verkäufer die Mehrwertsteuer berechnen und sie an das lokale Finanzamt abführen. Um das zu vermeiden, nutzt der Verkäufer die USt-IdNr. des Käufers als Beweis, dass keine Steuer fällig ist. Für den Verkäufer ist es also sehr wichtig, die USt-IdNr. des Käufers zu überprüfen, bevor er die Bestellung ohne Umsatzsteuer durchführt. Die häufigste Ursache für ungültige Umsatzsteuer-Identifikationsnummern sind einfache Tippfehler vom Kunden. Um den Bestellprozess schneller und einfacher zu gestalten, sollten Sie die USt-IdNr. mit einer Regex überprüfen, während der Kunde das Bestellformular ausfüllt. Das lässt sich mit etwas JavaScript-Code auf Clientseite oder im CGISkript auf Ihrem Webserver erreichen. Passt die Nummer nicht zum regulären Ausdruck, kann der Kunde den Tippfehler direkt korrigieren.
Lösung Leerzeichen, Bindestriche und Punkte entfernen Lesen Sie die vom Kunden eingegebene USt-IdNr. aus und speichern Sie sie in einer Variablen. Bevor Sie die Gültigkeit der Nummer prüfen, ersetzen Sie die durch folgenden regulären Ausdruck gefundenen Übereinstimmungen mit einem leeren String:
294 | Kapitel 4: Validierung und Formatierung
[-.z]
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby In Rezept 3.14 wird gezeigt, wie Sie diese Ersetzung durchführen können. Wir sind davon ausgegangen, dass der Kunde abgesehen von Bindestrichen, Punkten und Leerzeichen keine anderen Satzzeichen eingegeben hat. Jegliches weitere „falsche“ Zeichen wird von der nächsten Prüfung abgefangen.
Überprüfen der Nummer Nachdem Leerzeichen, Punkte und Bindestriche entfernt wurden, prüft dieser reguläre Ausdruck, ob die USt-IdNr. für einen der 27 EU-Staaten gültig ist: ^( (AT)?U[0-9]{8} | # Österreich (BE)?0?[0-9]{9} | # Belgien (BG)?[0-9]{9,10} | # Bulgarien (CY)?[0-9]{8}L | # Zypern (CZ)?[0-9]{8,10} | # Tschechische Republik (DE)?[0-9]{9} | # Deutschland (DK)?[0-9]{8} | # Dänemark (EE)?[0-9]{9} | # Estland (EL|GR)?[0-9]{9} | # Griechenland # Spanien (ES)?[0-9A-Z][0-9]{7}[0-9A-Z] | (FI)?[0-9]{8} | # Finnland (FR)?[0-9A-Z]{2}[0-9]{9} | # Frankreich (GB)?([0-9]{9}([0-9]{3})?|[A-Z]{2}[0-9]{3}) | # Großbritannien (HU)?[0-9]{8} | # Ungarn (IE)?[0-9]S[0-9]{5}L | # Irland (IT)?[0-9]{11} | # Italien (LT)?([0-9]{9}|[0-9]{12}) | # Litauen (LU)?[0-9]{8} | # Luxemburg (LV)?[0-9]{11} | # Lettland (MT)?[0-9]{8} | # Malta (NL)?[0-9]{9}B[0-9]{2} | # Niederlande (PL)?[0-9]{10} | # Polen (PT)?[0-9]{9} | # Portugal (RO)?[0-9]{2,10} | # Rumänien (SE)?[0-9]{12} | # Schweden (SI)?[0-9]{8} | # Slowenien (SK)?[0-9]{10} # Slowakei )$
Regex-Optionen: Freiform, Groß-/Kleinschreibung wird ignoriert Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Dieser reguläre Ausdruck verwendet den Freiform-Modus, um ein späteres Bearbeiten der Regex zu vereinfachen. Schließlich nimmt die EU gelegentlich auch neue Länder auf, oder die Staaten passen ihre Regeln für die USt-IdNr. an. Leider ist in JavaScript kein Freiform-Modus möglich. Dort müssen Sie alles in einer Zeile unterbringen:
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby In Rezept 3.6 wird beschrieben, wie Sie diesen regulären Ausdruck auf Ihrem Bestellformular unterbringen können.
Diskussion Leerzeichen, Punkte und Bindestriche entfernen Damit die Umsatzsteuer-Identifikationsnummern für Menschen leichter lesbar sind, werden sie häufig mit zusätzlichen Trennzeichen eingegeben. So könnte ein deutscher Kunde seine USt-IdNr. DE123456789 zum Beispiel als DE 123.456.789 eingeben. Ein einzelner regulärer Ausdruck, der die Nummern aus 27 Ländern in allen möglichen Schreibweisen erkennt, ist unmöglich zu realisieren. Da die Trennzeichen nur der Lesbarkeit dienen, ist es viel einfacher, zunächst alle diese Zeichen zu entfernen und dann die reine USt-IdNr. zu analysieren. Der reguläre Ausdruck ‹[-.z]› passt zu einem Zeichen, das ein Bindestrich, ein Punkt oder ein Leerzeichen ist. Ersetzt man alle Übereinstimmungen dieses regulären Ausdrucks durch einen leeren String, werden diese Zeichen aus den Nummern entfernt. Umsatzsteuer-Identifikationsnummern bestehen nur aus Buchstaben und Ziffern. Statt lediglich die am häufigsten eingegebenen Trennzeichen mit ‹[-.z]› zu entfernen, können Sie auch alle ungültigen Zeichen mit ‹[^A-Z0-9]› eliminieren.
Validieren der Nummer Die zwei regulären Ausdrücke für das Validieren der Nummer sind identisch. Nur nutzt der erste den Freiform-Modus, damit er leichter lesbar ist. JavaScript unterstützt diesen Modus nicht, aber bei den anderen Varianten haben die Sie freie Wahl. Die Regex nutzt eine große Alternation, um die Umsatzsteuer-Identifikationsnummern aller 27 EU-Staaten berücksichtigen zu können. Die Formate sehen so aus: Belgien 999999999 oder 0999999999 Bulgarien 999999999 oder 9999999999
296 | Kapitel 4: Validierung und Formatierung
Dänemark 99999999 Deutschland 999999999 Estland 999999999 Finnland 99999999 Frankreich XX999999999 Griechenland 999999999 Großbritannien 999999999, 999999999999 oder XX999 Irland 9S99999L Italien 99999999999 Lettland 99999999999 Litauen 999999999 oder 99999999999 Luxemburg 99999999 Malta 99999999 Niederlande 999999999B99 Österreich U99999999 Polen 999999999 Portugal 999999999 Rumänien 99, 999, 9999, 99999, 999999, 9999999, 99999999, 999999999 oder 9999999999 Schweden 99999999999 Slowakei 999999999 4.20 Europäische Umsatzsteuer-Identifikationsnummern | 297
Slowenien 99999999 Spanien X9999999X Tschechische Republik 99999999, 999999999 oder 9999999999 Ungarn 99999999 Zypern 99999999L Streng genommen ist der zweistellige Ländercode Teil der USt-IdNr. Aber viele lassen ihn häufig weg, da die Rechnungsanschrift schon den Staat angibt. Der reguläre Ausdruck akzeptiert daher die Nummern mit und ohne Ländercode. Wenn Sie wollen, dass er eingegeben werden muss, entfernen Sie alle Fragezeichen aus dem regulären Ausdruck. Dann sollten Sie aber auch in der Fehlermeldung, die den Anwender auf eine ungültige USt-IdNr. hinweist, erwähnen, dass der Ländercode eingegeben werden muss. Akzeptieren Sie Bestellungen nur aus bestimmten Ländern, können Sie die Länder aus der Regex weglassen, die in der Länderauswahl auf Ihrem Bestellformular vorhanden sind. Löschen Sie eine der Alternativen, müssen Sie auch den Operator ‹|› löschen, der die Alternativen untereinander trennt. Tun Sie das nicht, steht in Ihrem regulären Ausdruck ‹||›. Das führt aber zu einer Alternative, die einen leeren String akzeptiert, sodass Ihr Bestellformular letztendlich auch ohne USt-IdNr. fertiggestellt werden kann. Die 27 Alternativen sind in einer Gruppe zusammengefasst. Diese Gruppe umfasst den gesamten Bereich zwischen einem Zirkumflex und einem Dollar, wodurch der reguläre Ausdruck mit Anfang und Ende des zu überprüfenden Strings verbunden ist. Die gesamte Eingabe muss also eine gültige USt-IdNr. sein. Suchen Sie in einem längeren Text nach Umsatzsteuer-Identifikationsnummern, ersetzen Sie die Anker durch die Wortgrenzen ‹\b›.
Variationen Der Vorteil eines regulären Ausdrucks für alle 27 Staaten liegt darin, dass Sie in Ihrem Formular nur eine Regex-Überprüfung benötigen. Sie könnten aber auch 27 getrennte Regexes nutzen. Zuerst prüfen Sie das Land, das der Kunde in der Rechnungsanschrift angegeben hat. Dann validieren Sie die USt-IdNr. abhängig vom Land: Belgien ‹^(BE)?0?[0-9]{9}$›
Implementieren Sie Rezept 3.6, um die USt-IdNr. mit der ausgewählten Regex zu validieren. Damit erfahren Sie, ob die Nummer für das Land, das der Kunde angegeben hat gültig ist. Der Hauptvorteil der getrennten regulären Ausdrücke ist, dass die USt-IdNr. auf jeden Fall mit der korrekten Länderkennung beginnen kann, ohne dass der Kunde sie angeben muss. Passt der reguläre Ausdruck auf die angegebene Nummer, prüfen Sie den Inhalt der ersten einfangenden Gruppe. In Rezept 3.9 wird beschrieben, wie Sie das machen können. Ist die erste einfangende Gruppe leer, hat der Kunde den Ländercode nicht mit eingegeben. Sie können ihn dann selbst ergänzen, bevor Sie die überprüfte Nummer in Ihrer Bestelldatenbank ablegen. Griechische Umsatzsteuer-Identifikationsnummern können zwei verschiedene Ländercodes haben. EL wird traditionell für griechische Nummern verwendet, aber GR ist der ISO-Ländercode für Griechenland.
Siehe auch Der reguläre Ausdruck prüft nur, ob die Nummer wie eine gültige USt-IdNr. aussieht. Das reicht aus, um echte Fehler auszuschließen. Aber ein regulärer Ausdruck kann offensichtlich nicht prüfen, ob die Nummer der Firma zugeordnet ist, die die Bestellung aufgibt. Die Europäische Union hat eine Website (http://ec.europa.eu/taxation_customs/vies/ vieshome.do), auf der Sie prüfen können, zu welcher Firma eine bestimmte USt-IdNr. gehört – wenn sie denn überhaupt zugewiesen ist. Die Nummern werden mit der Datenbank des entsprechenden Staats verglichen. Manche Staaten bestätigen dabei allerdings nur eine Gültigkeit, ohne weitere Informationen über die entsprechende Firma herauszugeben. Die in diesem regulären Ausdruck genutzten Techniken werden in den Rezepten 2.3, 2.5 und 2.8 besprochen.
300 | Kapitel 4: Validierung und Formatierung
KAPITEL 5
Wörter, Zeilen und Sonderzeichen
Dieses Kapitel enthält Rezepte für das Finden und Bearbeiten von Texten. Mit einigen dieser Rezepte erreichen Sie Dinge, die Sie vielleicht von einer ausgefuchsten SuchEngine erwarten – so zum Beispiel das Finden eines von mehreren Wörtern oder das Finden von Wörtern, die nahe beieinanderstehen. Andere Beispiele helfen Ihnen dabei, ganze Zeilen zu finden, die bestimmte Wörter enthalten, Wortwiederholungen zu entfernen oder Meta-zeichen in regulären Ausdrücken zu maskieren. Vor allem geht es in diesem Kapitel aber darum, Regex-Konstrukte und -Techniken im echten Einsatz zu präsentieren. Lesen Sie sich die Rezepte durch, ist das wie ein Training für einen ganzen Reigen von Regex-Features. Damit fällt es Ihnen in Zukunft leichter, reguläre Ausdrücke auf eigene Probleme anzuwenden. In vielen Fällen ist das, wonach wir suchen, einfach, aber die Vorlagen, die wir in den Lösungen bereitstellen, ermöglichen es Ihnen, sie an Ihre eigenen Probleme anzupassen.
5.1
Ein bestimmtes Wort finden
Problem Sie haben die einfache Aufgabe, alle Vorkommen des Worts „rot“ zu finden, unabhängig von Groß- oder Kleinschreibung. Entscheidend ist, dass es ein vollständiges Wort sein muss. Sie wollen keine Teile längerer Wörter finden, wie zum Beispiel Brot, Karotte oder rotieren.
Lösung Tokens für Wortgrenzen sorgen für eine ganze einfache Lösung: \brot\b
In Rezept 3.7 wird gezeigt, wie Sie mit diesem regulären Ausdruck alle Übereinstimmungen finden können. In Rezept 3.14 ist beschrieben, wie Sie die Übereinstimmungen durch anderen Text ersetzen können.
Diskussion Die Wortgrenzen an beiden Enden des regulären Ausdrucks stellen sicher, dass rot nur dann gefunden wird, wenn es als eigenständiges Wort auftaucht. Genauer gesagt, erzwingen die Wortgrenzen, dass rot von anderem Text durch den Anfang oder das Ende des Strings, durch Whitespace, Satzzeichen oder andere Nicht-Wortzeichen getrennt ist. Regex-Engines betrachten Buchstaben, Ziffern und Unterstriche als Wortzeichen. Wortgrenzen werden detaillierter in Rezept 2.6 behandelt. Es kann aber ein Problem geben, wenn man in JavaScript, in der PCRE und in Ruby mit internationalen Texten arbeitet, da diese Regex-Varianten nur Buchstaben aus der ASCIITabelle zum Erstellen der Wortgrenzen in Betracht ziehen. Die Wortgrenzen werden also nur an Positionen zwischen ‹^|[^A-Za-z0-9_]› und ‹[A-Za-z0-9_]› oder zwischen ‹[A-Za-z0-9_]› und ‹[^A-Za-z0-9_]|$› gefunden. Das Gleiche gilt für Python, wenn die Option UNICODE oder U nicht gesetzt ist. Damit wird leider verhindert, dass man ‹\b› in Sprachen für Suchen nach ganzen Wörtern einsetzen kann, die akzentuierte Buchstaben oder Wörter mit nicht lateinischen Schriftsystemen enthalten. So wird zum Beispiel in JavaScript, in der PCRE und in Ruby durch ‹\büber\b› eine Übereinstimmung in darüber gefunden, aber nicht in dar über. In den meisten Fällen ist das genau das Gegenteil von dem, was Sie wollen. Das Problem ist, dass ü als Nicht-Wortzeichen angesehen und daher eine Wortgrenze zwischen den beiden Zeichen rü gefunden wird. Dagegen gibt es dann keine Wortgrenze zwischen einem Leerzeichen und ü, da beide als Nicht-Wortzeichen betrachtet werden. Sie können dieses Problem durch die Verwendung von Lookaheads und Lookbehinds (gemeinsam als Lookarounds bezeichnet) statt von Wortgrenzen umgehen. Wie Wortgrenzen passen Lookarounds auf Übereinstimmungen der Länge null. In der PCRE (wenn sie mit UTF-8-Unterstützung kompiliert wurde) und Ruby 1.9 können Sie auf Unicode basierende Wortgrenzen zum Beispiel durch ‹(? Wörter, vor denen nicht „rot“ steht (? Regex-Optionen: Groß-/Kleinschreibung wird ignoriert Regex-Variante: .NET (? Regex-Optionen: Groß-/Kleinschreibung wird ignoriert Regex-Varianten: .NET, Java, PCRE (? Regex-Optionen: Groß-/Kleinschreibung wird ignoriert Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby 1.9
Lookbehinds simulieren JavaScript und Ruby 1.8 haben gar kein Lookbehind-Feature, auch wenn sie Lookaheads unterstützen. Aber da das Lookbehind in dieser Lösung ganz am Anfang der Regex steht, kann man es wunderbar simulieren, indem man die Regex in zwei Teile aufteilt. Hier ein JavaScript-Beispiel: var subject = 'Mein Auto ist rot und gelb.', main_regex = /\b\w+/g, lookbehind = /\brot\W+$/i, lookbehind_type = false, // Negatives Lookbehind matches = [], match, left_context; while (match = main_regex.exec(subject)) { left_context = subject.substring(0, match.index); if (lookbehind_type == lookbehind.test(left_context)) { matches.push(match[0]); } else { main_regex.lastIndex = match.index + 1; } } // Passt auf:
['Mein','Auto','ist','rot','gelb']
314 | Kapitel 5: Wörter, Zeilen und Sonderzeichen
Diskussion Lookbehinds mit fester, endlicher und unendlicher Länge Der erste reguläre Ausdruck nutzt das negative Lookbehind ‹(? Lookbehinds simulieren In JavaScript gibt es keine Lookbehinds, aber der Beispielcode zeigt, wie Sie Lookbehinds, die am Anfang einer Regex stehen, simulieren können, indem Sie zwei reguläre Ausdrücke kombinieren. Damit gibt es auch keine Einschränkungen bezüglich der Textlänge, die durch das (simulierte) Lookbehind gefunden werden kann. Zunächst teilen wir den regulären Ausdruck ‹(? 5.6 Ein beliebiges Wort finden, das nicht hinter einem bestimmten Wort steht | 315
sie nur ganz am Ende des Ausgangstexts passt. Die Variable lookbehind_type gibt an, ob wir einen positiven (true) oder einen negativen (false) Lookbehind emulieren. Nachdem die Variablen eingerichtet sind, verwenden wir main_regex und die Methode exec, um über den Ausgangstext zu iterieren (in Rezept 3.11 finden Sie eine Beschreibung dieses Prozesses). Wird eine Übereinstimmung gefunden, wird der Teil des Ausgangstexts vor der Übereinstimmung in eine neue String-Variable kopiert (left_context), und wir prüfen, ob die Regex lookbehind auf diesen String passt. Durch den Anker am Ende der Regex lookbehind wird diese zweite Übereinstimmung immer direkt links von der ersten Übereinstimmung liegen. Durch einen Vergleich des Ergebnisses des LookbehindTests mit lookbehind_type können wir herausfinden, ob die Übereinstimmung alle Kriterien für eine erfolgreiche Suche erfüllt. Schließlich führen wir einen von zwei Schritten durch. Gibt es eine erfolgreiche Übereinstimmung, fügen wir den gefundenen Text an das Array matches an. Wenn nicht, passen wir die Position an, an der wir mit der Suche fortfahren wollen (mit main_regex.lastIndex). Wir fahren ein Zeichen nach der Ausgangsposition der letzten Übereinstimmung des Objekts main_regex fort, statt die nächste Iteration der Methode exec am Ende der aktuellen Übereinstimmung beginnen zu lassen. Puh! Fertig. Das ist ein fortgeschrittener Trick, der sich die Eigenschaft lastIndex zunutze macht. Diese Eigenschaft wird bei JavaScript-Regexes dynamisch aktualisiert, wenn diese die Option /g („global“) verwenden. Normalerweise wird lastIndex irgendwie automatisch angepasst oder zurückgesetzt. Hier verwenden wir sie, um das Vorangehen der RegexEngine im Ausgangstext zu beeinflussen. Damit können Sie aber nur Lookbehinds emulieren, die am Anfang einer Regex stehen. Mit ein paar Änderungen könnte dieser Code auch verwendet werden, um Lookbehinds am Ende einer Regex nachzubilden. Das ist natürlich kein vollständiger Ersatz für eine echte Lookbehind-Unterstützung. Aufgrund des Zusammenspiels zwischen Lookbehinds und Backtracking kann dieser Ansatz das Verhalten eines Lookbehind in der Mitte einer Regex nicht vollständig emulieren.
Variationen Wenn Sie nur Wörter finden wollen, vor denen rot steht (ohne rot und die folgenden Nicht-Wortzeichen in den gefundenen Text mit aufzunehmen), ändern Sie das negative Lookbehind in ein positives: (?(wort1)|(wort2)|(wort3)|(?(1)|(?(2)|(?(3)|(?!))))\w+)\b\W*?){3,8} (?(1)(?(2)(?(3)|(?!))|(?!))|(?!))
Abbildung 5-2: Viele Möglichkeiten, eine Menge zu ordnen
Die gleiche Regex, diesmal ohne atomare Gruppen (siehe Rezept 2.14), dafür mit einer normalen nicht-einfangenden Gruppe, damit sie auch in Python genutzt werden kann: \b(?:(?:(wort1)|(wort2)|(wort3)|(?(1)|(?(2)|(?(3)|(?!))))\w+)\b\W*?){3,8} (?(1)(?(2)(?(3)|(?!))|(?!))|(?!))
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert Regex-Varianten: .NET, PCRE, Perl, Python Die Quantoren ‹{3,8}› in den regulären Ausdrücken berücksichtigen die drei erforderlichen Wörter und ermöglichen null bis fünf andere Wörter dazwischen. Die leeren negativen Lookaheads ‹(?!)› passen niemals und werden daher genutzt, um bestimmte Pfade durch die Regex zu blockieren, bis eines oder mehrere der notwendigen Wörter gefunden wurden. Die Logik, die diese Pfade kontrolliert, ist durch zwei Sets an verschachtelten Bedingungen implementiert. Das erste Set verhindert, dass ein schon gefundenes Wort ‹\w+› gefunden wird, bis mindestens eines der notwendigen Wörter passt. Das zweite Set
320 | Kapitel 5: Wörter, Zeilen und Sonderzeichen
an Bedingungen am Ende zwingt die Regex-Engine dazu, per Backtracking zurückzugehen oder einen Misserfolg zu vermelden, bis nicht alle notwendigen Wörter gefunden wurden. Das ist nur eine kurze Beschreibung der Funktionsweise, aber statt hier noch tiefer einzusteigen und zu erklären, wie man zusätzliche notwendige Wörter ergänzt, wollen wir uns lieber eine verbesserte Implementierung anschauen, die mehr Regex-Varianten unterstützt und ein bisschen trickreicher vorgeht. Leere Rückwärtsreferenzen ausnutzen: Die hässliche Lösung funktioniert, ist aber eher ein Anwärter auf den Preis für die verwirrendste Regex, da sie sich nur schlecht lesen und handhaben lässt. Und mit jedem weiteren Wort würde es nur noch schlimmer werden. Glücklicherweise gibt es einen Regex-Hack, der deutlich einfacher zu verstehen ist und der zudem auch unter Java und Ruby läuft (die beide keine Bedingungen unterstützen). Das in diesem Abschnitt beschriebene Verhalten sollte in produktiven Anwendungen nur mit Vorsicht eingesetzt werden. Wir nutzen dabei Regex-Verhaltensweisen aus, die in den meisten Regex-Bibliotheken nicht dokumentiert sind. \b(?:(?>wort1()|wort2()|wort3()|(?>\1|\2|\3)\w+)\b\W*?){3,8}\1\2\3
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Mit diesem Konstrukt kann man auch sehr einfach weitere notwendige Wörter hinzufügen. Hier ein Beispiel mit vier erforderlichen Wörtern in beliebiger Reihenfolge, wobei höchstens fünf Wörter zwischen ihnen liegen dürfen: \b(?:(?>wort1()|wort2()|wort3()|wort4()| (?>\1|\2|\3|\4)\w+)\b\W*?){4,9}\1\2\3\4
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Diese regulären Ausdrücke nutzen mit Absicht leere einfangende Gruppe hinter jedem erforderlichen Wort. Da jeder Versuch, eine Rückwärtsreferenz wie ‹\1› zu finden, fehlschlägt, wenn die entsprechende einfangende Gruppe noch nicht an der Übereinstimmung beteiligt war, können Rückwärtsreferenzen auf leere Gruppen genutzt werden, um den Pfad der Regex-Engine durch ein Muster so zu kontrollieren, wie es mit den weiter
5.7 Wörter finden, die nahe beieinanderstehen | 321
oben vorgestellten umfangreicheren Bedingungen möglich war. War die entsprechende Gruppe schon an der Übereinstimmung beteiligt, wenn die Engine die Rückwärtsreferenz erreicht, wird sie einfach einen leere String finden und mit der Arbeit fortfahren. Hier verhindert die Gruppe ‹(?>\1|\2|\3)›, dass mit ‹\w+› ein Wort gefunden wird, bevor nicht mindestens eines der notwendigen Wörter passt. Die Rückwärtsreferenzen werden am Ende des Musters wiederholt, um zu verhindern, dass erfolgreich eine Übereinstimmung abgeschlossen werden kann, bevor nicht alle notwendigen Wörter gefunden wurden. Python unterstützt keine atomaren Gruppen, daher werden auch hier in dem Beispiel, das für Python genutzt werden kann, die atomaren Gruppen durch nicht-einfangende Gruppen ersetzt. Dadurch werden die Regexes zwar weniger effizient, aber die gefundenen Inhalte unterscheiden sich nicht. Die äußere Gruppe kann in keiner Variante atomar sein, denn die Regex-Engine muss innerhalb der äußeren Gruppe per Backtracking arbeiten können, wenn die Rückwärtsreferenzen am Ende des Musters keinen Erfolg haben. JavaScript-Rückwärtsreferenzen mit eigenen Regeln: Obwohl JavaScript die in der Python-Version dieses Musters genutzte Syntax vollständig unterstützt, gibt es dort zwei Unterschiede, die dafür sorgen, dass dieser Trick hier nicht funktioniert. Beim ersten geht es darum, was durch Rückwärtsreferenzen auf einfangende Gruppen gefunden wird, die noch nicht Teil einer Übereinstimmung sind. In der JavaScript-Spezifikation steht, dass solche Rückwärtsreferenzen einen leeren String finden, also immer erfolgreich sind. In so gut wie jeder anderen Regex-Variante gilt das Gegenteil: Sie passen niemals und sorgen damit dafür, dass die Regex-Engine per Backtracking zurückspringen muss, bis entweder die gesamte Suche ein Fehlschlag war oder bis die Gruppe, auf die sie verweisen, Teil der Übereinstimmung ist. Dann passt auch wieder die Rückwärtsreferenz. Der zweite Unterschied bei der JavaScript-Variante dreht sich um den Wert, der von einer einfangenden Gruppe in einer wiederholten äußeren Gruppe gefunden wird, zum Beispiel ‹((a)|(b))+›. Bei den meisten Regex-Varianten entspricht der Wert, den sich eine einfangende Gruppe innerhalb einer wiederholten Gruppe merkt, dem, was als Letztes gefunden wurde. Wenn zum Beispiel mit ‹(?:(a)|(b))+› der Text ab gefunden wurde, wird der Wert der Rückwärtsreferenz 1 auf a gesetzt sein. Nach der JavaScript-Spezifikation wird aber der Wert von Rückwärtsreferenzen in verschachtelten Gruppen jedes Mal zurückgesetzt, wenn die äußere Gruppe wiederholt wird. Findet also ‹(?:(a)|(b))+› wieder den Text ab, würde die Rückwärtsreferenz 1 hier auf eine nicht beteiligte einfangende Gruppe verweisen, was in JavaScript innerhalb der Regex selbst einem leeren String entspricht. Das von RegExp.prototype.exec zurückgegebene Array würde hier den Wert undefined liefern. Beide Verhaltensunterschiede führen in der JavaScript-Regex-Variante dazu, dass Bedingungen in einer Regex auf dem oben vorgestellten Weg nicht emuliert werden können.
322 | Kapitel 5: Wörter, Zeilen und Sonderzeichen
Mehrere Wörter, die einen beliebigen Abstand voneinander haben können Wenn Sie einfach nur prüfen wollen, ob in einem Text eine Liste von Wörtern gefunden werden kann, wobei der Abstand zwischen den Wörtern beliebig sein kann, bieten positive Lookaheads eine Möglichkeit, das in einer Suchoperation zu erledigen. In vielen Fällen ist es einfacher und effizienter, jeden Term einzeln zu suchen und zu prüfen, ob alle Tests erfolgreich waren.
\A(?=.*?\bwort1\b)(?=.*?\bwort2\b).*\Z
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert, Punkt passt auf Zeilenumbruch Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby ^(?=[\s\S]*?\bwort1\b)(?=[\s\S]*?\bwort2\b)[\s\S]*$
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert (Zirkumflex und Dollarzeichen passen auf Zeilenumbruch darf nicht gesetzt sein) Regex-Variante: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Diese regulären Ausdrücke passen auf den gesamten String, auf den sie angewendet werden, wenn alle Ihre gesuchten Wörter darin enthalten sind. Ansonsten gibt es keine Übereinstimmung. JavaScript-Programmierer können die erste Version nicht nutzen, da JavaScript die Anker ‹\A› und ‹\Z› oder die Option Punkt passt auf Zeilenumbruch nicht unterstützt. Sie können diese regulären Ausdrücke wie in Rezept 3.6 implementieren. Ändern Sie einfach die Platzhalter ‹wort1› und ‹wort2› in die gesuchten Begriffe. Wenn Sie nach mehr als zwei Wörtern suchen, können Sie so viele Lookaheads wie nötig in den regulären Ausdruck einfügen. Mit ‹\A(?=.*?\bwort1\b)(?=.*?\bwort2\b)(?=.*?\bwort3\b).*\Z› suchen Sie zum Beispiel nach drei Wörtern.
Siehe auch Rezepte 5.5 und 5.6.
5.8
Wortwiederholungen finden
Problem Sie bearbeiten ein Dokument und würden gern prüfen, ob Sie unabsichtlich Wörter wiederholt haben. Diese doppelten Wörter sollen auch dann gefunden werden, wenn sie in unterschiedlicher Groß- und Kleinschreibung eingetippt wurden, wie zum Beispiel bei „Wer wer“. Der Whitespace zwischen den Wörtern ist Ihnen ebenfalls egal. Er kann beliebig groß sein, auch wenn die Wörter damit auf unterschiedlichen Zeilen gelandet sind.
5.8 Wortwiederholungen finden | 323
Lösung Eine Rückwärtsreferenz passt auf etwas, das vorher gefunden wurde. Damit ist sie die wichtigste Zutat für dieses Rezept: \b([A-Z]+)\s+\1\b
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Wenn Sie diesen regulären Ausdruck verwenden wollen, um das erste Wort zu behalten, die Wiederholung aber zu entfernen, ersetzen Sie alle Übereinstimmungen durch die Rückwärtsreferenz 1. Sie können die Übereinstimmungen auch mit anderen Zeichen umschließen (zum Beispiel durch ein HTML-Tag), um sie bei einer späteren Überarbeitung schneller zu erkennen. In Rezept 3.15 erfahren Sie, wie Sie Rückwärtsreferenzen in Ihrem Ersetzungstext verwenden können. Wenn Sie nur doppelte Wörter finden wollen, um manuell zu prüfen, ob sie korrigiert werden müssen, finden Sie in Rezept 3.7 den notwendigen Code. Ein Texteditor oder ein grep-ähnliches Tool – wie die in „Tools für das Arbeiten mit regulären Ausdrücken“ in Kapitel 1 erwähnten – kann Ihnen dabei helfen, doppelte Wörter zu finden, während Sie gleichzeitig den Kontext sehen, in dem die fraglichen Wörter stehen.
Diskussion Man braucht zwei Dinge, um etwas zu finden, was schon vorher gefunden wurde: eine einfangende Gruppe und eine Rückwärtsreferenz. Stecken Sie das, was Sie mehr als einmal finden wollen, in eine einfangende Gruppe und suchen Sie es dann erneut mithilfe einer Rückwärtsreferenz. Das ist etwas anderes, als ein Token oder eine Gruppe mit einem Quantor zu wiederholen. Schauen Sie sich den Unterschied zwischen den beiden vereinfachten regulären Ausdrücken ‹(\w)\1› und ‹\w{2}› an. Die erste Regex nutzt eine einfangende Gruppe und eine Rückwärtsreferenz, mit der das gleiche Wortzeichen doppelt gefunden werden kann, während die zweite Regex einen Quantor nutzt, um zwei beliebige Wortzeichen zu finden. In Rezept 2.10 wird die Faszination von Rückwärtsreferenzen im Detail beschrieben. Aber zurück zum eigentlichen Problem. Dieses Rezept findet nur doppelte Wörter, die aus den Buchstaben A bis Z und a bis z bestehen (da die Option zum Ignorieren von Groß- und Kleinschreibung aktiv ist). Um auch akzentuierte Zeichen und Zeichen aus anderen Schriftsystemen zuzulassen, können Sie die Buchstabeneigenschaft für UnicodeZeichen verwenden (‹\p{L}›), wenn Ihre Regex-Variante das unterstützt (siehe „Unicode-Eigenschaften oder -Kategorien“ auf Seite 49). Zwischen der einfangenden Gruppe und der Rückwärtsreferenz passt ‹\s+› auf beliebig viele Whitespace-Zeichen, also auf Leerzeichen, Tabs und Zeilenumbrüche. Wollen Sie die Zeichen auf solche einschränken, die horizontale Abstände darstellen (also keine Zeilenumbrüche), ersetzen Sie ‹\s› durch ‹[^\S\r\n]›. Damit wird verhindert, dass Sie dop-
324 | Kapitel 5: Wörter, Zeilen und Sonderzeichen
pelte Wörter finden, die sich nicht gemeinsam in einer Zeile befinden. Bei der PCRE 7 und in Perl 5.10 gibt es die Zeichenklassenabkürzung ‹\h›, die Sie hier vielleicht lieber einsetzen wollen, weil damit nur horizontaler Whitespace gefunden wird. Schließlich stellen die Wortgrenzen am Anfang und am Ende des regulären Ausdrucks sicher, dass die Übereinstimmungen nicht innerhalb anderer Wörter liegen, wie zum Beispiel bei „die Diebe“. Beachten Sie, dass doppelte Wörter nicht immer falsch sind, daher ist es zu gefährlich, sie einfach ohne Kontrolle zu löschen. Die Konstrukte „die die“ oder „das das“ sind durchaus korrekt. Homonyme, Namen, lautmalerische Wörter (wie zum Beispiel „oink oink“ oder „ha ha“) und einige andere Konstrukte führen auch durchaus zu bewusst wiederholten Wörtern. Daher werden Sie in den meisten Fällen jede Übereinstimmung in ihrem Kontext überprüfen müssen.
Siehe auch Rezept 2.10 behandelt Rückwärtsreferenzen im Detail. Rezept 5.9 zeigt, wie man doppelte Textzeilen findet.
5.9
Doppelte Zeilen entfernen
Problem Sie haben eine Logdatei, die Ausgabe einer Datenbankabfrage oder eine andere Art von Datei oder String mit doppelten Zeilen. Sie müssen diese doppelten Einträge mit einem Texteditor oder einem ähnlichen Tool entfernen.
Lösung Es gibt eine Reihe von Softwaretools (wie zum Beispiel das Befehlszeilentool uniq unter Unix und das Windows PowerShell Cmdlet Get-Unique), mit denen Sie doppelte Zeilen in einer Datei oder einem String entfernen können. Die folgenden Abschnitte enthalten drei Regex-basierte Ansätze, die besonders dann hilfreich sein können, wenn man versucht, diese Aufgabe in einem nicht skriptbaren Texteditor umzusetzen, der aber immerhin mithilfe von regulären Ausdrücken suchen und ersetzen kann. Beim Programmieren sollten die Optionen 2 und 3 vermieden werden, da sie im Vergleich zu anderen verfügbaren Vorgehensweisen ineffizient sind. Hier sollte man zum Beispiel eher auf ein Hash-Objekt zurückgreifen, um die Eindeutigkeit von Zeilen zu gewährleisten. Aber die erste Option (bei der Sie die Zeilen vorher sortieren müssen, sofern Sie nicht nur direkt hintereinanderliegende doppelte Zeilen entfernen wollen) kann auch hier durchaus akzeptabel sein, da sie sich schnell und einfach implementieren lässt.
5.9 Doppelte Zeilen entfernen | 325
Option 1: Zeilen sortieren und hintereinanderliegende Duplikate entfernen Wenn Sie die Zeilen in der Datei oder dem String sortieren können, sodass doppelte Zeilen direkt hintereinanderliegen, sollten Sie das tun, sofern die Reihenfolge der Zeilen nicht beibehalten werden muss. Denn damit wird das Suchen und Ersetzen der doppelten Einträge viel einfacher und effizienter. Nach dem Sortieren der Zeilen verwenden Sie die folgende Regex mit dem ErsetzungsString, um die Duplikate loszuwerden: ^(.*)(?:(?:\r?\n|\r)\1)+$
Regex-Optionen: ^ und $ passen auf Zeilenumbruch (Punkt passt auf Zeilenumbruch darf nicht gesetzt sein) Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Zu ersetzen durch: $1
Ersetzungstextvarianten: Python, Ruby Dieser reguläre Ausdruck nutzt eine einfangende Gruppe und eine Rückwärtsreferenz (neben anderen Bestandteilen), um zwei oder mehr aufeinanderfolgende doppelte Zeilen zu finden. Mit einer Rückwärtsreferenz im Ersetzungstext fügen Sie nur die erste Zeile wieder ein. In Rezept 3.15 finden Sie Beispielcode, den Sie auch hier verwenden können.
Option 2: Das letzte Vorkommen jeder doppelten Zeile in einer unsortierten Datei behalten Wenn Sie einen Texteditor nutzen, mit dem Sie Zeilen nicht sortieren können, oder wenn es wichtig ist, die ursprüngliche Reihenfolge beizubehalten, können Sie mit der folgenden Lösung doppelte Zeilen entfernen, auch wenn es zwischen ihnen andere Zeilen gibt: ^([^\r\n]*)(?:\r?\n|\r)(?=.*^\1$)
Regex-Optionen: Punkt passt auf Zeilenumbruch, ^ und $ passen auf Zeilenumbruch Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Das Gleiche noch einmal als JavaScript-kompatible Regex, bei der die Option Punkt passt auf Zeilenumbruch nicht notwendig ist: ^(.*)(?:\r?\n|\r)(?=[\s\S]*^\1$)
Regex-Optionen: ^ und $ passen auf Zeilenumbruch (Punkt passt auf Zeilenumbruch darf nicht gesetzt sein) Regex-Variante: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Zu ersetzen durch: (Einen leeren String, also nichts.) Ersetzungstextvarianten: nicht notwendig
326 | Kapitel 5: Wörter, Zeilen und Sonderzeichen
Option 3: Das erste Vorkommen jeder doppelten Zeile in einer unsortierten Datei behalten Wenn Sie das erste Vorkommen jeder doppelten Zeile behalten wollen, brauchen Sie einen etwas anderen Ansatz. Dies sind der reguläre Ausdruck und der Ersetzungstext, die wir verwenden werden: ^([^\r\n]*)$(.*?)(?:(?:\r?\n|\r)\1$)+
Regex-Optionen: Punkt passt auf Zeilenumbruch, ^ und $ passen auf Zeilenumbruch Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Auch hier sind ein paar Änderungen nötig, um diese Regex mit JavaScript nutzen zu können, da JavaScript keine Option Punkt passt auf Zeilenumbruch besitzt. ^(.*)$([\s\S]*?)(?:(?:\r?\n|\r)\1$)+
Regex-Optionen: ^ und $ passen auf Zeilenumbruch (Punkt passt auf Zeilenumbruch darf nicht gesetzt sein) Regex-Variante: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Zu ersetzen durch: $1$2
Ersetzungstextvarianten: Python, Ruby Anders als bei den Regexes der Optionen 1 und 2 können in dieser Version nicht alle doppelten Zeilen mit einem Suchen-und-Ersetzen-Durchlauf erledigt werden. Sie werden Alle ersetzen so lange anwenden müssen, bis die Regex keine Übereinstimmungen mehr in Ihrem String findet. Denn erst dann werden keine doppelten Zeilen mehr vorhanden sein. In „Diskussion“ erfahren Sie mehr Details dazu.
Diskussion Option 1: Zeilen sortieren und hintereinanderliegende Duplikate entfernen Diese Regex entfernt bei aufeinanderfolgenden doppelten Zeilen alle bis auf die erste. Sie entfernt keine doppelten Zeilen, die durch andere Zeilen getrennt sind. Lassen Sie uns den Prozess Schritt für Schritt durchgehen. Zuerst passt der Zirkumflex (‹^›) am Anfang des regulären Ausdrucks auf den Zeilenanfang. Normalerweise würde er nur am Anfang des Gesamttexts passen, daher müssen Sie sicherstellen, dass die Option ^ und $ passen auf Zeilenumbruch aktiv ist (in Rezept 3.4 erfahren Sie, wie Sie Regex-Optionen einschalten). Als Nächstes passt ‹.*› innerhalb der einfangenden Klammern auf den gesamten Inhalt einer Zeile (selbst wenn sie leer ist), und der Wert wird als Rückwärtsreferenz 1 gespeichert. Damit das korrekt funktioniert, darf die Option Punkt passt auf Zeilenumbruch nicht aktiv sein. Ansonsten würde die Punkt-Stern-Kombination auf alles bis zum Ende des Strings passen.
5.9 Doppelte Zeilen entfernen | 327
Innerhalb einer äußeren, nicht-einfangenden Gruppe nutzen wir ‹(?:\r?\n|\r)›, um ein Zeilenende in Windows (‹\r\n›), Unix/Linux/OS X (‹\n›) oder im alten Mac OS (‹\r›) zu finden. Die Rückwärtsreferenz ‹\1› versucht dann, die Zeile zu finden, die wir gerade gefunden hatten. Wenn an dieser Stelle nicht die gleiche Zeile vorhanden ist, gibt es hier keine Übereinstimmung, und die Regex-Engine fährt mit ihrer Suche fort. Passt die Zeile, wiederholen wir die Gruppe (bestehend aus einer Zeilenumbruchfolge und der Rückwärtsreferenz 1) mit dem Quantor ‹+›, um noch weitere doppelte Zeilen zu finden. Schließlich nutzen wir das Dollarzeichen am Ende der Regex, um sicherzustellen, dass wir uns am Zeilenende befinden. Damit finden wir auch nur wirklich identische Zeilen und nicht solche, die nur mit den gleichen Zeichen beginnen wie die vorherige Zeile. Da wir hier suchen und ersetzen, wird jede vollständige Übereinstimmung (einschließlich der Originalzeile und der Zeilentrennzeichen) aus dem String entfernt. Wir ersetzen sie mit der Rückwärtsreferenz 1, um die Ursprungszeile beizubehalten.
Option 2: Das letzte Vorkommen jeder doppelten Zeile in einer unsortierten Datei behalten Im Vergleich zur Regex aus Option 1 gibt es hier eine Reihe von Änderungen, denn die erste Regex findet nur direkt aufeinanderfolgende doppelte Zeilen. So wurde in der Nicht-JavaScript-Version der Regex aus Option 2 der Punkt innerhalb der einfangenden Gruppe durch ‹[^\r\n]› ersetzt (jedes Zeichen außer einem Zeilenumbruch) und die Option Punkt passt auf Zeilenumbruch aktiviert. Das liegt daran, dass später noch ein Punkt genutzt wird, um beliebige Zeichen zu finden – eben auch Zeilenumbrüche. Und es wurde ein Lookahead ergänzt, um nach doppelten Zeilen weiter unten im String zu suchen. Da das Lookahead keine Zeichen konsumiert, ist der von der Regex gefundene Text immer eine einzelne Zeile (zusammen mit dem folgenden Zeilenumbruch), die auf jeden Fall weiter unten im String nochmals vorkommen wird. Ersetzt man alle Übereinstimmungen durch leere Strings, werden die doppelten Zeilen entfernt, und es bleibt nur deren jeweils letztes Vorkommen.
Option 3: Das erste Vorkommen jeder doppelten Zeile in einer unsortierten Datei behalten Da Lookbehinds in nicht so vielen Varianten unterstützt werden wie Lookaheads (und man dann auch nicht immer so weit zurückschauen kann, wie man es braucht), unterscheidet sich die Regex aus Option 3 deutlich von der vorherigen. Statt Zeilen zu finden, von denen man weiß, dass sie später noch einmal vorkommen (was die Taktik in Option 2 ist), passt diese Regex auf eine Zeile in einem String, die weiter unten folgende Dublette dieser Zeile und alle Zeilen dazwischen. Die ursprüngliche Zeile wird als Rückwärtsreferenz 1 gespeichert, die Zeilen dazwischen (wenn es denn welche gibt) als Rückwärtsreferenz 2. Indem Sie jede Übereinstimmung durch die Rückwärtsreferenzen 1 und 2 ersetzen, fügen Sie nur die Teile wieder ein, die Sie behalten wollen. Die abschließende doppelte Zeile und der direkt davorliegende Zeilenumbruch werden aber verworfen.
328 | Kapitel 5: Wörter, Zeilen und Sonderzeichen
Dieses alternative Vorgehen hat allerdings ein paar Haken. Erstens: Eine Übereinstimmung mit doppelten Zeilen kann wiederum andere Zeilen enthalten, in denen es auch (andere) Dubletten gibt. Diese werden aber durch ein Alles ersetzen übersprungen. Zweitens: Wenn eine Zeile nicht nur doppelt, sondern drei- oder mehrfach vorkommt, wird die Regex die ersten beiden entsprechenden Zeilen finden, aber nicht den Rest. Dort werden (bei mehr als dreifachem Vorkommen) wiederum eventuell Dubletten gefunden. Damit wird eine einzelne Operation Alles ersetzen höchstens jede zweite Dublette einer bestimmten Zeile entfernen. Um beide Probleme zu lösen und sicherzustellen, dass alle Dubletten entfernt sind, müssen Sie wiederholt suchen und ersetzen, bis die Regex keine Übereinstimmungen mehr findet. Schauen Sie sich einmal an, wie die Regex mit folgendem Ausgangstext arbeitet: Wert1 Wert2 Wert2 Wert3 Wert3 Wert1 Wert2
Um alle Dubletten aus diesem String zu entfernen, benötigt man drei Durchläufe. In Tabelle 5-1 werden die Ergebnisse jedes Durchlaufs gezeigt. Tabelle 5-1: Ersetzungsdurchläufe Durchlauf 1
Durchlauf 2
Durchlauf 3
Ergebnis
Matchtext
Wert1
Wert1
Wert1
Wert2
Wert2
Wert2
Wert2
Wert2
Wert2
Wert3
Wert3
Wert3
Wert3
Wert2
Wert3
Wert3
Wert1
Wert2
Wert2 Eine Übereinstimmung/Ersetzung
Zwei Übereinstimmungen/Ersetzungen
Eine Übereinstimmung/Ersetzung
Keine weiteren Dubletten
Siehe auch In Rezept 2.10 werden Rückwärtsreferenzen im Detail erklärt. In Rezept 5.8 erfahren Sie, wie Sie Wortdubletten finden können.
5.9 Doppelte Zeilen entfernen | 329
5.10 Vollständige Zeilen finden, die ein bestimmtes Wort enthalten Problem Sie wollen alle Zeilen finden, die das Wort Ninja enthalten.
Lösung ^.*\bNinja\b.*$
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert, ^ und $ passen auf Zeilenumbruch (Punkt passt auf Zeilenumbruch darf nicht gesetzt sein) Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Es ist häufig nützlich, vollständige Zeilen zu finden, um sie in einer Liste zu sammeln oder um sie zu entfernen. Um eine Zeile zu finden, die das Wort Ninja enthält, beginnen wir mit dem regulären Ausdruck ‹\bNinja\b›. Die Wortgrenzen-Tokens auf beiden Seiten stellen sicher, dass wir „Ninja“ nur finden, wenn es als vollständiges Wort erscheint (siehe Rezept 2.6). Um die Regex so zu erweitern, dass sie eine komplette Zeile findet, ergänzen Sie an beiden Enden ‹.*›. Die Punkt-Stern-Folge passt auf null oder mehr Zeichen innerhalb der aktuellen Zeile. Die Stern-Quantoren sind gierig, daher konsumieren sie so viel Text wie möglich. Die erste Punkt-Stern-Folge passt bis zum letzten Vorkommen von „Ninja“ auf der Zeile, während die zweite Punkt-Stern-Folge alle Zeichen danach (außer Zeilenumbrüchen) findet. Durch ein Zirkumflex am Anfang und ein Dollarzeichen am Ende des regulären Ausdrucks wird schließlich sichergestellt, dass die Übereinstimmung eine komplette Zeile umfasst. Streng genommen ist das Dollarzeichen am Ende überflüssig, weil Punkt und gieriger Stern immer nur bis zum Ende der Zeile laufen. Aber das Dollarzeichen tut nicht weh, und der reguläre Ausdruck wird dadurch etwas selbsterklärender. Durch das Hinzufügen von Zeilen- oder String-Ankern (sofern passend) lassen sich gelegentlich unerwartete Phänomene vermeiden, daher ist es keine schlechte Idee, sich dies anzugewöhnen. Beachten Sie, dass der Zirkumflex im Gegensatz zum Dollarzeichen nicht überflüssig ist, da man dadurch sicherstellt, dass die Regex die vollständige Zeile abdeckt, selbst wenn die Suche aus irgendeinem Grund erst in der Mitte der Zeile loslegt. Denken Sie daran, dass die drei wichtigsten Metazeichen zum Eingrenzen der Übereinstimmung auf eine einzelne Zeile (die Anker ‹^› und ‹$› sowie der Punkt) keine feste Bedeutung besitzen. Um sie an Zeilen zu orientieren, müssen Sie die Option aktivieren, mit der ‹^› und ‹$› an Zeilenumbrüchen passen, während die Option, mit der der Punkt
330 | Kapitel 5: Wörter, Zeilen und Sonderzeichen
auch einen Zeilenumbruch findet, eben nicht aktiviert sein darf. In Rezept 3.4 wird gezeigt, wie Sie diese Optionen in Ihrem Code setzen können. Wenn Sie JavaScript oder Ruby nutzen, müssen Sie sich nur um eine der beiden Optionen Gedanken machen, weil JavaScript keine Option anbietet, mit dem Punkt Zeilenumbrüche zu finden, und die Zirkumflex- und Dollarzeichen-Anker bei Ruby immer einen Zeilenumbruch finden.
Variationen Um nach Zeilen zu suchen, die eines von mehreren Wörtern enthalten, verwenden Sie eine Alternation: ^.*\b(eins|zwei|drei)\b.*$
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert, ^ und $ passen auf Zeilenumbruch (Punkt passt auf Zeilenumbruch darf nicht aktiv sein) Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Dieser reguläre Ausdruck passt auf jede Zeile, die mindestens eines der Wörter „eins“, „zwei“ oder „drei“ enthält. Die Klammern um die Wörter erfüllen gleich zwei Zwecke. Zum einen begrenzen sie die Reichweite der Alternation, und zum anderen fangen sie das Wort in der Rückwärtsreferenz 1 ein, das tatsächlich gefunden wurde. Wenn die Zeile mehr als eines der Wörter enthält, enthält die Rückwärtsreferenz das Wort, das in der Zeile am weitesten rechts steht. Das liegt daran, dass der Stern-Quantor vor den Klammern gierig ist und mit dem Punkt so viel Text wie möglich einfängt. Machen Sie den Stern genügsam, wie in ‹^.*?\b(eins|zwei|drei)\b.*$›, enthält die Rückwärtsreferenz 1 das Wort aus Ihrer Liste, das am weitesten links steht. Um Zeilen zu finden, die mehrere Wörter enthalten müssen, verwenden Sie Lookaheads: ^(?=.*?\beins\b)(?=.*?\bzwei\b)(?=.*?\bdrei\b).+$
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert, ^ und $ passen auf Zeilenumbruch (Punkt passt auf Zeilenumbruch darf nicht aktiv sein) Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Dieser reguläre Ausdruck verwendet positive Lookaheads, um Zeilen zu finden, die drei notwendige Wörter enthalten. Das ‹.+› wird genutzt, um die eigentliche Zeile zu finden, nachdem die Lookaheads festgestellt haben, dass die Zeile auch die Anforderungen erfüllt.
Siehe auch In Rezept 5.11 wird gezeigt, wie Sie vollständige Zeilen finden, die ein bestimmtes Wort nicht enthalten.
5.10 Vollständige Zeilen finden, die ein bestimmtes Wort enthalten | 331
5.11 Vollständige Zeilen finden, die ein bestimmtes Wort nicht enthalten Problem Sie wollen vollständige Zeilen finden, die nicht das Wort Ninja enthalten.
Lösung ^(?:(?!\bNinja\b).)*$
Regex-Optionen: Groß-/Kleinschreibung wird ignoriert, ^ und $ passen auf Zeilenumbruch (Punkt passt auf Zeilenumbruch darf nicht aktiv sein) Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Um eine Zeile zu finden, die etwas nicht enthält, nutzen Sie negative Lookaheads (beschrieben in Rezept 2.16). Beachten Sie, dass in diesem regulären Ausdruck ein negatives Lookahead und ein Punkt zusammen in einer nicht-einfangenden Gruppe wiederholt werden. Damit ist sichergestellt, dass die Regex ‹\bNinja\b› an jeder möglichen Position in der Zeile fehlschlägt. Die Anker ‹^› und ‹$› werden an den Anfang und das Ende des regulären Ausdrucks gesetzt, um auf jeden Fall eine vollständige Zeile zu finden. Die Optionen, die Sie auf diesen regulären Ausdruck anwenden, bestimmen, ob der gesamte Ausgangstext oder immer nur eine Zeile durchforstet wird. Mit der aktiven Option, ‹^› und ‹$› auf Zeilenumbrüche passen zu lassen, und der deaktivierten Option, Punkte auf Zeilenumbrüche passen zu lassen, arbeitet dieser reguläre Ausdruck wie gewünscht zeilenweise. Schalten Sie beide Optionen um, wird der reguläre Ausdruck jeden String finden, der nicht das Wort „Ninja“ enthält. Das Testen eines negativen Lookahead an jeder Position einer Zeile oder eines Strings ist ziemlich ineffizient. Diese Lösung sollte nur in Situationen genutzt werden, in denen ein einziger regulärer Ausdruck verwendet werden kann, zum Beispiel in Anwendungen, die nicht geskriptet werden können. Beim Programmieren ist Rezept 3.21 eine deutlich effizientere Lösung.
Siehe auch In Rezept 5.10 wird gezeigt, wie Sie vollständige Zeilen finden können, die ein bestimmtes Wort enthalten.
332 | Kapitel 5: Wörter, Zeilen und Sonderzeichen
5.12 Führenden und abschließenden Whitespace entfernen Problem Sie wollen führenden und abschließenden Whitespace aus einem String entfernen.
Lösung Um das Ganze einfach und schnell zu halten, nutzt man am besten zwei Ersetzungsdurchläufe – einen, um den führenden Whitespace zu entfernen, und einen für den abschließenden Whitespace: ^\s+
Regex-Optionen: Keine (^ und $ passen auf Zeilenumbrüche darf nicht aktiv sein) Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby \s+$
Regex-Optionen: Keine (^ und $ passen auf Zeilenumbrüche darf nicht aktiv sein) Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Ersetzen Sie die gefundenen Übereinstimmungen beider regulären Ausdrücke einfach durch einen leeren String. In Rezept 3.14 können Sie nachlesen, wie das funktioniert. Bei beiden regulären Ausdrücken müssen Sie nur die erste gefundene Übereinstimmung entfernen, da der gesamte führende beziehungsweise abschließende Whitespace in einem Schritt gefunden wird.
Diskussion Führenden und abschließenden Whitespace zu entfernen, ist eine einfache und häufig verlangte Aufgabe. Die beiden regulären Ausdrücke enthalten jeweils drei Elemente: einen Anker, um die Position am Anfang oder Ende des Strings sicherzustellen (‹^› und ‹$›), die Zeichenklassenabkürzung, um beliebige Whitespace-Zeichen zu finden (‹\s›), und den Quantor, mit dem die Klasse einmal oder mehrfach gefunden werden kann (‹+›). Viele Programmiersprachen stellen schon eine Funktion bereit, mit der führender oder abschließender Whitespace entfernt werden kann. Diese Funktionen heißen oft trim oder strip. In Tabelle 5-2 sehen Sie, wie Sie diese eingebauten Funktionen oder Methoden in einer Reihe von Programmiersprachen verwenden. Tabelle 5-2: Standardfunktionen, um führenden und abschließenden Whitespace zu entfernen Programmiersprache(n)
Beispielanwendung
C#, VB.NET
String.Trim([Zeichen])
Java
string.trim()
PHP
trim($String)
Python, Ruby
string.strip()
5.12 Führenden und abschließenden Whitespace entfernen | 333
JavaScript und Perl besitzen solche Funktionen nicht in ihren Standardbibliotheken, aber Sie können leicht eine eigene schreiben. In Perl: sub trim { my $string = shift; $string =~ s/^\s+//; $string =~ s/\s+$//; return $string; }
In JavaScript: function trim (string) { return string.replace(/^\s+/, '').replace(/\s+$/, ''); } // Alternativ trim zu einer Methode aller Strings machen: String.prototype.trim = function () { return this.replace(/^\s+/, '').replace(/\s+$/, ''); };
Sowohl in Perl als auch in JavaScript passt ‹\s› auf alle Zeichen, die durch den Unicode-Standard als Whitespace definiert sind – also mehr als nur Leerzeichen, Tab, Line Feed und Carriage Return.
Variationen Es gibt eigentlich eine ganze Reihe von Möglichkeiten, mit einem regulären Ausdruck einen String zu „trimmen“. Aber alle sind bei langen Strings (bei denen Performance durchaus ein Thema ist) langsamer als die beiden einfachen Ersetzungen. Im Folgenden finden Sie ein paar gebräuchliche Alternativen, die Sie in Betracht ziehen könnten. Alle sind in JavaScript geschrieben, und da JavaScript die Option Punkt passt auf Zeilenumbruch nicht besitzt, greifen die regulären Ausdrücke auf ‹[\s\S]› zurück, um ein beliebiges Zeichen zu finden – auch Zeilenumbrüche. In anderen Programmiersprachen verwenden Sie einen Punkt und aktivieren die Option Punkt passt auf Zeilenumbruch. string.replace(/^\s+|\s+$/g, '');
Dies ist die vermutlich gebräuchlichste Lösung. Sie kombiniert die beiden einfachen Regexes per Alternation (siehe Rezept 2.8) und nutzt die Option /g (global), um alle Übereinstimmungen zu ersetzen (wenn es sowohl führenden als auch abschließenden Whitespace gibt, passt die Regex zwei Mal). Das ist kein wirklich grässlicher Ansatz, aber er ist bei langen Strings langsamer als die Verwendung der beiden einfachen Ersetzungen. string.replace(/^\s*([\s\S]*?)\s*$/, '$1')
Dieser reguläre Ausdruck passt auf den gesamten String und fängt den Text vom ersten bis zum letzten Nicht-Whitespace-Zeichen (wenn es denn welchen gibt) in der Rückwärtsreferenz 1. Ersetzen Sie den gesamten String durch die Rückwärtsreferenz 1, erhalten Sie eine zurechtgestutzte Version des Strings. 334 | Kapitel 5: Wörter, Zeilen und Sonderzeichen
Dieser Ansatz ist prinzipiell einfach, aber der genügsame Quantor innerhalb der einfangenden Gruppe sorgt dafür, dass die Regex-Engine eine ganze Menge zu tun hat. Daher wird dieser reguläre Ausdruck bei langen Strings eher langsam sein. Hat die Regex-Engine die einfangende Gruppe erreicht, sorgt der genügsame Quantor dafür, dass die Zeichenklasse ‹[\s\S]› so wenig wie möglich gefunden wird. Daher greift die Regex-Engine immer nur auf ein Zeichen gleichzeitig zu und versucht, nach jedem Zeichen das restliche Muster (‹\s*$›) zu finden. Wenn das nicht funktioniert, weil es keine Nicht-Whitespace-Zeichen mehr nach der aktuellen Position im String gibt, nimmt die Engine ein weiteres Zeichen dazu und versucht dann erneut, das restliche Muster zu finden. string.replace(/^\s*([\s\S]*\S)?\s*$/, '$1')
Diese Regex ist der vorherigen sehr ähnlich, aber der genügsame Quantor wird aus Performancegründen durch einen gierigen ersetzt. Um sicherzustellen, dass die einfangende Gruppe trotzdem nur bis zum letzten Nicht-Whitespace-Zeichen läuft, nutzen wir ein abschließendes ‹\S›. Da die Regex dennoch auch Strings aus reinen Whitespaces erkennen muss, ist die gesamte einfangende Gruppe optional, indem ihr ein abschließender Fragezeichen-Quantor angehängt wird. Lassen Sie uns genauer anschauen, wie diese Regex arbeitet. Hier wiederholt der gierige Stern in ‹[\s\S]*› das Muster für jedes Zeichen, bis das Ende des Strings erreicht ist. Die Regex-Engine geht dann vom Ende des Strings per Backtracking ein Zeichen zurück, bis sie das folgende ‹\S› finden kann oder bis wieder das erste gefundene Zeichen innerhalb der einfangenden Gruppe erreicht wurde. Sofern es nicht mehr abschließende Whitespace-Zeichen als Text gibt, ist diese Regex im Allgemeinen schneller als die vorherige mit dem genügsamen Quantor. Aber auch sie kann die Performance der beiden einfachen Substitutionen nicht erreichen. string.replace(/^\s*(\S*(?:\s+\S+)*)\s*$/, '$1')
Das ist ein recht verbreiteter Ansatz, daher soll er hier als schlechtes Beispiel mit aufgeführt werden. Es gibt keinen guten Grund, diese Regex zu verwenden, da sie langsamer als alle anderen hier vorgestellten Regexes ist. Sie entspricht den letzten beiden Regexes, da sie den gesamten String findet und Sie ihn durch den Teil ersetzen, den Sie beibehalten wollen. Aber da die innere nicht-einfangende Gruppe immer nur ein Wort gleichzeitig findet, gibt es eine Reihe von getrennten Schritten, die die Regex-Engine durchführen muss. Beim Zurechtstutzen kurzer Strings wird man den Performanceverlust nicht bemerken, aber bei sehr langen Strings mit vielen Wörtern kann diese Regex zu einem echten Performanceproblem werden. Ein paar Regex-Implementierungen enthalten pfiffige Optimierungen, die die hier beschriebenen internen Suchprozesse verändern und damit einige der vorgestellten Regexes etwas schneller oder langsamer laufen lassen, als wir beschrieben haben. Aber die bestechende Einfachheit bei der vorgeschlagenen Lösung mit den beiden Ersetzungen führt zu einer durchgehend guten Performance – bei unterschiedlichen String-Längen und -Inhalten. Daher ist es die beste Lösung.
5.12 Führenden und abschließenden Whitespace entfernen | 335
Siehe auch Rezept 5.13.
5.13 Wiederholten Whitespace durch ein einzelnes Leerzeichen ersetzen Problem Um Benutzereingaben oder andere Daten zunächst „aufzuräumen”, wollen Sie wiederholte Whitespace-Zeichen durch ein einzelnes Leerzeichen ersetzen. Jegliche Tabs, Zeilenumbrüche oder andere Whitespace-Zeichen sollten ebenfalls durch ein Leerzeichen ersetzt werden.
Lösung Um einen der folgenden regulären Ausdrücke zu implementieren, ersetzen Sie einfach alle Übereinstimmungen durch ein einzelnes Leerzeichen. In Rezept 3.14 ist beschrieben, wie der Code dafür aussieht.
Alle Whitespace-Zeichen finden \s+
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Horizontale Whitespace-Zeichen finden [z\t]+
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Häufig soll man in einem Text alle Whitespace-Zeichen durch ein einzelnes Leerzeichen ersetzen. In HTML werden zum Beispiel wiederholte Whitespace-Zeichen beim Rendern einer Seite schlicht ignoriert (von ein paar Ausnahmen abgesehen), daher kann man durch das Entfernen von überflüssigem Whitespace die Dateigröße reduzieren, ohne sich negative Effekte einzuhandeln.
336 | Kapitel 5: Wörter, Zeilen und Sonderzeichen
Alle Whitespace-Zeichen finden In dieser Lösung wird jede Folge von Whitespace-Zeichen (Zeilenumbrüchen, Tabs, Leerzeichen und so weiter) durch ein einzelnes Leerzeichen ersetzt. Da der Quantor ‹+› die Whitespace-Klasse (‹\s›) ein Mal oder mehrfach findet, wird zum Beispiel selbst ein einfaches Tab-Zeichen durch ein Leerzeichen ersetzt. Ersetzen Sie das ‹+› durch ‹{2,}›, werden nur Folgen von zwei oder mehr Whitespace-Zeichen ersetzt. Das kann zu weniger Ersetzungsvorgängen und damit einer verbesserten Performance führen, aber so können auch Tab-Zeichen oder Zeilenumbrüche übrig bleiben, die ansonsten durch Leerzeichen ersetzt worden wären. Der bessere Ansatz hängt daher davon ab, was Sie erreichen wollen.
Horizontale Whitespace-Zeichen finden Diese Regex arbeitet genau so wie die vorherige Lösung, nur dass sie Zeilenumbrüche bestehen lässt. Lediglich Tabs und Leerzeichen werden ersetzt.
Siehe auch Rezept 5.12.
5.14 Regex-Metazeichen maskieren Problem Sie wollen einen literalen String verwenden, der von einem Anwender oder aus einer anderen Quelle stammt, um ihn in Ihren regulären Ausdruck einzubauen. Allerdings wollen Sie alle Regex-Metazeichen innerhalb des Strings maskieren, bevor Sie ihn in Ihre Regex einbetten, um unerwünschte Nebeneffekte zu verhindern.
Lösung Indem Sie vor jedem Zeichen, das in einem regulären Ausdruck potenziell eine besondere Bedeutung haben kann, einen Backslash einfügen, können Sie das sich so ergebende Muster problemlos verwenden, um eine literale Zeichenfolge zu finden. Von den in diesem Buch behandelten Programmiersprachen haben abgesehen von JavaScript alle eine eingebaute Funktion oder Methode, um diese Aufgabe zu erledigen (siehe Tabelle 5-3). Aus Gründen der Vollständigkeit zeigen wir hier aber, wie Sie das mit Ihrer eigenen Regex erreichen – auch in Sprachen, die schon eine fertige Lösung bieten.
Eingebaute Lösungen In Tabelle 5-3 sind die vorgefertigten Funktionen aufgeführt, die dieses Problem lösen.
5.14 Regex-Metazeichen maskieren | 337
Tabelle 5-3: Eingebaute Lösungen für das Maskieren von Regex-Metazeichen Sprache
Funktion
C#, VB.NET
Regex.Escape(str)
Java
Pattern.quote(str)
Perl
quotemeta(str)
PHP
preg_quote(str, [delimiter])
Python
re.escape(str)
Ruby
Regexp.escape(str)
In dieser Liste fehlt JavaScript – es hat keine eingebaute Funktion, die diesen Zweck erfüllt.
Regulärer Ausdruck Auch wenn am besten eine eingebaute Funktion genutzt wird (wenn es sie denn gibt), können Sie auch den folgenden regulären Ausdruck mit dem entsprechenden ErsetzungsString verwenden. Stellen Sie aber sicher, dass nicht nur der erste, sondern alle Übereinstimmungen ersetzt werden. In Rezept 3.15 finden Sie Code, mit dem Sie Übereinstimmungen durch Text ersetzen können, die eine Rückwärtsreferenz enthalten. Die Rückwärtsreferenz ist hier notwendig, um das gefundene Spezialzeichen zusammen mit einem führenden Backslash wieder einfügen zu können: [[\]{}()*+?.\\|^$\-,\s]
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Ersetzung Die folgenden Ersetzungs-Strings enthalten einen literalen Backslash. Die Strings werden ohne zusätzliche Backslashs aufgeführt, die notwendig sein können, um die Backslashs bei String-Literalen zu maskieren. In Rezept 2.19 finden Sie mehr Details über Ersetzungstextvarianten. \$&
Ersetzungstextvarianten: .NET, JavaScript \\$&
Ersetzungstextvariante: Perl \\$0
Ersetzungstextvarianten: Java, PHP \\\0
Ersetzungstextvarianten: PHP, Ruby
338 | Kapitel 5: Wörter, Zeilen und Sonderzeichen
\\\&
Ersetzungstextvariante: Ruby \\\g
Ersetzungstextvariante: Python
Beispielfunktion in JavaScript Hier sehen Sie ein Beispiel dafür, wie Sie in JavaScript mit dem regulären Ausdruck und dem Ersetzungstext eine statische Methode RegExp.escape erstellen können: RegExp.escape = function (str) { return str.replace(/[[\]{}()*+?.\\|^$\-,\s]/g, "\\$&"); }; // Testen var str = "Hello.World?"; var escaped_str = RegExp.escape(str); alert(escaped_str == "Hallo\\.Welt\\?"); // -> true
Diskussion Der reguläre Ausdruck in diesem Rezept steckt alle Regex-Metazeichen in eine einzelne Zeichenklasse. Lassen Sie uns jedes dieser Zeichen anschauen und erklären, warum es maskiert werden muss. Bei einigen ist das weniger offensichtlich als bei anderen: [] {} () ‹[› und ‹]› erzeugen Zeichenklassen. ‹{› und ‹}› erzeugen Intervall-Quantoren
und werden auch in verschiedenen anderen Konstrukten genutzt, wie zum Beispiel bei Unicode-Eigenschaften. ‹(› und ‹)› werden zum Gruppieren, Einfangen und in anderen Konstrukten verwendet. * + ?
Diese drei Zeichen sind Quantoren, die das vorherige Element null Mal oder häufiger, ein Mal oder häufiger oder null oder ein Mal wiederholen. Das Fragezeichen wird zudem nach einer öffnenden Klammern genutzt, um besondere Gruppen und andere Konstrukte zu erzeugen (das Gleiche gilt für den Stern in Perl 5.10 und PCRE 7). . \ |
Ein Punkt passt auf jedes Zeichen innerhalb einer Zeile oder eines Strings, ein Backslash maskiert ein Sonderzeichen oder sorgt dafür, dass ein literales Zeichen etwas Besonderes darstellt. Ein vertikaler Balken dient als Alternationsoption. ^ $
Zirkumflex und Dollar sind Anker, die den Anfang oder das Ende einer Zeile oder eines Strings finden. Der Zirkumflex kann zudem eine Zeichenklasse negieren. Die restlichen Zeichen, die von diesem regulären Ausdruck gefunden werden, haben nur in besonderen Fällen eine spezielle Bedeutung. Sie sind in der Liste enthalten, um auf Nummer sicher gehen zu können.
5.14 Regex-Metazeichen maskieren | 339
-
Ein Bindestrich erzeugt in einer Zeichenklasse einen Bereich. Er wird hier maskiert, um das unabsichtliche Erstellen von Bereichen zu vermeiden, wenn in einer Zeichenklasse Text eingefügt wird. Wenn Sie Text in eine Zeichenklasse einfügen, sollten Sie daran denken, dass damit nicht der eingebettete Text gefunden wird, sondern nur eines der Zeichen aus dem String. ,
Ein Komma wird innerhalb eines Intervall-Quantors wie zum Beispiel ‹{1,5}› verwendet. Da die meisten Regex-Varianten geschweifte Klammern als literale Zeichen betrachten, wenn sie keinen gültigen Quantor bilden, ist es möglich (wenn auch recht unwahrscheinlich), durch das Einfügen von Text ohne maskierte Kommata einen Quantor an einer Stelle zu erzeugen, an der dies so gar nicht gedacht war. &
Das Kaufmanns-Und ist in der Liste enthalten, weil zwei aufeinanderfolgende Kaufmanns-Und-Zeichen in Java eine Zeichenklassen-Schnittmenge erzeugen. In anderen Programmiersprachen kann man das Kaufmanns-Und gefahrlos aus der Liste der zu maskierenden Zeichen entfernen, aber es macht auch nichts, es drin zu lassen. # und Whitespace
Das Rautezeichen und Whitespace (durch ‹\s› gefunden) sind nur im FreiformModus Metazeichen. Auch für sie gilt, dass es nicht schadet, sie zu maskieren. Beim Ersetzungstext wird eines von fünf Tokens («$&», «\&», «$0», «\0» oder «\g») genutzt, um die gefundenen Zeichen zusammen mit einem führenden Backslash wieder einzufügen. In Perl ist $& eine echte Variable. Nutzt man sie in einem regulären Ausdruck, kann das nachteilige Auswirkungen auf alle regulären Ausdrücke haben. Wird $& irgendwo anders in Ihrem Perl-Programm verwendet, können Sie sie so viel verwenden, wie Sie wollen, weil der Performanceverlust nur einmal auftritt. Ansonsten ist es vermutlich besser, die gesamte Regex in eine einfangende Gruppe zu stecken und im Ersetzungstext $1 statt $& zu nutzen.
Variationen Wie schon in „Blockmaskierung“ auf Seite 29 in Rezept 2.1 beschrieben, können Sie in einem regulären Ausdruck eine Blockmaskierungsfolge erzeugen, indem Sie ‹\Q...\E› nutzen. Aber solche Blockmaskierungen werden nur in Java, der PCRE und Perl unterstützt, und selbst in diesen Sprachen sind sie nicht absolut idiotensicher. Um vollständig sicher zu sein, müssen Sie immer noch jedes Vorkommen von \E maskieren. In den meisten Fällen ist es vermutlich günstiger, einfach alle Regex-Metazeichen zu maskieren.
Siehe auch In Rezept 2.1 wird besprochen, wie man literale Zeichen findet. Die dort aufgeführte Liste mit zu maskierenden Zeichen ist kürzer, da sie sich nicht um Zeichen kümmert, die im Freiform-Modus oder beim Einfügen in ein längeres Muster maskiert werden müssen. 340 | Kapitel 5: Wörter, Zeilen und Sonderzeichen
KAPITEL 6
Zahlen
Reguläre Ausdrücke sind eigentlich dazu gedacht, mit Texten zu arbeiten. Sie verstehen nichts von der numerischen Bedeutung, die Menschen einer Folge von Ziffern zuordnen. Für einen regulären Ausdruck ist 56 nicht die Nummer sechsundfünfzig, sondern ein String mit zwei Zeichen, die die Ziffern 5 und 6 darstellen. Die Regex-Engine weiß, dass es sich um Ziffern handelt, da sie über die Zeichenklassenabkürzung ‹\d› gefunden werden können (siehe Rezept 2.3). Aber das war es auch schon. Die Engine weiß nicht, dass 56 eine höhere Bedeutung besitzt, so wie sie auch nicht weiß, dass man in :-) mehr sehen kann als drei Satzzeichen, die durch ‹\p{P}{3}› gefunden werden. Aber Zahlen sind ein wichtiger Bestandteil der Benutzereingaben oder Dateiinhalte, mit denen Sie arbeiten. Manchmal müssen Sie sie innerhalb eines regulären Ausdrucks finden, statt sie einfach an eine normale Programmiersprache zu geben – zum Beispiel wenn Sie herausbekommen möchten, ob eine Zahl im Bereich von 1 bis 100 liegt. Deshalb handelt dieses ganze Kapitel davon, wie man mit regulären Ausdrücken alle möglichen Arten von Zahlen finden kann. Wir beginnen mit ein paar Rezepten, die vielleicht trivial erscheinen, aber wichtige grundlegende Konzepte aufzeigen. Die folgenden Rezepte drehen sich dann um kompliziertere Regexes, und dort wird davon ausgegangen, dass Sie diese Konzepte verstanden haben.
6.1
Integer-Zahlen
Problem Sie wollen verschiedene Arten von ganzen Zahlen in einem längeren Text finden oder prüfen, ob eine String-Variable eine dezimale Ganzzahl enthält.
| 341
Lösung Positive dezimale Ganzzahlen (Integer-Zahlen) in einem längeren Text finden: \b[0-9]+\b
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Prüfen, ob ein String nur aus einer positiven dezimalen Ganzzahl besteht: \A[0-9]+\Z
Regex-Optionen: Keine Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby ^[0-9]+$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python Finden einer positiven dezimalen Ganzzahl, die in einem größeren Text für sich steht: (? 4, 'V' => 5, 'IX' => 9, 'X' => 10, 'XL' => 40, 'L' => 50, 'XC' => 90, 'C' => 100, 'CD' => 400, 'D' => 500, 'CM' => 900, 'M' => 1000); my $decimal = 0; while ($roman =~ m/[MDLV]|C[MD]?|X[CL]?|I[XV]?/ig) { $decimal += $r2d{uc($&)}; } return $decimal; } else { # Keine römische Zahl return 0; } }
Siehe auch Rezepte 2.3, 2.8, 2.9, 2.12, 2.16, 3.9 und 3.11.
366 | Kapitel 6: Zahlen
KAPITEL 7
URLs, Pfade und Internetadressen
Neben den Zahlen, die Thema des vorherigen Kapitels waren, sind in vielen Programmen die verschiedenen Pfade und Locators zum Finden von Daten wichtig: • URLs, URNs und entsprechende Strings • Domainnamen • IP-Adressen • Datei- und Ordnernamen in Microsoft Windows Speziell das URL-Format hat sich als so flexibel und nützlich herausgestellt, dass es für eine ganze Reihe von Ressourcen verwendet wird, die gar nichts mit dem World Wide Web zu tun haben. Damit werden sich die regulären Ausdrücke aus diesem Kapitel in erstaunlich vielen Situationen nutzen lassen.
7.1
URLs validieren
Problem Sie wollen prüfen, ob ein Text eine für Ihre Zwecke gültige URL enthält.
Lösung So gut wie jede URL zulassen: ^(https?|ftp|file)://.+$
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Domainname ist erforderlich, Benutzername oder Passwort sind nicht zulässig. Das Schema (http oder ftp) kann weggelassen werden, wenn es sich aus der Subdomain ermitteln lässt (www oder ftp): \A ((https?|ftp)://|(www|ftp)\.) [a-z0-9-]+(\.[a-z0-9-]+)+ ([/?].*)? \Z
# # # # #
Anker Schema oder Subdomain Domain Pfad und/oder Parameter Anker
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python Domainname und Pfad auf eine Bilddatei sind erforderlich. Benutzername, Passwort oder Parameter sind nicht zulässig: \A (https?|ftp):// [a-z0-9-]+(\.[a-z0-9-]+)+ (/[\w-]+)* /[\w-]+\.(gif|png|jpe?g) \Z
Diskussion Sie können keinen regulären Ausdruck erstellen, der jede gültige URL findet und ungültige URLs immer aussortiert. Der Grund liegt darin, dass nahezu alles eine gültige URL sein kann – eventuell in noch nicht erfundenen Schemata. Das Überprüfen von URLs ist nur dann sinnvoll, wenn wir den Kontext kennen, in dem diese URLs gültig sein müssen. Wir können die zu akzeptierenden URLs auf Schemata begrenzen, die von der von uns verwendeten Software verarbeitet werden können. Alle regulären Ausdrücke aus diesem Rezept sind für URLs gedacht, die von Webbrowsern verwendet werden. Solche URLs haben die Form: scheme://user:[email protected]:80/path/file.ext?param=value¶m2 =value2#fragment
Alle diese Teile sind optional. Eine URL vom Typ file: hat nur einen Pfad. http:-URLs benötigen nur einen Domainnamen. Der erste reguläre Ausdruck in dieser Lösung prüft, ob die URL mit einem der für Webbrowser gebräuchlichen Schemata beginnt: http, https, ftp oder file. Der Zirkumflex verankert die Regex am Anfang des Strings (Rezept 2.5). Eine Alternation (Rezept 2.8) wird verwendet, um die möglichen Schemata aufzuführen. Mit ‹https?› kombiniert man ‹http|https›. Da die erste Regex recht unterschiedliche Schemata zulässt, wie zum Beispiel http und file, versucht sie gar nicht erst, den Text nach dem Schema zu überprüfen. ‹.+$› schnappt sich einfach alles bis zum Ende des Strings, sofern dieser keinen Zeilenumbruch enthält. Standardmäßig passt der Punkt (Rezept 2.4) auf alle Zeichen außer Zeilenumbrüchen, und das Dollarzeichen (Rezept 2.5) passt nicht an Zeilenumbrüchen. Ruby ist hier eine Ausnahme. Dort passen Zirkumflex und Dollar immer auf im String enthaltene Zeilenumbrüche, sodass wir stattdessen ‹\A› und ‹\Z› (Rezept 2.5) verwenden müssen. Streng genommen müssten Sie die gleichen Änderungen auch bei allen regulären Ausdrücke in diesem Rezept vornehmen – wenn Ihr Eingabe-String aus mehreren Zeilen bestehen kann und Sie vermeiden wollen, eine URL zu finden, die über mehrere Zeilen verteilt ist. Die nächsten beiden regulären Ausdrücke sind im Freiform-Modus angegeben (Rezept 2.18) und einfach andere Versionen der gleichen Regex. Die Freiform-Regex lässt sich leichter lesen, während die normale Version schneller eingetippt ist. JavaScript unterstützt keine Freiform-Regexes. Diese beiden Regexes akzeptieren nur Web- und FTP-URLs, wobei auf das HTTP- oder FTP-Schema etwas folgen muss, was wie ein gültiger Domainname aussieht. Der Domainname muss aus ASCII-Zeichen bestehen. Internationalisierte Domains (IDNs) werden nicht akzeptiert. Auf die Domain kann ein Pfad oder eine Liste mit Parametern folgen, die von der Domain entweder durch einen Schrägstrich oder ein Fragzeichen getrennt sind. Da sich das Fragezeichen innerhalb einer Zeichenklasse befindet
7.1 URLs validieren | 369
(Rezept 2.3), müssen wir es nicht maskieren, denn das Fragezeichen ist dort ein ganz normales Zeichen, während der Schrägstrich in einem regulären Ausdruck nirgendwo maskiert werden muss. (Wenn Sie ihn einmal in Quellcode maskiert sehen, liegt das daran, dass Perl und viele andere Programmiersprachen Schrägstriche nutzen, um literale reguläre Ausdrücke zu begrenzen.) Es wird nicht versucht, den Pfad oder die Parameter zu validieren. ‹.*› findet einfach alles, sofern es sich nicht um einen Zeilenumbruch handelt. Da sowohl der Pfad als auch die Parameter optional sind, wird ‹[/?].*› in eine Gruppe gesteckt, die durch ein Fragezeichen optional gemacht wird (Rezept 2.12). Diese regulären Ausdrücke und diejenigen, die noch folgen, lassen weder Benutzernamen noch Passwort als Teil der URL zu. Schon aus Sicherheitsgründen sollte man sowieso davon Abstand nehmen. Die meisten Webbrowser akzeptieren URLs, in denen das Schema nicht angegeben ist. In solchen Fällen ermitteln sie das Schema aus dem Domainnamen. So ist zum Beispiel www.regexbuddy.com eine Kurzform von http://www.regexbuddy.com. Um auch solche URLs zuzulassen, erweitern wir einfach die Liste der zulässigen Schemata so, dass auch die Subdomains www. und ftp. erlaubt sind. ‹(https?|ftp)://|(www|ftp)\.› ist dafür zuständig. Diese Liste hat zwei Alternativen, von denen jede wiederum zwei Alternativen besitzt. Die erste Alternative ermöglicht ‹https?› und ‹ftp›, auf das die Zeichen ‹://› folgen müssen. Die zweite Alternative ermöglicht ‹www› und ‹ftp›, denen ein (literaler) Punkt folgen muss. Sie können beide Listen problemlos um die Schemata und Subdomains erweitern, die die Regex akzeptieren soll.
Die letzten beiden regulären Ausdrücke erfordern ein Schema, einen ASCII-Domainnamen, einen Pfad und einen Dateinamen für eine Bilddatei vom Typ GIF, PNG oder JPEG. Pfad und Dateiname lassen alle Buchstaben und Ziffern in beliebigen Schriftsystemen zu sowie Unterstriche und Bindestriche. Die Zeichenklassenabkürzung ‹\w› enthält all diese Zeichen, abgesehen von den Bindestrichen (Rezept 2.3). Welchen dieser regulären Ausdrücke sollten Sie verwenden? Das hängt wirklich davon ab, was Sie erreichen wollen. In vielen Situationen kann die Antwort durchaus sein, gar keinen regulären Ausdruck zu verwenden. Versuchen Sie einfach, auf die URL zuzugreifen. Gibt sie einen sinnvollen Inhalt zurück, akzeptieren Sie sie. Erhalten Sie einen 404eroder einen anderen Fehler, weisen Sie sie zurück. Schließlich ist das der einzige wahre Test, um herauszufinden, ob eine URL gültig ist.
Siehe auch Rezepte 2.3, 2.8, 2.9 und 2.12.
370 | Kapitel 7: URLs, Pfade und Internetadressen
7.2
URLs in einem längeren Text finden
Problem Sie wollen URLs in einem längeren Text finden. URLs können von Satzzeichen umschlossen sein, zum Beispiel Klammern, die nicht Teil der URL sind.
Lösung URL ohne Leerzeichen: \b(https?|ftp|file)://\S+
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby URL ohne Leerzeichen oder abschließendes Satzzeichen: \b(https?|ftp|file)://[-A-Z0-9+&@#/%?=~_|$!:,.;]* [A-Z0-9+&@#/%=~_|$]
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby URL ohne Leerzeichen oder abschließendes Satzzeichen. Bei URLs, die mit der Subdomain www oder ftp beginnen, kann das Schema weggelassen werden: \b((https?|ftp|file)://|(www|ftp)\.)[-A-Z0-9+&@#/%?=~_|$!:,.;]* [A-Z0-9+&@#/%=~_|$]
Diskussion Was ist im folgenden Text die URL? Besuchen Sie http://www.somesite.com/page, um mehr Informationen zu erhalten.
Bevor Sie jetzt http://www.somesite.com/page sagen, überlegen Sie noch mal: Satzzeichen und Leerzeichen sind in URLs gültige Zeichen. Kommatas, Punkte und sogar Leerzeichen müssen nicht als %20 maskiert werden. Literale Leerzeichen sind auf jeden Fall gültig. Manche WYSIWYG-Webdesigntools machen es dem Anwender sogar sehr leicht, Leerzeichen in Datei- oder Ordnernamen zu verwenden und diese dann auch in Links darauf stehen zu haben. Nutzen wir also einen regulären Ausdruck, der alle gültigen URLs zulässt, findet er im Text folgende URL: http://www.somesite.com/page, um mehr Informationen zu erhalten.
7.2 URLs in einem längeren Text finden | 371
Die Wahrscheinlichkeit ist gering, dass die Person, die diesen Satz eingetippt hat, auch wollte, dass die Leerzeichen Teil der URL sind, da unmaskierte Leerzeichen in URLs doch recht selten sind. Der erste reguläre Ausdruck in der Lösung schließt sie mithilfe der Zeichenklassenabkürzung ‹\S› aus, wozu alle Zeichen gehören, die kein Whitespace sind. Auch wenn die Regex mit der Option Groß-/Kleinschreibung ignorieren angelegt wurde, muss das S großgeschrieben werden, da ‹\S› nicht das gleiche wie ‹\s› ist. Tatsächlich steht es sogar für genau das Gegenteil. In Rezept 2.3 erfahren Sie mehr dazu. Der erste reguläre Ausdruck ist trotzdem noch nicht so ganz das Wahre. Es ist zwar nicht ungewöhnlich, dass URLs Kommatas und andere Satzzeichen enthalten, aber diese tauchen selten am Ende der URL auf. Der nächste reguläre Ausdruck nutzt statt der Zeichenklassenabkürzung ‹\S› zwei Zeichenklassen. Die erste enthält mehr Satzzeichen als die zweite. Letztere schließt alle die Zeichen aus, die eher Teil des Satzes sind, wenn man die URL in einem normalen Satz aufführt. Die erste Zeichenklasse nutzt den Stern-Quantor (Rezept 2.12), um URLs beliebiger Länge zuzulassen. Die zweite Zeichenklasse hat keinen Quantor. Die URL muss also mit einem Zeichen aus dieser Klasse enden. Die Zeichenklassen enthalten keine Kleinbuchstaben – die Option Groß-/Kleinschreibung ignorieren kümmert sich schon darum. In Rezept 3.4 erfahren Sie, wie Sie solche Optionen in Ihrer Programmiersprache setzen. Die zweite Regex wird bei bestimmten URLs mit seltsamen Kombinationen aus Satzzeichen nicht korrekt arbeiten und diese nur teilweise finden. Aber sie löst das häufig vorkommende Problem eines Kommas oder Punkts direkt nach einer URL, während diese Zeichen trotzdem innerhalb der URL auftauchen dürfen. Die meisten Webbrowser akzeptieren URLs, in denen das Schema nicht angegeben ist. In solchen Fällen ermitteln sie das Schema aus dem Domainnamen. So ist zum Beispiel www.regexbuddy.com eine Kurzform von http://www.regexbuddy.com. Um auch solche URLs zuzulassen, erweitert die letzte Regex die Liste der zulässigen Schemata um die Subdomains www. und ftp.. ‹(https?|ftp)://|(www|ftp)\.› ist dafür zuständig. Diese Liste hat zwei Alternativen, von denen jede wiederum zwei Alternativen besitzt. Die erste Alternative ermöglicht ‹https?› und ‹ftp›, auf das die Zeichen ‹://› folgen müssen. Die zweite Alternative ermöglicht ‹www› und ‹ftp›, denen ein (literaler) Punkt folgen muss. Sie können beide Listen prob-
lemlos um die Schemata und Subdomains erweitern, die die Regex akzeptieren soll.
Siehe auch Rezepte 2.3 und 2.6.
372 | Kapitel 7: URLs, Pfade und Internetadressen
7.3
URLs in Anführungszeichen in längerem Text finden
Problem Sie wollen URLs in einem längeren Text finden. Dabei können die URLs durch Satzzeichen umschlossen sein, die zum Text und nicht zur URL gehören. Sie wollen den Anwendern die Möglichkeit geben, die URLs in Anführungszeichen zu setzen, sodass diese explizit festlegen können, ob Satzzeichen oder sogar Leerzeichen Teil der URL sein können.
Regex-Optionen: Freiform, Groß-/Kleinschreibung ignorieren, Punkt passt auf Zeilenumbruch, Anker passen auf Zeilenumbruch Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Diskussion Im vorhergehenden Rezept wurde beschrieben, wie man URLs mit normalem Text kombiniert und trotzdem zwischen den normalen Satzzeichen und URL-Zeichen unterscheidet. Obwohl die Lösung des vorigen Rezepts sehr nützlich ist und auch die meiste Zeit korrekte Ergebnisse liefert, kann keine Regex der Welt immer die perfekte Lösung bieten. Wird Ihre Regex für Texte verwendet, die erst noch entstehen sollen, können Sie Ihren Anwendern die Möglichkeit bieten, ihre URLs in Anführungszeichen zu setzen. Die von uns vorgestellten Lösungen lassen um die URL herum einzelne oder doppelte Anführungszeichen zu. Wenn eine URL in Anführungszeichen gesetzt wird, muss sie mit einem der Schemata ‹https?|ftp|file› oder einer von zwei Subdomains ‹www|ftp› beginnen. Nach dem Schema oder der Subdomain kann in der URL jedes Zeichen angegeben werden – mit Ausnahme von Zeilenumbrüchen –, bis das schließende Anführungszeichen folgt. Der reguläre Ausdruck ist insgesamt in drei Alternativen unterteilt. Die erste Alternative ist die Regex des vorhergehenden Rezepts, die eine nicht mit Anführungszeichen versehene URL findet und dabei versucht, zwischen normalen Satzzeichen und URL-Zeichen zu unterscheiden. Die zweite Alternative passt auf eine URL in doppelten Anführungszeichen, die dritte auf eine in einfachen Anführungszeichen. Wir nutzen lieber zwei Alternativen statt nur einer mit einer einfangenden Gruppe für das öffnende Anführungszeichen und einer Rückwärtsreferenz für das schließende Anführungszeichen, weil wir keine Rückwärtsreferenzen in der negierten Zeichenklasse verwenden können, mit der wir das Anführungszeichen von der URL ausschließen.
7.3 URLs in Anführungszeichen in längerem Text finden | 373
Wir haben uns für einfache und doppelte Anführungszeichen entschieden, weil URLs in HTML- und XHTML-Dateien häufig so auftauchen. So ist es für die meisten, die im Web unterwegs sind, ganz selbstverständlich. Sie können die Regex aber natürlich auch so anpassen, dass andere Zeichen als Begrenzung verwendet werden.
Siehe auch Rezepte 2.8 und 2.9.
7.4
URLs mit Klammern in längerem Text finden
Problem Sie wollen URLs in einem längeren Text finden. Dabei können links und rechts von den URLs Satzzeichen stehen, die aber nicht zur URL gehören. Sie wollen auch URLs finden, die Klammern enthalten, gleichzeitig aber eventuell vorhandene Klammern aus dem Suchergebnis heraushalten, die sich „außerhalb“ der URL befinden.
Diskussion In URLs sind so gut wie alle Zeichen zulässig – auch Klammern. Sie kommen zwar sehr selten vor, daher haben wir sie bisher in noch keiner Regex in den vorherigen Rezepten berücksichtigt, aber einige wichtige Websites verwenden sie durchaus: http://de.wikipedia.org/wiki/Zwingenberg_(Baden) http://msdn.microsoft.com/de-de/library/aa752574(en-us,VS.85).aspx
Eine Lösung bestünde darin, den Anwender solche URLs in Anführungszeichen setzen zu lassen. Eine andere sieht vor, die Regex so anzupassen, dass sie solche URLs akzeptiert. Schwierig daran ist, herauszufinden, ob eine schließende Klammer Teil der URL ist oder als Satzzeichen des normalen Texts zu betrachten ist, wie in diesem Beispiel: Die Website des RegexBuddy (unter http://www.regexbuddy.com) ist wirklich cool.
374 | Kapitel 7: URLs, Pfade und Internetadressen
Da es möglich ist, dass eine der Klammern direkt an die URL anschließt, während die andere dies nicht tut, können wir die Regexes aus dem vorherigen Rezept zum Setzen in Anführungszeichen nicht verwenden. Die einfachste Lösung ist, Klammern in URLs nur dann zuzulassen, wenn sie nicht verschachtelt sind und es immer eine öffnende und eine schließende Klammer gibt. Die URLs von Wikipedia und Microsoft erfüllen diese Anforderungen. Die beiden regulären Ausdrücke in der Lösung sind identisch, die erste nutzt jedoch den Freiform-Modus, um besser lesbar zu sein. Diese regulären Ausdrücke ähneln der letzten Regex aus der Lösung in Rezept 7.2. Sie bestehen aus drei Elementen: der Liste mit Schemata, gefolgt vom Hauptteil der URL, die mit dem Stern-Quantor eine beliebige Länge zulässt, und dem Ende der URL ohne Quantor (es muss also genau einmal vorkommen). In der ursprünglichen Regex in Rezept 7.2 bestehen sowohl der Rumpf der URL wie auch das Ende der URL jeweils aus nur einer Zeichenklasse. Die Lösungen für dieses Rezept ersetzen die beiden Zeichenklassen durch komplexere Elemente. Die mittlere Zeichenklasse [-A-Z0-9+&@#/%=~_|$?!:,.]
wurde nun zu: \([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[-A-Z0-9+&@#/%=~_|$?!:,.]
Die abschließende Zeichenklasse [A-Z0-9+&@#/%=~_|$]
wurde zu: \([-A-Z0-9+&@#/%=~_|$?!:,.]*\)|[A-Z0-9+&@#/%=~_|$]
Beide Zeichenklassen wurden durch Alternationen ersetzt (Rezept 2.8). Da eine Alternation die niedrigste Wertigkeit aller Regex-Operatoren besitzt, verwenden wir nicht-einfangende Gruppen (Rezept 2.9), um beide Alternativen zusammenzuhalten. Bei beiden Zeichenklassen haben wir die Alternative ‹\([-A-Z0-9+&@#/%=~_|$?!:,.]*\)› hinzugefügt, die alte Klasse aber als die andere Alternative beibehalten. Die neue Alternative passt auf ein Klammernpaar mit einer beliebigen Menge an in URLs erlaubten Zeichen dazwischen. Die letzte Zeichenklasse hat die gleiche Alternative erhalten, wodurch die URL mit Text in Klammern oder aber mit einem Zeichen enden kann, das eher nicht Teil des eigentlichen Satzes ist. Zusammen führt das zu einer Regex, die URLs mit beliebig vielen Klammern findet, auch solche ohne Klammern und sogar URLs, die nur aus Klammern bestehen – solange diese Klammern paarweise auftreten. Für den Hauptteil der URL haben wir den Stern-Quantor an das Ende der nicht-einfangenden Gruppe gestellt. Damit können in der URL beliebig viele Klammernpaare auftre-
7.4 URLs mit Klammern in längerem Text finden | 375
ten. Da der Stern nun die gesamte nicht-einfangende Gruppe betrifft, brauchen wir keinen mehr für die ursprüngliche Zeichenklasse. Tatsächlich müssen wir sogar darauf achten, auf keinen Fall den Stern mit aufzunehmen. Die Regex in der Lösung hat in der Mitte die Form ‹(ab*c|d)*›, wobei ‹a› und ‹c› die literalen Klammern sind und ‹b› und ‹d› die Zeichenklassen. Würde man das als ‹(ab*c|d*)*› schreiben, wäre es ein Fehler. Auf den ersten Blick wäre es logisch, weil wir damit beliebig viele Zeichen von ‹d› zulassen würden, aber der äußere ‹*› wiederholt ‹d› schon vollkommen ausreichend. Fügen wir ‹d› noch direkt einen inneren Stern hinzu, wird die Komplexität des regulären Ausdrucks exponentiell. ‹(d*)*› kann dddd auf vielen unterschiedlichen Wegen finden. So könnte der äußere Stern zum Beispiel für vier Wiederholungen sorgen, während der innere Stern jeweils nur einmal genutzt wird. Oder der äußere Stern wird drei Mal genutzt, während der innere Stern in der Häufigkeit 2-1-1, 12-1 oder 1-1-2 zum Tragen kommt. Der äußere Stern könnte auch zwei Mal genutzt werden, während der innere mit der Häufigkeit 2-2, 1-3 oder 3-1 gerufen wird. Sie können sich vorstellen, dass die Anzahl an Kombinationen mit wachsender String-Länge geradezu explodiert. Wir nennen das „katastrophales Backtracking”, ein Begriff, den wir in Rezept 2.15 eingeführt haben. Das Problem tritt dann auf, wenn der reguläre Ausdruck keine gültige Übereinstimmung liefern kann, weil Sie zum Beispiel etwas an die Regex angehängt haben, damit URLs mit etwas Bestimmtem enden.
Siehe auch Rezepte 2.8 und 2.9.
7.5
URLs in Links umwandeln
Problem Sie haben einen Text mit einer oder mehreren URLs. Sie wollen nun die URLs in Links umwandeln, indem Sie sie mit HTML-Anker-Tags umschließen. Die URL selbst wird dann als Ziel für den Link dienen, aber auch gleichzeitig der Text sein, den man anklicken kann.
Lösung Um die URLs im Text zu finden, nutzen Sie einen der regulären Ausdrücke aus den Rezepten 7.2 oder 7.4. Als Ersetzungstext verwenden Sie: $&
Ersetzungstextvariante: Python Beim Programmieren können Sie diesen Ersetzungsvorgang wie in Rezept 3.15 beschrieben implementieren.
Diskussion Die Lösung für dieses Problem ist recht einfach. Wir verwenden einen regulären Ausdruck, um eine URL zu finden, und ersetzen sie dann durch «URL», wobei URL für die gefundene URL steht. In den verschiedenen Programmiersprachen werden unterschiedliche Ersetzungstextsyntaxen genutzt, daher werden hier so viele Lösungen angegeben. Aber alle tun genau das Gleiche. In Rezept 2.20 wird die Ersetzungstextsyntax genauer beschrieben.
Siehe auch Rezepte 2.21, 3.15, 7.2 und 7.4.
7.6
URNs validieren
Problem Sie wollen prüfen, ob ein String aus einem gültigen Uniform Resource Name (URN) besteht (spezifiziert im RFC 2141), oder solche URNs in einem längeren Text finden.
Lösung Prüfen, ob ein String vollständig aus einem gültigen URN besteht: \Aurn: # Namensraum [a-z0-9][a-z0-9-]{0,31}: # Namensraumspezifischer String [a-z0-9()+,\-.:=@;$_!*'%/?#]+ \Z
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Finden eines URN in einem längeren Text, wobei davon ausgegangen wird, dass Satzzeichen am Ende des URN Teil des umgebenden Texts sind und nicht zum URN gehören: \burn: # Namensraum [a-z0-9][a-z0-9-]{0,31}: # Namensraumspezifischer String [a-z0-9()+,\-.:=@;$_!*'%/?#]*[a-z0-9+=@$/]
Diskussion Ein URN besteht aus drei Teilen. Der erste sind die vier Zeichen urn:, die wir als literale Zeichen in den regulären Ausdruck einfügen können. Der zweite Teil ist der Namensraum (Namespace Identifier, NID). Er ist zwischen 1 und 32 Zeichen lang. Das erste Zeichen muss ein Buchstabe oder eine Ziffer sein. Bei den weiteren Zeichen kann es sich um Buchstaben, Ziffern oder Bindestriche handeln. Wir finden diese Zeichen mit zwei Zeichenklassen (Rezept 2.3): Die erste passt auf einen Buchstaben oder eine Ziffer, während die zweite zwischen 0 und 31 Buchstaben, Ziffern und Bindestriche findet. Der NID muss durch einen Doppelpunkt abgeschlossen sein, den wir wieder literal in die Regex einfügen. Der dritte Teil des URN ist der namensraumspezifische String (Namespace Specific String, NSS). Er kann eine beliebige Länge haben und neben Buchstaben und Ziffern eine
378 | Kapitel 7: URLs, Pfade und Internetadressen
ganze Reihe von Satzzeichen enthalten. Das können wir wieder problemlos durch eine weitere Zeichenklasse abbilden. Das Plus nach der Zeichenklasse wiederholt diese einmal oder mehrfach (Rezept 2.12). Wenn Sie prüfen wollen, ob ein String aus einem gültigen URN besteht, müssen Sie nur noch am Anfang und Ende der Regex Anker einfügen, um damit auch den Anfang und das Ende des Strings zu finden. Das lässt sich außer in Ruby in allen Varianten mit ‹^› und ‹$› oder außer in JavaScript mit ‹\A› und ‹\Z› erreichen. In Rezept 2.5 sind diese Anker im Detail beschrieben. Wollen Sie URNs in längeren Texten finden, wird es etwas schwieriger. Die Probleme mit Satzzeichen in URLs, die wir in Rezept 7.2 besprochen haben, bestehen auch bei URNs. Schauen Sie sich den folgenden Text an: Der URN ist urn:nid:nss, oder nicht?
Die Frage ist hier, ob das Komma Teil des URN ist. URNs, die mit Kommatas enden, sind syntaktisch korrekt, aber jeder menschliche Leser des angegebenen Satzes wird das Komma als Teil des Satzes betrachten – nicht als Teil des URN. Der letzte reguläre Ausdruck aus dem Abschnitt „Lösung“ löst dieses Problem, indem er etwas strenger ist als der RFC 2141. Er beschränkt das letzte Zeichen des URN auf ein Zeichen, das für den NSS-Teil gültig ist, aber nicht unbedingt auf normale Satzzeichen passt. Das lässt sich leicht erreichen, indem man den Plus-Quantor (einen oder mehrere) durch einen Stern (null oder mehrere) ersetzt und eine zweite Zeichenklasse für das letzte Zeichen anfügt. Würden wir die Zeichenklasse ohne einen anderen Quantor ergänzen, müsste der NSS damit mindestens zwei Zeichen lang sein, was wir natürlich nicht wollen.
Siehe auch Rezepte 2.3 und 2.12.
7.7
Generische URLs validieren
Problem Sie wollen prüfen, ob ein Stück Text eine nach dem RFC 3986 gültige URL ist.
Diskussion Ein Großteil der bisherigen Rezepte in diesem Kapitel kümmert sich um URLs, und die regulären Ausdrücke in den Lösungen behandeln spezielle URL-Arten. Manche der Regexes sind an bestimmte „Umgebungen“ angepasst, um zum Beispiel herauszufinden, ob ein Satzzeichen Teil der URL oder des umgebenden Texts ist. Die regulären Ausdrücke in diesem Rezept kümmern sich um generische URLs. Sie sind nicht dazu gedacht, URLs in längeren Texten zu finden, sondern um Strings zu überprüfen, die URLs enthalten sollen, und um URLs in ihre Bestandteile zu zerlegen. Das schaffen sie für jede Art von URL, aber in der Praxis werden Sie die Regexes spezifischer gestalten wollen. Die Rezepte nach diesem Rezept stellen Beispiele für spezifischere Regexes vor. Der RFC 3986 beschreibt, wie eine gültige URL aussehen sollte. Er behandelt jede mögliche URL, auch relative URLs und URLs für Schemata, die noch nicht eingeführt wurden. Im Endergebnis ist der RFC 3986 sehr umfassend und der reguläre Ausdruck, mit dem er implementiert wird, ziemlich lang. Die regulären Ausdrücke in diesem Rezept implementieren nur die Grundlagen. Sie sind ausreichend, um die URL zuverlässig in ihre verschiedenen Bestandteile zu zerlegen, aber nicht, um jeden dieser Teile zu validieren. Wollte man alle Teile überprüfen, würde man spezifisches Wissen über jedes der URL-Schemata benötigen. Der RFC 3986 deckt nicht alle URLs ab, denen Sie sich in der freien Wildbahn eventuell gegenübersehen. So akzeptieren zum Beispiel viele Browser und Webserver URLs mit literalen Leerzeichen, während diese nach dem RFC 3986 als %20 zu maskieren sind. Eine absolute URL muss mit einem Schema beginnen, wie zum Beispiel http: oder ftp:. Das erste Zeichen des Schemas muss ein Buchstabe sein. Die folgenden Zeichen können Buchstaben, Ziffern und ein paar Satzzeichen sein. Beides lässt sich bequem mit zwei Zeichenklassen abhandeln: ‹[a-z][a-z0-9+\-.]*›.
382 | Kapitel 7: URLs, Pfade und Internetadressen
Viele URL-Schemata benötigen etwas, das der RFC 3986 als „Authority“ bezeichnet. Die Authority ist der Domainname oder die IP-Adresse des Servers, vor dem oder der optional ein Benutzername steht und worauf eine Portnummer folgen kann. Der Benutzername kann aus Buchstaben, Ziffern und einer Reihe von Satzzeichen bestehen. Er muss vom Domainnamen oder der IP-Adresse durch ein @-Zeichen abgegrenzt sein. Mit ‹[a-z0-9\-._~%!$&'()*+,;=]+@› findet man den Benutzernamen und das Trennzeichen. Der RFC 3986 ist in Bezug auf den Domainnamen ziemlich großzügig. In Rezept 7.15 ist beschrieben, was im Allgemeinen für Domains akzeptiert wird: Buchstaben, Ziffern, Bindestriche und Punkte. Der RFC 3986 erlaubt auch Tilden und durch die Prozentnotation beliebige andere Zeichen. Der Domainname muss nach UTF-8 konvertiert werden, und jedes Zeichen, das weder Buchstabe noch Ziffer, Bindestrich oder Tilde ist, muss als %FF kodiert werden, wobei es sich bei FF um die hexadezimale Darstellung des Byte handelt. Um unseren regulären Ausdruck einfach zu halten, prüfen wir nicht, ob auf jedes Prozentzeichen genau zwei hexadezimale Ziffern folgen. Es ist besser, solche Kontrollen erst durchzuführen, nachdem die verschiedenen Teile der URL getrennt wurden. Also finden wir den Hostnamen einfach mit ‹[a-z0-9\-._~%]+›, was auch IPv4-Adressen zulässt (die nach RFC 3986 erlaubt sind). Statt eines Domainnamens oder einer IPv4-Adresse kann der Host auch als IPv6-Adresse in eckigen Klammern oder sogar in einer zukünftig noch festzulegenden Version von IPAdressen angegeben werden. Wir finden die IPv6-Adressen per ‹\[[a-f0-9:.]+\]› und die zukünftigen Adressen mit ‹\[v[a-f0-9][a-z0-9\-._~%!$&'()*+,;=:]+\]›. Auch wenn wir IP-Adressen einer noch nicht definierten Version nicht überprüfen können, lassen sich IPv6-Adressen dennoch strenger kontrollieren. Aber auch hier ist es besser, dafür eine zweite Regex zu verwenden, nachdem wir die Adresse aus der URL extrahiert haben. In Rezept 7.17 sieht man, wie aufwendig es ist, IPv6-Adressen zu überprüfen. Die Portnummer ist, falls sie denn angegeben wurde, einfach eine Dezimalzahl, die vom Hostnamen durch einen Doppelpunkt getrennt ist. Hier benötigen wir nur die Regex ‹:[0-9]+›. Wenn eine Authority angegeben ist, muss ihr entweder ein absoluter Pfad folgen, oder es darf gar kein Pfad angegeben sein. Ein absoluter Pfad beginnt mit einem Schrägstrich, gefolgt von einem oder mehreren Segmenten, die ebenfalls durch Schrägstriche begrenzt sind. Ein Segment besteht aus einem oder mehreren Buchstaben, Ziffern oder Satzzeichen. Es kann keine direkt aufeinanderfolgenden Schrägstriche geben. Der Pfad kann zudem mit einem Schrägstrich enden. ‹(/[a-z0-9\-._~%!$&'()*+,;=:@]+)*/?› findet solche Pfade. Wenn die URL keine Authority enthält, kann der Pfad absolut, relativ oder gar nicht vorhanden sein. Absolute Pfade beginnen mit einen Schrägstrich, relative Pfade dagegen nicht. Da der führende Schrägstrich damit optional ist, brauchen wir eine etwas längere Regex, um sowohl absolute wie auch relative Pfade zu finden: ‹/?[a-z0-9\._~%!$&'()*+,;=:@]+(/[a-z0-9\-._~%!$&'()*+,;=:@]+)*/?›. 7.7 Generische URLs validieren | 383
Relative URLs enthalten kein Schema und daher auch keine Authority. Der Pfad ist dann aber verpflichtend und kann absolut oder relativ sein. Da die URL kein Schema enthält, darf das erste Segment eines relativen Pfads keinen Doppelpunkt enthalten. Ansonsten würde er als Trennzeichen für das Schema angesehen werden. Also brauchen wir zwei reguläre Ausdrücke, um den Pfad einer relativen URL zu finden. Die erste für die relativen Pfade ist ‹[a-z0-9\-._~%!$&'()*+,;=@]+(/[a-z0-9\-._~%!$&'()*+,;=:@]+)*/?›. Das ähnelt sehr stark der Regex für Pfade mit einem Schema, aber ohne Authority. Den einzigen Unterschied bildet der optionale Schrägstrich am Anfang, der hier fehlt, und die erste Zeichenklasse, die keinen Doppelpunkt enthält. Absolute Pfade finden wir mit ‹(/[a-z09\-._~%!$&'()*+,;=:@]+)+/?›. Das ist die gleiche Regex wie die für Pfade in URLs, die ein Schema und eine Authority enthalten, nur dass der Stern, der die Segmente des Pfads wiederholt, nun einem Pluszeichen gewichen ist. In relativen URLs muss mindestens ein Pfadsegment vorhanden sein. Der Query-Teil der URL ist optional. Gibt es ihn, muss er mit einem (literalen) Fragezeichen beginnen. Das Query-Element läuft bis zum ersten Rautezeichen in der URL oder bis zu ihrem Ende. Da das Rautezeichen keines der gültigen Satzzeichen für den QueryTeil der URL ist, können wir ihn leicht mit ‹\?[a-z0-9\-._~%!$&'()*+,;=:@/?]*› finden. Beide Fragezeichen in der Regex sind literale Zeichen. Das erste befindet sich außerhalb einer Zeichenklasse und muss daher maskiert werden. Das zweite steht innerhalb einer Zeichenklasse, wo es immer ein literales Zeichen ist. Der letzte Teil einer URL ist das Fragment, das ebenfalls optional sein darf. Es beginnt mit einem Rautezeichen und läuft bis zum Ende der URL. Per ‹\#[a-z0-9\-._~%!$&'()* +,;=:@/?]*› finden Sie auch dieses. Um die Arbeit mit den verschiedenen Elementen der URL zu vereinfachen, verwenden wir benannte Captures. In Rezept 2.11 ist beschrieben, wie benannte Captures in den verschiedenen Regex-Varianten genutzt werden können. .NET ist die einzige Variante, die mehrere Gruppen mit dem gleichen Namen so verarbeiten kann, als handele es sich um ein und dieselbe Gruppe. Das ist in dieser Situation sehr praktisch, da unsere Regex mehrere Wege kennt, den Pfad der URL zu finden – abhängig davon, ob das Schema und/oder die Authority angegeben sind. Geben wir diesen drei Gruppen den gleichen Namen, können wir einfach die Gruppe „path“ abfragen, um den Pfad zu erhalten – egal ob die URL ein Schema und/oder eine Authority besitzt. Die anderen Varianten bieten diese Möglichkeit für benannte Captures nicht an, auch wenn die meisten die gleiche Syntax wie .NET unterstützen. In den anderen Varianten haben die drei einfangenden Gruppen für den Pfad unterschiedliche Namen. Nur eine von ihnen wird tatsächlich den Pfad der URL enthalten, wenn es eine Übereinstimmung gibt. Die beiden anderen sind dann daran nicht beteiligt.
Siehe auch Rezepte 2.3, 2.8, 2.9 und 2.12.
384 | Kapitel 7: URLs, Pfade und Internetadressen
7.8
Das Schema aus einer URL extrahieren
Problem Sie wollen das Schema aus einem String extrahieren, der eine URL enthält. So wollen Sie zum Beispiel den Wert http für die URL http://www.regexcookbook.com erhalten.
Lösung Extrahieren des Schema aus einer URL, die schon validiert wurde ^([a-z][a-z0-9+\-.]*):
Diskussion Es ist sehr einfach, das Schema einer URL zu extrahieren, wenn Sie schon wissen, dass es sich beim Ausgangstext um eine gültige URL handelt. Das Schema steht immer ganz am Anfang der URL. Der Zirkumflex (Rezept 2.5) sorgt dafür, dass diese Anforderung in der Regex umgesetzt wird. Das Schema beginnt mit einem Buchstaben, gefolgt von weiteren Buchstaben, Ziffern, Pluszeichen, Bindestrichen und Punkten. Das finden wir mit den beiden Zeichenklassen ‹[a-z][a-z0-9+\-.]*› (Rezept 2.3). Das Schema wird vom Rest der URL durch einen Doppelpunkt begrenzt. Wir fügen der Regex diesen Doppelpunkt hinzu, um sicherzustellen, dass wir das Schema nur dann finden, wenn die URL wirklich damit beginnt. Relative URLs enthalten kein Schema. Die in RFC 3986 definierte URL-Syntax stellt sicher, dass relative URLs keine Doppelpunkte enthalten, solange es vor diesen Doppelpunkten nicht schon Zeichen gibt, die in einem Schema nicht zulässig sind. Darum mussten wir den Doppelpunkt aus einer der Zeichenklassen entfernen, die den Pfad in Rezept 7.7 finden. Nutzen Sie die Regexes in diesem Rezept für eine gültige, aber relative URL, wird gar kein Schema gefunden. Da die Regex auf mehr als nur das Schema passt (sie enthält auch den Doppelpunkt), haben wir dem regulären Ausdruck eine einfangende Gruppe hinzugefügt. Wenn die Regex eine Übereinstimmung findet, können Sie den Text der ersten (und einzigen) einfangenden Gruppe auslesen, um das Schema ohne den Doppelpunkt zu erhalten. In Rezept 2.9 erfahren Sie alles über einfangende Gruppen, und in Rezept 3.9 lernen Sie, wie Sie den von solchen einfangenden Gruppen gefundenen Text in Ihrer Programmiersprache auslesen. Sollten Sie nicht schon wissen, dass Ihr Ausgangstext eine gültige URL ist, können Sie eine vereinfachte Version der Regex aus Rezept 7.7 verwenden. Da wir das Schema extrahieren wollen, können wir relative URLs ausschließen, in denen es nicht angegeben ist. Damit wird der reguläre Ausdruck ein bisschen einfacher. Da diese Regex die gesamte URL findet, haben wir um den Teil der Regex, der das Schema findet, eine zusätzliche einfangende Gruppe gelegt. Lesen Sie den Text aus, der durch die erste einfangende Gruppe gefunden wurde, um das Schema der URL zu erhalten.
Siehe auch Rezepte 2.9, 3.9 und 7.7.
7.9
Den Benutzer aus einer URL extrahieren
Problem Sie wollen den Benutzer aus einem String extrahieren, der eine URL enthält. So wollen Sie zum Beispiel jan aus ftp://[email protected] erhalten.
386 | Kapitel 7: URLs, Pfade und Internetadressen
Lösung Extrahieren des Benutzers aus einer URL, die schon validiert ist ^[a-z0-9+\-.]+://([a-z0-9\-._~%!$&'()*+,;=]+)@
Extrahieren des Benutzers während des Validierens der URL \A [a-z][a-z0-9+\-.]*:// ([a-z0-9\-._~%!$&'()*+,;=]+)@ ([a-z0-9\-._~%]+ |\[[a-f0-9:.]+\] |\[v[a-f0-9][a-z0-9\-._~%!$&'()*+,;=:]+\]) (:[0-9]+)? (/[a-z0-9\-._~%!$&'()*+,;=:@]+)*/? (\?[a-z0-9\-._~%!$&'()*+,;=:@/?]*)? (\#[a-z0-9\-._~%!$&'()*+,;=:@/?]*)? \Z
# # # # # # # # #
Schema Benutzer Named Host IPv6 Host IPvFuture Host Port Pfad Query Fragment
Diskussion Das Extrahieren des Benutzers aus einer URL ist sehr einfach, wenn Sie schon wissen, dass Ihr Ausgangstext eine gültige URL ist. Der Benutzername steht, sofern er in der URL vorhanden ist, direkt nach dem Schema und den beiden Schrägstrichen, mit denen der „Authority“-Teil der URL beginnt. Er wird vom folgenden Hostnamen durch ein @-Zeichen getrennt. Da das @-Zeichen in Hostnamen nicht gültig ist, können wir sicher sein, dass wir den Benutzernamen aus einer URL extrahieren können, wenn wir nach den beiden ersten Schrägstrichen und vor dem folgenden Schrägstrich ein @-Zeichen finden. Schrägstriche sind in Benutzernamen nicht erlaubt, daher brauchen wir dafür keine besondere Prüfung einzubauen. Durch diese Regeln können wir sehr leicht den Benutzernamen extrahieren, wenn wir schon wissen, dass die URL gültig ist. Wir überspringen einfach das Schema mithilfe von ‹[a-z0-9+\-.]+› und den literalen Zeichen ://. Dann holen wir den folgenden Benutzer-
7.9 Den Benutzer aus einer URL extrahieren | 387
namen. Finden wir das @-Zeichen, wissen wir, dass die Zeichen davor der Benutzername sind. Die Zeichenklasse ‹[a-z0-9\-._~%!$&'()*+,;=]› führt alle Zeichen auf, die in Benutzernamen gültig sind. Diese Regex findet nur dann eine Übereinstimmung, wenn die URL auch tatsächlich einen Benutzernamen enthält. Ist das der Fall, passt die Regex sowohl auf das Schema als auch auf den Benutzerteil der URL. Daher haben wir eine einfangende Gruppe ergänzt. Bei einer Übereinstimmung können Sie den von der ersten (und einzigen) einfangenden Gruppe gefundenen Text auslesen, um den Benutzernamen ohne weitere Trennzeichen oder andere Elemente der URL zu erhalten. In Rezept 2.9 erfahren Sie alles über einfangende Gruppen, und in Rezept 3.9 lernen Sie, wie Sie den von solchen einfangenden Gruppen gefundenen Text in Ihrer Programmiersprache auslesen. Wenn Sie nicht schon wissen, dass Ihr Ausgangstext eine gültige URL ist, können Sie eine vereinfachte Version der Regex aus Rezept 7.7 nutzen. Da wir den Benutzer extrahieren wollen, können wir URLs vernachlässigen, in denen keine Authority angegeben ist. Die Regex in dieser Lösung passt nur auf URLs, die eine Authority mit einem Benutzernamen enthalten. Dadurch, dass der Authority-Teil der URL vorhanden sein muss, wird die Regex etwas einfacher. Sie ist sogar noch einfacher als die Regex aus Rezept 7.8. Da diese Regex auf die gesamte URL passt, haben wir eine zusätzliche einfangende Gruppe um den Teil eingefügt, der den Benutzer findet. Um den Benutzernamen zu erhalten, lesen Sie den von der ersten einfangenden Gruppe gefundenen Text aus. Wenn Sie eine Regex haben wollen, die alle gültigen URLs findet, auch die ohne Benutzer, können Sie eine der Regexes aus Rezept 7.7 verwenden. Die erste dieser Regexes fängt den Benutzer in der dritten einfangenden Gruppe ein (falls es ihn gibt). Die einfangende Gruppe wird das @-Zeichen ebenfalls enthalten. Sie können die Regex um eine zusätzliche einfangende Gruppe ergänzen, wenn Sie den Benutzernamen ohne das @Zeichen auslesen wollen.
Siehe auch Rezepte 2.9, 3.9 und 7.7.
7.10 Den Host aus einer URL extrahieren Problem Sie wollen den Host aus einem String extrahieren, der eine URL enthält. So wollen Sie zum Beispiel www.regexcookbook.com erhalten, wenn Sie den Text http://www.regexcookbook.com/ haben.
388 | Kapitel 7: URLs, Pfade und Internetadressen
Lösung Extrahieren des Hosts aus einer als valide bekannten URL \A [a-z][a-z0-9+\-.]*:// ([a-z0-9\-._~%!$&'()*+,;=]+@)? ([a-z0-9\-._~%]+ |\[[a-z0-9\-._~%!$&'()*+,;=:]+\])
# # # #
Schema Benutzer Named Host oder IPv4-Host IPv6+ Host
Diskussion Das Extrahieren des Host aus einer URL ist sehr einfach, wenn Sie schon wissen, dass Ihr Ausgangstext eine gültige URL ist. Wir verwenden ‹\A› oder ‹^›, um die Übereinstimmung mit dem Anfang des Strings zu verknüpfen. ‹[a-z][a-z0-9+\-.]*://› überspringt das Schema und ‹([a-z0-9\-._~%!$&'()*+,;=]+@)?› den optionalen Benutzernamen. Der Hostname folgt direkt darauf.
7.10 Den Host aus einer URL extrahieren | 389
Der RFC 3986 erlaubt zwei unterschiedliche Schreibweisen für den Host. Domainnamen und IPv4-Adressen werden ohne eckige Klammern angegeben, während IPv6- und zukünftige IP-Adressen mit eckigen Klammern geschrieben werden. Wir müssen beide Versionen getrennt behandeln, da die Schreibweise mit eckigen Klammern mehr Satzzeichen zulässt als die Schreibweise ohne. Insbesondere der Doppelpunkt ist in den eckigen Klammern erlaubt, aber nicht in Domainnamen oder IPv4-Adressen. Der Doppelpunkt wird auch dazu verwendet, den Hostnamen (mit oder ohne eckige Klammern) von der Portnummer zu trennen. ‹[a-z0-9\-._~%]+› passt auf Domainnamen und IPv4-Adressen. ‹\[[a-z0-9\-._~%!$&'() *+,;=:]+\]› kümmert sich um IPv6- und zukünftige IP-Adressen. Wir kombinieren beide
Regexes durch eine Alternation (Rezept 2.8) in einer Gruppe. Die einfangende Gruppe lässt uns zudem auch den Hostnamen auslesen. Diese Regex findet nur dann eine Übereinstimmung, wenn in der URL auch tatsächlich ein Host angegeben ist. In dem Fall passt die Regex auf Schema, Benutzer und Host, und Sie können den Text von der zweiten einfangenden Gruppe auslesen, um den Hostnamen ohne Begrenzungszeichen oder andere URL-Elemente zu erhalten. Die einfangende Gruppe wird für IPv6-Adressen die eckigen Klammern enthalten. In Rezept 2.9 erfahren Sie alles über einfangende Gruppen, und in Rezept 3.9 wird erläutert, wie Sie den von solchen einfangenden Gruppen gefundenen Text in Ihrer Programmiersprache auslesen. Wissen Sie noch nicht, ob Ihr Ausgangstext eine gültige URL ist, können Sie eine vereinfachte Version der Regex aus Rezept 7.7 nutzen. Da wir den Host extrahieren wollen, können wir URLs ausschließen, in denen keine Authority angegeben ist. Damit wird der reguläre Ausdruck ein bisschen einfacher. Er ähnelt der aus Rezept 7.9 genutzten Regex. Der einzige Unterschied ist, dass der Benutzerabschnitt der Authority wieder optional ist. Diese Regex nutzt ebenfalls eine Alternation für die verschiedenen Schreibweisen des Hosts, die durch eine einfangende Gruppe zusammengehalten wird. Lesen Sie den von der zweiten einfangenden Gruppe gefundenen Text aus, um den Host aus der URL zu erhalten. Wenn Sie eine Regex suchen, die eine beliebige URL findet, auch solche ohne Host, können Sie eine der Regexes aus Rezept 7.7 nutzen. Die erste Regex in diesem Rezept fängt den Host, sofern es ihn gibt, in der vierten einfangenden Gruppe ein.
Siehe auch Rezepte 2.9, 3.9 und 7.7.
7.11 Den Port aus einer URL extrahieren Problem Sie wollen die Portnummer aus einem String extrahieren, der eine URL enthält. So wollen Sie zum Beispiel 80 aus http://www.regexcookbook.com:80/ extrahieren. 390 | Kapitel 7: URLs, Pfade und Internetadressen
Lösung Extrahieren des Ports aus einer URL, die als gültig bekannt ist \A [a-z][a-z0-9+\-.]*:// ([a-z0-9\-._~%!$&'()*+,;=]+@)? ([a-z0-9\-._~%]+ |\[[a-z0-9\-._~%!$&'()*+,;=:]+\]) :(?<port>[0-9]+)
# # # # #
Schema Benutzer Named Host oder IPv4 Host IPv6+ Host Portnummer
Diskussion Das Extrahieren der Portnummer aus einer URL ist einfach, wenn Sie schon wissen, dass es sich beim Ausgangstext um eine gültige URL handelt. Wir verwenden ‹\A› oder ‹^›, um die Übereinstimmung mit dem Anfang des Strings zu verbinden. ‹[a-z][a-z0-9+\.]*://› überspringt das Schema, ‹([a-z0-9\-._~%!$&'()*+,;=]+@)?› den optionalen Benutzer und ‹([a-z0-9\-._~%]+|\[[a-z0-9\-._~%!$&'()*+,;=:]+\])› den Hostnamen.
7.11 Den Port aus einer URL extrahieren |
391
Die Portnummer wird vom Hostnamen durch einen Doppelpunkt getrennt, den wir dem regulären Ausdruck als literales Zeichen hinzufügen. Die Portnummer selbst besteht einfach aus einer Ziffernfolge, die sich leicht durch ‹[0-9]+› finden lässt. Diese Regex findet nur dann eine Übereinstimmung, wenn in der URL auch tatsächlich eine Portnummer angegeben ist. In dem Fall passt die Regex auf Schema, Benutzer, Host und Portnummer. Dann können Sie den von der dritten einfangenden Gruppe gefundenen Text auslesen, um die Portnummer ohne Trennzeichen oder andere URL-Elemente zu erhalten. Die anderen beiden Gruppe werden genutzt, um den Benutzernamen optional zu machen und die beiden Alternativen für den Hostnamen zusammenzuhalten. In Rezept 2.9 erfahren Sie alles über einfangende Gruppen, und in Rezept 3.9 wird erläutert, wie Sie den von solchen einfangenden Gruppen gefundenen Text in Ihrer Programmiersprache auslesen. Wenn Sie noch nicht wissen, ob Ihr Ausgangstext eine gültige URL ist, können Sie eine vereinfachte Version der Regex aus Rezept 7.7 verwenden. Da wir den Port extrahieren wollen, können wir URLs ausschließen, in denen keine Authority definiert ist. Damit wird die Regex ein bisschen einfacher. Sie ähnelt der aus Rezept 7.10. Der einzige Unterschied besteht darin, dass die Portnummer nicht optional ist und wir die einfangende Gruppe für die Portnummer so verschoben haben, dass der Doppelpunkt als Trennzeichen nicht mit enthalten ist. Die Nummer der einfangenden Gruppe ist 3. Wenn Sie eine Regex haben wollen, die eine beliebige gültige URL findet, auch eine ohne Port, können Sie eine der Regexes aus Rezept 7.7 verwenden. Die erste Regex in diesem Rezept fängt den Port in der fünften einfangenden Gruppe ein – sofern er denn angegeben ist.
Siehe auch Rezepte 2.9, 3.9 und 7.7.
7.12 Den Pfad aus einer URL extrahieren Problem Sie wollen den Pfad aus einem String extrahieren, der eine URL enthält. So wollen Sie zum Beispiel /index.html erhalten, wenn Sie http://www.regexcookbook.com/index.html oder /index.html#fragment als Ausgangstext haben.
392 | Kapitel 7: URLs, Pfade und Internetadressen
Lösung Extrahieren des Pfads aus einem String, der eine gültige URL enthält. Die folgende Regex findet alle URLs, auch wenn diese keinen Pfad enthalten: \A # Schema und Authority überspringen, wenn vorhanden ([a-z][a-z0-9+\-.]*:(//[^/?#]+)?)? # Pfad ([a-z0-9\-._~%!$&'()*+,;=:@/]*)
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Extrahieren des Pfads aus einem String, der eine gültige URL enthält. Nur URLs finden, die auch wirklich einen Pfad enthalten: \A # Schema und Authority überspringen, wenn vorhanden ([a-z][a-z0-9+\-.]*:(//[^/?#]+)?)? # Pfad (/?[a-z0-9\-._~%!$&'()*+,;=@]+(/[a-z0-9\-._~%!$&'()*+,;=:@]+)*/?|/) # Query, Fragment oder Ende der URL ([#?]|\Z)
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Extrahieren des Pfads aus einem String, der eine gültige URL enthält. Mit atomaren Gruppen nur die URLs finden, die auch einen Pfad enthalten: \A # Schema und Authority überspringen, wenn vorhanden (?>([a-z][a-z0-9+\-.]*:(//[^/?#]+)?)?) # Pfad ([a-z0-9\-._~%!$&'()*+,;=:@/]+)
Diskussion Zum Extrahieren des Pfads können Sie einen deutlich einfacheren regulären Ausdruck verwenden, wenn Sie schon wissen, dass es sich bei Ihrem Ausgangstext um eine gültige URL handelt. Die generische Regex in Rezept 7.7 enthält in Abhängigkeit von der Anwesenheit eines Schemas und/oder einer Authority drei verschiedene Wege, den Pfad zu finden. Die spezifische Regex, mit der der Pfad aus einer schon als gültig bekannten URL extrahiert werden kann, muss den Pfad nur einmal finden. Wir beginnen mit ‹\A› oder ‹^›, um die Übereinstimmung mit dem Anfang des Strings zu verknüpfen. ‹[a-z][a-z0-9+\-.]*:› überspringt das Schema und ‹//[^/?#]+› die Authority. Wir können diese sehr einfache Regex für die Authority nutzen, weil wir schon wissen, dass sie gültig ist und wir nicht daran interessiert sind, Benutzer, Host oder Port zu ermitteln. Die Authority beginnt mit zwei Schrägstrichen und endet mit dem Beginn des Pfads (Schrägstrich), der Query (Fragezeichen) oder des Fragments (Rautezeichen). Die negierten Zeichenklassen passen auf alles bis zum ersten Schrägstrich, Fragezeichen oder Rautezeichen (Rezept 2.3). Da die Authority optional ist, stecken wir sie in eine Gruppe, auf die der FragezeichenQuantor folgt. Das Schema ist ebenfalls optional. Wird es weggelassen, darf auch die Authority nicht angegeben werden. Um das in der Regex abzubilden, packen wir die Teile für das Schema und die optionale Authority in eine weitere Gruppe, die ebenfalls mit einem Fragezeichen versehen wird. Aus dem Wissen heraus, dass die URL gültig ist, können wir den Pfad mit einer einzelnen Zeichenklasse ‹[a-z0-9\-._~%!$&'()*+,;=:@/]*› abbilden, die den Schrägstrich enthält. Wir müssen daher nicht auf direkt aufeinanderfolgende Schrägstriche prüfen, die nicht erlaubt sind. Aber wir verwenden hier nach der Zeichenklasse für den Pfad als Quantor einen Stern anstelle des Pluszeichens. Es mag seltsam sein, dass man den Pfad in einer Regex optional macht, die doch nur dafür gedacht ist, den Pfad aus einer URL zu extrahieren. Aber das ist dringend nötig, denn wir nutzen recht radikale Abkürzungen, um das Schema und die Authority zu überspringen. In der generischen Regex für URLs in Rezept 7.7 haben wir drei verschiedene Wege, den Pfad zu finden – je nachdem, ob das Schema und/oder die Authority in der URL vorhanden sind. Damit ist sichergestellt, dass das Schema nicht unabsichtlich als Pfad gefunden wird. Jetzt versuchen wir, die Dinge möglichst einfach zu halten, indem wir nur eine Zeichenklasse für den Pfad verwenden. Schauen Sie sich die URL http://www.regexcookbook.com an, die ein Schema und eine Authority hat, aber keine Pfad. Der erste Teil der Regex wird das Schema und die Authority problemlos finden. Dann versucht sich die Regex-Engine an der Zeichenklasse für den Pfad, aber es sind keine Zeichen mehr übrig. Ist der Pfad optional (mit dem Stern-Quantor), ist die Engine trotzdem glücklich, weil sie ja nicht unbedingt Zeichen für den Pfad finden muss. Sie gelangt an das Ende der Regex und erklärt, dass sie tatsächlich etwas gefunden hat. 394 | Kapitel 7: URLs, Pfade und Internetadressen
Ist die Zeichenklasse für den Pfad nicht optional, führt die Regex-Engine ein Backtracking durch (wenn Sie mit Backtracking nicht vertraut sind, werfen Sie einen Blick in Rezept 2.13). Sie erinnert sich daran, dass die Authority- und Schema-Elemente in unserer Regex optional sind. Also versucht die Engine einen neuen Durchlauf, diesmal aber sorgt sie dafür, dass ‹(//[^/?#]+)?› nichts findet. ‹[a-z0-9\-._~%!$&'()*+,;=:@/]+› passt dann für den Pfad auf //www.regexcookbook.com, was wir natürlich gar nicht wollen. Würden wir eine exaktere Regex für den Pfad nutzen, mit dem doppelte Schrägstriche verboten wären, würde die Regex-Engine per Backtracking noch weiter zurückspringen und davon ausgehen, dass die URL kein Schema enthält. Mit einer exakten Regex für den Pfad würde sie nun http finden. Um auch das zu verhindern, müssten wir eine zusätzliche Prüfung einbauen, die testet, ob auf den Pfad eine Query, ein Fragment oder gar nichts mehr folgt. Fügt man all das zusammen, landet man wieder bei regulären Ausdrücken, die nur URLs mit Pfad finden. Die sind ein ganzes Stück komplizierter als die ersten beiden, nur um dafür zu sorgen, dass die Regex keine URLs ohne Pfad findet. Wenn Ihre Regex-Variante atomare Gruppen unterstützt, gibt es einen einfacheren Weg. Alle in diesem Buch behandelten Varianten – außer JavaScript und Python – unterstützen atomare Gruppen (siehe Rezept 2.15). Solch eine Gruppe weist die Regex-Engine an, kein Backtracking durchzuführen. Stecken wir die Elemente für Schema und Authority in eine atomare Gruppe, wird die Regex-Engine dazu gezwungen, die Übereinstimmungen für das Schema und die Authority beizubehalten, auch wenn damit für die Zeichenklasse für den Pfad nichts mehr übrig bleibt. Diese Lösung ist genauso effizient wie die für den optionalen Pfad. Unabhängig davon, welchen regulären Ausdruck Sie aus diesem Rezept wählen – die dritte einfangende Gruppe wird immer den Pfad enthalten. Diese kann einen leeren String zurückgeben (oder null in JavaScript), wenn Sie eine der ersten beiden Regexes verwenden, bei denen der Pfad optional sein darf. Wenn Sie noch nicht wissen, ob Ihr Ausgangstext eine gültige URL ist, können Sie die Regex aus Rezept 7.7 verwenden. Nutzen Sie .NET, können Sie die .NET-spezifische Regex nutzen, die drei Gruppen mit dem Namen „path“ enthält, um die drei Teile der Regex einzufangen, die den Pfad der URL enthalten können. Bei anderen Varianten, die benannte Captures kennen, wird eine der drei Gruppen „hostpath“, „schemepath“ oder „relpath“ den Pfad enthalten. Da nur eine der drei Gruppen tatsächlich etwas enthält, kann man den Pfad mit einem Trick auslesen. Man konkatiniert einfach den Inhalt der drei Gruppen. Zwei davon werden den leeren String zurückgeben, sodass man immer das erhält, was man haben möchte. Wenn Ihre Regex-Variante keine benannten Captures unterstützt, können Sie die erste Regex aus Rezept 7.7 nutzen. Sie fängt den Pfad in den Gruppen 6, 7 oder 8 ein. Auch hier können Sie den Inhalt der drei Gruppen konkatenieren, da zwei von ihnen immer einen leeren String zurückgeben werden. In JavaScript funktioniert das allerdings nicht. Dort geben Gruppen, die nicht am Suchergebnis beteiligt sind, ein undefined zurück.
7.12 Den Pfad aus einer URL extrahieren | 395
In Rezept 3.9 finden Sie mehr Informationen zum Auslesen von Text, der durch benannte oder nummerierte Gruppen gefunden wurde, in Ihrer Programmiersprache.
Siehe auch Rezepte 2.9, 3.9 und 7.7.
7.13 Die Query aus einer URL extrahieren Problem Sie wollen die Query aus einem String auslesen, der eine URL enthält. So wollen Sie zum Beispiel den Wert param=value erhalten, wenn Ihr Ausgangstext http://www.regexcookbook.com?param=value oder /index.html?param=value lautet.
Diskussion Das Extrahieren der Query aus einer URL ist trivial, wenn Sie wissen, dass es sich bei Ihrem Ausgangstext um eine gültige URL handelt. Die Query wird vom vorderen Teil der URL durch ein Fragezeichen abgetrennt. Das ist das erste Fragezeichen, das in URLs erlaubt ist. Daher können wir mit ‹^[^?#]+\?› einfach alles bis zu diesem ersten Fragezeichen überspringen. Das Fragezeichen ist nur außerhalb von Zeichenklassen ein Metazeichen, daher müssen wir es dort maskieren. Der erste ‹^› ist ein Anker (Rezept 2.5), während der zweite ‹^› die Zeichenklasse negiert (Rezept 2.3). Fragezeichen können in URLs als Teil des (optionalen) Fragments nach der Query auftauchen. Daher müssen wir ‹^[^?#]+\?› und nicht nur einfach ‹\?› verwenden, um sicherzustellen, dass wir das erste Fragezeichen in der URL erwischen und dass es sich dabei nicht um einen Teil des Fragments in einer URL handelt, die keine Query enthält. Die Query läuft bis zum Anfang des Fragments oder, wenn es keins gibt, bis zum Ende der URL. Das Fragment ist vom Rest der URL durch ein Rautezeichen abgetrennt. Da Rautezeichen nur im Fragment zulässig sind, brauchen wir zum Finden der Query lediglich ‹[^#]+›. Die negierte Zeichenklasse passt auf alles bis zum ersten Rautezeichen oder bis zum Ende des Ausgangstexts, wenn kein Rautezeichen vorhanden ist. Dieser reguläre Ausdruck findet nur dann eine Übereinstimmung, wenn die URL tatsächlich eine Query enthält. Gibt es eine Übereinstimmung, findet sich darin alles vom
396 | Kapitel 7: URLs, Pfade und Internetadressen
Anfang der URL an, daher umschließen wir den Teil ‹[^#]+› mit einer einfangenden Gruppe. Bei einem positiven Suchergebnis können Sie den von der ersten (und einzigen) einfangenden Gruppe gefundenen Text auslesen, um die Query ohne Trennzeichen oder andere URL-Elemente zu erhalten. In Rezept 2.9 erfahren Sie alles über einfangende Gruppen, und in Rezept 3.9 wird genauer erklärt, wie Sie den von solchen einfangenden Gruppen gefundenen Text in Ihrer Programmiersprache auslesen. Wenn Sie noch nicht wissen, ob Ihr Ausgangstext eine gültige URL enthält, können Sie eine der Regexes aus Rezept 7.7 verwenden. Die erste Regex in diesem Rezept fängt die Query (sofern es überhaupt eine gibt) in Gruppe Nr. 12 ein.
Siehe auch Rezepte 2.9, 3.9 und 7.7.
7.14 Das Fragment aus einer URL extrahieren Problem Sie wollen das Fragment aus einem String extrahieren, in dem sich eine URL befindet. So wollen Sie zum Beispiel den Wert top erhalten, wenn der Ausgangstext http://www.regexcookbook.com#top oder /index.html#top lautet.
Diskussion Das Extrahieren des Fragments aus einer URL ist einfach, wenn Sie wissen, dass es sich bei Ihrem Ausgangstext um eine gültige URL handelt. Das Fragment wird vom Rest der URL durch ein Rautezeichen abgeteilt. Da das Fragment der einzige Teil einer URL ist, in dem Rautezeichen zulässig sind, und es auch immer der letzte Teil einer URL ist, können wir es leicht extrahieren, indem wir das erste Rautezeichen finden und dann alles bis zum Ende des Strings einfangen. Das funktioniert mit ‹#.+›. Achten Sie darauf, den FreiformModus nicht aktiviert zu haben, da Sie das literale Rautezeichen ansonsten mit einem Backslash maskieren müssten. Dieser reguläre Ausdruck findet nur für URLs eine Übereinstimmung, die auch ein Fragment enthalten. Dabei wird dann auch nur das Fragment gefunden, allerdings zusammen mit dem Rautezeichen am Anfang. Die Lösung enthält eine zusätzliche einfangende Gruppe, um lediglich das Fragment ohne das begrenzende # auslesen zu können.
7.14 Das Fragment aus einer URL extrahieren | 397
Wenn Sie noch nicht wissen, ob es sich bei Ihrem Ausgangstext um eine gültige URL handelt, können Sie eine der Regexes aus Rezept 7.7 verwenden. Die erste Regex in diesem Rezept fängt das Fragment (sofern es vorhanden ist) in Gruppe Nr. 13 ein.
Siehe auch Rezepte 2.9, 3.9 und 7.7.
7.15 Domainnamen validieren Problem Sie wollen prüfen, ob ein String wie ein gültiger, vollständig qualifizierter Domainname aussieht, oder Sie wollen solche Domainnamen in einem längeren Text finden.
Lösung Prüfen, ob ein String wie ein gültiger Domainname aussieht: ^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Gültige Domainnamen in längerem Text finden: \b([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}\b
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Prüfen, ob jeder Teil der Domain nicht länger als 63 Zeichen ist: \b((?=[a-z0-9-]{1,63}\.)[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b
Prüfen, ob jeder Teil der Domäne nicht länger als 63 Zeichen ist und internationalisierte Domainnamen in Punycode-Notation zulassen: \b((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}\b
Diskussion Ein Domainname hat die Form domain.tld oder subdomain.domain.tld oder eine beliebige Zahl weiterer Subdomains. Die Top-Level-Domain (tld) besteht aus zwei oder mehr Buchstaben. Das ist der einfachste Teil der Regex: ‹[a-z]{2,}›. Die Domain und alle Subdomains bestehen aus Buchstaben, Ziffern und Bindestrichen. Bindestriche können weder direkt aufeinander folgen noch dürfen sie das erste oder letzte Zeichen einer Domain sein. Das erreichen wir über den regulären Ausdruck ‹[az0-9]+(-[a-z0-9]+)*›. Damit sind beliebig viele Buchstaben und Ziffern erlaubt, optional gefolgt von beliebig vielen Gruppen, die aus einem Bindestrich, gefolgt von einer weiteren Folge von Buchstaben und Ziffern, bestehen. Denken Sie daran, dass der Bindestrich in Zeichenklassen ein Metazeichen ist (Rezept 2.3), aber außerhalb von Zeichenklassen ganz normal verwendet werden kann. Daher braucht er in dieser Regex nicht maskiert zu werden. Die Domain und die Subdomains werden durch einen literalen Punkt getrennt, den wir über ‹\.› finden. Da es neben der Domain beliebig viele Subdomains geben kann, setzen wir den Domainnamen-Teil der Regex und den literalen Punkt in eine Gruppe, die wir wiederholen: ‹([a-z0-9]+(-[a-z0-9]+)*\.)+›. Weil die Subdomains der gleichen Syntax folgen wie die Domain, kann mit dieser Gruppe beides abgedeckt werden. Wenn Sie prüfen wollen, ob ein String einem gültigen Domainnamen entspricht, müssen jetzt nur noch an den Anfang und das Ende der Regex Anker gesetzt werden, die am Anfang und Ende des Strings passen. Das erreichen wir in allen Varianten außer in Ruby mit ‹^› und ‹$›, und in allen Varianten außer in JavaScript mit ‹\A› und ‹\Z›. In Rezept 2.5 erfahren Sie mehr über diese Anker. Möchten Sie Domainnamen in einem längeren Text finden, können Sie Wortgrenzen nutzen (‹\b›; siehe Rezept 2.6). Unsere ersten regulären Ausdrücke prüfen nicht, ob jeder Teil der Domain länger als 63 Zeichen ist oder nicht. Das lässt sich nicht so leicht umsetzen, da die Regex für einen Domainteil (‹[a-z0-9]+(-[a-z0-9]+)*›) drei Quantoren enthält. Es gibt keine Möglichkeit, der Regex-Engine mitzuteilen, dass dabei höchstens 63 Zeichen herauskommen sollen. Wir könnten ‹[-a-z0-9]{1,63}› nutzen, um einen Domainteil zu finden, der 1 bis 63 Zeichen lang ist, oder ‹\b([-a-z0-9]{1,63}\.)+[a-z]{2,63}› für den gesamten Domainnamen. Aber wir schließen damit keine Domains mehr aus, die an den falschen Stellen Bindestriche enthalten.
7.15 Domainnamen validieren | 399
Aber einen Lookahead können wir verwenden, um den gleichen Text doppelt zu finden. Falls Sie mit Lookaheads nicht so vertraut sind, machen Sie sich in Rezept 2.16 schlau. Wir verwenden die gleiche Regex ‹[a-z0-9]+(-[a-z0-9]+)*\.›, um einen Domainnamen mit gültigen Bindestrich-Kombinationen zu finden, und fügen innerhalb eines Lookahead die Regex ‹[-a-z0-9]{1,63}\.› hinzu, um zu prüfen, ob die Länge der Domain zwischen 1 und 63 Zeichen liegt. Das Ergebnis ist ‹(?=[-a-z0-9]{1,63}\.)[a-z0-9]+(-[az0-9]+)*\.›. Das Lookahead ‹(?=[-a-z0-9]{1,63}\.)› prüft zunächst, ob es bis zum nächsten Punkt 1 bis 63 Buchstaben, Ziffern und Bindestriche gibt. Es ist wichtig, den Punkt mit in das Lookahead aufzunehmen. Ohne ihn würden Domains, die länger als 63 Zeichen sind, die Lookahead-Bedingungen immer noch erfüllen. Nur durch den literalen Punkt im Lookahead stellen wir sicher, dass es nicht mehr als 63 Zeichen gibt. Das Lookahead konsumiert den gefundenen Text nicht. Ist es also erfolgreich, wird ‹[az0-9]+(-[a-z0-9]+)*\.› auf den gleichen Text angewendet, der schon vom Lookahead gefunden wurde. Wir wissen schon, dass die Domain nicht mehr als 63 Zeichen enthält, jetzt können wir prüfen, ob sich auch die Bindestriche an den richtigen Stellen befinden. Internationalisierte Domainnamen (IDNs) können theoretisch so gut wie jedes Zeichen enthalten. Die Liste der tatsächlich nutzbaren Zeichen hängt von der Registry-Organisation ab, die sich um die Top-Level-Domain kümmert. So sind für .es zum Beispiel Domainnamen mit spanischen Zeichen zulässig. In der Praxis sind internationalisierte Domainnamen häufig nach dem Punycode-Schema kodiert. Auch wenn der entsprechende Algorithmus ziemlich kompliziert ist, kommt es hier lediglich darauf an, dass er zu Domainnamen führt, die aus Buchstaben, Ziffern und Bindestrichen bestehen und den gleichen Regeln folgen wie bei den „normalen“ Namen. Der einzige Unterschied ist, dass die von Punycode erzeugten Domainnamen mit xn-beginnen. Um auch solche Domains von unseren regulären Ausdrücken finden zu lassen, müssen wir die Gruppe, die den Domainnamen findet, nur um ‹(xn--)?› ergänzen.
Siehe auch Rezepte 2.3, 2.12 und 2.16.
7.16 IPv4-Adressen finden Problem Sie wollen prüfen, ob ein bestimmter String aus einer gültigen IPv4-Adresse in der Notation 255.255.255.255 besteht. Optional wollen Sie diese Adresse auch noch in eine 32Bit-Integer-Zahl umwandeln.
400 | Kapitel 7: URLs, Pfade und Internetadressen
Lösung Regulärer Ausdruck Einfache Regex, mit der eine IP-Adresse geprüft wird: ^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Exakte Regex, mit der eine IP-Adresse geprüft wird: ^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Einfache Regex, mit der IP-Adressen aus einem längeren Text geholt werden: \b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Exakte Regex, mit der IP-Adresse aus einem längeren Text geholt werden: \b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Einfache Regex, die die vier Blöcke der IP-Adresse einfängt: ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Exakte Regex, die die vier Blöcke der IP-Adresse einfängt: ^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\. (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\. (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\. (25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Perl if ($subject =~ m/^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/) { $ip = $1 \b(?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4}\b
Gemischte Notation Finden einer IPv6-Adresse in gemischter Notation, die aus sechs 16-Bit-Wörtern in hexadezimaler Schreibweise und vier Bytes in dezimaler Schreibweise besteht. Die sechs Wörter sind durch Doppelpunkte getrennt, die Bytes durch Punkte. Ein Doppelpunkt trennt die Wörter von den Bytes. Führende Nullen sind sowohl für die hexadezimalen Wörter als auch für die dezimalen Bytes optional. Diese Notation wird in Situationen genutzt, in denen IPv4 und IPv6 gemischt werden und die IPv6-Adressen Erweiterungen der IPv4Adressen sind. 1762:0:0:0:0:B03:127.32.67.15 ist ein Beispiel für eine IPv6-Adresse in gemischter Notation. Prüfen, ob der gesamte Ausgangstext aus einer IPv6-Adresse in gemischter Notation besteht: ^(?:[A-F0-9]{1,4}:){6}(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby IPv6-Adressen in gemischter Notation in einem längeren Text finden: (? Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby JavaScript und Ruby 1.8 unterstützen kein Lookbehind. Wir müssen die Prüfung am Anfang der Regex entfernen, die verhindert, dass IPv6-Adressen innerhalb einer längeren Folge von hexadezimalen Ziffern und Doppelpunkten gefunden werden. Eine Wortgrenze kann einen Teil des Tests abhandeln: \b(?:[A-F0-9]{1,4}:){6}(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b
Standardnotation oder gemischte Notation Finden einer IPv6-Adresse in Standardnotation oder gemischter Notation. Prüfen, ob der gesamte Ausgangstext aus einer IPv6-Adresse in Standardnotation oder gemischter Notation besteht:
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python IPv6-Adressen in Standardnotation oder gemischter Notation in einem längeren Text finden: (? # # # #
Anker 6 Wörter 2 Wörter oder 4 Bytes
# Anker
Regex-Optionen: Freiform, Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby 1.9 JavaScript und Ruby 1.8 unterstützen kein Lookbehind. Wir müssen die Prüfung am Anfang der Regex entfernen, die verhindert, dass IPv6-Adressen innerhalb einer längeren Folge von hexadezimalen Ziffern und Doppelpunkten gefunden werden. Eine Wortgrenze kann einen Teil des Tests abhandeln: \b (?:[A-F0-9]{1,4}:){6} (?:[A-F0-9]{1,4}:[A-F0-9]{1,4} | (?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) )\b
Komprimierte Notation Finden einer IPv6-Adresse in komprimierter Notation. Die komprimierte Notation entspricht der Standardnotation, nur dass eine Folge von Wörtern mit dem Wert null weg-
7.17 IPv6-Adressen finden | 405
gelassen werden kann, sodass lediglich die Doppelpunkte vor und nach den weggelassenen Nullen stehen bleiben. Adressen in komprimierter Notation können anhand der zwei direkt aufeinanderfolgenden Doppelpunkte erkannt werden. Es darf nur eine Folge von Nullen weggelassen werden, da es ansonsten nicht möglich wäre, herauszufinden, welche Wörter fehlen. Befindet sich die weggelassene Folge von Nullen am Anfang oder Ende der IP-Adresse, beginnt beziehungsweise endet die Adresse mit zwei Doppelpunkten. Haben alle Zahlen den Wert null, besteht die komprimierte IPv6Adresse nur aus zwei Doppelpunkten ohne jegliche Ziffern. So ist zum Beispiel 1762::B03:1:AF18 die komprimierte Form von 1762:0:0:0:0:B03:1:AF18. Die regulären Ausdrücke in diesem Abschnitt passen sowohl auf komprimierte als auch auf „normale“ IPv6-Adressen. Prüfen, ob der gesamte Ausgangstext aus einer IPv6-Adresse in Standardnotation oder komprimierter Notation besteht: \A(?: # Standard (?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4} # Komprimiert mit höchstens 7 Doppelpunkten |(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4} \Z) # und ein Anker # und höchstens einem doppelten Doppelpunkt (([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:) )\Z
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python Finden von IPv6-Adressen in Standardnotation oder komprimierter Notation in einem längeren Text: (? Regex-Optionen: Freiform, Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby 1.9
406 | Kapitel 7: URLs, Pfade und Internetadressen
JavaScript und Ruby 1.8 unterstützen kein Lookbehind. Wir müssen die Prüfung am Anfang der Regex entfernen, die verhindert, dass IPv6-Adressen innerhalb einer längeren Folge von hexadezimalen Ziffern und Doppelpunkten gefunden werden. Eine Wortgrenze können wir hier nicht nutzen, da die Adresse mit einem Doppelpunkt (was kein Wortzeichen ist) beginnen kann: (?: # Standard (?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4} # Komprimiert mit höchstens 7 Doppelpunkten |(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4} (?![:.\w])) # und einem Anker # und höchstens einem doppelten Doppelpunkt (([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:) )(?![:.\w])
Komprimierte gemischte Notation Finden einer IPv6-Adresse in komprimierter gemischter Notation. Die komprimierte gemischte Notation ist die gleiche wie die gemischte Notation, nur dass eine Folge von einem oder mehreren Wörtern mit dem Wert null weggelassen werden darf. Dabei bleiben nur die Doppelpunkte vor und nach den weggelassenen Werten stehen. Die vier Dezimalbytes müssen immer angegeben werden, auch wenn sie null sind. Adressen in komprimierter gemischter Notation können durch die direkt aufeinanderfolgenden zwei Doppelpunkte im ersten Teil und die drei Punkte im zweiten Teil erkannt werden. Es darf nur eine Folge von Nullen weggelassen werden, da es ansonsten nicht möglich wäre, herauszufinden, welche Wörter fehlen. Befindet sich die weggelassene Folge von Nullen am Anfang der IP-Adresse, beginnt die Adresse mit zwei Doppelpunkten. So ist zum Beispiel die IPv6-Adresse 1762::B03:127.32.67.15 die komprimierte Form von 1762:0:0:0:0:B03:127.32.67.15. Die regulären Ausdrücke in diesem Abschnitt finden sowohl komprimierte als auch unkomprimierte IPv6-Adressen mit gemischter Notation. Prüfen, ob der gesamte Ausgangstext aus einer IPv6-Adresse in komprimierter oder unkomprimierter gemischter Notation besteht: \A (?: # Unkomprimiert (?:[A-F0-9]{1,4}:){6} # Komprimiert mit höchstens 6 Doppelpunkten
7.17 IPv6-Adressen finden | 407
|(?=(?:[A-F0-9]{0,4}:){0,6} (?:[0-9]{1,3}\.){3}[0-9]{1,3} # und 4 Bytes \Z) # und einem Anker # und höchstens einem doppelten Doppelpunkt (([0-9A-F]{1,4}:){0,5}|:)((:[0-9A-F]{1,4}){1,5}:|:) ) # 255.255.255. (?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} # 255 (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) \Z
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python Finden von IPv6-Adressen in komprimierter oder unkomprimierter gemischter Notation in einem längeren Text: (? Regex-Optionen: Freiform, Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby 1.9 JavaScript und Ruby 1.8 unterstützen kein Lookbehind. Wir müssen die Prüfung am Anfang der Regex entfernen, die verhindert, dass IPv6-Adressen innerhalb einer längeren Folge von hexadezimalen Ziffern und Doppelpunkten gefunden werden. Eine Wortgrenze können wir hier nicht nutzen, da die Adresse mit einem Doppelpunkt (der kein Wortzeichen ist) beginnen kann:
408 | Kapitel 7: URLs, Pfade und Internetadressen
(?: # Unkomprimiert (?:[A-F0-9]{1,4}:){6} # Komprimiert mit höchstens 6 Doppelpunkten |(?=(?:[A-F0-9]{0,4}:){0,6} (?:[0-9]{1,3}\.){3}[0-9]{1,3} # und 4 Bytes (?![:.\w])) # und einen Anker # und höchstens einem doppelten Doppelpunkt (([0-9A-F]{1,4}:){0,5}|:)((:[0-9A-F]{1,4}){1,5}:|:) ) # 255.255.255. (?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} # 255 (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) (?![:.\w])
Standardnotation, gemischte oder komprimierte Notation Finden einer IPv6-Adresse in beliebiger Notation (Standard, gemischt, komprimiert und komprimiert gemischt). Prüfen, ob der gesamte Ausgangstext aus einer IPv6-Adresse besteht: \A(?: # Gemischt (?: # Unkomprimiert (?:[A-F0-9]{1,4}:){6} # Komprimiert mit höchstens 6 Doppelpunkten |(?=(?:[A-F0-9]{0,4}:){0,6} (?:[0-9]{1,3}\.){3}[0-9]{1,3} # und 4 Bytes \Z) # und einen Anker # und höchstens einem doppelten Doppelpunkt (([0-9A-F]{1,4}:){0,5}|:)((:[0-9A-F]{1,4}){1,5}:|:) ) # 255.255.255. (?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} # 255 (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) |# Standard (?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4} |# Komprimiert mit höchstens 7 Doppelpunkten
7.17 IPv6-Adressen finden | 409
(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4} \Z) # und einen Anker # und höchstens einem doppelten Doppelpunkt (([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:) )\Z
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python Finden einer IPv6-Adresse in einem längeren Text: (? Regex-Optionen: Freiform, Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby 1.9 JavaScript und Ruby 1.8 unterstützen kein Lookbehind. Wir müssen die Prüfung am Anfang der Regex entfernen, die verhindert, dass IPv6-Adressen innerhalb einer längeren Folge von hexadezimalen Ziffern und Doppelpunkten gefunden werden. Eine Wortgrenze können wir hier nicht nutzen, da die Adresse mit einem Doppelpunkt (der kein Wortzeichen ist) beginnen kann.
410 | Kapitel 7: URLs, Pfade und Internetadressen
(?: # Gemischt (?: # Unkomprimiert (?:[A-F0-9]{1,4}:){6} # Komprimiert mit höchstens 6 Doppelpunkten |(?=(?:[A-F0-9]{0,4}:){0,6} (?:[0-9]{1,3}\.){3}[0-9]{1,3} # und 4 Bytes (?![:.\w])) # und einem Anker # und höchstens einem doppelten Doppelpunkt (([0-9A-F]{1,4}:){0,5}|:)((:[0-9A-F]{1,4}){1,5}:|:) ) # 255.255.255. (?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3} # 255 (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?) |# Standard (?:[A-F0-9]{1,4}:){7}[A-F0-9]{1,4} |# Komprimiert mit höchstens 7 Doppelpunkten (?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4} (?![:.\w])) # und einem Anker # und höchstens einem doppelten Doppelpunkt (([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:) )(?![:.\w])
Diskussion Aufgrund der verschiedenen Notationen ist das Finden einer IPv6-Adresse nicht annähernd so einfach wie das einer IPv4-Adresse. Die Komplexität Ihrer regulären Ausdrücke hängt stark davon ab, welche Schreibweisen Sie akzeptieren wollen. Prinzipiell gibt es zwei Notationen: Standard und gemischt. Sie können entscheiden, welche der Schreibweisen Sie zulassen wollen – eine der beiden oder beide. Damit erhalten wir drei Gruppen von regulären Ausdrücken. Sowohl die Standardnotation als auch die gemischte Notation gibt es auch in einer komprimierten Form, in der Nullen weggelassen werden. Lässt man die komprimierte Notation zu, erhält man drei weitere Gruppen von regulären Ausdrücken.
7.17 IPv6-Adressen finden | 411
Zudem unterscheiden sich die Regexes leicht, wenn Sie prüfen wollen, ob ein String komplett aus einer gültigen IPv6-Adresse besteht, oder wenn Sie IP-Adressen in einem längeren Text finden wollen. Um die IP-Adressen zu validieren, verwenden wir Anker (siehe Rezept 2.5). JavaScript nutzt die Anker ‹^› und ‹$›, während Ruby auf ‹\A› und ‹\Z› zurückgreift. Alle anderen Varianten kennen beides. Ruby unterstützt ebenfalls ‹^› und ‹$›, aber dort passen sie immer auch an Zeilenumbrüchen im String. Daher sollten Sie Zirkumflex und Dollar in Ruby nur dann verwenden, wenn Sie wissen, dass es in Ihrem String keine eingebetteten Zeilenumbrüche gibt. Um IPv6-Adressen innerhalb eines längeren Texts zu finden, verwenden wir ein negatives Lookbehind ‹(? Standardnotation Die Standardnotation lässt sich recht einfach mit einem regulären Ausdruck abbilden. Wir müssen acht Wörter in hexadezimaler Schreibweise finden, die durch sieben Doppelpunkte getrennt sind. ‹[A-F0-9]{1,4}› passt auf 1 bis 4 hexadezimale Zeichen – also genau das, was wir für ein 16-Bit-Wort mit optionalen führenden Nullen benötigen. Die Zeichenklasse (Rezept 2.3) führt nur die Großbuchstaben auf. Durch die Option Groß/Kleinschreibung ignorieren werden aber auch Kleinbuchstaben gefunden. In Rezept 3.4 erfahren Sie, wie Sie die Regex-Optionen in Ihrer Programmiersprache setzen können. Die nicht-einfangende Gruppe ‹(?:[A-F0-9]{1,4}:){7}› passt auf ein hexadezimales Wort, gefolgt von einem Doppelpunkt. Der Quantor wiederholt die Gruppe sieben Mal. Der erste Doppelpunkt in dieser Regex ist Teil der Syntax für nicht-einfangende Gruppen (siehe Rezept 2.9), und der zweite Doppelpunkt ist das literale Zeichen. In regulären Ausdrücken ist er kein Metazeichen, abgesehen von ein paar Sonderfällen. Daher brauchen wir keinen Backslash, um ihn zu maskieren. Natürlich könnten wir es tun, aber dadurch ließe sich die Regex nur noch schlechter lesen.
Gemischte Notation Die Regex für die gemischte IPv6-Notation besteht aus zwei Teilen. ‹(?:[A-F0-9] {1,4}:){6}› passt auf sechs hexadezimale Wörter, jeweils gefolgt von einem literalen Doppelpunkt. Das ist fast identisch mit der Regex für die Standardnotation. 412 | Kapitel 7: URLs, Pfade und Internetadressen
Anstelle nun aber eines weiteren hexadezimalen Worts am Ende steht dort eine vollständige IPv4-Adresse. Diese finden wir über die „exakte“ Regex aus Rezept 7.16.
Standardnotation oder gemischte Notation Will man sowohl die Standardnotation als auch die gemischte Notation zulassen, wird der reguläre Ausdruck ein Stückchen länger. Die beiden Notationen unterscheiden sich lediglich in der Darstellung der letzten 32 Bit der IPv6-Adresse. Die Standardnotation verwendet dafür zwei 16-Bit-Wörter, während die gemischte Notation wie in der IPv4 vier dezimale Bytes nutzt. Der erste Teil der Regex passt auf sechs hexadezimale Wörter – wie in der Regex, die nur eine gemischte Schreibweise findet. Der zweite Teil der Regex ist nun eine nicht-einfangende Gruppe mit den beiden Alternativen für die letzten 32 Bit. Wie in Rezept 2.8 beschrieben, besitzt der Alternationsoperator (der vertikale Balken) die niedrigste Wertigkeit aller Regex-Operatoren. Daher benötigen wir die nicht-einfangende Gruppe, um die sechs Wörter aus dieser Alternation herauszuhalten. Die erste Alternative links vom vertikalen Balken passt auf zwei hexadezimale Wörter mit einem literalen Doppelpunkt dazwischen. Die zweite Alternative passt auf eine IPv4Adresse.
Komprimierte Notation Lassen wir die komprimierte Notation zu, wird alles ein bisschen schwieriger. Der Grund liegt darin, dass bei der komprimierten Notation eine variable Anzahl von Null-Werten weggelassen werden kann. Die Adresse 1:0:0:0:0:6:0:0 kann auch als 1::6:0:0 oder 1:0:0:0:0:6:: geschrieben werden. Die Adresse darf maximal acht Wörter enthalten, aber es können auch weniger sein, und dann muss es eine Folge von zwei Doppelpunkten geben, die die ausgelassenen Nullen darstellen. Eine variable Wiederholung lässt sich mit regulären Ausdrücken problemlos umsetzen. Wenn eine IPv6-Adresse einen doppelten Doppelpunkt hat, kann es höchstens sieben Wörter vor und nach dem doppelten Doppelpunkt geben. Das ließe sich leicht schreiben als: ( ([0-9A-F]{1,4}:){1,7} | : ) ( (:[0-9A-F]{1,4}){1,7} | : )
# 1 bis 7 Wörter links # oder ein doppelter Doppelpunkt am Anfang
# 1 bis 7 Wörter rechts # oder ein doppelter Doppelpunkt am Ende
Dieser reguläre Ausdruck und die folgenden in dieser Diskussion funktionieren auch mit JavaScript, wenn Sie die Kommentare und die zusätzlichen Leerzeichen entfernen. JavaScript unterstützt alle Features in diesen Regexes, nur nicht den Freiform-Modus, den wir hier verwenden, um die Regexes verständlicher zu machen.
Dieser reguläre Ausdruck passt auf alle komprimierten IPv6-Adressen, aber er findet keine Adressen, die die normale, unkomprimierte Standardnotation nutzen. Diese Regex ist ziemlich einfach. Der erste Teil passt auf 1 bis 7 Wörter, gefolgt von einem Doppelpunkt, oder einfach auf den Doppelpunkt für Adressen, die links vom doppelten Doppelpunkt keine Werte haben. Der zweite Teil passt auf 1 bis 7 Wörter, vor denen ein Doppelpunkt steht, oder nur auf den Doppelpunkt für Adressen, bei denen rechts vom doppelten Doppelpunkt keine Werte mehr stehen. Zusammen werden durch diese Regex ein doppelter Doppelpunkt, ein doppelter Doppelpunkt mit 1 bis 7 Wörtern nur links davon, ein doppelter Doppelpunkt mit 1 bis 7 Wörtern nur rechts davon und ein doppelter Doppelpunkt mit 1 bis 7 Wörtern links und rechts davon gefunden. Der letzte Teil ist der, der Ärger macht. Die Regex ermöglicht auf beiden Seiten 1 bis 7 Wörter, aber sie definiert nicht, dass links und rechts zusammen höchstens 7 Wörter stehen dürfen. Eine IPv6-Adresse hat 8 Wörter. Der doppelte Doppelpunkt zeigt, dass wir mindestens ein Wort weglassen, daher bleiben höchstens 7 übrig. Reguläre Ausdrücke können aber nicht rechnen. Sie können zählen, ob etwas zwischen 1 und 7 Mal vorkommt. Aber sie können nicht ermitteln, ob zwei Dinge insgesamt 7 Mal vorkommen. Um das Problem besser zu verstehen, wollen wir ein einfaches, aber analoges Beispiel betrachten. Stellen Sie sich vor, Sie wollen etwas in der Form aaaaxbbb finden. Der String muss zwischen 1 und 8 Zeichen lang sein und aus 0 bis 7 a, genau einem x und 0 bis 7 b bestehen. Es gibt zwei Möglichkeiten, dieses mit einem regulären Ausdruck anzugehen. Die eine ist, alle Alternativen anzugeben. Der nächste Abschnitt, in dem die komprimierte gemischte Notation behandelt wird, nutzt diesen Weg. Das kann zu einer sehr langen Regex führen, aber sie ist leicht zu verstehen. \A(?:a{7}x | a{6}xb? | a{5}xb{0,2} | a{4}xb{0,3} | a{3}xb{0,4} | a{2}xb{0,5} | axb{0,6} | xb{0,7} )\Z
Dieser reguläre Ausdruck hat für jede der möglichen Vorkommen von a eine Alternative. Jede gibt vor, wie viele b erlaubt sind – nachdem eine gewisse Menge a und das x gefunden wurden. Die andere Lösung ist die Verwendung eines Lookahead. Das ist die Methode, die wir für die Regex in Abschnitt „Lösung“ verwendet haben. Wenn Sie mit Lookaheads nicht vertraut sind, werfen Sie zunächst einen Blick in Rezept 2.16. Mit Lookaheads können wir den gleichen Text im Prinzip zweimal finden und dabei auf zwei Bedingungen überprüfen. \A (?=[abx]{1,8}\Z) a{0,7}xb{0,7} \Z
Regex-Optionen: Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Das ‹\A› am Anfang der Regex verankert sie mit dem Anfang des Texts. Dann kommt das positive Lookahead ins Spiel. Es prüft, ob eine Folge von 1 bis 8 Buchstaben ‹a›, ‹b› und/oder ‹x› gefunden werden kann und das Ende des Strings erreicht ist, wenn diese 1 bis 8 Buchstaben gefunden wurden. Das ‹\Z› innerhalb des Lookahead ist unbedingt notwendig. Um die Regex auf Strings zu beschränken, die acht Zeichen lang sind oder weniger, muss das Lookahead sicherstellen, dass danach keine weiteren Zeichen mehr kommen. In einem anderen Szenario verwenden Sie vielleicht statt ‹\A› und ‹\Z› eine andere Form der Begrenzung. Wenn Sie bei aaaaxbbb immer nur nach ganzen Wörtern suchen wollen, können Sie Wortgrenzen nutzen. Aber um die Länge der Regex zu begrenzen, müssen Sie auf jeden Fall eine Grenze angeben, und diese Grenze muss sowohl am Ende des Lookahead als auch am Ende des regulären Ausdrucks selbst stehen. Wenn nicht, wird der reguläre Ausdruck auch Teile von Strings finden, die zu viele Zeichen enthalten. Wurden die Anforderungen des Lookahead erfüllt, gibt er die Zeichen wieder frei, die er gefunden hat. Wenn die Regex dann also versucht, ‹a{0,7}› zu finden, tut sie das wieder am Anfang des Strings. Die Tatsache, dass das Lookahead den von ihm gefundenen Text nicht konsumiert, ist der Hauptunterschied zwischen einem Lookahead und einer nichteinfangenden Gruppe. So können wir zwei Muster auf das gleiche Stück Text anwenden. Obwohl ‹a{0,7}xb{0,7}› allein bis zu 15 Zeichen finden könnte, passt es in diesem Fall nur auf 8 Zeichen, weil das Lookahead schon sichergestellt hat, dass es nur 8 Zeichen gibt. ‹a{0,7}xb{0,7}› muss jetzt nur noch prüfen, ob die Buchstaben in der richtigen Reihenfolge vorkommen. In diesem regulären Ausdruck würde sogar ‹a*xb*› ausreichen und den gleichen Effekt haben wie ‹a{0,7}xb{0,7}›. Das zweite ‹\Z› am Ende der Regex ist ebenfalls notwendig. So, wie das Lookahead sicherstellen muss, dass es nicht zu viele Buchstaben gibt, muss der zweite Test nach dem Lookahead dafür sorgen, dass alle Buchstaben in der richtigen Reihenfolge vorkommen. Damit ist klar, dass wir axba nicht finden, auch wenn es das Lookahead zufriedenstellt, weil es zwischen 1 und 8 Zeichen lang ist.
7.17 IPv6-Adressen finden | 415
Komprimierte gemischte Notation Die gemischte Notation kann wie die Standardnotation komprimiert werden. Auch wenn die vier Bytes am Ende immer anzugeben sind, ist die Anzahl an hexadezimalen Wörtern davor wieder variabel. Wenn alle hexadezimalen Wörter den Wert null haben, sieht die IPv6-Adresse am Ende so aus wie eine IPv4-Adresse mit zwei führenden Doppelpunkten. Beim Erstellen einer Regex für die komprimierte gemischte Notation kommen die gleichen Aspekte zum Tragen wie bei der komprimierten Standardnotation. Alle Punkte aus dem vorigen Abschnitt gelten auch hier. Der Hauptunterschied zwischen der Regex für die komprimierte gemischte Notation und die Regex für die komprimierte Standardnotation liegt darin, dass die komprimierte gemischte Notation noch die IPv4-Adresse nach den sechs hexadezimalen Wörtern prüfen muss. Wir führen diesen Test am Ende der Regex durch. Dafür verwenden wir die Regex für exakte IPv4-Addressen aus Rezept 7.16, die wir auch im Rezept für die unkomprimierte gemischte Notation genutzt haben. Wir müssen den IPv4-Teil der Adresse am Ende der Regex prüfen und ihn auch innerhalb des Lookahead berücksichtigen, um sicherzustellen, dass wir nicht mehr als sechs Doppelpunkte oder sechs hexadezimale Wörter in der IPv6-Adresse haben. Da wir den exakten Test schon am Ende der Regex durchführen, reicht für das Lookahead die einfache IPv4-Prüfung aus. Es muss den IPv4-Teil nicht validieren, da das schon die HauptRegex durchführt. Aber es muss ihn finden, sodass der String-Ende-Anker am Ende des Lookahead seine Aufgabe erfüllen kann.
Standardnotation, gemischte oder komprimierte Notation Die letzte Gruppe regulärer Ausdrücke fasst alles zusammen. Damit wird eine IPv6Adresse in beliebiger Notation gefunden: Standard oder gemischt, komprimiert oder unkomprimiert. Diese regulären Ausdrücke werden durch eine Alternation aus der Regex für die komprimierte gemischte Notation und der für die komprimierte Standardnotation gebildet. Diese Regexes verwenden schon eine Alternation, um sowohl die komprimierte als auch die unkomprimierte Variante der IPv6-Notation zu finden. Das Ergebnis ist ein regulärer Ausdruck mit drei Hauptalternativen, wobei die erste Alternative selbst aus zwei Alternativen besteht. Die erste Alternative passt auf eine IPv6Adresse in gemischter Notation – komprimiert oder unkomprimiert. Die zweite Alternative passt auf eine IPv6-Adresse in Standardnotation. Die dritte Alternative kümmert sich um die komprimierte Standardnotation. Hier haben wir drei statt zwei Hauptalternativen, die jeweils selbst zwei Alternativen enthalten, weil es keinen Grund gibt, die Alternativen für die Standardnotation und die komprimierte Notation zusammenzuführen. Bei der gemischten Notation fassen wir die komprimierte und die unkomprimierte Alternative zusammen, um den IPv4-Teil nicht doppelt angeben zu müssen.
416 | Kapitel 7: URLs, Pfade und Internetadressen
Im Prinzip haben wir also diese Regex: ^(6Wörter|komprimierte6Wörter)ip4$
und diese Regex: ^(8Wörter|komprimierte8Wörter)$
zu dieser Regex kombiniert: ^((6Wörter|komprimierte6Wörter)ip4|8Wörter|komprimierte8Wörter)$
7.18 Einen Pfad unter Windows validieren Problem Sie wollen prüfen, ob ein String wie ein gültiger Pfad auf einen Ordner oder eine Datei unter Microsoft Windows aussieht.
Lösung Pfade mit Laufwerkbuchstaben \A [a-z]:\\ (?:[^\\/:*?"|\r\n]+\\)* [^\\/:*?"|\r\n]* \Z
Diskussion Pfade mit Laufwerkbuchstaben Es ist sehr einfach, einen vollständigen Pfad auf eine Datei oder einen Ordner auf einem Laufwerk zu finden, das einen Laufwerkbuchstaben besitzt. Das Laufwerk wird durch einen einzelnen Buchstaben festgelegt, auf den ein Doppelpunkt und ein Backslash folgen. Dazu nutzen wir die Regex ‹[a-z]:\\›. Der Backslash ist in regulären Ausdrücken ein Metazeichen, daher müssen wir ihn durch einen weiteren Backslash maskieren, um ihn literal zu finden. Ordner und Dateinamen können unter Windows alle Zeichen enthalten außer \/:*?"|. Zeilenumbrüche sind auch nicht zugelassen. Wir können problemlos eine Folge aller zulässigen Zeichen mit der negierten Zeichenklasse ‹[^\\/:*?"|\r\n]+› finden. Der Backslash ist auch in Zeichenklassen ein Metazeichen, daher maskieren wir ihn. ‹\r› und
418 | Kapitel 7: URLs, Pfade und Internetadressen
‹\n› sind die beiden Zeichen für den Zeilenumbruch. In Rezept 2.3 erfahren Sie mehr
über (negierte) Zeichenklassen. Der Plus-Quantor (Rezept 2.12) legt fest, dass wir eines oder mehrere solcher Zeichen haben wollen. Ordner werden durch Backslashs getrennt. Wir können eine Folge von null oder mehr Ordnern mithilfe von ‹(?:[^\\/:*?"|\r\n]+\\)*› finden. Dabei wird die Regex für den Ordnernamen und einen literalen Backslash in eine nicht-einfangende Gruppe gesteckt (Rezept 2.9), die durch den Stern null Mal oder häufiger gefunden werden kann (Rezept 2.12). Um den Dateinamen zu finden, nutzen wir ‹[^\\/:*?"|\r\n]*›. Der Stern sorgt dafür, dass der Dateiname optional ist und man Pfade mit einem Backslash zulassen kann. Wenn Sie nicht wollen, dass Pfade mit einem Backslash enden, ändern Sie das letzte ‹*› in der Regex in ein ‹+›.
Pfade mit Laufwerkbuchstaben und UNC-Pfade Dateien auf Netzwerklaufwerken, die nicht auf einem Laufwerkbuchstaben abgebildet sind, können durch UNC-Pfade (Universal Naming Convention) erreicht werden. UNCPfade haben die Form \\server\freigabe\ordner\datei. Wir können die Regex für die Pfade mit Laufwerkbuchstaben problemlos so erweitern, dass sie auch UNC-Pfade unterstützt. Dazu müssen wir nur den Teil ‹[a-z]:›, der den Laufwerkbuchstaben findet, durch etwas ersetzen, dass einen Laufwerkbuchstaben oder einen Servernamen findet. Diese Aufgabe erledigt ‹(?:[a-z]:|\\\\[a-z0-9_.$]+\\[a-z0-9_.$]+)›. Der vertikale Balken ist der Alternationsoperator (Rezept 2.8). Damit haben Sie die Wahl zwischen einem Laufwerkbuchstaben, der durch ‹[a-z]:› gefunden wird, und einem Server mit einem Freigabenamen, auf den ‹\\\\[a-z0-9_.$]+\\[a-z0-9_.$]+› passt. Der Alternationsoperator hat die niedrigste Wertigkeit aller Regex-Operatoren. Um die beiden Alternativen zu gruppieren, verwenden wir eine nicht-einfangende Gruppe. Wie in Rezept 2.9 erklärt, ist die Zeichenfolge ‹(?:› der Anfang einer nicht-einfangenden Gruppe. Das Fragezeichen hat hier nicht seine normale Bedeutung als Quantor. Der Rest des regulären Ausdrucks kann so bleiben. Der Freigabename in UNC-Pfaden wird durch den Teil der Regex gefunden, der auch die Ordnernamen findet.
Pfade mit Laufwerkbuchstaben, UNC-Pfade und relative Pfade Ein relativer Pfad ist einer, der mit einem Ordnernamen beginnt (eventuell mit dem speziellen Namen .., der den übergeordneten Ordner auswählt) oder nur aus einem Dateinamen besteht. Um relative Pfade zu unterstützen, ergänzen wir den „Laufwerks“Abschnitt der Regex um eine dritte Alternative. Diese passt auf den Anfang eines relativen Pfads, aber nicht auf einen Laufwerkbuchstaben oder einen Servernamen.
7.18 Einen Pfad unter Windows validieren | 419
‹\\?[^\\/:*?"|\r\n]+\\?› findet den Anfang des relativen Pfads. Der Pfad kann mit einem Backslash beginnen, er muss es aber nicht. ‹\\?› passt auf den Backslash, aber auch, wenn dieser nicht vorhanden ist. ‹[^\\/:*?"|\r\n]+› passt auf einen Ordner oder
einen Dateinamen. Wenn der relative Pfad nur aus einem Dateinamen besteht, gibt es keine Übereinstimmung mit dem abschließenden ‹\\?› – so wie es auch keine Übereinstimmung gibt mit den „Ordner“- und „Datei“-Elementen der Regex, die beide optional sind. Ist im relativen Pfad ein Ordner angegeben, passt das abschließende ‹\\?› auf den Backslash, der den ersten Ordner im relativen Pfad vom Rest des Pfads trennt. Der Ordner-Teil passt dann auf die restlichen Ordner im Pfad (sofern noch welche angegeben sind) und der Datei-Teil auf den Dateinamen. Der reguläre Ausdruck, mit dem relative Pfade gefunden werden, verwendet nicht länger unterschiedliche Elemente der Regex, um auch unterschiedliche Teile des Ausgangstexts zu finden. Der Teil „relativer Pfad“ findet nämlich einen Ordner oder Dateinamen, wenn der Pfad relativ ist. Sind im relativen Pfad ein oder mehrere Ordner angegeben, findet der Teil „relativer Pfad“ den ersten Ordner, und die Teile „Ordner“ und „Datei“ holen sich das, was übrig bleibt. Handelt es sich beim relativen Pfad nur um einen Dateinamen, wird er vom Teil „relativer Pfad“ gefunden, und für die Ordner- und Dateielemente bleibt nichts mehr übrig. Da wir nur daran interessiert sind, den Pfad zu validieren, macht das nichts aus. Die Kommentare in der Regex sollen lediglich dabei helfen, sie zu verstehen. Wenn wir die Teile des Pfads über einfangende Gruppen auslesen wollen, müssen wir mehr darauf achten, das Laufwerk, den/die Ordner und den Dateinamen getrennt zu finden. Das nächste Rezept geht auf dieses Problem ein.
Siehe auch Rezepte 2.3, 2.8, 2.9 und 2.12.
7.19 Pfade unter Windows in ihre Bestandteile aufteilen Problem Sie wollen prüfen, ob ein String wie ein gültiger Pfad auf einen Ordner oder eine Datei unter Microsoft Windows aussieht. Handelt es sich bei dem String um einen gültigen Windows-Pfad, wollen Sie auch das Laufwerk, den/die Ordner und den Dateinamen extrahieren.
420 | Kapitel 7: URLs, Pfade und Internetadressen
Lösung Pfade mit Laufwerkbuchstaben \A (?[a-z]:)\\ (?(?:[^\\/:*?"|\r\n]+\\)*) (?[^\\/:*?"|\r\n]*) \Z
Pfade mit Laufwerkbuchstaben, UNC-Pfade und relative Pfade Diese regulären Ausdrücke können auch den leeren String finden. Im Diskussionsabschnitt ist das detaillierter beschrieben, und es gibt eine alternative Lösung. \A (?[a-z]:\\|\\\\[a-z0-9_.$]+\\[a-z0-9_.$]+\\|\\?) (?(?:[^\\/:*?"|\r\n]+\\)*) (?[^\\/:*?"|\r\n]*) \Z
Diskussion Die regulären Ausdrücke in diesem Rezept ähneln stark denen aus dem vorhergehenden. Diese Diskussion baut daher auf der aus dem vorigen Rezept auf.
Pfade mit Laufwerkbuchstaben Wir haben an den regulären Ausdrücken für Pfade mit Laufwerkbuchstaben nur eine Änderung vorgenommen – drei einfangende Gruppen, die Sie nutzen können, um die verschiedenen Elemente des Pfads auszulesen: ‹drive›, ‹folder› und ‹file›. Sie können diese Namen verwenden, wenn Ihre Regex-Variante benannte Captures unterstützt (Rezept 2.11). Wenn nicht, müssen Sie auf die einfangenden Gruppen über ihre Nummern zugreifen: 1, 2 und 3. In Rezept 3.9 erfahren Sie, wie Sie den von benannten oder nummerierten Gruppen gefundenen Text in Ihrer Programmiersprache auslesen können.
Pfade mit Laufwerkbuchstaben und UNC-Pfade Wir haben die gleichen drei einfangenden Gruppen auch den Regexes für UNC-Pfade hinzugefügt.
Pfade mit Laufwerkbuchstaben, UNC-Pfade und relative Pfade Wenn wir auch relative Pfade zulassen wollen, wird alles ein bisschen komplizierter. In obigem Rezept konnten wir einfach eine dritte Alternative für den Laufwerk-Teil ergänzen, um den Anfang des relativen Pfads zu erwischen. Hier ist das nicht möglich. Bei einem relativen Pfad sollte die einfangende Gruppe für das Laufwerk leer bleiben. Stattdessen wird nun der literale Backslash, der im Abschnitt „Pfade mit Laufwerkbuchstaben und UNC-Pfade“ hinter der einfangenden Gruppe steht, in die Gruppe hineingezogen. Dort steht er nun am Ende der Alternativen für den Laufwerkbuchstaben und die Netzwerkfreigabe. Wir ergänzen eine dritte Alternative mit einem optionalen Backslash für relative Pfade. Da die dritte Alternative optional ist, ist auch die gesamte Gruppe für das Laufwerk optional. Der so entstandene reguläre Ausdruck findet alle Windows-Pfade. Das Problem liegt nun darin, dass wir durch den optionalen Laufwerk-Teil eine Regex haben, in der alles optional ist. Die Teile für Ordner und Dateien waren schon vorher optional. Mit anderen Worten: Unser regulärer Ausdruck passt auch auf den leeren String. Wenn wir sicherstellen wollen, dass die Regex keine leeren Strings findet, müssen wir zusätzliche Alternativen ergänzen, die relative Pfade mit einem Ordner (wo der Dateiname optional ist) und ohne Ordner (wo der Dateiname notwendig ist) finden: \A (?: (?[a-z]:|\\\\[a-z0-9_.$]+\\[a-z0-9_.$]+)\\ (?(?:[^\\/:*?"|\r\n]+\\)*) (?[^\\/:*?"|\r\n]*)
7.19 Pfade unter Windows in ihre Bestandteile aufteilen | 423
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python Leider müssen wir einen Preis dafür zahlen, Strings der Länge null zu vermeiden. Jetzt haben wir sechs Gruppen, die die drei verschiedenen Teile des Pfads einfangen. Sie müssen selbst entscheiden, ob es in Ihrem Szenario einfacher ist, auf leere Strings zu prüfen oder sich mit mehreren einfangenden Gruppen herumzuschlagen. Wenn Sie die .NET-Regex-Variante nutzen, können Sie mehreren benannten Gruppen den gleichen Namen verpassen. Dies ist die einzige Variante, die Gruppen mit dem gleichen Namen so behandelt, als wäre es eine einzelne einfangende Gruppe. Mit dieser Regex in .NET können Sie einfach die Übereinstimmung für die Ordner- oder Datei-
424 | Kapitel 7: URLs, Pfade und Internetadressen
gruppe auslesen, ohne sich darum kümmern zu müssen, welche der beiden Ordnergruppen oder der drei Dateigruppen nun den Inhalt enthält: \A (?: (?[a-z]:|\\\\[a-z0-9_.$]+\\[a-z0-9_.$]+)\\ (?(?:[^\\/:*?"|\r\n]+\\)*) (?[^\\/:*?"|\r\n]*) | (?\\?(?:[^\\/:*?"|\r\n]+\\)+) (?[^\\/:*?"|\r\n]*) | (?[^\\/:*?"|\r\n]+) ) \Z
7.20 Den Laufwerkbuchstaben aus einem Pfad unter Windows extrahieren Problem Sie haben einen String mit einem (syntaktisch) gültigen Pfad auf eine Datei oder einen Ordner auf einem Windows-PC oder im Netzwerk. Sie wollen den Laufwerkbuchstaben aus dem Pfad auslesen, falls einer vorhanden ist. So wollen Sie zum Beispiel c aus c:\folder\file.ext extrahieren.
Diskussion Das Extrahieren des Laufwerkbuchstaben aus einem String, der einen gültigen Pfad enthält, ist trivial, selbst wenn Sie nicht wissen, ob der Pfad tatsächlich mit einem Laufwerkbuchstaben beginnt. Der Pfad kann auch einen relativen Pfad oder einen UNC-Pfad enthalten.
7.20 Den Laufwerkbuchstaben aus einem Pfad unter Windows extrahieren | 425
Doppelpunkte sind in Pfaden unter Windows nicht zugelassen – außer als Kennzeichen für den Laufwerkbuchstaben. Wenn wir also am Anfang des Strings einen Buchstaben gefolgt von einem String finden, wissen wir, dass es sich bei dem Buchstaben um den Laufwerkbuchstaben handelt. Der Anker ‹^› passt am Anfang des Strings (Rezept 2.5). Hier macht es auch nichts aus, dass der Zirkumflex in Ruby auch an Zeilenumbrüchen passt, da gültige Pfade keinen solchen Zeilenumbruch enthalten. Die Zeichenklasse ‹[a-z]› passt auf einen einzelnen Buchstaben (Rezept 2.3). Wir stecken die Zeichenklasse in ein paar Klammern (die eine einfangende Gruppe bilden), sodass Sie den Laufwerkbuchstaben ohne den literalen Doppelpunkt auslesen können, der ebenfalls durch den regulären Ausdruck gefunden wird. Der Doppelpunkt ist in der Regex enthalten, um sicherzustllen, dass wir wirklich einen Laufwerkbuchstaben vor uns haben und nicht den ersten Buchstaben in einem relativen Pfad.
Siehe auch In Rezept 2.9 werden einfangende Gruppen erklärt. In Rezept 3.9 erfahren Sie, wie Sie in Ihrer Programmiersprache Text aus einfangenden Gruppen auslesen können. Sollten Sie nicht sicher sein, ob Ihr String einen gültigen Windows-Pfad enthält, schauen Sie sich die Regexes aus Rezept 7.19 an.
7.21 Den Server und die Freigabe aus einem UNC-Pfad extrahieren Problem Sie haben einen String mit einem (syntaktisch) gültigen Pfad auf eine Datei oder einen Ordner auf einem Windows-PC oder im Netzwerk. Wenn es sich bei dem Pfad um einen UNC-Pfad handelt, wollen Sie den Namen des Netzwerkservers und den Freigabenamen auslesen. So wollen Sie zum Beispiel die Werte server und freigabe aus \\server\ freigabe\ordner\datei.ext ermitteln.
Diskussion Das Extrahieren des Netzwerkservers und des Freigabenamens aus einem String mit einem gültigen Pfad ist einfach, selbst wenn Sie nicht wissen, ob es sich bei dem Pfad um einen UNC-Pfad handelt. Es könnte sich auch um einen relativen Pfad oder um einen mit einem Laufwerkbuchstaben handeln. UNC-Pfade beginnen mit zwei Backslashs. In Windows-Pfaden sind zwei direkt aufeinanderfolgende Backslashs nur am Anfang eines UNC-Pfads erlaubt. Beginnt also ein als gültig bekannter Pfad mit zwei Backslashs, wissen wir, dass danach der Servername und der Freigabename folgen müssen. Der Anker ‹^› passt auf den Anfang des Strings (Rezept 2.5). Hier macht es auch nichts aus, dass der Zirkumflex in Ruby auch an Zeilenumbrüchen passt, da gültige Pfade keinen solchen Zeilenumbruch enthalten. ‹\\\\› passt auf zwei literale Backslashs. Da es sich bei einem Backslash in einem regulären Ausdruck um ein Metazeichen handelt, müssen wir ihn durch einen weiteren Backslash maskieren, um ihn als literales Zeichen nutzen zu können. Die erste Zeichenklasse ‹[a-z0-9_.$]+› passt auf den Namen des Netzwerkservers. Die zweite nach einem weiteren literalen Backslash passt auf den Freigabenamen. Wir haben beide Zeichenklassen in Klammern gesteckt, um zwei einfangende Gruppen zu erhalten. So können Sie den Servernamen aus der ersten und den Freigabenamen aus der zweiten einfangenden Gruppe auslesen. Das gesamte Suchergebnis enthält \\server\freigabe.
Siehe auch In Rezept 2.9 erfahren Sie alles über einfangende Gruppen. In Rezept 3.9 erfahren Sie, wie Sie in Ihrer Programmiersprache Text aus einfangenden Gruppen auslesen können. Sollten Sie nicht sicher sein, ob Ihr String einen gültigen Windows-Pfad enthält, schauen Sie sich die Regexes aus Rezept 7.19 an.
7.22 Die Ordnernamen aus einem Pfad unter Windows extrahieren Problem Sie haben einen String mit einem (syntaktisch) gültigen Pfad auf eine Datei oder einen Ordner auf einem Windows-PC oder im Netzwerk und wollen die Ordnernamen auslesen. So wollen Sie zum Beispiel den Wert \ordner\unterordner\ aus c:\ordner\unterordner\datei.ext oder \\server\freigabe\ordner\unterordner\datei.ext erhalten.
7.22 Die Ordnernamen aus einem Pfad unter Windows extrahieren | 427
Diskussion Das Auslesen der Ordnernamen aus einem Windows-Pfad ist ein bisschen schwieriger, wenn man auch UNC-Pfade unterstützen will, da wir nicht einfach den Teil des Pfads zwischen den Backslashs auslesen können. Denn dann würden wir auch den Server- und Freigabenamen des UNC-Pfads erhalten. Der erste Teil der Regex, ‹^([a-z]:|\\\\[a-z0-9_.$]+\\[a-z0-9_.$]+)?›, überspringt den Laufwerkbuchstaben oder den Netzwerkserver und den Freigabenamen am Anfang des Pfads. Dieser Teil der Regex besteht aus einer einfangenden Gruppe mit zwei Alternativen. Die erste Alternative passt auf den Laufwerkbuchstaben (siehe Rezept 7.20), die zweite auf den Server und die Freigabe in UNC-Pfaden (siehe Rezept 7.21). In Rezept 2.8 wird der Alternationsoperator näher erläutert. Das Fragezeichen nach der Gruppe sorgt dafür, dass sie optional wird. Damit können wir auch relative Pfade unterstützen, die keinen Laufwerkbuchstaben und keinen Freigabenamen enthalten. Die Ordner lassen sich dann einfach mit ‹(?:[^\\/:*?"|\r\n]+\\)+› finden. Die Zeichenklasse passt auf einen Ordnernamen. Die nicht-einfangende Gruppe passt auf einen Ordnernamen, gefolgt von einem literalen Backslash, der die Ordner untereinander und vom Dateinamen trennt. Wir wiederholen diese Gruppe ein Mal oder häufiger. Das bedeutet, dass unser regulärer Ausdruck nur solche Pfade findet, die auch tatsächlich einen Ordner enthalten. Pfade, die nur einen Dateinamen, ein Laufwerk oder eine Netzwerkfreigabe enthalten, werden nicht gefunden. Wenn der Pfad mit einem Laufwerkbuchstaben oder einer Netzwerkfreigabe beginnt, muss darauf ein Backslash folgen. Ein relativer Pfad kann mit einem Backslash beginnen, er muss es aber nicht. Daher müssen wir einen optionalen Backslash am Anfang der Gruppe einfügen, die die Ordner im Pfad findet. Wir verwenden unsere Regex nur auf gültigen Pfaden, daher müssen wir in Bezug auf Backslashs bei Laufwerkbuchstaben oder Netzwerkfreigaben nicht so streng sein. Wir müssen sie nur zulassen. Da die Regex mindestens einen Ordner finden soll, müssen wir sicherstellen, dass sie nicht den Wert e\ als Ordner in \\server\freigabe\ findet. Das ist der Grund dafür, dass wir ‹(\\|^)› statt ‹\\?› verwenden, um den optionalen Backslash am Anfang der einfangenden Gruppe nutzen.
428 | Kapitel 7: URLs, Pfade und Internetadressen
Wenn Sie sich fragen, warum \\server\freigabe als Laufwerk und e\ als Ordner gefunden werden könnten, werfen Sie einen Blick in Rezept 2.13. Regex-Engines nutzen Backtracking. Schauen Sie sich diese Regex an: ^([a-z]:|\\\\[a-z0-9_.$]+\\[a-z0-9_.$]+)? ((?:\\?(?:[^\\/:*?"|\r\n]+\\)+)
Bei der Regex aus dieser Lösung muss es mindestens ein (Nicht-Backslash-)Zeichen und einen Backslash für den Pfad geben. Wenn die Regex für das Laufwerk in \\server\ freigabe die Übereinstimmung \\server\freigabe gefunden hat und dann mit der Ordner-Gruppe nichts mehr findet, gibt sie nicht einfach auf, sondern sie versucht sich an verschiedenen Permutationen der Regex. In diesem Fall hat sich die Engine gemerkt, dass die Zeichenklasse ‹[a-z0-9_.$]+›, die die Netzwerkfreigabe findet, nicht unbedingt alle passenden Zeichen konsumieren muss. Um das ‹+› zu bedienen, reicht ein Zeichen aus. Die Engine geht also per Backtracking zurück und nötigt die Zeichenklasse, wieder ein Zeichen herzugeben. Damit versucht sie einen neuen Anlauf. Jetzt gibt es zwei verbleibende Zeichen im Ausgangstext, mit denen der Ordner gefunden werden kann: e\. Diese beiden Zeichen reichen aus, um mit ‹(?:[^\\/:*?"|\r\n]+\\)+› etwas zu finden. Damit haben wir ein erfolgreiches Gesamtergebnis, das wir aber eigentlich gar nicht wollen. Durch ‹(\\|^)› statt ‹\\?› wird dieses Problem behoben. So kann am Anfang immer noch ein optionaler Backslash stehen. Fehlt dieser aber, muss der Ordner am Anfang des Strings stehen. Falls also ein Laufwerk gefunden wurde und die Regex-Engine schon über den Anfang des Strings hinaus ist, muss es einen Backslash geben. Können keine Ordner gefunden werden, wird die Regex-Engine immer noch versuchen, per Backtracking mehr Erfolg zu haben, dieses Mal aber ohne Erfolg, weil ‹(\\|^)› nicht passt. Das Backtracking wird so lange durchgeführt, bis der Anfang des Strings erreicht ist. Die einfangende Gruppe für den Laufwerkbuchstaben und die Netzwerkfreigabe ist optional, daher wird die Engine versuchen, den Ordner am Anfang des Strings zu finden. ‹(\\|^)› wird hier zwar passen, aber nicht der Rest der Regex, weil ‹(?:[^\\/:*?"|\r\n]+\\)+› den Doppelpunkt nach dem Laufwerkbuchstaben oder den doppelten Backslash der Netzwerkfreigabe nicht zulässt. Vielleicht fragen Sie sich, warum wir diese Technik nicht in den Rezepten 7.18 und 7.19 verwendet haben. Das liegt daran, dass diese regulären Ausdrücke keinen Ordner benötigen. Da alles nach dem Teil, der das Laufwerk findet, optional ist, führt die Regex-Engine kein Backtracking durch. Natürlich kann dieses Vorgehen zu anderen Problemen führen, die auch in Rezept 7.19 besprochen werden. Findet dieser reguläre Ausdruck eine Übereinstimmung, enthält die erste einfangende Gruppe den Laufwerkbuchstaben oder die Netzwerkfreigabe und die zweite Gruppe die Ordner. Bei einem relativen Pfad wird die erste einfangende Gruppe leer sein. Die zweite Gruppe wird immer mindestens einen Ordner enthalten. Wenn Sie diese Regex auf einen Pfad anwenden, in dem kein Ordner angegeben ist, findet die Regex gar keine Übereinstimmung.
7.22 Die Ordnernamen aus einem Pfad unter Windows extrahieren | 429
Siehe auch In Rezept 2.9 erfahren Sie alles über einfangende Gruppen. In Rezept 3.9 erfahren Sie, wie Sie in Ihrer Programmiersprache Text aus einfangenden Gruppen auslesen können. Sollten Sie nicht sicher sein, ob Ihr String einen gültigen Windows-Pfad enthält, schauen Sie sich die Regexes aus Rezept 7.19 an.
7.23 Den Dateinamen aus einem Pfad unter Windows extrahieren Problem Sie haben einen String mit einem (syntaktisch) gültigen Pfad auf eine Datei oder einen Ordner auf einem Windows-PC oder im Netzwerk und wollen den Dateinamen auslesen (wenn einer angegeben ist). So wollen Sie zum Beispiel den Wert datei.ext erhalten, wenn Sie den Text c:\ordner\datei.ext als Ausgangstext haben.
Diskussion Das Extrahieren des Dateinamens aus einem String mit einem gültigen Pfad ist einfach, selbst wenn Sie nicht wissen, ob der Pfad wirklich mit einem Dateinamen endet. Der Dateiname steht immer am Ende des Strings. Er darf keine Doppelpunkte oder Backslashs enthalten, daher kann man nicht mit Ordnern, Laufwerkbuchstaben oder Netzwerkfreigaben durcheinanderkommen. Der Anker ‹$› passt auf das Ende des Strings (Rezept 2.5). Hier macht es nichts aus, dass das Dollarzeichen in Ruby auch an Zeilenumbrüchen passt, da gültige Pfade keinen solchen Zeilenumbruch enthalten. Die negierte Zeichenklasse ‹[^\\/:*?"|\r\n]+› (Rezept 2.3) passt auf die Zeichen, die in Dateinamen vorkommen können. Obwohl die Regex-Engine den String von links nach rechts durcharbeitet, stellt der Anker am Ende der Regex sicher, dass nur das letzte Vorkommen von Dateinamen-Zeichen im String gefunden werden, womit wir unseren Dateinamen erhalten. Endet der String mit einem Backslash, wie zum Beispiel bei Pfaden, die keinen Dateinamen enthalten, findet die Regex keine Übereinstimmung. Wenn es eine gibt, wird nur
430 | Kapitel 7: URLs, Pfade und Internetadressen
der Dateiname gefunden, daher brauchen wir keine einfangenden Gruppen, mit denen der Dateiname vom restlichen Pfad getrennt ausgelesen werden kann.
Siehe auch In Rezept 3.7 erfahren Sie, wie Sie in Ihrer Programmiersprache Text aus einfangenden Gruppen auslesen können. Sollten Sie nicht sicher sein, ob Ihr String einen gültigen Windows-Pfad enthält, schauen Sie sich die Regexes aus Rezept 7.19 an.
7.24 Die Dateierweiterung aus einem Pfad unter Windows extrahieren Problem Sie haben einen String mit einem (syntaktisch) gültigen Pfad auf eine Datei oder einen Ordner auf einem Windows-PC oder im Netzwerk und wollen die Dateierweiterung auslesen (wenn eine angegeben ist). So wollen Sie zum Beispiel den Wert .ext aus dem Ausgangstext c:\folder\file.ext ziehen.
Diskussion Um die Dateierweiterung zu erhalten, können wir die gleichen Techniken anwenden wie zum Extrahieren des gesamten Dateinamens in Rezept 7.23. Der einzige Unterschied besteht im Umgang mit den Punkten. Die Regex in Rezept 7.23 enthält keinerlei Punkte. Die negierte Zeichenklasse in dieser Regex passt einfach auf alle Punkte, die im Dateinamen vorkommen. Eine Dateierweiterung muss mit einem Punkt beginnen. Daher fügen wir der Regex ‹\.› hinzu, um am Anfang der Regex einen literalen Punkt zu finden. Dateinamen wie Version 2.0.txt können mehr als einen Punkt enthalten. Der letzte Punkt ist derjenige, der die Erweiterung vom Dateinamen trennt. Die Erweiterung selbst darf keine Punkte enthalten. Das setzen wir in der Regex um, indem wir einen Punkt in die Zeichenklasse einfügen. Dort ist er ein normales literales Zeichen, daher müssen wir ihn nicht maskieren. Der Anker ‹$› am Ende der Regex stellt sicher, dass wir .txt und nicht .0 finden.
7.24 Die Dateierweiterung aus einem Pfad unter Windows extrahieren | 431
Wenn der String mit einem Backslash endet oder mit einem Dateinamen, der keine Punkte enthält, bleibt die Suche erfolglos. Gibt es eine Übereinstimmung, ist dies auch gleich die Erweiterung zusammen mit dem Punkt, der den Dateinamen und die Erweiterung voneinander trennt.
Siehe auch Sollten Sie nicht sicher sein, ob Ihr String einen gültigen Windows-Pfad enthält, schauen Sie sich die Regexes aus Rezept 7.19 an.
7.25 Ungültige Zeichen aus Dateinamen entfernen Problem Sie wollen Zeichen aus einem String entfernen, die in Dateinamen unter Windows nicht gültig sind. Das können Sie zum Beispiel nutzen, um aus einem String mit dem Titel eines Dokuments einen Standarddateinamen abzuleiten, wenn der Anwender das erste Mal das Dokument speichern möchte.
Lösung Regulärer Ausdruck [\\/:"*?|]+
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Ersetzung Lassen Sie den Ersetzungstext leer. Ersetzungstextvarianten: .NET, Java, JavaScript, PHP, Perl, Python, Ruby
Diskussion Die Zeichen \/:"*?| sind in Dateinamen unter Windows nicht gültig. Sie werden genutzt, um Laufwerke und Ordner zu trennen, um Pfade mit Leerzeichen nutzen zu können oder um Jokerzeichen und Umleitungen an der Befehlszeile zu ermöglichen. Wir können diese Zeichen leicht mit der Zeichenklasse ‹[\\/:"*?|]› finden. Der Backslash ist auch innerhalb von Zeichenklassen ein Metazeichen, deshalb müssen wir ihn durch einen weiteren Backslash maskieren. Alle anderen Zeichen sind innerhalb von Zeichenklassen immer literale Zeichen.
432 | Kapitel 7: URLs, Pfade und Internetadressen
Wir wiederholen die Zeichenklasse aus Effizienzgründen durch ein ‹+›. Enthält der String mehrere direkt aufeinanderfolgende ungültige Zeichen, wird die gesamte Folge auf einmal gelöscht und nicht Zeichen für Zeichen. Sie werden nicht wirklich einen Performanceunterschied bemerken, wenn Sie mit sehr kurzen Strings (wie zum Beispiel Dateinamen) arbeiten, aber man sollte diese Technik im Hinterkopf haben, wenn man mit größeren Datenmengen arbeitet, die auch eher längere Folgen von zu löschenden Zeichen enthalten. Da wir die ungültigen Zeichen einfach nur löschen wollen, führen wir ein Suchen und Ersetzen mit einem leeren String als Ersetzungstext durch.
Siehe auch In Rezept 3.14 wird erklärt, wie Sie in Ihrer Programmiersprache suchen und durch einen feststehenden Text ersetzen können.
7.25 Ungültige Zeichen aus Dateinamen entfernen | 433
KAPITEL 8
Markup und Datenaustausch
Dieses letzte Kapitel dreht sich um Aufgaben, die häufig bei der Arbeit mit Auszeichnungssprachen und -formaten auftreten: HTML, XHTML, XML, CSV und INI. Auch wenn wir davon ausgehen, dass Sie mit diesen Technologien zumindest ein bisschen vertraut sind, finden Sie direkt am Anfang des Kapitels eine kurze Beschreibung. So können wir sichergehen, dass wir uns auf Augenhöhe unterhalten. Die Beschreibungen konzentrieren sich auf die grundlegenden Syntaxregeln, die notwendig sind, um die Datenstrukturen jedes Formats korrekt durchsuchen zu können. Weitere Details werden dann vorgestellt, wenn sie für die einzelnen Themen notwendig sind. Auch wenn es nicht immer so scheint: Manche dieser Formate können erstaunlich komplex sein, wenn es um die Verarbeitung geht, zumindest in Bezug auf reguläre Ausdrücke. Im Allgemeinen ist es am besten, statt regulärer Ausdrücke dedizierte Parser und APIs zu verwenden, um viele der Aufgaben in diesem Kapitel zu erledigen, insbesondere, wenn Genauigkeit eine große Rolle spielt (zum Beispiel weil durch die Verarbeitung Sicherheitsaspekte betroffen sein könnten). Trotzdem finden Sie in diesen Rezepten nützliche Techniken, die in vielen, auch kleineren Aufgaben, eine große Hilfe sein können. Lassen Sie uns also anschauen, mit was wir es hier zu tun haben. Viele der Probleme, denen wir uns in diesem Kapitel gegenübersehen, entstehen durch die folgenden Regeln, die manchmal recht überraschend angegangen werden können. Hypertext Markup Language (HTML) HTML wird verwendet, um die Struktur, Semantik und Erscheinung von Milliarden Webseiten und anderen Dokumenten zu beschreiben. HTML wird häufig mit regulären Ausdrücken verarbeitet, aber Sie sollten sich darüber im Klaren sein, dass die Sprache nicht so richtig gut auf die Strenge und Präzision reguläre Ausdrücke ausgelegt ist. Das gilt insbesondere für das misshandelte HTML, das auf vielen Webseiten im Einsatz ist und das der außerordentlichen Toleranz der Webbrowser für schlechtes HTML geschuldet ist. In diesem Kapitel werden wir uns auf die Regeln konzentrieren, die zum Verarbeiten wohlgeformten HTML-Codes notwendig sind: Elemente (und ihre Attribute), Zeichenreferenzen, Kommentare und Document-Type-Deklarationen. Dieses Buch behandelt HTML 4.01, das im Jahr 1999 abgeschlossen wurde und damit der letzte „offizielle“ Standard ist. | 435
Die grundlegenden HTML-Bestandteile sind die Elemente. Elemente werden mithilfe von Tags geschrieben, die durch spitze Klammern umschlossen sind. Dabei gibt es die Elemente entweder als Block- (zum Beispiel Absätze, Überschriften, Listen, Tabellen und Formulare) oder als Inline-Elemente (zum Beispiel Hyperlinks, Zitate, Kursivschrift und Eingabefelder auf einem Formular). Elemente haben normalerweise ein Start-Tag (zum Beispiel ) und ein End-Tag (). Das Start-Tag eines Elements kann Attribute enthalten, die später noch beschrieben werden. Zwischen den Tags befindet sich der Inhalt des Elements, der aus Text und weiteren Elementen bestehen kann, eventuell aber auch leer bleibt. Elemente können verschachtelt werden, dürfen sich aber nicht überlappen (so ist zum Beispiel in Ordnung, <span> aber nicht). Bei manchen Elementen (wie zum Beispiel
, durch das ein Absatz ausgezeichnet wird), ist das End-Tag optional. Elemente mit einem optionalen End-Tag werden automatisch durch neue Blockelemente geschlossen. Ein paar Elemente (unter anderem , durch das eine Zeile beendet wird) können keinen Inhalt haben und verwenden niemals ein End-Tag. Trotzdem kann ein leeres Element immer noch Attribute besitzen. Die Namen von HTML-Elementen beginnen mit einem Buchstaben von A bis Z. Alle gültigen Elemente nutzen für ihren Namen nur Buchstaben und Ziffern, Groß- und Kleinschreibung ist unwichtig. <script>- und <style>-Elemente haben besondere Eigenschaften. In ihnen kann
Code von Skriptsprachen und Stylesheets in Ihrem Dokument eingebettet werden. Diese Elemente enden nach dem ersten Auftauchen von oder , auch wenn dies in einem Kommentar oder einem String innerhalb des Stylesheets beziehungsweise der Skriptsprache steht. Attribute finden sich innerhalb des Start-Tags nach dem Elementnamen. Sie werden durch einen oder mehrere Whitespace-Zeichen unterteilt. Die meisten Attribute werden als Name/Wert-Paare aufgeschrieben. So zeigt das folgende Beispiel ein (Anker)-Element mit zwei Attributen und dem Inhalt „Klick mich!“: Klick mich!
Wie man sieht, werden Name und Wert eines Attributs durch ein Gleichheitszeichen und optionalen Whitespace getrennt. Der Wert wird in einfachen oder doppelten Anführungszeichen geschrieben. Um das entsprechende Anführungszeichen auch innerhalb des Werts zu verwenden, müssen Sie eine Zeichenreferenz nutzen (die gleich beschrieben wird). Besteht der Wert nur aus den Zeichen A–Z, a–z, 0–9 sowie Unterstrich, Punkt, Doppelpunkt und Bindestrich (als Regex wäre das ‹^[-.0-9:A-Z_az]+$›), sind die Anführungszeichen optional. Ein paar Attribute (wie zum Beispiel selected und checked bei bestimmten Formular-Elementen) beeinflussen das Element, in dem sie stehen, einfach durch ihre Anwesenheit und erfordern keinen Wert. In diesen Fällen wird auch das Gleichheitszeichen weggelassen, das Name und Wert eines Attributs verbindet. Alternativ können diese „minimierten“ Attribute ihren eigenen Namen als Wert verwenden (zum Beispiel selected="selected"). Attributnamen beginnen mit einem Buchstaben im Bereich A bis Z. Alle gültigen Attribute nutzen nur 436 | Kapitel 8: Markup und Datenaustausch
Buchstaben und Bindestriche in ihrem Namen. Sie können in beliebiger Reihenfolge auftreten, und Groß- und Kleinschreibung ist beim Namen unwichtig. HTML 4 definiert 252 benannte Zeichenreferenzen und über eine Million numerische Zeichenreferenzen (zusammen bezeichnen wir diese als Zeichenreferenzen). Numerische Zeichenreferenzen beziehen sich über Unicode-Codepoints auf ein Zeichen und verwenden das Format nnnn; oder hhhh;, wobei nnnn eine oder mehrere Dezimalziffern von 0 bis 9 und hhhh eine oder mehrere hexadezimale Ziffern von 0 bis 9 und A bis F (oder a bis f) sind. Benannte Zeichenreferenzen werden als &entityname; geschrieben (anders als an den meisten anderen Stellen in HTML ist hier Groß- und Kleinschreibung durchaus relevant) und sind außerordentlich hilfreich, wenn man literale Zeichen eingeben möchte, die in einem bestimmten Kontext besondere Auswirkungen haben können. Dazu gehören zum Beispiel spitze Klammern (< und >), doppelte Anführungszeichen (") sowie das Kaufmanns-Und (&). Sehr gebräuchlich ist auch das Zeichen (No-Break Space an der Position 0xA0), denn alle Vorkommen dieses Zeichens werden gerendert. Leerzeichen, Tabs und Zeilenumbrüche werden normalerweise als einzelnes Leerzeichen gerendet, auch wenn sie mehrfach aufeinanderfolgen. Das Kaufmanns-Und-Zeichen (&) kann nicht außerhalb von Zeichenreferenzen verwendet werden. HTML-Kommentare haben die folgende Syntax:
Text innerhalb von Kommentaren hat keine spezielle Bedeutung mehr und wird von den meisten Clients verborgen. Zwischen dem schließenden -- und > darf sich Whitespace befinden. Aus Kompatibilitätsgründen zu antiken Browsern (von vor 1995) stecken manche Entwickler den Inhalt von <script>- und <style>-Elementen in einen HTML-Kommentar. Moderne Browser ignorieren diese Kommentare und verarbeiten den Skript- oder Style-Inhalt ganz normal. Schließlich beginnen HTML-Dokumente häufig mit einer Document Type Declaration (gern auch „DOCTYPE“ genannt), die eine vom Rechner lesbare Spezifikation der zugelassenen und verbotenen Inhalte für das Dokument definiert. Der DOCTYPE sieht ein bisschen wie ein HTML-Element aus. Die folgende Zeilen zeigen das für ein Dokument, das der HTML 4.01 Strict-Definition entsprechen soll:
Das ist kurz zusammengefasst die Struktur eines HTML-Dokuments. Denken Sie daran, dass echter HTML-Code häufig von diesen Regeln abweicht, weil die meisten Browser da sehr großzügig sind. Abgesehen von diesen grundlegenden Regeln besitzt jedes Element Einschränkungen bezüglich möglicher Inhalte und Attribute, die eigentlich berücksichtigt werden müssen, damit ein HTML-Dokument gültig ist. Die Beschreibung dieser inhaltlichen Regeln würde aber den Rahmen dieses Buchs
Markup und Datenaustausch | 437
sprengen. HTML & XHTML – Das umfassende Handbuch von Chuck Musciano und Bill Kennedy (O’Reilly) ist aber eine gute Quelle, wenn Sie mehr Informationen brauchen. Da die Struktur von HTML der von XHTML und XML sehr ähnelt (beide werden noch beschrieben), sind viele reguläre Ausdrücke in diesem Kapitel so geschrieben, dass sie alle drei Markup-Sprachen unterstützen.
Extensible Hypertext Markup Language (XHTML) XHTML wurde als Nachfolger von HTML 4.01 entworfen und überführt HTML, das eine SGML-Herkunft hat, auf eine XML-Basis. Allerdings wird auch HTML weiterentwickelt, daher sollte man XHTML eher als Alternative zu HTML bezeichnen. Dieses Buch behandelt XHTML 1.0 und 1.1. Auch wenn diese Versionen des Standards größtenteils abwärtskompatibel zu HTML sind, gibt es ein paar wichtige Unterschiede zur oben beschriebenen HTML-Struktur: • XHTML-Dokumente können mit einer XML-Deklaration wie beginnen. • Nicht leere Elemente benötigen ein schließendes Tag. Leere Elemente müssen entweder ein schließendes Tag nutzen oder mit /> enden. • Element- und Attributnamen reagieren auf Groß- und Kleinschreibung und verwenden Kleinbuchstaben. • Aufgrund der Berücksichtigung von XML-Namensraum-Präfixen können die Namen von Elementen und Attributen auch einen Doppelpunkt enthalten. • Attributwerte ohne einfache oder doppelte Anführungszeichen sind nicht zulässig. • Attribute müssen einen passenden Wert haben. Es gibt noch eine Reihe weiterer Unterschiede zwischen HTML und XHTML, die aber größtenteils nur Grenzfälle und die Fehlerbehandlung betreffen und normalerweise keinen Einfluss auf die Regexes in diesem Kapitel haben. Wenn Sie mehr über die Unterschiede zwischen HTML und XHTML erfahren möchten, werfen Sie einmal einen Blick auf http://www.w3.org/TR/xhtml1/#diffs und http://wiki.whatwg.org/ wiki/HTML_vs._XHTML. Da die Syntax von XHTML und HTML sehr ähnlich ist und auf XML basiert, sind viele reguläre Ausdrücke in diesem Kapitel so geschrieben, dass sie alle drei Markup-Sprachen unterstützen. Rezepte, die sich auf „(X)HTML“ beziehen, behandeln HTML und XHTML gleich. Normalerweise können Sie sich auch nicht darauf verlassen, dass ein Dokument nur HTML- oder nur XHTML-Konventionen folgt, da Kombinationen aus beidem häufig vorkommen und sich die Browser sowieso nicht darum scheren.
438 | Kapitel 8: Markup und Datenaustausch
Extensible Markup Language (XML) XML ist eine Sprache, die vor allem dafür gedacht ist, strukturierte Daten auszutauschen. Sie dient als Grundlage für eine ganze Reihe von Markup-Sprachen – unter anderem auch das eben erläuterte XHTML. Dieses Buch behandelt die XML-Versionen 1.0 und 1.1. Eine vollständige Beschreibung der XML-Features und der Grammatik geht über den Rahmen dieses Buchs hinaus, aber was unsere Themen angeht, gibt es auch nur ein paar wenige Unterschiede zur schon beschriebenen HTMLStruktur: • XML-Dokumente können mit einer XML-Deklaration wie beginnen und andere, ähnlich formatierte Verarbeitungsanweisungen enthalten. So legt zum Beispiel fest, dass die XSL-Transformationsdatei transform.xslt auf das Dokument angewendet werden soll. • Der DOCTYPE kann innerhalb von eckigen Klammern auch interne MarkupDeklarationen enthalten, zum Beispiel: ]>
• CDATA-Abschnitte werden genutzt, um Textblöcke zu maskieren. Sie beginnen mit dem String . • Nicht leere Elemente benötigen ein schließendes Tag. Leere Elemente müssen entweder ein schließendes Tag nutzen oder mit /> enden. • XML-Namen (die in den Regeln für Elemente, Attribute und Zeichen-Referenznamen vorkommen) reagieren auf Groß- und Kleinschreibung und können eine ganze Reihe von Unicode-Zeichen nutzen: A bis Z, a bis z, ein Doppelpunkt (:) und der Unterstrich (_) sind erlaubt, nach dem ersten Zeichen auch 0 bis 9, der Bindestrich (-) und der Punkt (.). In Rezept 8.4 finden Sie weitere Details. • Attributwerte ohne einfache oder doppelte Anführungszeichen sind nicht zulässig. • Attribute müssen einen passenden Wert haben. Es gibt eine Reihe weiterer Regeln, an die man sich halten muss, wenn man wohlgeformte XML-Dokumente erstellen oder einfach nur seinen eigenen standardkonformen XML-Parser schreiben will. Aber die hier beschriebenen Regeln (die die schon für HTML-Dokumente erläuterte Struktur ergänzen) sind für einfache RegexSuchen allgemein genug gehalten. Da die Struktur von XML der von HTML stark ähnelt und sie auch die Grundlage für XHTML ist, sind viele reguläre Ausdrücke in diesem Kapitel so geschrieben, dass sie alle drei Markup-Sprachen behandeln. Rezepte, die sich auf ein Markup im „XML-Stil“ beziehen, können XML, XHTML und HTML gleichwertig verarbeiten.
Markup und Datenaustausch | 439
Kommaseparierte Daten (CSV) CSV (Comma-Separated Values) ist ein altes, aber immer noch sehr verbreitetes Dateiformat für tabellenkalkulationsartige Daten. Das CSV-Format wird von den meisten Tabellenkalkulationen und Datenbanksystemen verstanden und ist insbesondere beim Datenaustausch zwischen Anwendungen sehr beliebt. Auch wenn es keine offizielle CSV-Spezifikation gibt, wurde im Oktober 2005 mit dem RFC 4180 der Versuch einer allgemeinen Definition veröffentlicht und bei der IANA als MIME-Type „text/csv“ registriert. Vor der Veröffentlichung des RFC waren die CSV-Konventionen von Microsoft Excel der De-facto-Standard. Da die Regeln im RFC denen von Excel stark ähneln, ist das kein großes Problem. Dieses Kapitel behandelt die vom RFC 4180 spezifizierten CSV-Formate, die auch von Microsoft Excel 2003 und neuer genutzt werden. Wie der Name schon andeutet, enthalten CSV-Dateien eine Liste mit Werten oder Feldern, die durch Kommata getrennt sind. Jeder Datensatz steht in seiner eigenen Zeile. Hinter dem letzten Feld in einem Datensatz steht kein Komma mehr. Auf den letzten Datensatz in einer Datei kann noch ein Zeilenumbruch folgen, er muss es aber nicht. In der gesamten Datei sollte jeder Datensatz die gleiche Anzahl an Feldern besitzen. Der Wert jedes CSV-Felds kann für sich stehen oder durch doppelte Anführungszeichen eingeschlossen sein. Felder können auch komplett leer sein. Ein Feld, das Kommata, doppelte Anführungszeichen oder Zeilenumbrüche enthält, muss in doppelte Anführungszeichen gesetzt sein. Ein doppeltes Anführungszeichen innerhalb eines Felds wird durch ein weiteres doppeltes Anführungszeichen maskiert. Der erste Datensatz in einer CSV-Datei wird manchmal als Kopfzeile mit den Namen für jede einzelne Spalte verwendet. Das lässt sich aus dem Inhalt einer CSVDatei nicht per Programm ermitteln, daher bitten manche Anwendungen den Benutzer, selbst zu entscheiden, wie die erste Zeile behandelt werden soll. In RFC 4180 ist festgelegt, dass führende und abschließende Leerzeichen in einem Feld Teil des Werts sind. Manche älteren Excel-Versionen ignorieren diese Leerzeichen, aber Excel 2003 und die neueren Versionen halten sich hier an den RFC. Es gibt keine Vorgabe, wie Fehler, zum Beispiel vergessene doppelte Anführungszeichen, zu behandeln sind. In grenzwertigen Fällen kann das Fehler-Handling von Excel durchaus ungewöhnlich sein, daher ist es wichtig, darauf zu achten, dass doppelte Anführungszeichen maskiert sind, Felder mit solchen Zeichen auch selbst in Anführungszeichen gesetzt werden und solche Felder außerhalb der Anführungszeichen keine führenden oder abschließenden Leerzeichen enthalten. Das folgende CSV-Beispiel enthält viele der gerade besprochenen Regeln. Es enthält zwei Datensätze mit jeweils drei Feldern: aaa,b b,"""c"" cc" 1,,"333, drei, immer noch drei"
440 | Kapitel 8: Markup und Datenaustausch
In Tabelle 8-1 wird gezeigt, wie der Inhalt dieser CSV-Daten in einer Tabelle aussieht. Tabelle 8-1: Ausgabe für das CSV-Beispiel aaa
b b
"c" cc
1
(leer)
333, drei, immer noch drei
Wir haben zwar die CSV-Regeln beschrieben, die im Allgemeinen berücksichtigt werden, aber es gibt durchaus Unterschiede beim Lesen und Schreiben von CSVDateien. Viele Anwendungen lassen in Dateien mit der Endung „csv“ beliebige Trennzeichen zu, nicht nur Kommata. Bei anderen Varianten werden Kommata (oder andere Fehldtrenner), doppelte Anführungszeichen und Zeilenumbrüche in Feldern anders maskiert oder führende und abschließende Leerzeichen ignoriert. Initialisierungsdateien (INI) Das schlanke INI-Dateiformat wird häufig für Konfigurationsdateien genutzt. Es ist nicht wirklich gut definiert, daher gibt es bei der Interpretation dieses Formats durchaus Unterschiede in den Programmen und Systemen. Die Regexes in diesem Kapitel orientieren sich an den gebräuchlichsten Konventionen, die wir hier beschreiben. INI-Datei-Parameter sind Name/Wert-Paare, die durch ein Gleichheitszeichen und optionale Leerzeichen oder Tabs getrennt sind. Werte können in einfachen oder doppelten Anführungszeichen stehen, wodurch sie führende und abschließende Leerzeichen und andere Sonderzeichen enthalten können. Parameter können in Sektionen untergebracht werden. Diese beginnen mit dem Namen der Sektion, der in einer eigenen Zeile in eckigen Klammern steht. Sektionen enden erst mit der nächsten Sektion oder dem Dateiende. Verschachtelungen sind nicht möglich. Ein Semikolon steht für den Anfang eines Kommentars, der bis zum Ende der Zeile geht. Ein Kommentar kann in der gleichen Zeile wie ein Parameter oder ein Sektionsanfang stehen. Alles, was in einem Kommentar steht, wird ignoriert. Dies ist eine Beispiel-INI-Datei mit einem einführenden Kommentar (einer Anmerkung dazu, wann die Datei das letzte Mal geändert wurde), zwei Sektionen („user“ und „post“) und insgesamt drei Parametern („name“, „title“ und „content“): ; zuletzt geändert am 25.11.2009 [user] name=J. Random Hacker [post] title = Reguläre Ausdrücke sind cool! content = "Lass mich zählen, auf wie viele Arten ..."
Markup und Datenaustausch | 441
8.1
Tags im XML-Stil finden
Problem Sie wollen beliebige HTML-, XHTML- oder XML-Tags in einem String finden, um sie zu entfernen, zu verändern, zu zählen oder etwas anderes mit ihnen zu machen.
Lösung Welche Lösung am sinnvollsten ist, hängt von vielen Faktoren ab: der notwendigen Genauigkeit, der gewünschten Effizienz und der Toleranz bezüglich falscher Auszeichnungen. Nachdem Sie herausgefunden haben, welches Vorgehen für Sie am besten ist, gibt es eine Reihe von Dingen, die Sie mit den Ergebnissen anstellen wollen. Aber egal ob Sie die Tags entfernen, in ihnen suchen, Attribute ergänzen oder löschen oder die Tags durch andere ersetzen wollen – der erste Schritt ist immer, sie zu finden. Seien Sie gewarnt! Dies wird ein langes Rezept, gespickt mit Subtilitäten, Ausnahmen und Variationen. Wenn Sie nach einer schnellen Lösung suchen und den Aufwand scheuen, die beste Lösung für Ihre Bedürfnisse zu finden, sollten Sie vielleicht direkt in den Abschnitt „(X)HTML-Tags (nicht streng)“ springen, in dem eine Lösung mit einer guten Kombination aus Toleranz und Sorgfalt zur Verfügung steht.
Quick and dirty Diese erste Lösung ist einfach und wird häufiger verwendet, als Sie vielleicht annehmen, aber wir haben sie hier eher zu Vergleichszwecken aufgenommen und um zu zeigen, wo ihre Schwächen liegen. Sie mag ausreichend sein, wenn Sie genau wissen, mit was für Inhalten Sie arbeiten, und wenn eventuelle Fehler keine dramatischen Auswirkungen haben. Diese Regex sucht nach einem < und beendet die Suche beim nächsten >: ]*>
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
Zulassen von > in Attributwerten Die nächste Regex ist wieder eher einfach und kann auch nicht mit allen Fällen korrekt umgehen. Aber vielleicht reicht sie ja für Ihre Bedürfnisse aus, wenn Sie sie nur für kurze Abschnitte gültigen (X)HTML-Codes verwenden. Ihr Vorteil gegenüber der vorherigen Regex ist, dass sie >-Zeichen innerhalb von Attributwerten korrekt überspringt: "']|"[^"]*"|'[^']*')*>
Regex-Optionen: Keine Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby
442 | Kapitel 8: Markup und Datenaustausch
Hier die gleiche Regex mit zusätzlichem Whitespace und Kommentaren für eine bessere Lesbarkeit: < (?: [^>"'] # Zeichen nicht in Anführungszeichen oder ... | "[^"]*" # Attributwerte in doppelten Anführungszeichen oder ... | '[^']*' # Attributwerte in einfachen Anführungszeichen )* >
Regex-Optionen: Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Die beiden Regexes sind funktional völlig identisch, daher können Sie sich aussuchen, welche Sie nutzen wollen. Diejenigen, die in JavaScript programmieren, müssen allerdings die erste Regex nutzen, weil JavaScript die Freiform-Option nicht anbietet.
(X)HTML-Tags (nicht streng) Diese Regex lässt nicht nur >-Zeichen in Attributwerten zu, sondern emuliert auch noch die nicht so strengen Regeln, die Browser im Allgemeinen für (X)HTML-Tag-Namen implementieren. So vermeiden Sie mit der Regex, Texte zu finden, die nur so aussehen wie ein Tag, zum Beispiel Kommentare, DOCTYPEs und nicht kodierte "']|"[^"]*"|'[^']*')*>
Schließende Tags zulassen Namen des Tags in Rückwärtsreferenz 1 speichern Zeichen nicht in Anführungszeichen oder ... Attributwerte in doppelten Anführungszeichen oder ... Attributwerte in einfachen Anführungszeichen
Regex-Optionen: Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Die beiden Regexes sind funktional völlig identisch, daher können Sie sich aussuchen, welche Sie nutzen wollen. Wer in JavaScript programmiert, muss allerdings die erste Regex nutzen, weil JavaScript die Freiform-Option nicht anbietet.
8.1 Tags im XML-Stil finden | 443
(X)HTML-Tags (streng) Diese Regex ist komplizierter als die bisherigen, da sie sich wirklich an die Regeln für (X)HTML-Tags hält, die wir am Anfang dieses Kapitels beschrieben haben. Das ist nicht immer wünschenswert, weil sich Browser nicht so eng daran halten. Sie verhindern also mit diesem regulären Ausdruck, Text zu finden, der nicht wie ein gültiges (X)HTML-Tag aussieht, aber eventuell verpassen Sie Text, der von Browsern als Tag interpretiert wird (wenn Ihr Markup zum Beispiel einen Attributnamen mit Zeichen nutzt, die nicht dafür gedacht sind, oder wenn Attribute in einem schließenden Tag stehen). Die Regeln für HTML- und XHTML-Tags werden zusammen behandelt, da sie häufig vermischt werden. Der Tag-Name wird in einer der Rückwärtsreferenzen 1 oder 2 gespeichert (abhängig davon, ob es sich um ein öffnendes oder einschließendes Tag handelt):
Regex-Optionen: Groß-/Kleinschreibung ignorieren Regex-Varianten: .NET, Java, JavaScript, PCRE, Perl, Python, Ruby Auch hier wieder die gleiche Regex im Freiform-Modus mit zusätzlichen Kommentaren: < (?: ([_:A-Z][-.:\w]*) (?: \s+ [_:A-Z][-.:\w]* \s*=\s* (?: "[^"]*" | '[^']*' ) )* \s* /? | / ([_:A-Z][-.:\w]*) \s* ) >
# # Zweig für öffnende Tags ... # Einfangen des öffnenden Tag-Namen in Rückwärtsreferenz 1 # Null oder mehr Attribute zulassen ... # ... durch Whitespace getrennt # Attributname # Attributname/Wert-Trenner # Attributwert in doppelten Anführungszeichen # Attributwert in einfachen Anführungszeichen # # # Abschließenden Whitespace zulassen # Selbst-schließende Tags zulassen # Zweig für schließende Tags ... # # Einfangen des schließenden Tag-Namen in Rücwärtsreferenz 2 # Abschließenden Whitespace zulassen # #
Regex-Optionen: Groß-/Kleinschreibung ignorieren, Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Wie die vorherigen beiden Lösungen für (X)HTML-Tags fangen diese Regexes den TagNamen in den Rückwärtsreferenzen 1 oder 2 – je nachdem, ob ein öffnendes oder schließendes Tag gefunden wurde. Die Regex für XML-Tags ist etwas kürzer als die (X)HTMLVersion, da sie sich nicht mit der nur in HTML vorkommenden Syntax herumschlagen muss (minimierte Attribute und nicht in Anführungszeichen gesetzte Werte). Zudem sind mehr Zeichen für die Element- und Attributnamen zugelassen.
Diskussion Eine Warnung Auch wenn man häufig Tags im XML-Stil mithilfe von regulären Ausdrücken sucht, muss man sorgfältig die verschiedenen Aspekte abwägen und sich der Daten bewusst sein, mit denen man arbeitet. Aufgrund dieser Schwierigkeiten haben einige Leute die Verwendung von Regexes für jegliche XML- oder (X)HTML-Verarbeitung aufgegeben und sind auf spezialisierte Parser und APIs umgestiegen. Das sollten Sie durchaus ebenfalls in Betracht ziehen, da solche Tools normalerweise darauf optimiert sind, die ihnen zugedachten Aufgaben zügig durchzuführen. Dazu verstehen sie sich auf die robuste Erkennung und Behandlung von falschem Markup. Im Browserumfeld ist es zum Beispiel normalerweise am besten, auf das baumbasierte Document Object Model (DOM) 8.1 Tags im XML-Stil finden | 445
zurückzugreifen, wenn Sie HTML suchen und verändern wollen. Ansonsten greifen Sie eventuell besser auf einen SAX-Parser oder XPath zurück. Aber gelegentlich werden Sie durchaus vor Situationen stehen, in denen die Verwendung von regulären Ausdrücken angeraten ist. Nachdem nun diese Worte der Warnung ausgesprochen sind, wollen wir die Regexes aus diesem Rezept genauer anschauen. Die ersten beiden Lösungen sind in den meisten Fällen dann doch zu simpel, aber sie behandeln alle Markup-Sprachen im XML-Stil gleichwertig. Die letzten drei folgen strengeren Regeln und sind auf die entsprechenden Markup-Sprachen zugeschnitten. Aber auch in diesen Lösungen werden HTML und XHTML zusammen behandelt, da sie häufig gemeinsam (oft unabsichtlich) eingesetzt werden. So kann ein Autor zum Beispiel ein selbstschließendes Tag im XHTMLStil in einem HTML-Dokument nutzen oder einen Elementnamen in einem Dokument in Großbuchstaben schreiben, das einen XHTML-DOCTYPE besitzt.
Quick and dirty Der Vorteil dieser Lösung ist ihre Einfachheit. Man kann sie sich leicht merken und eintippen, zudem arbeitet sie schnell. Der Nachteil ist, dass sie bestimmte gültige und ungültige XML- und (X)HTML-Konstrukte falsch behandelt. Wenn Sie mit Markup arbeiten, das Sie selbst geschrieben haben, und wissen, dass solche Fälle in Ihrem Ausgangstext nie vorkommen, oder wenn Sie sich um die Folgen solcher falschen Suchen keinen großen Kopf machen müssen, kann das in Ordnung sein. Ein weiterer Fall, in dem diese Lösung ausreichend sein kann, ist die Arbeit in einem Texteditor, bei dem Sie die Regex-Übereinstimmungen vor einer weiteren Verarbeitung überprüfen können. Zu Beginn der Regex wird ein literales ‹]*› genutzt, um null oder mehr folgende Zeichen zu finden, die kein > sind. Damit werden der Name des Tags, die Attribute und ein führendes oder abschließendes / gefunden. Wir könnten stattdessen auch einen genügsamen Quantor (‹[^>]*?›) nutzen, aber damit würde sich nichts ändern, außer dass die Regex ein bisschen langsamer werden würde, weil das Backtracking häufiger durchzuführen wäre. (In Rezept 2.13 wird der Grund dafür erklärt.) Um das Tag auch abzuschließen, wird dann ein literales ‹>› gefunden. Wenn Sie statt der negierten Zeichenklasse lieber einen Punkt nehmen wollen, können Sie das gern tun. Das klappt genauso gut, solange Sie einen genügsamen Stern nutzen (‹.*?›) und auch die Option Punkt passt auf Zeilenumbruch aktiviert haben (in JavaScript können Sie stattdessen ‹[\s\S]*?› verwenden). Ein Punkt mit einem gierigen Stern (also dem Muster ‹›) würde die Bedeutung der Regex ändern, da damit alles vom ersten < bis zum letzten > im Ausgangstext gefunden wird, auch wenn sich die Regex dazwischen eine ganze Reihe von weiteren Tags einverleiben müsste.
446 | Kapitel 8: Markup und Datenaustausch
Zeit für ein paar Beispiele. Diese Regex passt vollständig auf jede der drei folgenden Zeilen:
Regex-Optionen: Punkt passt auf Zeilenumbruch Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Das ist ziemlich einfach. Wie üblich muss man in JavaScript anders vorgehen. Da dort die Option Punkt passt auf Zeilenumbruch nicht vorhanden ist, müssen Sie den Punkt durch eine Zeichenklasse ersetzen, die wirklich alles findet. Denn sonst finden Sie keine Kommentare, die über mehr als eine Zeile laufen. Folgende Version funktioniert mit JavaScript: ›. Da keines dieser Zeichen in der Regex-Syntax eine besondere Bedeutung hat (außer innerhalb von Zeichenklassen, in denen Bindestriche Bereiche beschreiben), müssen sie nicht maskiert werden. Damit bleibt nur ‹.*?› oder ‹[\s\S]*?› in der Mitte der Regex für eine genauere Untersuchung übrig. Dank der Option Punkt passt auf Zeilenumbruch passt der Punkt in der Regex auf jedes einzelne Zeichen. In der JavaScript-Version wird dafür die Zeichenklasse ‹[\s\S]›
484 | Kapitel 8: Markup und Datenaustausch
genutzt. Aber die beiden Regexes erledigen genau die gleiche Aufgabe. ‹\s› passt auf jedes Whitespace-Zeichen, während ‹\S› auf alles andere passt. Zusammen wird damit jedes Zeichen gefunden. Der genügsame Quantor ‹*?› wiederholt das vorherige Element „jedes Zeichen“ null Mal oder häufiger, aber so wenig wie möglich. Daher wird dieses Token nur bis zum ersten Auftauchen von --> wiederholt. Ohne Fragezeichen würde das Token bis zum Ende des Ausgangstexts laufen, um dann per Backtracking bis zum letzten --> zurückzugehen. (In Rezept 2.13 erfahren Sie mehr über Backtracking mit genügsamen und gierigen Quantoren.) Diese einfache Strategie funktioniert gut, da Kommentare im XML-Stil nicht verschachtelt werden dürfen. Sie enden also immer nach dem ersten Auftreten von -->.
Wann Kommentare nicht entfernt werden dürfen Die meisten Webentwickler sind mit HTML-Kommentaren vertraut, die innerhalb von <script>- und <style>-Elementen stehen, um eine Abwärtskompatibilität zu antiken Browsern herzustellen. Heutzutage sind das größtenteils ziemlich überflüssige Elemente, aber dank des gerne genutzten Copy-and-Paste leben sie weiter. Wir gehen davon aus, dass Sie beim Entfernen von Kommentaren das eingebettete JavaScript und CSS beibehalten wollen. Vermutlich wollen Sie auch die Inhalte von -Elementen, CDATA-Abschnitten und Attributwerten innerhalb von Tags am Leben lassen. Weiter oben haben wir gesagt, dass es nicht schwierig ist, Kommentare zu entfernen. Das stimmt aber nur, solange Sie einige der kritischeren Bereiche von (X)HTML oder XML auslassen, in denen die Syntaxregeln anders aussehen. Mit anderen Worten: Wenn Sie die schwierigen Teile des Problems ignorieren, ist es einfach. Natürlich kann es Situationen geben, in denen Sie den Markup-Text begutachten und entscheiden, dass es durchaus in Ordnung ist, diese problematischen Fälle zu ignorieren – vielleicht weil Sie den zu durchsuchenden Text selbst geschrieben haben und wissen, was Sie erwartet. Es kann auch in Ordnung sein, wenn Sie das Ersetzen in einem Texteditor vornehmen, in dem Sie vorher sehen, was Sie austauschen wollen. Aber wie können wir das ansonsten umgehen? In „Schwierige (X)HTML- und XMLAbschnitte überspringen“ auf Seite 453 haben wir ähnliche Probleme besprochen, als es um das Finden von Tags im XML-Stil ging. Wir können hier ähnlich vorgehen, wenn wir nach Kommentaren suchen. Mit dem Code in Rezept 3.18 suchen wir zunächst mit der folgenden Regex nach den kritischen Abschnitten und ersetzen dann die Kommentare durch den leeren String nur in den Bereichen zwischen den vorher gefundenen Übereinstimmungen: "']|"[^"]*"|'[^']*')*? (?:/>|>.*?)|"']|"[^"]*"|'[^']*')*>|
Regex-Optionen: Groß-/Kleinschreibung ignorieren, Punkt passt auf Zeilenumbruch Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby
8.8 Kommentare im XML-Stil entfernen |
485
Mit etwas Whitespace und ein paar Kommentaren in der Regex lässt sich das im Freiform-Modus viel besser überblicken: # Besonderes Element: Tag und Inhalt "'] # Attributnamen ... | "[^"]*" # ... und Werte finden | '[^']*' # )*? (?: # Singleton-Tag /> | # Ansonsten den Inhalt des Elements und das schließende Tag mit aufnehmen > .*? ) | # Standard-Element: nur Tag "'] # den Rest des Tag-Namen finden | "[^"]*" # ... zusammen mit Attributnamen | '[^']*' # ... und Werten )* > | # CDATA-Abschnitt
Regex-Optionen: Groß-/Kleinschreibung ignorieren, Punkt passt auf Zeilenumbruch, Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Hier eine äquivalente Version für JavaScript, in dem es die Optionen Punkt passt auf Zeilenumbruch und Freiform nicht gibt: "']|"[^"]*"|'[^']*')*? (?:/>|>[\s\S]*?)|"']|"[^"]*"|'[^']*')*>|
Variationen Gültige Kommentare im XML-Stil finden Es gibt ein paar wenige Syntaxregeln für (X)HTML- und XML-Kommentare, die über die Regel „Beginnt mit einem aufgrund der beiden Bindestriche in der Mitte ungültig. • Vor dem schließenden Element darf kein Bindestrich als Teil des Kommentars stehen. So ist zum Beispiel aber erlaubt. • Zwischen dem schließenden -- und > darf Whitespace stehen. So ist zum Beispiel zu finden (wie dies notwendig ist, wenn Sie diesen Musterabschnitt in die Regex zum Finden von Kommentaren einfügen), muss die Engine alle möglichen Wiederholungskombinationen ausprobieren, bevor sie merkt, dass es wirklich keine Übereinstimmung gibt, und fortfährt. Diese Anzahl an Möglichkeiten wächst mit jedem zu prüfenden Zeichen extrem stark. Verschachtelte
8.8 Kommentare im XML-Stil entfernen |
487
Quantoren sind aber nicht kritisch, wenn man diese Situation vermeidet. So stellt zum Beispiel das Muster ‹(?:-[^-]+)*› keine Gefahr dar, obwohl es einen verschachtelten Quantor ‹+› enthält. Denn die potenzielle Zahl an Backtracking-Punkten wächst linear mit der Länge des Ausgangstexts, da nun genau ein Bindestrich pro Gruppenwiederholung gefunden werden muss. Eine andere Möglichkeit, potenzielle Backtracking-Probleme zu vermeiden, ist die Verwendung einer atomaren Gruppe. Die folgende Regex entspricht der ersten Regex aus diesem Abschnitt, sie ist aber ein paar Zeichen kürzer und kann nicht in JavaScript und Python genutzt werden:
Regex-Optionen: Punkt passt auf Zeilenumbruch Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby In JavaScript gibt es keine Option Punkt passt auf Zeilenumbruch, aber Sie können statt des Punkts eine Zeichenklasse verwenden, die alle Zeichen enthält: auftaucht: \bTODO\b(?=(?:(?!)
Diskussion Zweistufiges Vorgehen In Rezept 3.13 finden Sie den Code, den Sie benötigen, um innerhalb von Übereinstimmungen einer anderen Regex zu suchen. Man benötigt dafür eine innere und eine äußere Regex. Die Kommentar-Regex dient als äußere Regex, während ‹\bTODO\b› die innere Regex bildet. Am wichtigsten ist hier der genügsame Quantor ‹*?›, der auf den Punkt oder die Zeichenklasse in der Kommentar-Regex folgt. Wie in Rezept 2.13 beschrieben, geht die Übereinstimmung damit nur bis zum ersten --> (demjenigen, das den Kommentar beendet), statt bis zum letzten Vorkommen von --> in Ihrem Ausgangstext.
Einstufiges Vorgehen Diese Lösung ist komplexer und langsamer. Der Vorteil ist aber, dass sie die beiden Schritte des vorigen Ansatzes in einer Regex kombiniert. Damit kann sie auch dann genutzt werden, wenn man mit einem Texteditor, einer IDE oder einem anderen Tool arbeitet, mit dem man nicht innerhalb der Übereinstimmungen einer anderen Regex suchen kann. Lassen Sie uns diese Regex im Freiform-Modus anschauen und jeden Teil genauer betrachten:
490 | Kapitel 8: Markup und Datenaustausch
\b TODO \b (?= (?: (?! " finden #
Regex-Optionen: Punkt passt auf Zeilenumbruch, Freiform Regex-Varianten: .NET, Java, PCRE, Perl, Python, Ruby Diese kommentierte Version der Regex funktioniert nicht in JavaScript, da es dort die Optionen Freiform und Punkt passt auf Zeilenumbruch nicht gibt. Beachten Sie, dass die Regex ein negatives Lookahead enthält, das in einem äußeren, positiven Lookahead verschachtelt ist. Damit ist sichergestellt, dass auf jede Übereinstimmung von TODO ein --> folgt und dass gefunden. Wir brauchen das ‹.*?› am Anfang des Lookaheads, da die Regex ansonsten nur dann etwas finden würde, wenn auf TODO direkt --> folgt, also ohne weitere Zeichen dazwischen. Der Quantor ‹*?› wiederholt den Punkt null Mal oder häufiger, aber so wenig wie möglich. Das ist genau das, was wir brauchen, um das erste folgende --> zu finden. Die Regex könnte übrigens bisher auch als ‹\bTODO(?=.*?-->)\b› geschrieben werden, indem das zweite ‹\b› hinter das Lookahead verschoben wird. Das hätte keinen Effekt auf den Text, der damit gefunden werden könnte, da sowohl die Wortgrenze als auch das Lookahead Zusicherungen der Länge null sind (siehe „Lookaround“ auf Seite 81). Aber trotzdem ist es besser, aus Gründen der Effizienz und Lesbarkeit die Wortgrenze als Erstes zu nutzen. In der Mitte einer teilweisen Übereinstimmung kann die Regex-Engine schneller eine Wortgrenze testen und muss sich nicht mit dem Prüfen eines längeren Lookahead befassen, falls TODO an der Stelle kein vollständiges Wort ist. Okay, die Regex ‹\bTODO\b(?=.*?-->)› scheint soweit zu funktionieren. Aber was passiert, wenn wir sie auf den Ausgangstext TODO )›. In JavaScript, in dem die notwendige Option Punkt passt auf Zeilenumbruch fehlt, können wir ‹\bTODO\b(?=(?: (?! dazwischen vorkommt. Es gibt ein paar Gründe, warum wir das nicht geprüft haben: • Sie erreichen normalerweise auch ohne diese doppelte Prüfung Ihr Ziel, insbesondere, da die Ein-Schritt-Regex dafür gedacht ist, in Texteditoren und ähnlichen Tools verwendet zu werden, in denen Sie Ihre Ergebnisse noch überprüfen können. • Muss man weniger überprüfen, kostet das auch weniger Zeit (das heißt, es ist schneller). • Am wichtigsten ist aber, dass Sie nicht wissen, wie weit Sie rückwärts schauen müssten. Damit wird ein Lookbehind mit unbegrenzter Länge benötigt, was nur von der .NET-Variante unterstützt wird. Wenn Sie mit .NET arbeiten und diese zusätzliche Prüfung einbauen wollen, nutzen Sie die folgende Regex: (?