03.07 Reguläre Ausdrücke

Reguläre Ausdrücke (RegExp, Regex, regular expressions) stellen ein Muster in Form einer Zeichenkette dar, anhand dessen eine weitere Zeichenkette aufgeteilt, durchsucht, manipuliert oder überprüft werden kann. Dieses Kapitel bietet Ihnen einen Einstieg in reguläre Ausdrücke.

Sie haben bereits mit regulären Ausdrücken gearbeitet – im Kapitel 03.03. Nützliche String-Methoden. Dort wurden den Methoden split und replaceAll reguläre Ausdrücke übergeben um einen String dementsprechend zu manipulieren. Wie schon oben erwähnt, wird eine regular expression in Form eines Strings behandelt.

Eine regular Expression kann eine ganz normale Zeichenkette sein – z. B. „12345“ um nach der Zahl „12345“ zu suchen. Aber Sie können auch variable Ausdrücke verwenden – z. B. „j-p“ um alle klein geschriebenen Buchstaben zwischen j und p zu finden. Nachfolgend finden Sie einige Anwendungsbeispiele.

Gewöhnliche Zeichen

Wie Sie bereits wissen teilt die String#split-Methode eine gegebene Zeichenkette anhand einer RegExp in ein String-Array auf. Dabei lassen sich ganz gewöhnliche Zeichen wie z. B. das Leerzeichen, ein „A“, das Semikolon oder „xyz“ verwenden. Bei jedem Vorkommen dieser Zeichenkette in unserem zu bearbeitenden String, wird selbiger aufgeteilt und die gefundene Stelle gelöscht.

String blub = "Ich werde Wort für Wort aufgeteilt";
String[] words = blub.split(" ");
for (int i = 0; i < words.length; i++) {
  System.out.println(words[i]);
}

Bitte beachten Sie, dass manche Zeichen maskiert werden müssen (Siehe Escape Sequenzen). Dazu gehört z. B. der Punkt (.). Wird dieser nicht maskiert, zählt er für jedes beliebige Zeichen. Zum Maskieren wird – wie in Java selbst auch – der Backslash verwendet. Hierdurch muss der Backslash doppelt maskiert werden, was teilweise zu recht unübersichtlichen Aneinanderreihungen von Zeichen führt.

String str = "1\\2\\3\\4\\5";
String[] array = str.split("\\\\");
for (int i = 0; i < array.length; i++) {
  System.out.println(array[i]);
}
str = "1.2.3.4.5";
array = str.split("\\.");
for (int i = 0; i < array.length; i++) {
  System.out.println(array[i]);
}

Zeichen-Klassen

Ein String kann nicht nur an gewöhnlichen Zeichen(ketten) getrennt werden, sondern auch anhand abstrakter Definitionen. Wenn Sie eine Zeichenkette z. B. an jeder Zahl zwischen 2 und 6 trennen möchten, so können Sie folgenden Ausdruck verwenden:

String blub = "1 eins 2 zwei 3 drei 4 vier 5 fünf 6 sechs 7 sieben 8 acht 9 neun";
String[] test = blub.split("[2-6]");
for (int i = 0; i < test.length; i++) {
  System.out.println(test[i]);
}

Dieser Block (eckige Klammer) wird als Zeichen-Klasse (engl. character-class) bezeichnet – die Zusammenfassung von mehreren Ausdrücken und/oder Zeichen.

Mit „^“ kann der Ausdruck negiert werden. So könnten Sie mit der Methode String#replaceAll (welche bekanntlich auch einen regulären Ausdruck erwartet) beispielsweise aus einem String alle Zeichen entfernen, die keine Zahlen repräsentieren.

String number = "123a43Bc a sd l43";
String realNumber = number.replaceAll("[^0-9]", "");
System.out.println(realNumber);

