Kapitel 4. Mustervergleiche mit regulären Ausdrücken

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

4.0 Einleitung

Angenommen, du bist seit ein paar Jahren im Internet unterwegs und speicherst deine gesamte Korrespondenz, nur für den Fall, dass du (oder deine Anwälte oder die Staatsanwaltschaft) eine Kopie brauchst. Das Ergebnis ist, dass du eine 5 GB große Festplattenpartition für deine gespeicherten E-Mails hast. Nehmen wir weiter an, du erinnerst dich daran, dass irgendwo darin eine E-Mail von jemandem namens Angie oder Anjie ist. Oder war es Angy? Aber du erinnerst dich nicht mehr daran, wie du sie genannt hast oder wo du sie gespeichert hast. Dann musst du natürlich danach suchen.

Aber während einige von euch versuchen, alle 15.000.000 Dokumente in einem Textverarbeitungsprogramm zu öffnen, finde ich es einfach mit einem einfachen Befehl. In jedem System, das reguläre Ausdrücke unterstützt, kann ich auf verschiedene Arten nach dem Muster suchen. Am einfachsten zu verstehen ist:

Angie|Anjie|Angy

was wahrscheinlich bedeutet, dass du einfach nach einer der Varianten suchen sollst. Eine prägnantere Form (mehr denken, weniger tippen) ist:

An[^ dn]

Die Syntax wird im Laufe dieses Kapitels klar werden. Kurz gesagt, das "A" und das "n" passen zueinander und finden Wörter, die mit "An" beginnen, während das kryptische [^ dn] verlangt, dass dem "An" ein anderes Zeichen folgt als(^ bedeutet in diesem Zusammenhang nicht ) ein Leerzeichen (um das sehr häufige englische Wort "an" am Anfang eines Satzes zu eliminieren) oder "d" (um das häufige Wort "und" zu eliminieren) oder "n" (um "Anne", "Ankündigung" usw. zu eliminieren). Ist dein Textverarbeitungsprogramm schon über seinen Startbildschirm hinausgekommen? Das macht nichts, denn ich habe die fehlende Datei bereits gefunden. Um die Antwort zu finden, habe ich einfach diesen Befehl eingegeben:

grep 'An[^ dn]' *

Reguläre Ausdrücke, oder kurz Regexe, ermöglichen eine präzise Spezifikation von Mustern, die in einem Text abgeglichen werden sollen. Man kann sich reguläre Ausdrücke als eine kleine Sprache zum Abgleichen von Zeichenmustern in Text, der in Zeichenketten enthalten ist, vorstellen. Eine API für reguläre Ausdrücke ist einInterpreterzum Abgleichen regulärer Ausdrücke.

Ein weiteres Beispiel für die Leistungsfähigkeit regulärer Ausdrücke ist das Problem der Massenaktualisierung hunderter Dateien. Als ich mit Java anfing, lautete die Syntax für die Deklaration von Array-Referenzen baseType arrayVariableName[]. Eine Methode mit einem Array-Argument, wie z. B. die Main-Methode eines jeden Programms, wurde üblicherweise so geschrieben:

