Einführung in reguläre Ausdrücke mit der Java-API

Ein regulärer Ausdruck (engl. regular expression, kurz Regex) ist die Beschreibung eines Musters (engl. pattern). Reguläre Ausdrücke werden bei der Zeichenkettenverarbeitung beim Suchen und Ersetzen eingesetzt. Für folgende Szenarien bietet die Java-Bibliothek entsprechende Unterstützung an:

  • Frage nach einer kompletten Übereinstimmung: Passt eine Zeichenfolge komplett auf ein Muster? Wir nennen das vollständigen Match. Die Rückgabe einer solchen Anfrage ist einfach wahr oder falsch.
  • Finde Teil-Strings: Das Pattern beschreibt einen Teil-String, und gesucht sind alle Vorkommen dieses Musters in einem Such-String.
  • Ersetze Teilfolgen: Das Pattern beschreibt Zeichenfolgen, die durch andere Zeichenfolgen ersetzt werden.
  • Zerlegen einer Zeichenfolge: Das Muster steht für Trennzeichen, sodass nach dem Zerlegen eine Sammlung von Zeichenfolgen entsteht.

Tipp: Regex-Ausdrücke lassen sich über Tools visualisieren. Online ist das zum Beispiel mit https://www.debuggex.com/ oder http://regexper.com/ möglich, http://xenon.stanford.edu/~xusch/regexp/ »liest« reguläre Ausdrücke vor.

Regex-API

Ein Pattern-Matcher ist die »Maschine«, die reguläre Ausdrücke verarbeitet. Zugriff auf diese Mustermaschine bietet die Klasse Matcher. Dazu kommt die Klasse Pattern, die die regulären Ausdrücke in einem internen vorcompilierten Format repräsentiert. Beide Klassen befinden sich im Paket java.util.regex. Um die Sache etwas zu vereinfachen, gibt es bei String zwei kleine Hilfsmethoden, die im Hintergrund auf die Klassen verweisen, um eine einfachere API anbieten zu können; diese nennen sich auch Fassaden-Methoden. Wir werden am Anfang erst mit den String-Methoden arbeiten und uns später die Klasse Pattern genauer anschauen. Die die Objektmethode matches(…) der Klasse String testet, ob ein regulärer Ausdruck eine Zeichenfolge komplett beschreibt.

Konstruktion von regulären Ausdrücken

In diesem Abschnitt wollen wir schauen, was ein Pattern-Matcher alles erkennen kann und wie die Syntax dafür aussieht.

Literale Zeichen

Der einfachste reguläre Ausdruck besteht aus einzelnen Zeichen, den Literalen.

Ausdruck Ergebnis
„tutego“.matches( „tutego“ ) true
„tutego“.matches( „Tutego“ ) false
„tutego“.matches( „-tutego-“ ) false

Tabelle: Einfache reguläre Ausdrücke und ihr Ergebnis

Für diesen Fall benötigen wir keine regulären Ausdrücke, ein equals(…) würde reichen.

Hinweis: Bei Java ist es immer so, dass der reguläre Ausdruck den gesamten String komplett treffen muss, und nicht nur einen Teilstring. In nahezu jeder anderen Sprache und Bibliothek ist das nicht so, hier zählt ein Teilstring als „match“. In einer JavaScript-Konsole:

„tutego“.match( /tutego/ )

[„tutego“, index: 0, input: „tutego“, groups: undefined]

„Tutego“.match( /tutego/ )

null

„-tutego-„.match( /tutego/ )

[„tutego“, index: 1, input: „-tutego-„, groups: undefined]

Spezialzeichen (Metazeichen)