Ein paar weitere Ausdrücke:

  • [abc] a, b oder c
  • [^abc] Alles außer a, b oder c
  • [a-zA-Z] Das Alphabet in Groß- und Kleinschreibung
  • [0-9] Alle Zahlen

Vordefinierte Zeichen-Klassen

Des weiteren gibt es auch einige vordefinierte Zeichen-Klassen. Diese beginnen mit einem Backslash gefolgt von einem entsprechenden Zeichen.

  • \d Eine beliebige Zahl
  • \D Keine Zahl
  • \s Ein beliebiges Whitespace-Zeichen (Leerzeichen, Zeilenumbruch, Tabulator, …)
  • \S Kein Whitespace-Zeichen
  • \w Ein Wort-Zeichen ([a-zA-Z_0-9])
  • \W Kein Wort-Zeichen

Selbstverständlich müssen Sie den zugehörigen Backslash in Ihrem Java-Programm ein weiteres Mal maskieren.

Sie finden eine umfangreiche Erklärung/Auflistung von regulären Ausdrücken in der Klassenbeschreibung der Pattern-Klasse in der Java-API Dokumentation.

Überprüfen einer Zeichenkette

Sie können eine Zeichenkette dahingehend überprüfen, ob Sie mit einem regulären Ausdruck übereinstimmt. Hierzu verwenden Sie die Methode String#matches.

String onlyNumbers = "123a3432";
String onlyRegex = "[0-9]*";
if (onlyNumbers.matches(onlyRegex)) {
  System.out.println("Nur zahlen");
}
String noNumbers = "Ich bestehe aus keinen Zahlen!";
String noRegex = "[^0-9]*";
if (noNumbers.matches(noRegex)) {
  System.out.println("Keine Zahlen");
}

Ein Stern hinter einer Zeichen-Klasse bedeutet, dass diese Zeichen-Klasse beliebig oft hintereinander vorkommen darf (also auch kein Mal). Nicht zu verwechseln mit dem Plus (+), welches festlegt, dass die Klasse beliebig oft vorkommen darf, aber mindestens einmal vorkommen muss.

Selbstverständlich gibt es auch komplexere Beispiele, als einen String auf das Vorkommen von Zahlen zu überprüfen. Mit regulären Ausdrücken können z. B. auch IP-Adressen oder E-Mail Anschriften auf Korrektheit überprüft werden.

String number_between_0_and_255 = "(([0-9]{1,2})|([01][0-9]{2})|(2((5[0-5])|([0-4][0-9]))))"; 
String valid_ipv4 = "(" + number_between_0_and_255 + "\\.){3}" + number_between_0_and_255;
String valid_mail = "^[\\w\\.=-]+@[\\w\\.-]+\\.[\\w]{2,4}$";
System.out.println("192.168.178.0".matches(valid_ipv4));
System.out.println("192.168.178.".matches(valid_ipv4));
System.out.println("9.9.9.9".matches(valid_ipv4));
System.out.println("192.168.256.1".matches(valid_ipv4));
System.out.println("127.1.0.0".matches(valid_ipv4));
System.out.println("webmaster@java-blog-buch.de".matches(valid_mail));
System.out.println("a@b.c".matches(valid_mail));
System.out.println("asdf.de".matches(valid_mail));
System.out.println("ich@du.info".matches(valid_mail));
System.out.println("devil666@hell.com".matches(valid_mail));

Eine geschweifte Klammer hinter einem Ausdruck gibt an, wie oft der vorhergehende Ausdruck hintereinander vorkommen muss. Finden sich zwei Zahlen in diesen Klammern – separiert durch ein Komma – repräsentieren diese einen entsprechenden von-bis-Richtwert (eine Zahl gefolgt von einem Komma bedeutet „mindestens x-Mal“). Auch können logische Operatoren wie | oder & verwendet werden um Ausdrücke zu verknüpfen.

