Umgang mit Dateinamen

(Auszug aus "Reguläre Ausdrücke" von Jeffrey E. F. Friedl)

Beim Umgang mit Dateinamen ergeben sich viele gute Beispiele für reguläre Ausdrücke, sei es mit Pfadnamen unter Unix wie /usr/local/bin/perl oder mit Windows-Dateinamen wie \Program Files\Yahoo!\Messenger. Ausprobieren ist kurzweiliger als Lesen, deshalb baue ich diese Ausdrücke in Programmbeispiele in Perl, PHP (die preg-Funktionen), Java und VB.NET ein. Wenn Sie an einer bestimmten Sprache nicht interessiert sind, lassen Sie die Programmlistings aus – es sind die regulären Ausdrücke, die hier zählen.

Verzeichnisangabe aus Pfadnamen entfernen

Als erstes Beispiel wollen wir die Verzeichnisangabe eines Pfadnamens entfernen, so dass nur der Dateiname übrig bleibt, es soll also beispielsweise /usr/local/bin/gcc in gcc verwandelt werden.

Oft hilft es schon, ein Problem auf eine Art zu formulieren, die eine Lösung nahelegt. In diesem Fall soll alles bis und mit dem letzten Slash (Backslash bei Windows) entfernt werden. Wenn kein Slash vorhanden ist, brauchen wir gar nichts zu tun. Ich habe mehrfach vor zu leichtfertigem Gebrauch von ˹.*˼ gewarnt, aber hier wird gerade das gierige Verhalten ausgenutzt. In der Regex ˹^.*/˼ verbraucht das ˹.*˼ zunächst die ganze Zeile, aber die Regex-Maschine geht dann bis zum letzten Slash zurück (Backtracking) und findet einen globalen Treffer.

Die folgende Tabelle zeigt, wie man in unseren vier Beispielsprachen sicherstellt, dass ein Pfadname in der Variable f keine führenden Pfadelemente enthält:

Programmiersprache Code
Perl $f =~ s{^.*/}{};
PHP $f = preg_replace('{^.*/}', '', $f);
java.util.regex f = f.replaceFirst("^.*/", "");
VB.NET f = Regex.Replace(f, "^.*/", "")

Der reguläre Ausdruck (bzw. der String, der als regulärer Ausdruck interpretiert wird) ist unterstrichen; die mit der Mustersuche zusammenhängenden Komponenten sind halbfett dargestellt.

Bei Windows-Pfadnamen wird als Pfad-Trennzeichen der Backslash statt dem Slash verwendet, die Regex wird zu ˹^.*\\˼. Für die Regex-Maschine muss dieser Backslash verdoppelt werden, aber die mittleren zwei Beispiele zeigen, dass je nach Sprache noch mehr Backslashes notwendig sind:

Programmiersprache Code
Perl $f =~ s/^.*\\//;
PHP $f = preg_replace('/^.*\\\/', '', $f);
java.util.regex f = f.replaceFirst("^.*\\\\", "");
VB.NET f = Regex.Replace(f, "^.*\\", "")

Die Unterschiede in den einzelnen Sprachen für Unix und für Windows sind schon bemerkenswert, insbesondere der vervierfachte Backslash in Java (siehe unter Strings als reguläre Ausdrücke).

Und was ist nun, wenn die Regex nicht passt? Wenn der Pfadname in f keinen Slash enthält, passt die Regex nicht, die Substitution wird nicht ausgeführt, und der String bleibt, wie er war. Genau das wollen wir.

Für Effizienz-Betrachtungen muss man untersuchen, wie eine NFA-basierte Regex-Maschine vorgeht. Schauen wir, was geschieht, wenn wir den Zirkumflex am Anfang weglassen (das kann leicht passieren) und dann die Regex auf einen String ohne Slash anwenden. Die Maschine beginnt wie immer am Stringanfang. Das ˹.*˼ läuft sofort bis ans Ende des Strings, dann muss jedes einzelne erkannte Zeichen zurückgenommen werden, bis ein Slash gefunden wird. Irgendwann wird durch dieses Backtracking wieder der Ausgangspunkt erreicht, und es wurde noch immer kein Treffer erzielt. Die Maschine entscheidet, dass es kein Matching vom Anfang des Strings aus gibt. Damit ist die Suche aber noch nicht beendet.

Das »Getriebe« geht zum nächsten Zeichen im String und setzt die Regex-Maschine darauf an. Dies muss (theoretisch) für jede Position im String wiederholt werden. Dateinamen sind meist kurz, aber bei anderen ähnlichen Fällen kann der Text sehr lang sein und erzeugt damit eine große Zahl von Backtracking-Operationen. (Bei einem DFA tritt dieses Problem natürlich nicht auf.)

In der Praxis »merkt« ein gutes Getriebe, dass eine Regex nie passen kann, wenn sie mit ˹.*˼ beginnt und schon am Anfang des Strings nicht passt. Es spart sich die Verschiebungen zu den weiteren Positionen im String und gibt auf (siehe unter »Implizite Zeilenanker«-Optimierung). Trotzdem ist es klüger, den Zeilenanker explizit hinzuschreiben, wie wir das getan haben.