In regulären Ausdrücken sind einige Zeichen reserviert, weshalb sie nicht als einfaches Literal gewertet werden. Zu diesen Zeichen mit besonders Bedeutung zählen: \ (Backslash), ^ (Caret), $ (Dollarzeichen), . (Punkt), | vertikaler Strich, ? (Fragezeichen), * (Sternchen), + (Pluszeichen), (, ) (runde Klammer auf und zu), [, ] (eckige Klammer auf und zu), { (geschweifte Klammer auf).

Um diese Zeichen als Literal verwenden zu können ist eine Ausmaskierung mit \ nötig.

Ausdruck Ergebnis
„1+1“.matches( „1+1“ ) false
„1+1“.matches( „1\\+1“ ) true
„11111“.matches( „1+1“ ) true
„+1“.matches( „+1“ ) PatternSyntaxException: Dangling meta character ‚+‘ near index 0

Metazeichen müssen ausmaskiert werden

Der Ausdruck „11111“.matches(„1+1“) macht deutlich, dass + ein besonders Symbol ist, was für Wiederholungen steht. Wird es, wie im letzten Fall, falsch angewendet, folgt eine Ausnahme.

Zeichenklassen

Mit einer Zeichenklasse ist es möglich ein von mehren Zeichen aus einer Menge zu matchen. So steht [aeiou] für eines der Zeichen a, e, i, o oder u. Die Reihenfolge der Zeichen spielt keine Rolle. Mit einem Minuszeichen lassen sich Bereiche definieren. So steht [0-9a-fA-F] für die Zeichen 0, 1, 2, …, 9 oder Groß-/Kleinbuchstaben a, b, c, d, e, f, A, B, C, D, E, F. Auch hier spielt die Reihenfolge keine Rolle, es hätte auch [a-fA-F0-9] heißen können. Mehrere Bereiche mit rechteckigen Klammern lassen sich hintereinander stellen.

Die Metazeichen wie * oder + können ohne Ausmaskierung in Zeichenklassen verwendet werden, nur eben das Minus nicht, es sei denn, es steht am Anfang oder am Ende.

 

Ausdruck Ergebnis
„tutego“.matches(„[tT]utego“ ) true
„Tutego“.matches(„[tT]utego“ ) true
„Nr. 1“.matches( „Nr\\. [0-9]“ ) true
„1*2“.matches( „[0-9][+*/][0-9]“ ) true
„1*2“.matches( „[0-9][+*/][0-9]“ ) true
„1-2“.matches( „[0-9][+*/][0-9]“ ) PatternSyntaxException: Illegal character range near index 8
„1-2“.matches( „[0-9][+\\-*/][0-9]“ ) true

Beispiel für Zeichenklassen

Hinweis: Es ist wichtig daran zu denken, dass es immer nur einzelnen Zeichen sind und keine Zahlenbereiche. Wenn wir [1-99] vor uns haben, dann ist das mitnichten ein regulärer Ausdruck für Zahlen von 1 bis 99, sondern nur die Ziffern 1, 2, 3, …, 9 und dann noch einmal die 9 extra, was redundant ist, und gekürzt werden kann auf [1-9].

Negative Zeichenklassen

Steht direkt hinter der öffnenden eckigen Klammer ein ^, definiert das negative Zeichenklassen. Der Match ist dann auf allen Zeichen, die in der negativen Zeichenklasse nicht vorkommen.

 

Ausdruck Ergebnis
„1“.matches( „[^-+*/]“ ) true
„ß“.matches( „[^-+*/]“ ) true
„/“.matches( „[^-+*/]“ ) false
„“.matches( „[^-+*/]“ false

Beispiel für negative Zeichenklassen

Das letzte Beispiel macht deutlich, dass eine negative Zeichenklasse auch für ein Zeichen, nämlich für ein Zeichen, das eben nicht +, -, *, / ist. Kein Zeichen kann das nicht sein.

Jedes Zeichen (.)

Der . (Punkt) ist ein mächtiges Metazeichen und steht für (fast) jedes erdenkliche Zeichen.

Hinweis: Der . (Punkt) matcht standardmäßig keinen Zeilenumbruch. Das hat historische Gründe, denn die ersten Werkzeuge arbeiteten zeilenbasiert, und da war es nicht gewünscht, wenn der Ausdruck mit auf die nächste Zeile ging. In Java können wir einstellen, ob der Punkt auch einen Zeilenumbruch erkennen soll.

Ausdruck Ergebnis
„Filk“.matches( „F..k“ ) true
„123“.matches( „\\d\\d\\d“ ) true
„a b“.matches( „a\\sb“ ) true
„a\nb“.matches( „a\\sb“ ) true
„\n“.matches( „.“ ) false

Beispiel mit dem Punkt

Vordefinierte Zeichenklassen

Gewisse Zeichenklassen kommen immer wieder vor, etwa für Ziffern. Daher gibt es vordefinierte Zeichenklassen, die uns Schreibarbeit ersparen und den regulären Ausdruck übersichtlicher machen. Die wichtigsten sind:

Zeichenklasse Enthält
\d Ziffer: [0-9]
\D Keine Ziffer: [^0-9] bzw. [^\d]
\s Weißraum: [ \t\n\x0B\f\r]
\S Keinen Weißraum: [^\s]
\w Wortzeichen: [a-zA-Z_0-9]
\W Keine Wortzeichen: [^\w]
\p{Blank} Leerzeichen oder Tab: [ \t]
\p{Lower}, \p{Upper} Einen Klein-/Großbuchstaben: [a-z] bzw. [A-Z]
\p{Alpha} Einen Buchstaben: [\p{Lower}\p{Upper}]
\p{Alnum} Ein alphanumerisches Zeichen: [\p{Alpha}\p{Digit}]
\p{Punct} Ein Interpunktionszeichen: !“#$%&'()*+,-./:;<=>?@[\]^_`{|}~
\p{Graph} Ein sichtbares Zeichen: [\p{Alnum}\p{Punct}]
\p{Print} Ein druckbares Zeichen: [\p{Graph}]

Tabelle: Vordefinierte Zeichenklassen

Bei den Wortzeichen handelt es sich standardmäßig um die ASCII-Zeichen und nicht um deutsche Zeichen mit unseren Umlauten oder allgemeine Unicode-Zeichen. Eine umfassende Übersicht liefert die API-Dokumentation der Klasse java.util.regex.Pattern unter https://docs.oracle.com/javase/10/docs/api/java/util/regex/Pattern.html, die auch weitere vordefinierte Zeichenklassen auflistet.

 

Ausdruck Ergebnis
„123“.matches( „\\d\\d\\d“ ) true
„a b“.matches( „a\\sb“ ) true
„a\nb“.matches( „a\\sb“ ) true

Beispiel mit vordefinierten Zeichenklassen

Vorhanden oder nicht?

Steht hinter einem Zeichen ein Fragezeichen, so ist es optional. Das Fragezeichen nennen wir auch Quantifizierer. Auch hinter einer Zeichenklasse, vordefinierten Zeichenklasse ist ein Fragezeichen erlaubt. Mehrere Zeichen können durch runde Klammen zusammengefasst werden.

 

Ausdruck Ergebnis
„Lyric“.matches( „Lyrics?“ ) true
„Lyrics“.matches( „Lyrics?“ ) true
„1“.matches( „\\d?“ ) true
„“.matches( „\\d?“ ) true
„Christian“.matches( „Chris(tian)?“ ) true
„Chris“.matches( „Chris(tian)?“ ) true

Beispiel mit optionalen Zeichenfolgen

Beliebige Wiederholungen

Neben dem Fragzeichen an gibt es weitere Quantifizierer. Für eine Zeichenkette X gilt:

 

Quantifizierer Anzahl an Wiederholungen
X? X kommt einmal oder keinmal vor.
X* X kommt keinmal oder beliebig oft vor.
X+ X kommt einmal oder beliebig oft vor.

Tabelle: Quantifizierer im Umgang mit einer Zeichenkette X

Sehen wir uns ein paar Ausdrücke an:

Ausdruck Ergebnis
„Gooooo“.matches( „Go+“ ) true
„Go“.matches( „Go+“ ) true
„G“.matches( „Go+“ ) false
„Go“.matches( „Go*“ ) true
„G“.matches( „Go*“ ) true
„lalala“.matches( „(la)+“ ) true
„yo 4711“.matches( „yo \\d+“ ) true

Tabelle: Beispiele für reguläre Ausdrücke mit Wiederholungen

Eine Sonderform ist X(?!Y) – das drückt aus, dass der reguläre Ausdruck Y dem regulären Ausdruck X nicht folgen darf (die API-Dokumentation spricht von »zero-width negative lookahead«).

Ein weiterer Quantifizierer kann die Anzahl einschränken und die Anzahl eines Vorkommens genauer beschreiben:

  • X{n}. X muss genau n-mal vorkommen.
  • X{n,}. X kommt mindestens n-mal vor.
  • X{n,m}. X kommt mindestens n-, aber maximal m-mal vor.

Beispiel

Eine E-Mail-Adresse endet mit einem Domain-Namen, der zwei oder drei Zeichen lang ist. Ein einfacher regulärer Ausdruck sieht aus aus: „[\\w|-]+@\\w[\\w|-]*\\.[a-z]{2,3}“.

Die Klassen Pattern und Matcher, Pattern.matches(…) bzw. String#matches(…)

Der Aufruf der Objektmethode matches(String) auf einem String-Objekt bzw. das statische Pattern.matches(String, CharSequence) ist nur eine Abkürzung für die Übersetzung eines Patterns und Anwendung von matches() auf einem Matcher-Objekt.

String#matches(…) Pattern.matches(…)
public boolean
matches(String regex) {
return Pattern.matches(regex, this);
}
public static boolean
matches(String regex, CharSequence input) {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(input);
return m.matches();
}

Tabelle: Implementierungen der beiden matches(…)-Methoden

Während also die Objektmethode matches(String) von String zu Pattern.matches(String, CharSequence) delegiert, steht hinter der statischen Fassadenmethode in Pattern die wirkliche Nutzung der beiden zentralen Klassen Pattern für das Muster und Matcher für die Mustermaschine. Wenn wir also schreiben „Filk“.matches(„F..k“) ist das äquivalent zu Pattern.matches(„F..k“, „Filk“) und das ist äquivalent zu

Pattern p = Pattern.compile( „F..k“ );
Matcher m = p.matcher( „Filk“ );
boolean b = m.matches();

Über Christian Ullenboom

Ich bin Christian Ullenboom und Autor der Bücher ›Java ist auch eine Insel. Einführung, Ausbildung, Praxis‹ und ›Java SE 8 Standard-Bibliothek. Das Handbuch für Java-Entwickler‹. Seit 1997 berate ich Unternehmen im Einsatz von Java. Sun ernannte mich 2005 zum ›Java-Champion‹.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.