public static void main(String args[]) {

Aber mit der Zeit wurde den Verwaltern der Java-Sprache klar, dass es besser wäre, sie als baseType[] arrayVariableName zu schreiben, etwa so:

public static void main(String[] args) {

Das ist der bessere Java-Stil, weil er die "Array-Eigenschaft" des Typs mit dem Typ selbst verbindet und nicht mit dem Namen des lokalen Arguments, und der Compiler akzeptiert immer noch beide Modi. Ich wollte alle Vorkommen von main, die auf die alte Art geschrieben wurden, in die neue Art ändern. Ich habe das Muster main(String [a-z] mit dem bereits beschriebenen grep-Dienstprogramm, um die Namen aller Dateien zu finden, die Main-Deklarationen im alten Stil enthalten (d.h. main(String, gefolgt von einem Leerzeichen und einem Namenszeichen anstelle einer offenen eckigen Klammer). Dann habe ich ein weiteres Regex-basiertes Unix-Werkzeug, den Stream-Editor sed, in einem kleinen Shell-Skript verwendet, um alle Vorkommen in diesen Dateien von main(String *([a-z][a-z]*)[] inmain(String[] $1 zu ändern (die hier verwendete Regex-Syntax wird später in diesem Kapitel erläutert). Auch hier war der Regex-basierte Ansatz um Größenordnungen schneller als die interaktive Bearbeitung, selbst mit einem einigermaßen leistungsfähigen Editor wie vi oder emacs, ganz zu schweigen von der Verwendung eines grafischen Textverarbeitungsprogramms.

In der Vergangenheit hat sich die Syntax von Regexen geändert, da sie in immer mehr Tools und Sprachen integriert werden. Die genaue Syntax in den vorherigen Beispielen entspricht also nicht genau der, die du in Java verwenden würdest, aber sie vermittelt die Prägnanz und Leistungsfähigkeit des Regex-Mechanismus.1

Ein drittes Beispiel ist das Parsen einer Apache-Webserver-Logdatei, bei der einige Felder durch Anführungszeichen, andere durch eckige Klammern und wieder andere durch Leerzeichen abgegrenzt sind. Ad-hoc-Code zu schreiben, um dies zu analysieren, ist in jeder Sprache unübersichtlich, aber eine gut formulierte Regex kann die Zeile in einem Arbeitsgang in alle Felder aufteilen, aus denen sie besteht (dieses Beispiel wird in Rezept 4.10 entwickelt).

Den gleichen Zeitgewinn können Java-Entwickler/innen erzielen. Die Unterstützung regulärer Ausdrücke ist seit langem in der Standard-Java-Laufzeitumgebung enthalten und gut integriert (z.B. gibt es Regex-Methoden in der Standardklasse und im neuen I/O-Paket), gibt es Regex-Methoden in der Standardklasse java.lang.String und im neuen I/O-Paket). Es gibt noch ein paar andere Regex-Pakete für Java, und du wirst gelegentlich auf Code stoßen, der sie verwendet, aber so gut wie jeder Code aus diesem Jahrhundert dürfte das eingebaute Paket verwenden. Die Syntax der Java-Regexe selbst wird inRezept 4.1 besprochen, und die Syntax der Java-API zur Verwendung von Regexen wird in Rezept 4.2 beschrieben. Die restlichen Rezepte zeigen einige Anwendungen der Regex-Technologie in Java.

Siehe auch

Mastering Regular Expressions von Jeffrey Friedl (O'Reilly) ist der maßgebliche Leitfaden für alle Details der regulären Ausdrücke. In den meisten Einführungsbüchern zu Unix und Perl werden Regexes erwähnt; Unix Power Tools von Mike Loukides, Tim O'Reilly, Jerry Peek und Shelley Powers (O'Reilly) widmet ihnen ein eigenes Kapitel.

4.1 Syntax der regulären Ausdrücke

Problem

Du musst die Syntax der regulären Ausdrücke in Java lernen.

Lösung

In Tabelle 4-1 findest du eine Liste der Zeichen für reguläre Ausdrücke.

Diskussion

Mit diesen Musterzeichen kannst du sehr mächtige Regexe erstellen. Beim Erstellen von Mustern kannst du jede beliebige Kombination aus normalem Text und den Metazeichen oder Sonderzeichen in Tabelle 4-1 verwenden. Diese können in jeder sinnvollen Kombination verwendet werden. Zum Beispiel steht a+ für eine beliebige Anzahl von Vorkommen des Buchstabens a, von eins bis zu einer Million oder einer Gazillion. Das Muster Mrs?\. passt zu Mr. oderMrs.. Und .* steht für ein beliebiges Zeichen, das beliebig oft vorkommt, und ist von der Bedeutung her ähnlich wie die meisten Befehlszeileninterpreter, die nur \* verwenden. Das Muster \d+ steht für eine beliebige Anzahl von numerischen Ziffern. \d{2,3} steht für eine zwei- oder dreistellige Zahl.

Tabelle 4-1. Syntax der Meta-Zeichen für reguläre Ausdrücke
Unterausdruck Streichhölzer Anmerkungen

Allgemein

\^

Anfang der Zeile/des Strings

$

Ende der Zeile/des Strings

\b

Wort-Grenze

\B

Nicht ein Wort Grenze

\A

Anfang des gesamten Strings

\z

Ende des gesamten Strings

\Z

Ende der gesamten Zeichenkette (mit Ausnahme des zulässigen letzten Zeilenabschlusses)

Siehe Rezept 4.9

.

Ein beliebiges Zeichen (mit Ausnahme des Zeilenabschlusses)

[…​]

"Charakterklasse"; ein beliebiger Charakter aus der Liste

[\^…​]

Ein beliebiges Zeichen, das nicht auf der Liste steht

Siehe Rezept 4.2

Abwechslung und Gruppierung

(…​)

Gruppierung (Erfassungsgruppen)

Siehe Rezept 4.3

|

Abwechslung

(?:_re_ )

Nicht-einfangende Klammer

\G

Ende des vorherigen Spiels

+\+n

Rückverweis auf die Nummer der Erfassungsgruppe n

Normale (gierige) Quantoren

{m,n }

Quantifizierer für von m bis n Wiederholungen

Siehe Rezept 4.4

{ m ,}

Quantifizierer für m oder mehr Wiederholungen

{ m }

Quantifizierer für genau m Wiederholungen

Siehe Rezept 4.10

{,n }

Quantifizierer für 0 bis zu n Wiederholungen

\*

Quantifizierer für 0 oder mehr Wiederholungen

Kurz für {0,}

+

Quantifizierer für 1 oder mehr Wiederholungen

Kurz für {1,}; siehe Rezept 4.2

?

Quantifizierer für 0 oder 1 Wiederholungen (d.h. genau einmal vorhanden oder gar nicht)

Kurz für {0,1}

Zurückhaltende (nicht-freudige) Quantoren

{m,n }?

Zögernder Quantifizierer für von m bis n Wiederholungen

{ m ,}?

Zurückhaltender Quantifizierer für m oder mehr Wiederholungen

{,n }?

Zurückhaltender Quantifizierer für 0 bis zu n Wiederholungen

\*?

Zögernder Quantifizierer: 0 oder mehr

+?

Zurückhaltender Quantifizierer: 1 oder mehr

Siehe Rezept 4.10

??

Zögernder Quantifizierer: 0 oder 1 Mal

Possessive (sehr gierige) Quantoren

{m,n }+

Possessivquantor für von m bis n Wiederholungen

{ m ,}+

Possessivquantor für m oder mehr Wiederholungen

{,n }+

Possessivquantor für 0 bis zu n Wiederholungen

\*+

Possessivquantor: 0 oder mehr

++

Possessivquantor: 1 oder mehr

?+

Possessivquantor: 0 oder 1 Mal

Fluchten und Abkürzungen

\

Escape-Zeichen (Anführungszeichen): schaltet die meisten Meta-Zeichen aus; macht nachfolgende Buchstaben zu Meta-Zeichen

\Q

Escape (Anführungszeichen) alle Zeichen bis zu \E

\E

Beendet die Zitate, die mit \Q

\t

Tabulatorzeichen

\r

Return (Wagenrücklauf) Zeichen

\n

Zeilenumbruchzeichen

Siehe Rezept 4.9

\f

Formularvorschub

\w

Charakter in einem Wort

Verwende \w+ für ein Wort; siehe Rezept 4.10

\W

Ein Nicht-Wort-Zeichen

\d

Numerische Ziffer

Verwende \d+ für eine Ganzzahl; siehe Rezept 4.2

\D

Ein nicht-ziffriges Zeichen

\s

Whitespace

Leerzeichen, Tabulator usw., wie von java.lang.Character.isWhitespace()

\S

Ein Zeichen, das kein Leerzeichen ist

Siehe Rezept 4.10

Unicode-Blöcke (repräsentative Beispiele)

\p{InGreek}

Ein Zeichen im griechischen Block

(Einfacher Block)

\P{InGreek}

Jedes Zeichen, das nicht im griechischen Block steht

\p{Lu}

Ein Großbuchstabe

(Einfache Kategorie)

\p{Sc}

Ein Währungssymbol

Zeichenklassen im POSIX-Stil (nur für US-ASCII definiert)

\p{Alnum}

Alphanumerische Zeichen

[A-Za-z0-9]

\p{Alpha}

Alphabetische Zeichen

[A-Za-z]

\p{ASCII}

Jedes ASCII-Zeichen

[\x00-\x7F]

\p{Blank}

Leerzeichen und Tabulatorzeichen

\p{Space}

Leerzeichen

[ \t\n\x0B\f\r]

\p{Cntrl}

Kontrollzeichen

[\x00-\x1F\x7F]

\p{Digit}

Numerische Zeichen

[0-9]

\p{Graph}

Druckbare und sichtbare Zeichen (keine Leerzeichen oder Steuerzeichen)

\p{Print}

Druckbare Zeichen

Dasselbe wie \p{Graph}

\p{Punct}

Interpunktionszeichen

Einer der !"#$%&'()\*+,-./:;<=>?@[]\^_`{|}\~

\p{Lower}

Kleinbuchstaben

[a-z]

\p{Upper}

Großbuchstaben

[A-Z]

\p{XDigit}

Hexadezimale Zahlenzeichen

[0-9a-fA-F]

Regexe passen auf jede mögliche Stelle in der Zeichenkette. Muster, die von gierigen Quantifizierern gefolgt werden (der einzige Typ, den es in den traditionellen Unix-Regexen gab), verbrauchen (passen) so viel wie möglich, ohne nachfolgende Unterausdrücke zu beeinträchtigen. Muster, die von Possessivquantoren gefolgt werden, passen so weit wie möglich, ohne die folgenden Unterausdrücke zu berücksichtigen. Muster, die von zurückhaltenden Quantifizierern gefolgt werden, verbrauchen so wenige Zeichen wie möglich, um trotzdem eine Übereinstimmung zu erzielen.

Im Gegensatz zu Regex-Paketen in anderen Sprachen wurde das Java Regex-Paket von Anfang an für den Umgang mit Unicode-Zeichen entwickelt. Die Standard-Java-Escape-Sequenz \u+nnnn wird verwendet, um ein Unicode-Zeichen im Muster anzugeben. Wir verwenden die Methoden von java.lang.Character, um die Eigenschaften von Unicode-Zeichen zu bestimmen, z. B. ob es sich bei einem bestimmten Zeichen um ein Leerzeichen handelt. Auch hier ist zu beachten, dass der Backslash verdoppelt werden muss, wenn es sich um eine Java-Zeichenkette handelt, die kompiliert wird, da der Compiler dies sonst als "Backslash-u" gefolgt von einigen Zahlen parsen würde.

Damit du lernst, wie Regexe funktionieren, stelle ich ein kleines Programm namens REDemo zur Verfügung.2 Der Code für REDemo ist zu lang, um ihn in das Buch aufzunehmen; im Online-Verzeichnis regex des darwinsys-api repo findest du REDemo.java, das du ausführen kannst, um zu erkunden, wie Regexe funktionieren.

Gib im obersten Textfeld (siehe Abbildung 4-1) das Regex-Muster ein, das du testen möchtest. Während du die einzelnen Zeichen eingibst, wird die Regex auf ihre Syntax geprüft; wenn die Syntax in Ordnung ist, siehst du ein Häkchen daneben. Dann kannst du "Übereinstimmen", "Suchen" oder "Alles suchen" auswählen. Übereinstimmen bedeutet, dass die gesamte Zeichenfolge mit der Regex übereinstimmen muss, und Suchen bedeutet, dass die Regex irgendwo in der Zeichenfolge gefunden werden muss (Alles suchen zählt die Anzahl der gefundenen Vorkommen). Darunter gibst du eine Zeichenfolge ein, mit der die Regex übereinstimmen soll. Experimentiere nach Lust und Laune. Wenn du die Regex so hast, wie du sie haben willst, kannst du sie in dein Java-Programm einfügen. Du musst alle Zeichen, die sowohl vom Java-Compiler als auch vom Java-Regex-Paket besonders behandelt werden, wie den Backslash selbst, doppelte Anführungszeichen und andere, mit einem Escape (Backslash) versehen. Wenn du eine Regex nach deinen Vorstellungen erstellt hast, kannst du sie mit der Schaltfläche Kopieren (auf diesen Screenshots nicht zu sehen) in die Zwischenablage exportieren - je nachdem, wie du sie verwenden möchtest, mit oder ohne Backslash-Verdopplung.

Tipp

Erinnere dich daran, dass eine Regex als Zeichenkette eingegeben wird, die von einem Java-Compiler kompiliert wird. Deshalb brauchst du in der Regel zwei Escape-Ebenen für alle Sonderzeichen, einschließlich Backslash und Anführungszeichen. Zum Beispiel die Regex (die die doppelten Anführungszeichen enthält):

"You said it\."

muss so typisiert sein, um eine gültige Java-Sprache zur Kompilierzeit zu sein String:

String pattern = "\"You said it\\.\""

In Java 14+ kannst du auch einen Textblock verwenden, um die Anführungszeichen zu umgehen:

String pattern = """
	"You said it\\.""""

Ich kann dir gar nicht sagen, wie oft ich schon den Fehler gemacht habe, den zusätzlichen Backslash in \d+, \w+ und Konsorten zu vergessen!

In Abbildung 4-1 habe ich qu in das Musterfeld des Programms REDemo eingegeben, was ein syntaktisch gültiges Regex-Muster ist: Alle gewöhnlichen Zeichen stehen als Regex für sich selbst, also wird nach dem Buchstaben q gefolgt von u gesucht. In der oberen Version habe ich nur ein q in die Zeichenfolge eingegeben, das nicht gefunden wird. In der zweiten Version habe ich quack und den q eines zweiten quack eingegeben. Da ich Alle suchen ausgewählt habe, zeigt die Zählung eine Übereinstimmung an. Sobald ich die zweite u eingebe, wird die Anzahl auf zwei aktualisiert, wie in der dritten Version zu sehen ist.

Regexe können weit mehr als nur Zeichen abgleichen. Die Zwei-Zeichen-Regex ^T würde zum Beispiel auf den Anfang einer Zeile (^) passen, die unmittelbar von einem großen T gefolgt wird - also auf jede Zeile, die mit einem großen T beginnt.

Aber hier sind wir nicht sehr weit voraus. Haben wir wirklich so viel Mühe in die Regex-Technologie investiert, nur um das tun zu können, was wir bereits mit der Methode java.lang.String startsWith() tun konnten? Hmmm, ich kann hören, wie einige von euch unruhig werden. Bleibt auf euren Plätzen! Was wäre, wenn du nicht nur einen Buchstaben T an der ersten Stelle finden wolltest, sondern auch einen Vokal direkt danach, gefolgt von einer beliebigen Anzahl von Buchstaben in einem Wort, gefolgt von einem Ausrufezeichen? Sicherlich könntest du das in Java tun, indem du startsWith("T") und charAt(1) == 'a' || charAt(1) == 'e' überprüfst, und so weiter? Ja, aber bis du das getan hast, hättest du eine Menge hochspezialisierten Code geschrieben, den du in keiner anderen Anwendung verwenden könntest. Mit regulären Ausdrücken kannst du einfach das Muster ^T[aeiou]\w*! angeben. Das heißt, ^ und T wie zuvor, gefolgt von einer Zeichenklasse, die die Vokale auflistet, gefolgt von einer beliebigen Anzahl von Wortzeichen (\w*), gefolgt von einem Ausrufezeichen.

jcb4 0401
Abbildung 4-1. REDemo mit einfachen Beispielen

"Aber warte, da ist noch mehr!", wie mein verstorbener, großartiger ChefYuri Rubinsky zu sagen pflegte. Was ist, wenn du das Muster, nach dem du suchst, zur Laufzeit ändern möchtest? Erinnerst du dich an den Java-Code, den du gerade geschrieben hast, um T in Spalte 1 zu finden, plus einen Vokal, einige Wortzeichen und ein Ausrufezeichen? Nun, es ist an der Zeit, ihn wegzuwerfen. Denn heute Morgen müssen wir Q abgleichen, gefolgt von einem anderen Buchstaben als u, gefolgt von einer Reihe von Ziffern, gefolgt von einem Punkt. Während einige von euch anfangen, eine neue Funktion dafür zu schreiben, schlendern wir anderen einfach zur RegEx Bar & Grille, bestellen beim Barkeeper ein ^Q[^u]\d+\.. und machen uns auf den Weg.

OK, wenn du eine Erklärung willst: [^u] bedeutet, dass ein beliebiges Zeichen übereinstimmt, das nicht das Zeichen u ist. Das \d+ bedeutet eine oder mehrere numerische Ziffern. + ist ein Quantifizierer, der ein oder mehrere Vorkommen dessen bedeutet, was er folgt, und \d ist eine beliebige numerische Ziffer. \d+ bedeutet also eine Zahl mit einer, zwei oder mehr Ziffern. Und schließlich das \.? Nun, . ist an sich ein Meta-Zeichen. Die meisten einzelnen Metazeichen werden ausgeschaltet, indem ihnen ein Escape-Zeichen vorangestellt wird. Natürlich nicht mit der Esc-Taste auf deiner Tastatur. Das Escape-Zeichen der Regex ist der Backslash. Wenn du einem Metazeichen wie . dieses Escape-Zeichen voranstellst, wird seine besondere Bedeutung ausgeschaltet, so dass wir nach einem buchstäblichen Punkt und nicht nach einem beliebigen Zeichen suchen. Wenn du einigen ausgewählten alphabetischen Zeichen (z. B. n, r, t, s, w) ein Escape-Zeichen voranstellst, werden sie zu Metazeichen. Abbildung 4-2 zeigt die ^Q[^u]\d+\..Regex in Aktion. Im ersten Bild habe ich einen Teil der Regex als ^Q[^u eingegeben. Da die eckige Klammer nicht geschlossen ist, ist das Syntax-OK-Flag ausgeschaltet; wenn ich die Regex vervollständige, wird es wieder eingeschaltet. Im zweiten Bild habe ich die Regex fertig getippt und den Datenstring als QA577 eingegeben (du solltest erwarten, dass er mit $$^Q[^u]\d+$$ übereinstimmt, aber nicht mit dem Punkt, da ich ihn nicht getippt habe). Im dritten Frame habe ich den Punkt eingegeben, sodass das Flag Übereinstimmungen auf Ja gesetzt ist.

jcb4 0402
Abbildung 4-2. REDemo mit "Q nicht gefolgt von u" Beispiel

Da beim Einfügen der Regex in Java-Code Backslashes escaped werden müssen, gibt es in der aktuellen Version von REDemo sowohl einen Copy Pattern Button, der die Regex wortwörtlich für die Verwendung in der Dokumentation und in Unix-Befehlen kopiert, als auch einen Copy Pattern Backslashed Button, der die Regex mit verdoppelten Backslashes in die Zwischenablage kopiert, um sie in Java-Strings einzufügen.

Inzwischen solltest du zumindest ein Grundverständnis dafür haben, wie Regexe in der Praxis funktionieren. Der Rest dieses Kapitels enthält weitere Beispiele und erklärt einige der mächtigeren Themen, wie z. B. Capture-Gruppen. Wie Regexe in der Theorie funktionieren - und es gibt eine Menge theoretischer Details und Unterschiede zwischen den Regex-Varianten -, erfährst du in Mastering Regular Expressions. In der Zwischenzeit wollen wir lernen, wie man Java-Programme schreibt, die reguläre Ausdrücke verwenden.

4.2 Regexe in Java verwenden: Auf ein Muster testen

Problem

Du bist bereit, die Verarbeitung regulärer Ausdrücke zu nutzen, um deinen Java-Code zu verbessern, indem du prüfst, ob ein bestimmtes Muster in einer bestimmten Zeichenkette vorkommt.

Lösung

Verwende das Java Regular Expressions Package, java.util.regex.

Diskussion

Die gute Nachricht ist, dass die Java-API für Regexe wirklich einfach zu benutzen ist. Wenn du nur herausfinden willst, ob eine bestimmte Regex mit einer Zeichenkette übereinstimmt, kannst du die praktische Methode boolean matches() der Klasse String verwenden, die ein Regex-Muster in String Form als Argument akzeptiert:

if (inputString.matches(stringRegexPattern)) {
    // it matched... do something with it...
}

Dies ist jedoch eine Komfortroutine, und Komfort hat immer seinen Preis. Wenn die Regex mehr als ein- oder zweimal in einem Programm verwendet werden soll, ist es effizienter, eine Pattern und ihre Matcher(s) zu erstellen und zu verwenden. Hier wird ein komplettes Programm gezeigt, das eine Pattern konstruiert und sie für match verwendet:

public class RESimple {
    public static void main(String[] argv) {
        String pattern = "^Q[^u]\\d+\\.";
        String[] input = {
            "QA777. is the next flight. It is on time.",
            "Quack, Quack, Quack!"
        };

        Pattern p = Pattern.compile(pattern);

        for (String in : input) {
            boolean found = p.matcher(in).lookingAt();

            System.out.println("'" + pattern + "'" +
            (found ? " matches '" : " doesn't match '") + in + "'");
        }
    }
}

Das Paket java.util.regex enthält zwei Klassen, Pattern und Matcher, die die in Beispiel 4-1 gezeigte öffentliche API bereitstellen.

Beispiel 4-1. Regex öffentliche API
/**
 * The main public API of the java.util.regex package.
 */

package java.util.regex;

public final class Pattern {
    // Flags values ('or' together)
    public static final int
        UNIX_LINES, CASE_INSENSITIVE, COMMENTS, MULTILINE,
        DOTALL, UNICODE_CASE, CANON_EQ;
    // No public constructors; use these Factory methods
    public static Pattern compile(String patt);
    public static Pattern compile(String patt, int flags);
    // Method to get a Matcher for this Pattern
    public Matcher matcher(CharSequence input);
    // Information methods
    public String pattern();
    public int flags();
    // Convenience methods
    public static boolean matches(String pattern, CharSequence input);
    public String[] split(CharSequence input);
    public String[] split(CharSequence input, int max);
}

public final class Matcher {
    // Action: find or match methods
    public boolean matches();
    public boolean find();
    public boolean find(int start);
    public boolean lookingAt();
    // "Information about the previous match" methods
    public int start();
    public int start(int whichGroup);
    public int end();
    public int end(int whichGroup);
    public int groupCount();
    public String group();
    public String group(int whichGroup);
    // Reset methods
    public Matcher reset();
    public Matcher reset(CharSequence newInput);
    // Replacement methods
    public Matcher appendReplacement(StringBuffer where, String newText);
    public StringBuffer appendTail(StringBuffer where);
    public String replaceAll(String newText);
    public String replaceFirst(String newText);
    // information methods
    public Pattern pattern();
}

/* String, showing only the RE-related methods */
public final class String {
    public boolean matches(String regex);
    public String replaceFirst(String regex, String newStr);
    public String replaceAll(String regex, String newStr);
    public String[] split(String regex);
    public String[] split(String regex, int max);
}

Diese API ist so umfangreich, dass sie einige Erklärungen erfordert. Dies sind die normalen Schritte für den Regex-Abgleich in einem Produktionsprogramm:

  1. Erstelle eine Pattern, indem du die statische Methode Pattern.compile() aufrufst.

  2. Fordere ein Matcher aus dem Muster an, indem du pattern.matcher(CharSequence) für jedes String (oder andere CharSequence) aufrufst, das du durchsehen möchtest.

  3. Rufe (einmal oder mehrmals) eine der Finder-Methoden (die später in diesem Abschnitt besprochen werden) in der resultierenden Matcher auf.

Die Schnittstelle java.lang.CharSequence ermöglicht einen einfachen Nur-Lese-Zugriff auf Objekte, die eine Sammlung von Zeichen enthalten. Die Standardimplementierungen sind String und StringBuffer/StringBuilder (beschrieben in Kapitel 3) sowie die neue I/O-Klasse java.nio.CharBuffer.

Natürlich kannst du den Regex-Abgleich auch auf andere Art und Weise durchführen, z. B. mit den Komfortmethoden in Pattern oder sogar in java.lang.String, wie hier:

public class StringConvenience {
    public static void main(String[] argv) {

        String pattern = ".*Q[^u]\\d+\\..*";
        String line = "Order QT300. Now!";
        if (line.matches(pattern)) {
            System.out.println(line + " matches \"" + pattern + "\"");
        } else {
            System.out.println("NO MATCH");
        }
    }
}

Aber die dreistufige Liste ist das Standardmuster für den Abgleich. Du würdest die String Routine wahrscheinlich in einem Programm verwenden, das die Regex nur einmal verwendet; wenn die Regex mehr als einmal verwendet wird, lohnt es sich, die Zeit für die Kompilierung zu nehmen, weil die kompilierte Version schneller läuft.

Außerdem hat Matcher mehrere Finder-Methoden, die mehr Flexibilität bieten als dieString Komfortroutine match(). Dies sind die Matcher Methoden:

match()

Wird verwendet, um die gesamte Zeichenkette mit dem Muster zu vergleichen; dies ist die gleiche Routine wie in java.lang.String. Da sie die gesamte String abgleicht, musste ich .*vor und nach dem Muster einfügen.

lookingAt()

Wird verwendet, um das Muster nur am Anfang der Zeichenkette zu finden.

find()

Wird verwendet, um das Muster in der Zeichenkette abzugleichen (nicht notwendigerweise am ersten Zeichen der Zeichenkette), beginnend am Anfang der Zeichenkette oder, wenn die Methode zuvor aufgerufen wurde und erfolgreich war, am ersten Zeichen, das bei der vorherigen Übereinstimmung nicht gefunden wurde.

Jede dieser Methoden gibt boolean zurück, wobei true für eine Übereinstimmung und false für keine Übereinstimmung steht. Um zu prüfen, ob eine bestimmte Zeichenkette mit einem bestimmten Muster übereinstimmt, musst du nur etwas wie das Folgende eingeben:

Matcher m = Pattern.compile(patt).matcher(line);
if (m.find( )) {
    System.out.println(line + " matches " + patt)
}

Aber vielleicht möchtest du auch den Text extrahieren, der übereinstimmt, was das Thema des nächsten Rezepts ist.

Die folgenden Rezepte behandeln die Verwendung der Matcher-API. Zunächst verwenden die Beispiele nur Argumente des Typs String als Eingabequelle. Die Verwendung von anderen CharSequence Typen wird in Rezept 4.5 behandelt.

4.3 Den übereinstimmenden Text finden

Problem

Du musst den Text finden, auf den die Regex passt.

Lösung

Manchmal musst du mehr wissen als nur, ob eine Regex auf eine Zeichenfolge passt. In Editoren und vielen anderen Tools willst du genau wissen, welche Zeichen gefunden wurden. Erinnere dich daran, dass bei Quantifizierern wie * die Länge des übereinstimmenden Textes nicht unbedingt mit der Länge des übereinstimmenden Musters übereinstimmt. Unterschätze nicht das mächtige.*, das gerne Tausende oder Millionen von Zeichen abgleicht, wenn man es zulässt. Wie du im vorigen Rezept gesehen hast, kannst du herausfinden, ob ein bestimmter Treffer erfolgreich war, indem dufind() oder matches() verwendest. Aber in anderen Anwendungen möchtest du die Zeichen erhalten, auf die das Muster passt.

Nach einem erfolgreichen Aufruf einer der vorangegangenen Methoden kannst du diese Informationsmethoden auf Matcher verwenden, um Informationen über das Spiel zu erhalten:

start(), end()

Gibt die Zeichenposition in der Zeichenkette der übereinstimmenden Anfangs- und Endzeichen zurück.

groupCount()

Gibt die Anzahl der eingeklammerten Fanggruppen zurück, falls vorhanden; gibt 0 zurück, wenn keine Gruppen verwendet wurden.

group(int i)

Gibt die übereinstimmenden Zeichen der Gruppe i des aktuellen Treffers übereinstimmen, wenni größer oder gleich Null und kleiner oder gleich dem Rückgabewert von groupCount() ist. Gruppe 0 ist der gesamte Treffer, also gibt group(0) (oder nur group()) den gesamten Teil der Eingabe zurück, der übereinstimmt.

Das Konzept der Klammern oder Fanggruppen ist ein zentraler Bestandteil der Regex-Verarbeitung. Regexe können beliebig komplex verschachtelt werden. Mit der Methode group(int) kannst du die Zeichen abrufen, die mit einer bestimmten Klammergruppe übereinstimmen. Wenn du keine expliziten Klammern verwendet hast, kannst du einfach alle übereinstimmenden Zeichen als Stufe Null behandeln. Beispiel 4-2 zeigt einen Teil von REMatch.java.

Beispiel 4-2. Teil von main/src/main/java/regex/REMatch.java
public class REmatch {
    public static void main(String[] argv) {

        String patt = "Q[^u]\\d+\\.";
        Pattern r = Pattern.compile(patt);
        String line = "Order QT300. Now!";
        Matcher m = r.matcher(line);
        if (m.find()) {
            System.out.println(patt + " matches \"" +
                m.group(0) +
                "\" in \"" + line + "\"");
        } else {
            System.out.println("NO MATCH");
        }
    }
}

Wenn sie ausgeführt wird, wird sie gedruckt:

Q[\^u]\d+\. matches "QT300." in "Order QT300. Now!"

Wenn die Schaltfläche Match aktiviert ist, zeigt REDemo alle Capture-Gruppen in einer bestimmten Regex an; ein Beispiel ist inAbbildung 4-3 zu sehen.

jcb4 0403
Abbildung 4-3. REDemo in Aktion

Es ist auch möglich, die Anfangs- und Endindizes und die Länge des Textes, auf den das Muster passt, zu erhalten (erinnere dich daran, dass Begriffe mit Quantifizierern, wie \d+ in diesem Beispiel, auf eine beliebige Anzahl von Zeichen in der Zeichenkette passen können). Du kannst diese in Verbindung mit den Methoden von String.substring() wie folgt verwenden:

        String patt = "Q[^u]\\d+\\.";
        Pattern r = Pattern.compile(patt);
        String line = "Order QT300. Now!";
        Matcher m = r.matcher(line);
        if (m.find()) {
            System.out.println(patt + " matches \"" +
                line.substring(m.start(0), m.end(0)) +
                "\" in \"" + line + "\"");
        } else {
            System.out.println("NO MATCH");
        }

Angenommen, du musst mehrere Elemente aus einer Zeichenkette extrahieren. Wenn die Eingabe

Smith, John
Adams, John Quincy

und du willst aussteigen

John Smith
John Quincy Adams

benutze einfach das Folgende:

public class REmatchTwoFields {
    public static void main(String[] args) {
        String inputLine = "Adams, John Quincy";
        // Construct an RE with parens to "grab" both field1 and field2
        Pattern r = Pattern.compile("(.*), (.*)");
        Matcher m = r.matcher(inputLine);
        if (!m.matches())
            throw new IllegalArgumentException("Bad input");
        System.out.println(m.group(2) + ' ' + m.group(1));
    }
}

4.4 Ersetzen des übereinstimmenden Textes

Problem

Nachdem du einen Text mit Hilfe eines Musters gefunden hast, möchtest du ihn durch einen anderen Text ersetzen, ohne den Rest der Zeichenfolge zu verändern.

Lösung

Wie wir im vorigen Rezept gesehen haben, können Regex-Muster mit Quantifizierern eine Menge Zeichen mit sehr wenigen Metazeichen übereinstimmen. Wir brauchen eine Möglichkeit, den Text, auf den die Regex zutrifft, zu ersetzen, ohne andere Texte davor oder danach zu verändern. Wir könnten das manuell mit der Methode String substring() machen. Da dies jedoch eine so häufige Anforderung ist, bietet die Java-API für reguläre Ausdrücke einige Ersetzungsmethoden.

Diskussion

Die Klasse Matcher bietet mehrere Methoden, um nur den Text zu ersetzen, der mit dem Muster übereinstimmt. Bei all diesen Methoden übergibst du den Ersetzungstext oder die "rechte Seite" der Ersetzung (dieser Begriff ist historisch bedingt: Bei dem Ersetzungsbefehl eines Texteditors ist die linke Seite das Muster und die rechte Seite der Ersetzungstext). Dies sind die Ersetzungsmethoden:

replaceAll(newString)

Ersetzt alle Vorkommen, die übereinstimmen, durch die neue Zeichenfolge

replaceFirst(newString)

Wie oben, aber nur das erste Vorkommen

appendReplacement(StringBuffer, newString)

Kopien bis vor dem ersten Spiel, plus die angegebene newString

appendTail(StringBuffer)

Fügt Text nach der letzten Übereinstimmung an (wird normalerweise nach appendReplacement verwendet)

Trotz ihrer Namen verhalten sich die Methoden von replace* im Einklang mit der Unveränderlichkeit von Strings (siehe "Zeitlos, unveränderlich und unveränderbar"): Sie erstellen ein neues String Objekt mit der durchgeführten Ersetzung; sie ändern die Zeichenkette, auf die sich das Matcher Objekt bezieht, nicht (und können sie auch nicht ändern).

Beispiel 4-3 zeigt die Anwendung dieser drei Methoden.

Beispiel 4-3. main/src/main/java/regex/ReplaceDemo.java
/**
 * Quick demo of RE substitution: correct U.S. 'favor'
 * to Canadian/British 'favour', but not in "favorite"
 * @author Ian F. Darwin, http://www.darwinsys.com/
 */
public class ReplaceDemo {
    public static void main(String[] argv) {

        // Make an RE pattern to match as a word only (\b=word boundary)
        String patt = "\\bfavor\\b";

        // A test input
        String input = "Do me a favor? Fetch my favorite.";
        System.out.println("Input: " + input);

        // Run it from a RE instance and see that it works
        Pattern r = Pattern.compile(patt);
        Matcher m = r.matcher(input);
        System.out.println("ReplaceAll: " + m.replaceAll("favour"));

        // Show the appendReplacement method
        m.reset();
        StringBuffer sb = new StringBuffer();
        System.out.print("Append methods: ");
        while (m.find()) {
            // Copy to before first match,
            // plus the word "favor"
            m.appendReplacement(sb, "favour");
        }
        m.appendTail(sb);        // copy remainder
        System.out.println(sb.toString());
    }
}

Wenn du es ausführst, tut es genau das, was wir erwarten:

Input: Do me a favor? Fetch my favorite.
ReplaceAll: Do me a favour? Fetch my favorite.
Append methods: Do me a favour? Fetch my favorite.

Die Methode replaceAll() behandelt den Fall, dass die gleiche Änderung in der gesamten Zeichenkette vorgenommen werden soll. Wenn du jedes übereinstimmende Vorkommen in einen anderen Wert ändern willst, kannst du replaceFirst() in einer Schleife verwenden, wie in Beispiel 4-4. In diesem Beispiel wird die gesamte Zeichenkette durchlaufen, wobei jedes Vorkommen von cat oder dog in feline oder canine umgewandelt wird. Dies ist eine Vereinfachung eines realen Beispiels, bei dem nach bit.ly-URLs gesucht und diese durch die tatsächliche URL ersetzt wurden; die Methode computeReplacement verwendete den Netzwerk-Client-Code aus Rezept 12.1.

Beispiel 4-4. main/src/main/java/regex/ReplaceMulti.java
/**
 * To perform multiple distinct substitutions in the same String,
 * you need a loop, and must call reset() on the matcher.
 */
public class ReplaceMulti {
    public static void main(String[] args) {

        Pattern patt = Pattern.compile("cat|dog");
        String line = "The cat and the dog never got along well.";
        System.out.println("Input: " + line);
        Matcher matcher = patt.matcher(line);
        while (matcher.find()) {
            String found = matcher.group(0);
            String replacement = computeReplacement(found);
            line = matcher.replaceFirst(replacement);
            matcher.reset(line);
        }
        System.out.println("Final: " + line);
    }

    static String computeReplacement(String in) {
        switch(in) {
        case "cat": return "feline";
        case "dog": return "canine";
        default: return "animal";
        }
    }
}

Wenn du auf Teile des Vorkommens verweisen musst, die mit der Regex übereinstimmen, kannst du sie mit zusätzlichen Klammern im Muster markieren und auf den übereinstimmenden Teil mit $1, $2 usw. in der Ersetzungszeichenfolge verweisen.Beispiel 4-5 verwendet dies, um zwei Felder zu vertauschen, in diesem Fall, um Namen in der Form Firstname Lastname in Lastname, FirstName zu verwandeln.

Beispiel 4-5. main/src/main/java/regex/ReplaceDemo2.java
public class ReplaceDemo2 {
    public static void main(String[] argv) {

        // Make an RE pattern 
        String patt = "(\\w+)\\s+(\\w+)";

        // A test input
        String input = "Ian Darwin";
        System.out.println("Input: " + input);

        // Run it from a RE instance and see that it works
        Pattern r = Pattern.compile(patt);
        Matcher m = r.matcher(input);
        m.find();
        System.out.println("Replaced: " + m.replaceFirst("$2, $1"));
        
        // The short inline version:
        // System.out.println(input.replaceFirst("(\\w+)\\s+(\\w+)", "$2, $1"));
    }
}

4.5 Alle Vorkommnisse eines Musters drucken

Problem

Du musst alle Zeichenfolgen finden, die mit einer bestimmten Regex in einer oder mehreren Dateien oder anderen Quellen übereinstimmen.

Lösung

Dieses Beispiel liest eine Datei Zeile für Zeile durch. Jedes Mal, wenn eine Übereinstimmung gefunden wird, extrahiere ich sie aus der line und gebe sie aus.

Dieser Code nimmt die Methoden group() aus Rezept 4.3, die Methode substring aus der Schnittstelle CharacterIterator und die Methode match() aus dem Regex und fügt sie einfach alle zusammen. Ich habe ihn so kodiert, dass er alle Namen aus einer bestimmten Datei extrahiert; wenn das Programm durchläuft, gibt es die Wörter import, java, until, regex usw. jeweils in einer eigenen Zeile aus:

C:\> java ReaderIter.java ReaderIter.java
import
java
util
regex
import
java
io
Print
all
the
strings
that
match
given
pattern
from
file
public
...
C:\\>

Ich habe sie hier unterbrochen, um Papier zu sparen. Dies kann auf zwei Arten geschrieben werden: ein zeilenweises Muster, wie in Beispiel 4-6 gezeigt, und eine kompaktere Form, die neue E/A verwendet, wie in Beispiel 4-7 gezeigt (das neue E/A-Paket, das in beiden Beispielen verwendet wird, wird in Kapitel 10 beschrieben).

Beispiel 4-6. main/src/main/java/regex/ReaderIter.java
public class ReaderIter {
    public static void main(String[] args) throws IOException {
        // The RE pattern
        Pattern patt = Pattern.compile("[A-Za-z][a-z]+");
        // See the I/O chapter
        // For each line of input, try matching in it.
        Files.lines(Path.of(args[0])).forEach(line -> {
            // For each match in the line, extract and print it.
            Matcher m = patt.matcher(line);
            while (m.find()) {
                // Simplest method:
                // System.out.println(m.group(0));

                // Get the starting position of the text
                int start = m.start(0);
                // Get ending position
                int end = m.end(0);
                // Print whatever matched.
                // Use CharacterIterator.substring(offset, end);
                System.out.println(line.substring(start, end));
            }
        });
    }
}
Beispiel 4-7. main/src/main/java/regex/GrepNIO.java
public class GrepNIO {
    public static void main(String[] args) throws IOException {

        if (args.length < 2) {
            System.err.println("Usage: GrepNIO patt file [...]");
            System.exit(1);
        }

        Pattern p=Pattern.compile(args[0]);
        for (int i=1; i<args.length; i++)
            process(p, args[i]);
    }

    static void process(Pattern pattern, String fileName) throws IOException {

        // Get a FileChannel from the given file
        FileInputStream fis = new FileInputStream(fileName);
        FileChannel fc = fis.getChannel();

        // Map the file's content
        ByteBuffer buf = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());

        // Decode ByteBuffer into CharBuffer
        CharBuffer cbuf =
            Charset.forName("ISO-8859-1").newDecoder().decode(buf);

        Matcher m = pattern.matcher(cbuf);
        while (m.find()) {
            System.out.println(m.group(0));
        }
        fis.close();
    }
}

Die in Beispiel 4-7 gezeigte nicht-blockierende E/A (NIO)-Version beruht auf der Tatsache, dass ein NIO Buffer als CharSequence verwendet werden kann. Dieses Programm ist allgemeiner, da das Musterargument aus dem Befehlszeilenargument übernommen wird. Es gibt die gleiche Ausgabe wie das vorherige Beispiel aus, wenn es mit dem Musterargument aus dem vorherigen Programm auf der Befehlszeile aufgerufen wird:

java regex.GrepNIO "[A-Za-z][a-z]+"  ReaderIter.java

Du könntest \w+ als Muster verwenden; der einzige Unterschied ist, dass mein Muster nach wohlgeformten, großgeschriebenen Wörtern sucht, während \w+ Java-zentrische Merkwürdigkeiten wie theVariableName einschließen würde, die Großbuchstaben an nicht standardmäßigen Positionen haben.

Beachte auch, dass die NIO-Version wahrscheinlich effizienter ist, weil sie Matcher nicht bei jeder Eingabezeile auf eine neue Eingabequelle zurücksetzt, wie es ReaderIter tut.

4.6 Zeilen mit einem Muster drucken

Problem

Du musst in einer oder mehreren Dateien nach Zeilen suchen, die einer bestimmten Regex entsprechen.

Lösung

Schreibe ein einfaches grep-ähnliches Programm.

Diskussion

Wie ich bereits erwähnt habe, kannst du, sobald du ein Regex-Paket hast, ein grep-ähnliches Programm schreiben. Ich habe bereits ein Beispiel für das Unix-Programm grep ( ) gegeben. grep wird mit einigen optionalen Argumenten aufgerufen, gefolgt von einem erforderlichen Muster für einen regulären Ausdruck, gefolgt von einer beliebigen Anzahl von Dateinamen. Es gibt jede Zeile aus, die das Muster enthält, im Gegensatz zu Rezept 4.5, das nur den übereinstimmenden Text selbst ausgibt. Hier ist ein Beispiel:

grep "[dD]arwin" *.txt 

Der Code sucht nach Zeilen, die entweder darwin oder Darwin enthalten, in jeder Zeile jeder Datei, deren Name auf .txt endet.3 Beispiel 4-8 ist der Quellcode für die erste Version eines Programms, das dies tut, namens Grep0. Es liest Zeilen von der Standardeingabe und nimmt keine optionalen Argumente entgegen, aber es verarbeitet alle regulären Ausdrücke, die die Klasse Pattern implementiert (es ist also nicht identisch mit den gleichnamigen Unix-Programmen). Wir haben das Paket java.io für die Ein- und Ausgabe noch nicht behandelt (siehe Kapitel 10), aber unsere Verwendung hier ist so einfach, dass du sie wahrscheinlich intuitiv nachvollziehen kannst. Die Online-Quelle enthält Grep1, das dasselbe tut, aber besser strukturiert (und deshalb länger) ist. Später in diesem Kapitel wird in Rezept 4.11 ein JGrep-Programm vorgestellt, das eine Reihe von Kommandozeilenoptionen parst.

Beispiel 4-8. main/src/main/java/regex/Grep0.java
public class Grep0 {
    public static void main(String[] args) throws IOException {
        BufferedReader is =
            new BufferedReader(new InputStreamReader(System.in));
        if (args.length != 1) {
            System.err.println("Usage: MatchLines pattern");
            System.exit(1);
        }
        Pattern patt = Pattern.compile(args[0]);
        Matcher matcher = patt.matcher("");
        String line = null;
        while ((line = is.readLine()) != null) {
            matcher.reset(line);
            if (matcher.find()) {
                System.out.println("MATCH: " + line);
            }
        }
    }
}

4.7 Groß- und Kleinschreibung in regulären Ausdrücken kontrollieren

Problem

Du willst den Text unabhängig von der Groß- und Kleinschreibung finden.

Lösung

Kompiliere Pattern und füge das Argument flags Pattern.CASE_INSENSITIVE hinzu , um anzugeben, dass der Abgleich unabhängig von der Groß- und Kleinschreibung erfolgen soll (d.h., dass die Groß- und Kleinschreibung ignoriert werden soll). Wenn dein Code in verschiedenen Sprachumgebungen laufen könnte (sieheRezept 3.12), solltest du Pattern.UNICODE_CASE hinzufügen. Ohne diese Flags ist der normale Abgleich unter Berücksichtigung der Groß- und Kleinschreibung die Standardeinstellung. Dieses Flag (und andere) werden an die Pattern.compile() Methode übergeben, etwa so:

// regex/CaseMatch.java
Pattern  reCaseInsens = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE |
    Pattern.UNICODE_CASE);
reCaseInsens.matches(input);        // will match case-insensitively

Dieses Flag muss bei der Erstellung von Pattern übergeben werden. Da Pattern Objekte unveränderlich sind, können sie nach der Erstellung nicht mehr verändert werden.

Der vollständige Quellcode für dieses Beispiel ist online als CaseMatch.java verfügbar.

4.8 Übereinstimmende akzentuierte oder zusammengesetzte Zeichen

Problem

Du möchtest, dass die Zeichen unabhängig von der Form, in der sie eingegeben werden, übereinstimmen.

Lösung

Kompiliere die Pattern mit dem flags Argument Pattern.CANON_EQ für kanonische Gleichheit.

Diskussion

Zusammengesetzte Zeichen können in verschiedenen Formen eingegeben werden. Nehmen wir als einziges Beispiel den Buchstaben e mit einem akuten Akzent. Dieses Zeichen kann in verschiedenen Formen im Unicode-Text vorkommen, z. B. als einzelnes Zeichen é (Unicode-Zeichen \u00e9) oder als Zwei-Zeichen-Folge (e gefolgt vom Unicode-Kombinationsakzent, \u0301). Damit du solche Zeichen unabhängig davon finden kannst, welche der möglicherweise mehreren vollständig zerlegten Formen zur Eingabe verwendet werden, verfügt das regex-Paket über eine Option für canonical matching, die jede der Formen als gleichwertig behandelt. Diese Option wird aktiviert, indem du CANON_EQ als (eines) der Flags im zweiten Argument von Pattern.compile() übergibst. Dieses Programm zeigt, wie CANON_EQ verwendet wird, um mehrere Formen abzugleichen:

public class CanonEqDemo {
    public static void main(String[] args) {
        String pattStr = "\u00e9gal"; // egal
        String[] input = {
                "\u00e9gal", // egal - this one had better match :-)
                "e\u0301gal", // e + "Combining acute accent"
                "e\u02cagal", // e + "modifier letter acute accent"
                "e'gal", // e + single quote
                "e\u00b4gal", // e + Latin-1 "acute"
        };
        Pattern pattern = Pattern.compile(pattStr, Pattern.CANON_EQ);
        for (int i = 0; i < input.length; i++) {
            if (pattern.matcher(input[i]).matches()) {
                System.out.println(
                    pattStr + " matches input " + input[i]);
            } else {
                System.out.println(
                    pattStr + " does not match input " + input[i]);
            }
        }
    }
}

Dieses Programm erkennt den kombinierenden Akzent richtig und lehnt die anderen Zeichen ab, von denen einige leider wie der Akzent auf einem Drucker aussehen, aber nicht als kombinierende Akzentzeichen gelten:

égal matches input égal
égal matches input e?gal
égal does not match input e?gal
égal does not match input e'gal
égal does not match input e´gal

Weitere Details findest du in den Zeichentabellen.

4.9 Zeilenumbrüche im Text abgleichen

Problem

Du musst Zeilenumbrüche im Text abgleichen.

Lösung

Verwende \n oder \r in deinem Regex-Muster. Siehe auch die Flaggenkonstante Pattern.MULTILINE, die dafür sorgt, dass Zeilenumbrüche als Zeilenanfang und Zeilenende erkannt werden (\^ und $).

Diskussion

Obwohl zeilenorientierte Unix-Tools wie sed und grep reguläre Ausdrücke zeilenweise abgleichen, tun das nicht alle Tools. Der sam Texteditor von Bell Laboratories war das erste mir bekannte interaktive Werkzeug, das mehrzeilige reguläre Ausdrücke zuließ; die Skriptsprache Perl folgte kurz darauf. In der Java-API hat das Zeilenumbruchszeichen standardmäßig keine besondere Bedeutung. Die Methode BufferedReader readLine() entfernt normalerweise alle Zeilenumbruchzeichen, die sie findet. Wenn du viele Zeichen mit einer anderen Methode als readLine() einliest, kann es sein, dass du eine Reihe von \n, \r oder \r\n Sequenzen in deinem Textstring hast.4 Normalerweise werden alle diese Sequenzen als gleichwertig mit \n behandelt. Wenn du möchtest, dass nur \n übereinstimmt, verwende das Flag UNIX_LINES für die Methode Pattern.compile().

Unter Unix werden ^ und $ üblicherweise verwendet, um den Anfang bzw. das Ende einer Zeile abzugleichen. In dieser API ignorieren die Regex-Metazeichen \^ und $ Zeilenabschlüsse und passen nur am Anfang bzw. am Ende der gesamten Zeichenfolge. Wenn du jedoch das Flag MULTILINE an Pattern.compile() übergibst, passen diese Ausdrücke genau nach bzw. genau vor einem Zeilenende; $ passt auch auf das Ende der Zeichenfolge. Da das Zeilenende nur ein gewöhnliches Zeichen ist, kannst du es mit . oder ähnlichen Ausdrücken abgleichen; und wenn du genau wissen willst, wo es sich befindet, passen auch \n oder \r im Muster dazu. Mit anderen Worten: Für diese API ist ein Zeilenumbruch nur ein weiteres Zeichen ohne besondere Bedeutung. Siehe die Seitenleiste "Pattern.compile() Flags". Ein Beispiel für den Abgleich von Zeilenumbrüchen ist in Beispiel 4-9 zu sehen.

Beispiel 4-9. main/src/main/java/regex/NLMatch.java
public class NLMatch {
    public static void main(String[] argv) {

        String input = "I dream of engines\nmore engines, all day long";
        System.out.println("INPUT: " + input);
        System.out.println();

        String[] patt = {
            "engines.more engines",
            "ines\nmore",
            "engines$"
        };

        for (int i = 0; i < patt.length; i++) {
            System.out.println("PATTERN " + patt[i]);

            boolean found;
            Pattern p1l = Pattern.compile(patt[i]);
            found = p1l.matcher(input).find();
            System.out.println("DEFAULT match " + found);

            Pattern pml = Pattern.compile(patt[i], 
                Pattern.DOTALL|Pattern.MULTILINE);
            found = pml.matcher(input).find();
            System.out.println("MultiLine match " + found);
            System.out.println();
        }
    }
}

Wenn du diesen Code ausführst, passt das erste Muster (mit dem Platzhalterzeichen .) immer, während das zweite Muster (mit $) nur passt, wenn MATCH_MULTILINE gesetzt ist:

> java regex.NLMatch
INPUT: I dream of engines
more engines, all day long

PATTERN engines
more engines
DEFAULT match true
MULTILINE match: true

PATTERN engines$
DEFAULT match false
MULTILINE match: true

4.10 Programm: Apache Logfile Parsing

Der Apache Webserver ist der weltweit führende Webserver und war dies für den größten Teil der Geschichte des Internets. Er ist eines der bekanntesten Open-Source-Projekte der Welt und das erste von vielen, die von der Apache Foundation gefördert werden. Es wird oft behauptet, dass der Name Apache ein Wortspiel mit den Ursprüngen des Servers ist: Die Entwickler begannen mit dem freien NCSA-Server und hackten so lange an ihm herum, bis er tat, was sie wollten. Als er sich ausreichend vom Original unterschieden hatte, wurde ein neuer Name benötigt. Da es sich nun um einen patchbaren Server handelte, wurde der Name Apache gewählt. Von offizieller Seite wird diese Geschichte zwar bestritten, aber sie ist trotzdem niedlich. Ein Ort, an dem sich die tatsächliche Uneinheitlichkeit zeigt, ist das Logfile-Format. Siehe Beispiel 4-10.

Beispiel 4-10. Auszug aus der Apache-Logdatei
123.45.67.89 - - [27/Oct/2000:09:27:09 -0400] "GET /java/javaResources.html
HTTP/1.0" 200 10450 "-" "Mozilla/4.6 [en] (X11; U; OpenBSD 2.8 i386; Nav)"

Das Dateiformat wurde offensichtlich für die menschliche Inspektion, aber nicht für das einfache Parsen entwickelt. Das Problem ist, dass verschiedene Begrenzungszeichen verwendet werden: eckige Klammern für das Datum, Anführungszeichen für die Anfragezeile und Leerzeichen überall. Versuch doch mal, eine StringTokenizer zu verwenden; du könntest es vielleicht hinbekommen, aber du würdest eine Menge Zeit damit verbringen, daran herumzufummeln. Eigentlich nicht, du würdest es nicht hinbekommen. Aber dieser etwas verdrehte reguläre Ausdruck5 macht es jedoch einfach, ihn zu analysieren (es handelt sich um einen einzigen Regex in Moby-Größe; wir mussten ihn auf zwei Zeilen aufteilen, damit er in die Buchränder passt):

\^([\d.]+) (\S+) (\S+) \[([\w:/]+\s[+\-]\d{4})\] "(.+?)" (\d{3}) (\d+)
  "([\^"]+)" "([\^"]+)"

Vielleicht findest du es informativ, wenn du dir die Tabelle 4-1 ansiehst und dir die vollständige Syntax ansiehst, die hier verwendet wird. Beachte vor allem die Verwendung des Quantifizierers +? in \"(.+?)\", um eine Zeichenkette in Anführungszeichen abzugleichen; du kannst nicht einfach .+ verwenden, weil das zu viel abgleichen würde (bis zum Anführungszeichen am Ende der Zeile). Der Code zum Extrahieren der verschiedenen Felder wie IP-Adresse, Anfrage, Referrer-URL und Browserversion wird in Beispiel 4-11 gezeigt.

Beispiel 4-11. main/src/main/java/regex/LogRegExp.java
public class LogRegExp {

    final static String logEntryPattern = 
            "^([\\d.]+) (\\S+) (\\S+) \\[([\\w:/]+\\s[+-]\\d{4})\\] " +
            "\"(.+?)\" (\\d{3}) (\\d+) \"([^\"]+)\" \"([^\"]+)\"";

    public static void main(String argv[]) {

        System.out.println("RE Pattern:");
        System.out.println(logEntryPattern);

        System.out.println("Input line is:");
        String logEntryLine = LogParseInfo.LOG_ENTRY_LINE;
        System.out.println(logEntryLine);

        Pattern p = Pattern.compile(logEntryPattern);
        Matcher matcher = p.matcher(logEntryLine);
        if (!matcher.matches() || 
            LogParseInfo.MIN_FIELDS > matcher.groupCount()) {
            System.err.println("Bad log entry (or problem with regex):");
            System.err.println(logEntryLine);
            return;
        }
        System.out.println("IP Address: " + matcher.group(1));
        System.out.println("UserName: " + matcher.group(3));
        System.out.println("Date/Time: " + matcher.group(4));
        System.out.println("Request: " + matcher.group(5));
        System.out.println("Response: " + matcher.group(6));
        System.out.println("Bytes Sent: " + matcher.group(7));
        if (!matcher.group(8).equals("-"))
            System.out.println("Referer: " + matcher.group(8));
        System.out.println("User-Agent: " + matcher.group(9));
    }
}

Die implements Klausel ist für eine Schnittstelle, die nur den Eingabestring definiert; sie wurde in einer Demonstration verwendet, um den Modus für reguläre Ausdrücke mit der Verwendung eines StringTokenizer zu vergleichen. Der Quelltext für beide Versionen befindet sich in den Online-Quellen zu diesem Kapitel. Wenn du das Programm mit der Beispieleingabe aus Beispiel 4-10 ausführst, erhältst du diese Ausgabe:

Using regex Pattern:
\^([\d.]+) (\S+) (\S+) \[([\w:/]+\s[+\-]\d{4})\] "(.+?)" (\d{3}) (\d+) "([\^"]+)"
"([\^"]+)"
Input line is:
123.45.67.89 - - [27/Oct/2000:09:27:09 -0400] "GET /java/javaResources.html
HTTP/1.0" 200 10450 "-" "Mozilla/4.6 [en] (X11; U; OpenBSD 2.8 i386; Nav)"
IP Address: 123.45.67.89
Date&Time: 27/Oct/2000:09:27:09 -0400
Request: GET /java/javaResources.html HTTP/1.0
Response: 200
Bytes Sent: 10450
Browser: Mozilla/4.6 [en] (X11; U; OpenBSD 2.8 i386; Nav)

Das Programm hat den gesamten Eintrag im Logfile-Format mit einem Aufruf von matcher.matches() erfolgreich geparst.

4.11 Programm: Full Grep

Nachdem wir nun gesehen haben, wie das Paket für reguläre Ausdrücke funktioniert, ist es an der Zeit, JGrep zu schreiben, eine vollwertige Version des Zeilenvergleichsprogramms mit Optionsparsing. In Tabelle 4-2 sind einige typische Befehlszeilenoptionen aufgelistet, die eine Unix-Implementierung von grep enthalten könnte. Für diejenigen, die mit grep nicht vertraut sind: grep ist ein Befehlszeilenprogramm, das nach regulären Ausdrücken in Textdateien sucht. Es gibt drei oder vier Programme in der Standard grep-Familie und einige neuere Ersatzprogramme wie ripgrep oder rg. Dieses Programm ist meine Ergänzung zu dieser Programmfamilie.

Tabelle 4-2. Grep-Befehlszeilenoptionen
Option Bedeutung

-c

Nur zählen; Zeilen nicht ausdrucken, nur zählen

-C

Kontext; einige Zeilen über und unter jeder übereinstimmenden Zeile ausgeben (in dieser Version nicht implementiert; als Übung für den Leser belassen)

-f Muster

Muster aus der Datei mit dem Namen -f statt aus der Kommandozeile übernehmen

-h

Dateinamen vor den Zeilen nicht ausgeben

-i

Fall ignorieren

-l

Nur Dateinamen auflisten: keine Zeilen ausgeben, sondern nur die Namen, in denen sie vorkommen

-n

Zeilennummern vor passenden Zeilen drucken

-s

Drucken bestimmter Fehlermeldungen unterdrücken

-v

Invertieren: nur Zeilen drucken, die NICHT mit dem Muster übereinstimmen

In der Unix-Welt gibt es mehrere getopt-Bibliotheksroutinen zum Parsen von Kommandozeilenargumenten, also habe ich diese in Java neu implementiert. Da main() in einem statischen Kontext läuft, unsere Anwendungshauptzeile aber nicht, könnten wir wie üblich eine Menge Informationen in den Konstruktor übergeben. Um Platz zu sparen, verwendet diese Version nur globale Variablen, um die Einstellungen aus der Befehlszeile zu verfolgen. Im Gegensatz zum Unix-Tool grep kann dieses Tool noch nicht mit kombinierten Optionen umgehen, daher ist -l -r -i in Ordnung, aber -lri wird aufgrund einer Einschränkung im verwendeten GetOpt Parser fehlschlagen.

Das Programm liest im Grunde nur Zeilen, vergleicht das Muster darin und gibt, wenn eine Übereinstimmung gefunden wird (oder nicht gefunden wird, mit -v), die Zeile aus (und optional auch einige andere Dinge). Nach all dem wird der Code in Beispiel 4-12 gezeigt.

Beispiel 4-12. darwinsys-api/src/main/java/regex/JGrep.java
/** A command-line grep-like program. Accepts some command-line options,
 * and takes a pattern and a list of text files.
 * N.B. The current implementation of GetOpt does not allow combining short 
 * arguments, so put spaces e.g., "JGrep -l -r -i pattern file..." is OK, but
 * "JGrep -lri pattern file..." will fail. Getopt will hopefully be fixed soon.
 */
public class JGrep {
    private static final String USAGE =
        "Usage: JGrep pattern [-chilrsnv][-f pattfile][filename...]";
    /** The pattern we're looking for */
    protected Pattern pattern;
    /** The matcher for this pattern */
    protected Matcher matcher;
    private boolean debug;
    /** Are we to only count lines, instead of printing? */
    protected static boolean countOnly = false;
    /** Are we to ignore case? */
    protected static boolean ignoreCase = false;
    /** Are we to suppress printing of filenames? */
    protected static boolean dontPrintFileName = false;
    /** Are we to only list names of files that match? */
    protected static boolean listOnly = false;
    /** Are we to print line numbers? */
    protected static boolean numbered = false;
    /** Are we to be silent about errors? */
    protected static boolean silent = false;
    /** Are we to print only lines that DONT match? */
    protected static boolean inVert = false;
    /** Are we to process arguments recursively if directories? */
    protected static boolean recursive = false;

    /** Construct a Grep object for the pattern, and run it
     * on all input files listed in args.
     * Be aware that a few of the command-line options are not
     * acted upon in this version - left as an exercise for the reader!
     * @param args args
     */
    public static void main(String[] args) {

        if (args.length < 1) {
            System.err.println(USAGE);
            System.exit(1);
        }
        String patt = null;

        GetOpt go = new GetOpt("cf:hilnrRsv");

        char c;
        while ((c = go.getopt(args)) != 0) {
            switch(c) {
                case 'c':
                    countOnly = true;
                    break;
                case 'f':    /* External file contains the pattern */
                    try (BufferedReader b = 
                        new BufferedReader(new FileReader(go.optarg()))) {
                        patt = b.readLine();
                    } catch (IOException e) {
                        System.err.println(
                            "Can't read pattern file " + go.optarg());
                        System.exit(1);
                    }
                    break;
                case 'h':
                    dontPrintFileName = true;
                    break;
                case 'i':
                    ignoreCase = true;
                    break;
                case 'l':
                    listOnly = true;
                    break;
                case 'n':
                    numbered = true;
                    break;
                case 'r':
                case 'R':
                    recursive = true;
                    break;
                case 's':
                    silent = true;
                    break;
                case 'v':
                    inVert = true;
                    break;
                case '?':
                    System.err.println("Getopts was not happy!");
                    System.err.println(USAGE);
                    break;
            }
        }

        int ix = go.getOptInd();

        if (patt == null)
            patt = args[ix++];

        JGrep prog = null;
        try {
            prog = new JGrep(patt);
        } catch (PatternSyntaxException ex) {
            System.err.println("RE Syntax error in " + patt);
            return;
        }

        if (args.length == ix) {
            dontPrintFileName = true; // Don't print filenames if stdin
            if (recursive) {
                System.err.println("Warning: recursive search of stdin!");
            }
            prog.process(new InputStreamReader(System.in), null);
        } else {
            if (!dontPrintFileName)
                dontPrintFileName = ix == args.length - 1; // Nor if only one file
            if (recursive)
                dontPrintFileName = false;                // unless a directory!

            for (int i=ix; i<args.length; i++) { // note starting index
                try {
                    prog.process(new File(args[i]));
                } catch(Exception e) {
                    System.err.println(e);
                }
            }
        }
    }

    /**
     * Construct a JGrep object.
     * @param patt The regex to look for
     * @throws PatternSyntaxException if pattern is not a valid regex
     */
    public JGrep(String patt) throws PatternSyntaxException {
        if (debug) {
            System.err.printf("JGrep.JGrep(%s)%n", patt);
        }
        // compile the regular expression
        int caseMode = ignoreCase ?
            Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE :
            0;
        pattern = Pattern.compile(patt, caseMode);
        matcher = pattern.matcher("");
    }

    /** Process one command line argument (file or directory)
     * @param file The input File
     * @throws FileNotFoundException If the file doesn't exist
     */
    public void process(File file) throws FileNotFoundException {
        if (!file.exists() || !file.canRead()) {
            throw new FileNotFoundException(
                "Can't read file " + file.getAbsolutePath());
        }
        if (file.isFile()) {
            process(new BufferedReader(new FileReader(file)), 
                file.getAbsolutePath());
            return;
        }
        if (file.isDirectory()) {
            if (!recursive) {
                System.err.println(
                    "ERROR: -r not specified but directory given " + 
                    file.getAbsolutePath());
                return;
            }
            for (File nf : file.listFiles()) {
                process(nf);    // "Recursion, n.: See Recursion."
            }
            return;
        }
        System.err.println(
            "WEIRDNESS: neither file nor directory: " + file.getAbsolutePath());
    }

    /** Do the work of scanning one file
     * @param    ifile    Reader    Reader object already open
     * @param    fileName String    Name of the input file
     */
    public void process(Reader ifile, String fileName) {

        String inputLine;
        int matches = 0;

        try (BufferedReader reader = new BufferedReader(ifile)) {

            while ((inputLine = reader.readLine()) != null) {
                matcher.reset(inputLine);
                if (matcher.find()) {
                    if (listOnly) {
                        // -l, print filename on first match, and we're done
                        System.out.println(fileName);
                        return;
                    }
                    if (countOnly) {
                        matches++;
                    } else {
                        if (!dontPrintFileName) {
                            System.out.print(fileName + ": ");
                        }
                        System.out.println(inputLine);
                    }
                } else if (inVert) {
                    System.out.println(inputLine);
                }
            }
            if (countOnly)
                System.out.println(matches + " matches in " + fileName);
        } catch (IOException e) {
            System.err.println(e);
        }
    }
}

1 Nicht-Unix-Fans müssen keine Angst haben, denn du kannst Tools wie grep auf Windows-Systemen mit einem von mehreren Paketen verwenden. Eines davon ist ein Open-Source-Paket, das abwechselnd CygWin (nach Cygnus Software) oder GnuWin32 genannt wird. Ein anderes ist der Befehl findstr von Microsoft für Windows. Oder du kannst mein Grep-Programm in Rezept 4.6 verwenden, wenn du grep nicht auf deinem System hast. Der Name grep stammt übrigens von einem alten Unix-Zeileneditor-Befehl g/RE/p, der die Regex global in allen Zeilen im Editierpuffer sucht und die übereinstimmenden Zeilen ausgibt - genau das, was das Programm grep mit Zeilen in Dateien macht.

2 REDemo wurde durch ein ähnliches Programm inspiriert, das mit dem inzwischen eingestellten Apache Jakarta Regular Expressions-Paket geliefert wurde (aber keinen Code daraus verwendet).

3 Unter Unix expandiert die Shell oder der Kommandozeileninterpreter *.txt in alle passenden Dateinamen, bevor das Programm ausgeführt wird, aber der normale Java-Interpreter macht das für dich auf Systemen, auf denen die Shell nicht energisch oder intelligent genug ist, um das zu tun.

4 Oder ein paar verwandte Unicode-Zeichen, einschließlich der Zeichen für die nächste Zeile (\u0085), den Zeilentrenner (\u2028) und den Absatztrenner (\u2029).

5 Man könnte meinen, dass dies eine Art Weltrekord für Komplexität in Regex-Wettbewerben ist, aber ich bin mir sicher, dass es schon oft übertroffen wurde.

Get Java Kochbuch, 4. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.