Dateinamen aus einem Pfadnamen herauslösen

Wir können auch ganz anders vorgehen: Wir suchen das letzte Element – den Dateinamen – im Pfadnamen und speichern den gefundenen String in einer anderen Variablen. Der Dateiname ist »alles am Ende, das kein Slash ist«: ˹[^/]*$˼. Diesmal ist der Anker nicht nur eine Optimierung; wir brauchen das Dollarzeichen am Ende wirklich. Wir können in Perl etwa schreiben:

$Pfad =~ m{([^/]*)$};      # Variable $Pfad mit Regex testen
$DateiName = $1;           # gefundenen Text abspeichern

Es fällt auf, dass ich gar nicht überprüfe, ob das Matching erfolgreich war, weil ich weiß, dass die Regex immer passt. Die einzige zwingende Vorschrift in der Regex ist die, dass der Treffer am Ende des Strings enden muss, und sogar der leere String hat ein Ende. Wenn ich also nachher $1 benutze, um den von den Klammern eingefangenen Wert abzuspeichern, bin ich sicher, dass etwas gefunden wurde – auch wenn es der leere String ist, wenn der Pfadname auf einen Slash endet.

Noch eine Bemerkung zur Effizienz: Bei einem NFA ist ein Ausdruck wie ˹[^\/]*$˼ sehr ineffizient. Wenn man die einzelnen Schritte einer NFA-Maschine verfolgt, findet man eine große Anzahl von Backtrackings. Sogar bei einem relativ kurzen Beispiel wie ›/usr/local/bin/perl‹ sind bereits über 40 Backtrackings involviert, bis der Treffer gefunden wird. Betrachten wir einen Matching-Versuch, der bei ...▵local/... beginnt. ˹[^/]*˼ passt auf alles bis zum zweiten l, dann wird das ˹$˼ mit dem Slash verglichen (negativ), und für jedes der Zeichen l, a, c, o, l muss ein gespeicherter Zustand hervorgeholt werden. Nicht genug, der größte Teil wird gleich beim nächsten Versuch, der von ...l▵ocal/... ausgeht, wiederholt; dann nochmals bei ...lo▵cal/... usw.

Das soll uns aber in diesem Fall keine Sorgen machen, denn Dateinamen sind kurz, und 40 Backtrackings sind wenig – problematisch wird es bei 40 Millionen Backtrackings! Der Gedanke kann aber wichtig sein, wenn mit viel größeren Suchtexten umgegangen wird.

Auch wenn dies ein Buch über reguläre Ausdrücke ist, muss ich an dieser Stelle betonen, dass reguläre Ausdrücke nicht immer die Antwort auf alle Probleme sind. Die meisten Programmiersprachen bieten spezialisierte Routinen für den Umgang mit Pfadnamen. Trotzdem, um des Beispiels willen, gehe ich sogar noch weiter.

Sowohl Verzeichnis- als auch Dateinamen herauslösen

Die nächste Stufe ist das Zerlegen eines vollständigen Pfadnamens in eine Dateinamen- und eine Verzeichniskomponente. Dazu gibt es viele Möglichkeiten, je nachdem, was gefordert ist. Zunächst könnte man versucht sein, ˹^(.*)/(.*)$˼ zu verwenden und mit $1 und $2 auf die entsprechenden Teile zuzugreifen. Die Regex sieht hübsch ausgeglichen aus, und mit dem, was wir über gierige Quantifier wissen, sind wir sicher, dass nie etwas mit einem Slash in $2 landen wird. Der einzige Grund, dass das erste ˹.*˼ überhaupt etwas übrig lässt, ist der Slash, der das Backtracking auslöst. Für das zweite ˹.*˼ bleiben genau die Zeichen übrig, die das erste beim Backtracking zurückgeben musste. Damit erhalten wir den Verzeichnisteil in $1 und den Dateinamen (oder jedenfalls die letzte Komponente das Pfadnamens) in $2.

Wir verlassen uns hier auf das gierige erste ˹(.*)/˼ und sind nur deshalb sicher, dass das zweite ˹(.*)˼ keinen Slash einfangen wird. Wir verstehen gierige Quantifier und können das tun. Ich will mich aber genauer ausdrücken und lasse mit ˹[^/]*˼ für den Dateinamen-Teil explizit keinen Slash zu: ˹^(.*)/([^/]*). Dieser Ausdruck erklärt sich zudem wesentlich besser von selbst.

Ein Problem mit dieser Regex ist, dass sie mindestens einen Slash braucht, damit sie einen Treffer findet. Wenn wir sie auf etwas wie datei.txt anwenden, passt sie nicht. Das kann gewollt sein, wenn dies durch das Programm abgefangen wird:

if ( $Pfad =~ m!^(.*)/([^/]*)$! ) {
    # Treffer -- $1 und $2 sind gültig.
    $Verzeichnis = $1;
    $DateiName = $2;
} else {
    # Kein Treffer, also enthält der Pfadname kein /.
    $Verzeichnis = ".";    # "datei.txt" wird zu "./datei.txt" ("." ist das Arbeitsverzeichnis).
    $DateiName = $Pfad;
}

  

<< 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