Natürlich gibt es bei solch einfachen regulären Ausdrücken immer wieder eigentlich gültige Zeichenketten, die als ungültig markiert werden, und ungültige Ausdrücke die als gültig markiert werden. Um alle Möglichkeiten abzudecken wird ein dementsprechend komplexer Regex benötigt. Oftmals ist es aber nicht nötig wirklich alle fehlerhaften Eingaben auszuschließen. Gültige Eingaben sollten aber nach Möglichkeit alle akzeptiert werden. Beachten Sie auch, dass ein komplexerer Regex viel Rechen-Ressourcen beansprucht.

Eine Vielzahl guter und sehr komplexer regulärer Ausdrücke finden sich auf regexlib.com.

Reguläre Ausdrücke vorkompilieren

Anstelle der Methode String#matches können Sie auch die Klassen Pattern und Matcher verwenden. Diese bieten Ihnen zudem weitere Möglichkeiten um mit regulären Ausdrücken zu arbeiten.

Eine Zeichenkette durchsuchen

Mit Java haben Sie nicht nur die Möglichkeiten zu ersetzen, zu überprüfen und zu teilen, sondern Sie können auch in einer Zeichenkette suchen. Hierzu benötigen Sie die eben erwähnten Klassen Pattern und Matcher (alle im Package java.util.regex).

Nehmen wir an, Sie wollen aus einem Fließtext alle IP-Adressen und den dazugehörige Bezeichnung extrahieren. Zusätzlich wissen Sie, dass die Bezeichnung immer unmittelbar vor der IP-Adresse steht. Einen Regex für IP-Adressen kennen Sie bereits. Dieser wird nun dahingehend erweitert, dass ein zusätzliches Wort vor der IP-Adresse für einen Treffer benötigt wird:

String number_between_0_and_255 = "(([0-9]{1,2})|([01][0-9]{2})|(2((5[0-5])|([0-4][0-9]))))"; 
String valid_ipv4 = "(" + number_between_0_and_255 + "\\.){3}" + number_between_0_and_255;
String regex = "[\\S]*[\\s](" + valid_ipv4 + ")";

Es werden also beliebig viele Zeichen, die keinen Whitespace (\S) repräsentieren gefolgt von einem Whitespace Zeichen (\s) und einer gültigen IP-Adresse gesucht.

Der Text lautet wie folgt:

String text = "Der Client 192.168.178.20 hat die Subnetzmaske " +
  "255.255.255.0 und verbindet sich über sein Standardgateway " +
  "192.168.178.1 zur Außenwelt.";

Mit der Klasse Pattern sollten Sie nun Ihren regulären Ausdruck kompilieren und ein neues Objekt erzeugen.

Häufig vorkommende reguläre Ausdrücke sollten über die Pattern-Klasse kompiliert und somit ein neues Objekt dieser Klasse erzeugt werden. Wenn Sie später mit diesem Ausdruck arbeiten möchten, sparen Sie sich durch diese Maßnahme viel Rechenzeit des Computers.

Pattern pattern = Pattern.compile(regex);

Die Klasse Matcher ist für die Ergebnisse zuständig. Um ein neues Objekt von ihr zu erzeugen, verwenden wir die matcher-Methode unseres Pattern-Objekts.

Matcher matcher = pattern.matcher(text);

Die Methode Matcher#find liefert true zurück, solange im übergebenen Text noch Übereinstimmungen mit dem gewünschten regulären Ausdruck bestehen. Über Matcher#group können Sie den aktuellen Treffer ausgeben. Es ergibt sich also folgende While-Schleife:

while (matcher.find()) {
  System.out.println(matcher.group());
}

Hier nochmal der vollständige Code zum Kompilieren und Ausführen:

