Reguläre Ausdrücke Name 1 AnPr Klasse Datum Allgemeines Reguläre Ausdrücke (engl. Regular Expressions) dienen dazu, Texte bzw. Textpassagen zu beschreiben, indem Texte als Mengen von Zeichen interpretiert werden und die Beschreibung einem theoretischen Ansatz auf Basis der Mengenlehre folgt. Aufgrund der Fähigkeit zu beschreiben folgen auch die wichtigsten Anwendungsgebiete von Regulären Ausdrücken, dem Filtern von Texten, dem Extrahieren und dem Ersetzen von Textfragmenten. Speziell unter UNIX werden Reguläre Ausdrücke gerne für Verarbeitungsskripte verwendet, da hier sehr einfach auf die Ausgaben von Batchprogrammen zugegriffen werden kann und mit Hilfe des Programms „grep“ (global/regular expression/print) die gewünschten Inhalte gesucht werden können. Die Anwendung von Regulären Ausdrücken auf Texte erfolgt durch entsprechende Implementierungen, welche (leider) unterschiedliche Funktionsumfänge und Dialekte hervorgebracht haben. Am verbreitetsten ist PCRE (Perl Compatible Regular Expressions), welches sich an der Perl Implementierung orientiert. Weiterhin wird bei den Regulären Ausdrücken zwischen den grundlegenden (basic) und den erweiterten (extended) Regulären Ausdrücken unterschieden, wobei die Erweiterungen die ursprüngliche theoretische Basis der Mengenlehre sprengen. Als Beispiel sei hier die Rückwärtsreferenzen (backtracking) genannt, welche es ermöglichen auf bereits gefundene Textfragmente wiederum Reguläre Ausdrücke anzuwenden. Das hier vorliegende Dokument dient dazu, die wesentlichen Elemente von Regulären Ausdrücke verstehen zu lernen und eine Java Implementierung zur Anwendung und zum Üben von Regulären Ausdrücken zu realisieren. 2 Java Implementierung Java bietet in der Bibliothek javax.util.regex.* (siehe: http://docs.oracle.com/javase/7/docs/api/java/util/regex/package-summary.html bzw. http://java.sun.com/ docs/ books/ tutorial/essential/regex/index.html) die notwendigen Klassen und Methoden an, um Reguläre Ausdrücke anwenden zu können. Hierbei gibt es zwei wesentliche Klassen: Pattern: Dies ist eine Klasse, welche den Regulären Ausdruck kompiliert und somit mehrfach anwendbar macht, wobei die Anwendung durch den Machter durchgeführt wird. Alternativ kann ein Pattern auch direkt den Regulären Ausdruck anwenden, wobei dies bei mehrfacher Ausführung eine ungünstiger Performance mit sich bringt, da der Ausdruck bei jedem Match (Treffer) neu kompiliert wird. Matcher: Der Machter ist in der Lage ein kompiliertes Pattern anzuwenden und verschiedene komfortable Methoden (z.B.: Suchen, Ersetzen) abhängig von den Matches durchzuführen. Was: Anwendung von Regulären Ausdrücken Programmiersprache: Java Pattern p1 = Pattern.compile("u", Pattern.CASE_INSENSITIVE); Matcher m1 = p1.matcher("Berufsschule"); String s1 = m1.replaceAll("a"); Pattern p2 = Pattern.compile("[0-9][^0-9]*"); Matcher m2 = p2.matcher("8tung, ein Text"); boolean b2 = m2.matches(); Pattern p3 = Pattern.compile("\b\S*a\S*\b"); Matcher m3 = p3.matcher("Peter Paul Mary"); while(m3.find()) { System.out.println(m3.group()); } Erklärung: Pattern p1 Deklaration eines Pattern Objektes Pattern.compile( Compillierung der Expression und Erzeugung eines Patterns, wobei als "u", Pattern. zweiter Parameter die Flags angegeben sind. Diese werden als Bitmuster CASE_INSENSITIVE); in einer int Variable codiert. ANPR_01_RegexInJava_v03.docx Seite 1 Reguläre Ausdrücke AnPr Was: Anwendung von Regulären Ausdrücken Programmiersprache: Java Matcher m1 Deklaration eines Matcher Objektes p1.matcher( Anwendung der Expression auf den Text und Erzeugung eines Matcher "Berufsschule"); Objektes. m1.replaceAll("a"); Ersetzen jeder Fundstelle mit dem angegebenen String (hier „a“). m2.matches(); Prüfung, ob der Ausdruck exakt matcht m3.find() Prüfung, ob das Pattern im Text gefunden wurde, incl. Positionierung eines Zeigers auf diese Fundstelle. m2.group(); Ausgabe der gefundenen Stelle, auf dem der aktuelle Zeiger steht. Programmierhinweis: Nutzung eines Matchers Aufgrund der komfortablen Methoden ist es prinzipiell sinnvoll einen Matcher zu verwenden. Bei einmaliger Prüfung, ob der Text der Expression entspricht kann jedoch auch folgende Methode verwendet werden: Pattern.matches("0-9][^0-9]*", "8tung, ein Text"); Wichtig ist jedoch, dass Matcher nicht Threadsicher (thread save) sind. Die wichtigsten Methoden vom Matcher sind: matches(): Liefert true zurück, wenn der gesamte untersuchte Text dem Regulären Ausdruck entspricht. find(): Liefert true zurück, wenn innerhalb des Textes eine Passage gefunden wurde, welche dem Regulären Ausdruck entspricht. Die Suche beginnt beim ersten Aufruf am Anfang des Textes und endet mit dem letzten Zeichen, welches dem Regulären Ausdruck entspricht. Sofern reset() nicht aufgerufen wurde, sucht die Methode beim zweiten Aufruf beim nächsten Zeichen nach dem vorausgegangenen Match weiter, es können somit sequenziell alle Matches abgearbeitet werden. group(): Liefert, sofern nach find() aufgerufen, die Textpassage zurück, welche mit dem vorausgegangenen find() gefunden wurde. start(): Liefert, sofern nach find() aufgerufen, die Position des ersten Zeichens der Textpassage zurück, welche mit dem vorausgegangenen find() gefunden wurde. stop(): Liefert, sofern nach find() aufgerufen, die Position des letzten Zeichens der Textpassage zurück, welche mit dem vorausgegangenen find() gefunden wurde. reset(): Setzt den Zeiger der letzten durch find() gefundenen Textpassage wieder auf die Position 0 zurück. Die Flags, welche für die Kompilierung des Patterns akzeptiert werden sind wie folgt: Kanonische Äquivalenz – also wenn zwei UNICODE Zeichen zur selben Darstellung führen, werden sie als identisch angesehen. CASE_INSENSITIVE: Großbuchstabe und Kleinbuchstabe werden als identisch angesehen. COMMENTS: Kommentare (Zeichen # bis Zeilenende) innerhalb des Regulären Ausdrucks werden ignoriert. DOTALL: Der Punktoperator berücksichtigt auch das Zeilentrennzeichen. LITERAL: Metazeichen werden nicht als solches berücksichtigt, sondern werden literal interpretiert. MULTILINE: Die Operatoren ^ und $ gelten für den Anfang und das Ende jeder Zeile, nicht des gesamten Textes. UNICODE_CASE: Bei CASE_INSENSITIVE wird der Vergleich über den UNICODE Standard durchgeführt. UNIX_LINES: Als Zeilentrenner wird lediglich \n interpretiert. CANON_EQ: Bei mehreren zu setzenden Flags, müssen diese bitweise mit dem „Oder Operator“ gesetzt und anschließend übergeben werden. Darüber hinaus bietet Java auch in der String Klasse Methoden zur Prüfung des Strings, wie z.B. matches(strRegex), replaceAll(strRegex, strReplacement) an. Dies ist für einfache, einmalige Aufgaben gedacht. Details unter http://java.sun.com/javase/6/docs/api/java/lang/String.html. Seite 2 AnPr 3 Reguläre Ausdrücke Syntax von Regulären Ausdrücken Grundsätzlich hilft bei Regulären Ausdrücken das Netz weiter – es gibt hunderte von Seiten, die einem weiterhelfen. Eine sehr gut gestaltete Seite ist: http://www.regular-expressions.info/ Hier finden sich neben dem hier dargelegten noch viele weitere nützliche Tipps. Zum schnellen validieren kann man sich auf http://www.regexplanet.com umsehen. 3.1 Identifikation von Zeichen Die einfachste Form von Regulären Ausdrücken ist die Identifikation von einzelnen Zeichen. Hierbei wird der zu suchende Buchstabe einfach als Regulärer Ausdruck interpretiert. Beachtung von Groß/Kleinschreibung wird in der Regel als Parameter in der RegEx Implementierung gesetzt. Bei Java ist dies das Flag CASE_INSENSITIVE. RegEx: u Ersetzung: a Text: Berufsschule Führt bei Ersetzung zu: Berafsschale Genauso können mehrere Zeichen als Kette gesucht werden: RegEx: ul Ersetzung: a Text: Berufsschule Führt bei Ersetzung zu: Berufsschae Zu beachten ist, dass Metazeichen mit „\“ zu escapen sind: []\^$.|?*+() Achtung: Im Java String muss das Escapezeichen ebenfalls escaped werden. Für das Escapen von [ darf somit nicht \[ , sondern muss mit \\[ gesetzt werden. Weiterhin sind noch besondere Zeichen definiert worden, wobei diese z.T. abhängig von der Implementierung sind. Die hier angegebenen Zeichen entstammen der Klassenbeschreibung von java.util.regex.Pattern: Sonderzeichen: \\ \0n \0nn \0mnn \xhh \uhhhh \t \n \r \f \a \e \cx Bedeutung Das backslash Zeichen Das Zeichen mit dem Oktalwert 0n (0 <= n <= 7) Das Zeichen mit dem Oktalwert 0nn (0 <= n <= 7) Das Zeichen mit dem Oktalwert 0mnn (0 <= m <= 3, 0 <= n <= 7) Das Zeichen mit dem Hexadezimalwert 0xhh (0 <= h <= F) Das Zeichen mit dem Hexadezimalwert 0xhhhh (0 <= h <= F) als Unicode Das Tabulatorzeichen ('\u0009') Das newline (line feed) Zeichen ('\u000A') Das carriage-return Zeichen ('\u000D') Das form-feed Zeichen ('\u000C') Das Alarm (bell) Zeichen ('\u0007') Das escape Zeichen ('\u001B') Das Kontroll-Zeichen zu x (bspw. ctrl a: „\ca“) Zusatzinfo Zeilenterminierung: Zeilenterminierungen (bzw. Lineterminators) haben eine besondere Bedeutung, da sie zum einen üblicherweise nicht ausgegeben werden und zum anderen eine unterschiedliche Interpretation bei UNIX und Windows existiert. Darüber hinaus besteht unter Windows die Zeilenterminierung aus zwei Zeichen. Seite 3 Reguläre Ausdrücke AnPr Hier die relevanten Zeilenumbruch - Zeichen: Terminator: \n \r\n \r \u0085 \u2028 \u2029 Bedeutung: Newline (bzw. Line feed) Zeichen (UNIX) Carriage Return Zeichen gefolgt von einem Newline Zeichen (Windows) Einzelnes Carriage Return Zeichen Next Line Zeichen Line Separator Zeichen Absatz Separator Zeichen Weiterhin sind noch POSIX Zeichenklassen für den US – ASCII Code und eigene Java Identifikationen definiert worden. Diese können in der Klassenbeschreibung von java.util.regex.Pattern ersehen werden (http://docs.oracle.com/javase/7/docs/api/java/util/regex/Pattern.html). 3.2 Identifikation von Zeichengruppen Es ist möglich, verschiedene Zeichen als bestimmte Zeichengruppen anzugeben. Hierzu gibt es verschiedene Möglichkeiten. Die einfachste Möglichkeit ist die Zusammenfassung in Eckigen Klammern [ ]. Innerhalb dieser können wiederum verschiedene Ausdrücke stehen: Einzelne Zeichen werden auch einzeln betrachtet, insofern gibt [aeiou] an, dass nach allen a, e, i, o bzw. u gesucht wird. RegEx: [aeiou] Ersetzung: + Text: Berufsschule Führt bei Ersetzung zu: B+r+fssch+l+ Zusammenhängende Zeichengruppen können mit Bindestrich angegeben werden. [a-f] sucht somit alle Zeichen von a bis f, wobei die ASCII Codetabelle für die Bestimmung der Bereiche herangezogen wird. RegEx: [a-f] Ersetzung: + Text: Berufsschule Führt bei Ersetzung zu: B+ru+ss+hul+ Durch Aneinanderreihung von einzelnen Zeichen bzw. zusammenhängenden Zeichengruppen können diese auch kombiniert werden. [aeiou][a-f] sucht somit nach Stellen, an denen a, e, i, o bzw. u vor einem a, b, c, d, e oder f stehen. RegEx: [aeiou][a-f] Ersetzung: + Text: Berufsschule Führt bei Ersetzung zu: Ber+sschule Die Zeichengruppen können auch negiert werden, indem ein Zirkumflex (^) Zeichen nach der öffnenden Klammer platziert wird. [^a-f] sucht somit nach allen Zeichen, außer a, b, c, d, e oder f. RegEx: [^a-f] Ersetzung: + Text: Berufsschule Führt bei Ersetzung zu: +e++f++c+++e Weiterhin gibt es vordefinierte Zeichengruppen, welche mit Ausnahme des Punktes mit Hilfe von Zeichengruppen selbst formuliert werden können. Zeichengruppe: . \d \D \s \S \w 1 Bedeutung: Jedes beliebige Zeichen1 Eine beliebige Ziffer Alles außer Ziffern Ein „Whitespace“ Zeichen Alles außer Whitespace Zeichen Ein beliebiges „Wort Zeichen“ (incl. Unterstrich, ohne Umlaute) Ersatzausdruck: nicht sinnvoll [0-9] [^0-9] [\t\n\x0B\f\r] [^\t\n\x0B\f\r] [a-zA-Z_0-9] Je nach Setting der Implementierung wird der Line Terminator ignoriert. Bei Java Pattern wird dies durch das setzen des Flags DOTALL erreicht. Seite 4 AnPr Reguläre Ausdrücke Zeichengruppe: \W Bedeutung: Alles außer “Wort Zeichen” Ersatzausdruck: [^a-zA-Z_0-9] Implizit wurde bereits auf die Union von zwei Gruppen hingewiesen ([aeiou][a-f]). Darüber hinaus existiert noch die Intersection („&&“), welche nur die Zeichen identifiziert, welche in beiden Ausdrücken vorhanden ist. Bspw. sucht [a-z&&[^aeiou]] alle Zeichen des Alphabets außer a, e, I, o bzw. u. Eine weitere Operation stellt die Alternative (|) dar, in der zwei Optionen angegeben werden (cats|dogs) würde beim Satz „It is raining cats and dogs“ zwei Matches haben. Diese beiden Funktionen existieren jedoch nur in wenigen Implementationen – Java ist eine davon: (http://docs.oracle.com/javase/tutorial/essential/regex/char_classes.html) 3.3 Quantoren Um zu spezifizieren, wie oft gesuchte Sequenzen vorkommen dürfen, werden Quantoren benötigt. Sie werden an das gesuchte Zeichen oder Zeichengruppen angehängt (bswp. sucht a{3} alle Vorkommnisse von aaa). Quantor: ? + * {n} {min,} {,max} {min,max} Bedeutung: Voranstehender Ausdruck muss 0 oder einmal vorkommen. Voranstehender Ausdruck muss 1 oder mehrmal vorkommen. Voranstehender Ausdruck muss 0, 1 oder mehrmal vorkommen. Voranstehender Ausdruck muss exakt n mal vorkommen. Voranstehender Ausdruck muss mindestens min mal vorkommen. Voranstehender Ausdruck darf maximal max mal vorkommen. Voranstehender Ausdruck muss mindestens min mal und maximal max mal vorkommen. Wichtig zu beachten ist, dass sich die Quantoren auf den vorausgegangen Ausdruck beziehen, nicht auf die gefundene Sequenz. So findet [0-9]{3} sowohl die 000, als auch die 007 im Text „von 000 bis 007 ist ein weiter Weg“. 3.3.1 Gieriges Verhalten (greedy) Grundsätzlich verhalten sich die Quantoren „gierig“ (engl. greedy). Dies bedeutet, dass sämtliche möglichen Matches gefunden (und bspw. bei Ersetzung auch angewendet) werden. Folgendes Beispiel soll dies verdeutlichen: RegEx: s+ Ersetzung: + Text: Berufsschule Führt bei Ersetzung zu: Beruf+chule Der Ausdruck sucht somit ein, oder mehrere „s“, interpretiert sie als einen Treffer und ersetzt sie mit einem „+“. Der Match hat somit den gesamten Bereich als „seinen Treffer“ aufgefasst. Anschließend fängt er mit einer neuen Suche an. Gierig bedeutet also, dass der letzte mögliche Match verwendet wird. 3.3.2 Genügsames Verhalten (reluctant) Nun kann man in den meisten Implementierungen von Regulären Ausdrücken (den Perl Vorgaben folgend) die Quantoren auch „genügsam“ (engl. reluctant, oder non-greedy) auszuführen. Hierfür wird im Regelfall ein „?“ nachgestellt: RegEx: s+? Ersetzung: + Text: Berufsschule Führt bei Ersetzung zu: Beruf++chule Man sieht am Ergebnis, dass Java pro Wort zwei Matches gefunden hat. Der erste begnügt sich also mit dem ersten gefundenen Buchstaben und beginnt anschließend wieder erneut mit einer Suche. Genügsam bedeutet also, dass der erste mögliche Match verwendet wird. 3.3.3 Possesives Verhalten (possessive) Mit dem possesiven Verhalten ist es möglich, trotz gierigem Verhalten, den gesamten Match auf eine zusammenhängende Gruppe zu reservieren. Hierzu muss ein „+“ nachgestellt werden. Um das Verhalten zu verdeutlichen, werden hier der normale gierige und der possesive Ausdruck dargestellt: Seite 5 Reguläre Ausdrücke RegEx: . .+ AnPr Ersetzung: + + Text: Bs7 Bs7 Führt bei Ersetzung zu: +++ + Der sinnvollste Anwendungsfall für possesives Verhalten ist weniger die Ersetzung von Text, als die Filterung. Eine Prüfung auf ein Matching der oberen Zeile würde false, auf die untere true ergeben. Possesives Verhalten ist in nur wenigen Implementierungen realisiert, Java ist eine davon. 3.4 Grenzen Oftmals ist es notwendig, Zeichenketten anhand ihrer Position in Zeilen oder Wörtern zu identifizieren. Zum Beispiel erwirkt der Operator \b, dass die Zeichenkette an der Grenze eines Wortes stehen muss: RegEx: \ber er\b \ber\b Ersetzung: + + + Text: er, der Herr erfährt mehr. er, der Herr erfährt mehr. er, der Herr erfährt mehr. Führt bei Ersetzung zu: +, der Herr +fährt mehr. +, d+ Herr erfährt mehr +, der Herr erfährt mehr. Folgende Grenzen kennt Java: Grenze: ^ $ \b \B \A \G \Z \z Bedeutung: Anfang einer Zeile (Zeichen nicht mit dem Negierer verwechseln – dieser steht nicht am Anfang)2 Ende einer Zeile2 Wortgrenze Nichtwortgrenze (also das Pattern darf nicht an einer Wortgrenze stehen) Anfang des untersuchten Strings Ende des vorausgegangenen Matches Ende des untersuchten Strings, ohne dem finalen Terminator, sofern er existiert Ende des untersuchten Strings 3.5 Nummerierung und Capturing Groups Eine fortgeschrittene Möglichkeit mit Regulären Ausdrücken zu hantieren sind die Capturing Groups, welche beim sogenannten „backreferencing“ genutzt werden. Zuerst ist es wichtig zu verstehen, dass Ausdrücke in Klammern durchnummeriert sind. Beispielsweise hat der Ausdruck ((A)(B(C))) vier Gruppen: 1. ((A)(B(C))) 2. (A) 3. (B(C)) 4. (C) Die Nummerierung folgt den öffnenden Klammern. Wenn nun Ausdrücke verwendet werden, so kann mittels einer Rückwärtsreferenz („backreference“) diese wieder mittels \n referenziert werden, wobei n die Nummer des referenzierten Ausdrucks ist. Beispielsweise sucht der Ausdruck (\d\d)\1 zwei aufeinanderfolgende Ziffern, wobei diese zweimal hintereinander stehen müssen (dies wird durch die Referenz \1 gefordert, also suche zwei beliebige Ziffern hintereinander „\d\d“ und dann das gleiche nochmal „\1“): RegEx: (\d\d)\1 (\d\d)\1 Ersetzung: + + Text: 33121233 33122133 Führt bei Ersetzung zu: 33+33 33122133 In der unteren Zeile wird das Pattern nicht wiedergefunden, da es kein Ziffernpärchen gibt, welches zweimal hintereinander vorkommt. 2 Multiline Identification muss im Regelfall in der Implementierung aktiviert werden Seite 6 AnPr Reguläre Ausdrücke 3.6 Look around assertions Es ist möglich Ausdrücke entsprechend ihrer Position eines zweiten Ausdruckes zu bestimmen. Wenn z.B. ein Buchstabe „u“ nur dann gefunden werden soll, wenn er vor einem „f“ steht, kann dies mit einem entsprechenden Ausdruck realisiert werden. Grundsätzlich kann der vorausgehende Ausdruck (look ahead) oder der nachstehende Ausdruck (look back) gesucht werden. 3.6.1 Look ahead assertion Bei einer vorausschauenden Prüfung wird mit (?=X) geprüft, ob der gesuchte Ausdruck vor einem Referenzausdruck X steht (positive look ahead assertion). Der Ausdruck (?!X) negiert dies und prüft, ob der gesuchte Ausdruck nicht vor einem Referenzausdruck X steht. RegEx: u(?=f) u(?!f) Ersetzung: + + Text: Berufsschule Berufsschule Führt bei Ersetzung zu: Ber+fsschule Berufssch+le 3.6.2 Look back assertion Die zurückschauende Prüfung wird mit (?<=X) formuliert und prüft, ob der gesuchte Ausdruck nach einem Referenzausdruck X steht (positive look behind assertion). Der Ausdruck (?<!X) negiert dies und prüft, ob der gesuchte Ausdruck nicht nach einem Referenzausdruck X steht. RegEx: (?<=r)u (?<!r)u Ersetzung: + + Text: Berufsschule Berufsschule Führt bei Ersetzung zu: Ber+fsschule Berufssch+le 3.7 Sonderfall Zeilenumbruch Wie oben bereits angedeutet, hat der Punkt Operator „.“ nicht zwingend die Eigenschaft, den Zeilenumbruch als eigenes Zeichen zu interpretieren, dies muss je nach Implementierung aktiviert werden. Wenn z.B. der Text zwischen zwei Klammern identifiziert werden muss, so kann folgender Ausdruck ein Problem darstellen: \(.*\) Wenn die öffnende Klammer in Zeile1 und die schließende Klammer in Zeile 2 steht, würde bei Nichtinterpretation des Zeilenumbruches kein Match gefunden werden, da zwischen den beiden Klammern ja nicht eine beliebige Anzahl von Zeichen stehen würde sondern zwei voneinander getrennte Gruppen von Zeichen. Seite 7 Reguläre Ausdrücke 4 AnPr Lizenz Diese(s) Werk bzw. Inhalt von Maik Aicher (www.codeconcert.de) steht unter einer Creative Commons Namensnennung - Nicht-kommerziell - Weitergabe unter gleichen Bedingungen 3.0 Unported Lizenz. Seite 8