Ein Wiedersehen mit verdoppelten Wörtern
(Auszug aus "Reguläre Ausdrücke" von Jeffrey E. F. Friedl)
Das Problem mit den verdoppelten Wörtern aus Einführung in reguläre Ausdrücke hat hoffentlich Ihren Appetit auf die Kraft von regulären Ausdrücken geweckt. Ich habe außerdem am Anfang dieses Kapitels ein paar wilde Zeilen Code aufgetischt, die ich die Lösung des Problems genannt habe:
while (<>) {
next if !s/\b([a-z]+)((?:\s|<[^>]+>)+)(\1\b)/\e[7m$1\e[m$2\e[7m$3\e[m/ig;
s/^(?:[^\e]*\n)+//mg; # Nicht markierte Zeilen löschen.
s/^/$ARGV: /mg; # Dateinamen voranstellen.
print;
}
Jetzt, da Sie etwas Perl verstehen, können Sie hoffentlich die generelle Form verstehen – das <>, die drei s/.../.../ und das print. Es ist wahrscheinlich noch immer verwirrend. Wenn dies Ihr erster Kontakt mit Perl (und mit regulären Ausdrücken) ist, mag das Folgende etwas schwer verständlich sein.
Von Nahem besehen, sind diese regulären Ausdrücke gar nicht so kompliziert. Bevor wir das aber tun, ist es sinnvoll, die Problemstellung genauer zu umschreiben und ein Beispiel der Ausgabe des fertigen Programms zu betrachten:
% perl -w FindeDoppelt kap1.txt
kap1.txt: aufspürt. Solche Verdopplungen (wie »das das«) entstehen
kap1.txt: und Kleinschreibung wie bei ›Das das...‹ ignoriert,
kap1.txt: fett gedruckt hervorzuheben: '...das ist <B>sehr</B>
kap1.txt: sehr wichtig...'.
kap1.txt: Ausdrücken denkt; sodass Sie sie anwenden
kap1.txt: sehr verschieden sein. Ein Programm kennt
kap1.txt: kennt vielleicht dieses oder jenes Zeichen nicht,
.
.
.
Nun zum Programm, zunächst in Perl. Danach betrachten wir auch eine Version in Java und werden dort einen anderen Ansatz zu regulären Ausdrücken kennenlernen. Dieses Mal verwende ich die Form s{Regex}{Ersatztext}Modifikatoren mit den geschweiften Klammern, außerdem den /x-Modifikator, damit ich die Regex kommentieren kann (außerdem haben wir jetzt Platz für das etwas lesbarere ›next unless‹ statt ›next if !‹). Abgesehen davon ist das folgende Beispiel identisch mit dem vom Anfang des Kapitels.
»Verdoppelte Wörter« in Perl
$/ = ".\n"; # Ein spezieller Einlese-Modus; jede »Zeile« endet mit Punkt-Newline.
while (<>)
{
next unless s{ # (Regex beginnt hier.)
### Ein Wort erkennen:
\b # Wortanfang ...
( [a-z]+ ) # Wort, setzt $1 (und \1).
### Whitespace und/oder <TAGS> dazwischen:
( # Zwischenraum in $2 speichern.
(?: # (Dieses nicht-einfangende Klammerpaar gruppiert nur.)
\s # Whitespace (inkl. Newline, gut in diesem Fall)
| # -oder-
<[^>]+> # etwas wie <TAG>.
)+ # Mindestens eins davon, aber auch mehr.
)
### Und das gleiche Wort noch mal erkennen:
(\1\b) # \b, damit nicht Wortteile erkannt werden. Setzt $3.
# (Regex endet hier.)
}
# Ersatz-String folgt hier, mit den Modifikatoren /i, /g und /x.
{\e[7m$1\e[m$2\e[7m$3\e[m}igx;
s/^(?:[^\e]*\n)+//mg; # Nicht markierte Zeilen löschen.
s/^/$ARGV: /mg; # Jeder Zeile den Dateinamen voranstellen.
print;
}
Es werden einige Dinge benutzt, die Sie noch nicht kennen. Ich werde diese kurz erläutern, muss aber für Details auf die Perl-Dokumentation (oder, wenn es sich um reguläre Ausdrücke handelt, auf Perl) verweisen. In der folgenden Beschreibung heißt »magisch« so etwas wie »wegen eines Perl-Features, das Sie vielleicht noch nicht kennen«.
- Zeile 1: Weil verdoppelte Wörter auf aufeinanderfolgenden Zeilen vorkommen können, scheitert eine Lösung, die wie im E-Mail-Beispiel Zeile für Zeile arbeitet. Wenn die Spezialvariable $/ (ja, das ist eine Variable) auf den angegebenen Wert gesetzt wird, dann liest <> nicht Zeilen, sondern (mehr oder weniger) ganze Abschnitte. Zurückgegeben wird immer noch ein String, der nun aber Newlines und damit mehrere logische Zeilen enthalten kann.
- Zeile 3: Haben Sie bemerkt, dass der Wert von <> gar nicht einer Variablen zugewiesen wird? Wenn man <> im Bedingungsteil einer while-Anweisung auf diese Art benutzt, weist Perl das Eingelesene einer magischen Variablen (Anmerkung: Diese Variable heißt $_ (jawohl, auch das ist eine Variable). $_ wird bei vielen Operatoren und Funktionen per Voreinstellung benutzt, wenn nicht explizit eine andere Variable angegeben wird.) zu. Diese Variable wird auch für den String benutzt, der von s/.../.../ bearbeitet wird, und wird von dem print am Ende ausgegeben. Das Arbeiten mit diesen voreingestellten Variablen macht das Programm sehr übersichtlich, aber für Ungeübte auch schwerer verständlich. Ich empfehle Ihnen, die ausführliche Form zu benutzen, bis Sie sich an die voreingestellten Werte gewöhnt haben.
- Zeile 5: Mit dem next unless vor der Substitution geht Perl zum nächsten Zyklus der while-Schleife, wenn der reguläre Ausdruck in der Substitution nichts gefunden hat und die Substitution keine Wirkung hatte. Wenn keine verdoppelten Wörter gefunden werden, brauchen wir uns nicht weiter mit dem eingelesenen String zu befassen und lesen den nächsten.
- Zeile 25: Der Ersatz-String ist eigentlich nur "$1$2$3", mit einigen ANSI-Escape-Sequenzen dazwischen, die die gefundenen doppelten Wörter hervorheben, nicht aber, was dazwischen ist. Diese Sequenzen sind \e[7m (Anfang des Hervorhebens) und \e[m (Ende). \e ist die Perl-Abkürzung für das ASCII-ESC-Zeichen in Strings und regulären Ausdrücken, mit dem ANSI-Escape-Sequenzen beginnen.
Wenn wir die Klammerung der Regex genau anschauen, sehen wir, dass "$1$2$3" alle Zeichen enthält, die auf die Regex gepasst haben. Damit ist die ganze Substitution bis auf das Hinzufügen der Escape-Sequenzen eigentlich nur ein (langsames) NOP, eine »Nicht-Operation«.
Wir wissen, dass $1 das Gleiche enthält wie $3 (das ist ja der Sinn des Programms!), also könnte man im Ersatz-String zweimal $1 benutzen und auf das vierte Klammerpaar verzichten. Aber die zwei können sich bezüglich Groß- und Kleinschreibung unterscheiden, daher benutze ich beide.
- Zeile 27: Der String kann mehrere logische Zeilen enthalten. Nachdem die Substitution alle verdoppelten Wörter markiert hat, wollen wir nur die mit einem ESC-Zeichen markierten Zeilen behalten. Die anderen werden gelöscht. Wir benutzen den /m-Modifikator, daher finden wir mit ˹^([^\e]*\n)+˼ logische Zeilen, die keine ESC-Zeichen enthalten.
Mit dieser Regex in der Substitution werden diese Zeilen gelöscht. Übrig bleiben die logischen Zeilen, die ein ESC-Zeichen enthalten, also die Zeilen, die eines oder beide der verdoppelten Wörter enthalten. (Anmerkung: Hier wird angenommen, dass die Datei nicht schon von vornherein ASCII-ESC-Zeichen enthält. Wenn dem nicht so ist, werden Zeilen ohne doppelte Wörter fälschlich ausgegeben.)
- Zeile 28: Die Variable $ARGV enthält auf ebenso magische Weise den Namen der Eingabedatei, von der zuletzt gelesen wurde. Zusammen mit /m und /g stellt diese Substitution jeder logischen Zeile im String diesen Dateinamen voran. Cool!
Am Schluss spuckt print aus, was im String übrig geblieben ist, mit Dateinamen und Escape-Sequenzen. Die while-Schleife wiederholt die ganze Verarbeitung mit allen Strings (es werden hier nicht Zeilen, sondern ganze Abschnitte gelesen) aus der Eingabedatei.
Reguläre Ausdrücke mit Operatoren, Funktionen oder Objekten
Wie ich am Anfang von Erweiterte einführende Beispiele betont habe, dient Perl hier als Werkzeug, um Konzepte zu demonstrieren. Es ist ein sehr brauchbares Werkzeug, wie sich herausgestellt hat, aber ich will zeigen, dass sich das Problem ohne Schwierigkeiten auch in anderen Sprachen lösen lässt.
Dennoch, in Perl ist das alles ein bisschen einfacher zu demonstrieren, weil in Perl die regulären Ausdrücke sozusagen »Bürger erster Klasse« sind. Die regulären Ausdrücke sind so innig in Perl eingebaut wie die Operatoren + und - für Zahlen. Dadurch reduziert sich das Gepäck, das auch noch herumgetragen werden muss.
In den meisten Programmiersprachen ist das anders. Aus Gründen, die unter Features und Dialekte näher erläutert werden, gibt es in den meisten modernen Sprachen Funktionen und Objekte, mit denen man die regulären Ausdrücke manipuliert. Es gibt da beispielsweise eine Funktion, die zwei Strings erwartet: Der eine wird als regulärer Ausdruck interpretiert, der zweite als String, der abgesucht werden soll. Die Funktion gibt »wahr« zurück, wenn die Mustersuche erfolgreich war. Meist sind jedoch die zwei Schritte (erstens die Interpretation des Strings als regulärer Ausdruck und zweitens die Anwendung des Ausdrucks auf den Text) auf zwei oder mehr Funktionen aufgeteilt, wie beispielsweise im folgenden Java-Listing. Das Programm benutzt das java.util.regex-Package, das in Java ab Version 1.4 enthalten ist.
Oben im Programm erkennt man die gleichen drei regulären Ausdrücke wie in der Perl-Version, die als Strings an die Routine Pattern.compile übergeben werden. Beim Vergleich erkennt man ein paar zusätzliche Backslashes, aber das ist eine direkte Folge davon, dass in Java reguläre Ausdrücke als Strings eingegeben werden müssen. Wenn ein Backslash zur Regex gehört, muss er in Java durch einen weiteren Backslash maskiert werden, damit er nicht vom String-Parser von Java auf seine eigene Art interpretiert wird.
»Verdoppelte Wörter« in Java
import java.io.*;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
public class TwoWord
{
public static void main(String [] args)
{
Pattern regex1 = Pattern.compile(
"\\b([a-z]+)((?:\\s|\\<[^>]+\\>)+)(\\1\\b)",
Pattern.CASE_INSENSITIVE);
String replace1 = "\033[7m$1\033[m$2\033[7m$3\033[m";
Pattern regex2 = Pattern.compile("^(?:[^\\e]*\\n)+", Pattern.MULTILINE);
Pattern regex3 = Pattern.compile("^([^\\n]+)", Pattern.MULTILINE);
// Für alle auf der Befehlszeile angegebenen Argumente ...
for (int i = 0; i < args.length; i++)
{
try {
BufferedReader in = new BufferedReader(new FileReader(args[i]));
String text;
// Für jeden Abschnitt der Datei ...
while ((text = getPara(in)) != null)
{
// Drei Substitutionen.
text = regex1.matcher(text).replaceAll(replace1);
text = regex2.matcher(text).replaceAll("");
text = regex3.matcher(text).replaceAll(args[i] + ": $1");
// Resultat ausgeben.
System.out.print(text);
}
} catch (IOException e) {
System.err.println("Kann Datei ["+args[i]+"] nicht lesen: " + e.getMessage());
}
}
}
// getPara liest den nächsten Abschnitt und gibt ihn als einen einzigen String zurück.
static String getPara(BufferedReader in) throws java.io.IOException
{
StringBuffer buf = new StringBuffer();
String line;
while ((line = in.readLine()) != null &&
(buf.length() == 0 || line.length() != 0))
{
buf.append(line + "\n");
}
return buf.length() == 0 ? null : buf.toString();
}
}
Es fällt außerdem auf, dass die regulären Ausdrücke nicht da angegeben werden, wo sie in Aktion treten, sondern am Anfang des Programms, im Initialisierungsteil. Die Funktion Pattern.compile analysiert die regulären Ausdrücke nur und baut daraus ihre eigenen, internen, »kompilierten Versionen« auf. Diese werden Pattern-Variablen (regex1 usw.) zugewiesen. Im Textverarbeitungsteil des Programms werden diese kompilierten Versionen mit der Routine regex1.matcher(text) eingesetzt und durch den Ersatztext ersetzt. Die Details dazu lernen Sie unter Features und Dialekte. Hier geht es mir darum zu zeigen, dass in jeder Programmiersprache zwei Dinge erforderlich sind: einmal der Dialekt der regulären Ausdrücke und zweitens die Art, wie man die regulären Ausdrücke in dieser bestimmten Sprache einsetzt.
<< zurück | vor >> |
Tipp der data2type-Redaktion: Zum Thema Reguläre Ausdrücke bieten wir auch folgende Schulungen zur Vertiefung und professionellen Fortbildung an: |
Copyright der deutschen Ausgabe © 2008 by O’Reilly Verlag GmbH & Co. KG
Für Ihren privaten Gebrauch dürfen Sie die Online-Version ausdrucken.
Ansonsten unterliegt dieses Kapitel aus dem Buch "Reguläre Ausdrücke" denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.
O’Reilly Verlag GmbH & Co. KG, Balthasarstr. 81, 50670 Köln