package de.test;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegExTest {
	
  public static void main(String[] args) {

    String number_between_0_and_255 = "(([0-9]{1,2})|([01][0-9]{2})|(2((5[0-5])|([0-4][0-9]))))"; 
    String valid_ipv4 = "(" + number_between_0_and_255 + "\\.){3}" + number_between_0_and_255;
    String regex = "[\\S]*[\\s](" + valid_ipv4 + ")";
    String text = "Der Client 192.168.178.20 hat die Subnetzmaske " +
      "255.255.255.0 und verbindet sich über sein Standardgateway " +
      "192.168.178.1 zur Außenwelt.";
    Pattern pattern = Pattern.compile(regex);
    Matcher matcher = pattern.matcher(text);
    while (matcher.find()) {
      System.out.println(matcher.group());
    }
  }
}

7 Replies to “03.07 Reguläre Ausdrücke”

  1. user unknown

    Charakter-Klassen?

    Bei Marx gibt es den Klassencharakter, aber eine character-class würde ich entweder nicht übersetzen, oder mit Zeichenklasse.

    Zeichenklasse ist doch gut.

    Zurück zu Marx: Charaktermasken und Warencharakter gibt es da, aber Klassencharakter vielleicht doch nicht. 🙂

  2. Stefan Kiesel

    Hallo „user unknown“,

    Vielen Dank für den Hinweis 🙂 ! Charakter-Klasse war wohl wirklich ein bisschen unglücklich ausgedrückt. Ich habe den Namen in Zeichen-Klasse abgeändert.

    Gruß
    Stefan

  3. rt

    Warum wird hier nicht mit Patterns und Matchern gearbeitet? Ich kenne reguläre Ausdrücke bisher eigentlich nur in der Variante, habe mich jedoch noch nicht soviel damit beschäftigt. Wo liegt der Unterschied? Als Beispiel: im Prinzip könnte ich ja auch hier einfach die Wörter auftrennen (z.B. bei Leerzeichen) und danach mit bestimmten Wörtern vergleichen. Viele Grüße.

  4. Stefan Kiesel

    Hallo rt,

    Es wird doch mit Pattern und Matchern gearbeitet!?

    Der Unterschied liegt u. a. in der Einfachheit, wie die Ausdrücke verwendbar sind. Ein einfaches split, das sofort ein Ergebnis in Form eines Arrays zurückliefert, ist natürlich viel einfacher zu verwenden, als das Ganze über Pattern und Matcher zu realisieren. Zudem wird die split-Variante von Einsteigern vermutlich eher verstanden, als die mit Matcher und Pattern. Natürlich lässt sich auch nicht alles ohne Pattern und Matcher realisieren, weshalb diese Klassen (wie gesagt) auf der zweiten Seite angesprochen werden. Ein weiterer Vorteil bei der Verwendung von den „richtigen“ Klassen liegt darin, dass ein Performancegewinn bei komplexeren Ausdrücken erzielt wird, da die regulären Ausdrücke bereits vorkompiliert sind (wird auch auf der zweiten Seite erläutert).

    Letztendlich ist es wichtig beide Varianten zu kennen und ggf. auch zu können.

    Gruß
    Stefan

  5. javauser

    „^[\\w\\.=-]+@[\\w\\.-]+\\.[\\w]{2,4}$“;
    wieso riecht ^[\\w\\.=-] aus um alle längen von Wörtern abzufangen? Müsste da nicht wie oben beschrieben ein * stehen? für mich erlaubt es ^ ein zeichen . = – in einer beliebigen reihenfolge einmal.

  6. Stefan Kiesel

    Hallo javauser,

    zusätzlich zu dem Stern (*), gibt es auch noch das Plus (+). Dieses bedeutet, dass die vorangestellte Zeichen-Klasse beliebig oft vorkommen kann, aber mindestens einmal vorkommen muss. Bei einem Stern ist es aber auch erlaubt, dass die Klasse kein einziges Mal vorkommt.

    Ich hoffe der Unterschied und die Funktionsweise ist jetzt klarer!?

    Gruß
    Stefan

    Ps: Ich werde den Beitrag entsprechend ergänzen.

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.