![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
||||||||
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
|||||||
![]() |
|||||||
![]() |
|
||||||
![]() |
Irgendwo in den 3000 Unterverzeichnissen auf meiner Festplatte habe ich doch vor drei Wochen mal ein C-Programm angefangen ... bloß -- wo war das gleich wieder? Wie hieß die Datei noch? Wieder test.c? t.c? Keine Ahnung mehr, Hilfe! Wer wie ich öfter vor solchen Problemen steht, wird das heutige Skript zu schätzen wissen: Es wühlt sich durch alle Textdateien auf der Festplatte und prüft, ob deren Inhalt komplexen Bedingungen wie "Soll push_back enthalten, aber nicht printf" oder "printf soll nicht mehr als 10 Worte von #Hello World' entfernt stehen" genügt.
In Unterverzeichnisse abzusteigen und in die Tiefe vorzudringen geht dank des Perl-Modules File::Find problemlos, nur "Pattern Matching", das über reguläre Ausdrücke hinausgeht, war bislang noch mit Aufwand verbunden. Das Modul Text::Query von Eric Bohlman und Loic Dachary vom CPAN schafft da Abhilfe, denn es implementiert einen Query-Prozessor, der sich mit simple_text und advanced_text in zwei verschiedenen Modi betreiben lässt.
Im simple_text-Modus reagiert Text::Query wie ein "Simple Query" auf AltaVista.com. Ein Query besteht aus mehreren Worten oder in doppelte Anführungszeichen eingeschlossenen Ausdrücken. So sucht die Vorgabe "hello world" nach Dokumenten, die entweder "hello" oder "world" enthalten -- nicht notwendigerweise direkt hintereinander oder in dieser Reihenfolge. "+hello -world" hingegen stellt zur Bedingung, dass "hello" vorkommt, "world" jedoch nicht.
Tabelle 1: Beispiel-Ausdrücke | |
'this module' | Match, da Groß- oder Kleinschreibung irrelevant |
'object cowboy' | Match, da #1 vorkommt und im simple-Modus die oder-Verknüpfung zählt |
'object +cowboy' | Kein Match, da 'cowboy' zwingend vorgeschrieben |
'+object -cowboy' | Match, da 'object' vorkommt und 'cowboy' nicht |
'"module provides"' | Match, da wörtlich so vorkommend |
'"provides object"' | Kein Match, da nicht wörtlich so vorkommend |
-a 'object AND cowboy' | Kein Match, da der advanced-Modus eingestellt ist und nicht beide Ausdrücke vorkommen |
-a 'object AND NOT cowboy' | Match, da 'object' vorkommt und 'cowboy' nicht |
-a 'object OR cowboy' | Match, da object vorkommt und das genügt |
-a 'module NEAR expression' | Kein Match, da elf Worte dazwischen liegen |
-a 'module NEAR object' | Match, da zwei Worte dazwischen liegen |
-a '"module provides" | Match, da wörtlich so vorkommend |
-a '"provides object" | Kein Match, da nicht wörtlich so vorkommend |
Und "hello world" (in doppelten Anführungszeichen) passt nur auf Dokumente, in denen wörtlich irgendwo "hello world" steht. Normalerweise achtet der Matcher nicht auf Groß- und Kleinschreibung, dies kann aber, wie später gezeigt wird, aktiviert werden.
advanced_text hingegen bietet logische Operatoren, ähnlich Altavistas "Advanced Search": "hello AND world" passt auf Dokumente, die "hello" und "world" irgendwo enthalten, "hello OR world" ist zufrieden, wenn nur eines dieser Worte vorkommt. "hello NEAR world" fordert, dass beide Worte in einem einstellbaren Abstand stehen; üblicherweise dürfen nicht mehr als zehn Worte dazwischen liegen. Worte, die nicht auftauchen dürfen, schließt NOT aus und Phrasen, die Leerzeichen enthalten, halten doppelte Anführungszeichen zusammen: ""hello world" AND NOT programming" sucht nach Dateien, die zwar den Ausdruck "hello world" enthalten, in denen aber nicht von "programming" die Rede ist. Eine wörtliche Suche nach den Schlüsselworten AND, OR, NEAR oder NOT lässt sich bewerkstelligen, indem man sie in doppelte Anführungszeichen einschließt: ""and" OR "or"" sucht nach Dokumenten, die entweder "and" oder "or" enthalten. Ein neues Query-Objekt entsteht mit
$query = Text::Query-new ($query_string, -mode = $mode);
wobei den Query-String enthält (zum Beispiel "hello AND world") und entweder auf simple_text oder advanced_text gesetzt ist. Anschließend stellt
$yesno = $query-match($text);
fest, ob der Text auf den eingestellten Query passt oder nicht und liefert dementsprechend einen wahren oder falschen Wert zurück. Einmal aufgesetzt, speichert das Text::Query-Objekt den Query intern optimiert und lässt beliebig viele Aufrufe der match-Methode auf verschiedene Textstücke zu. Außer dem simple_text- oder advanced_text-Modus nimmt der Matcher noch weitere Optionen entgegen: -case = 1 macht den Matcher für Groß- und Kleinschreibung empfänglich. Normalerweise spielt es keine Rolle, ob "Hello" oder "hello" im Text steht -- der Query-String hello passt auf beide Stellen. Steht der -case-Schalter auf 1, spricht der Parser nur auf die zweite Textstelle an.
Außer den boolschen Konstrukten, die einen Query zusammenstricken, erlaubt Text::Query noch reguläre Perl-5-Ausdrücke, falls der -regexp-Schalter auf 1 gesetzt ist. Das Query-Objekt
$query = Text::Query-new ('\\bprint\b', -regexp = 1, -mode = "advanced_text");
würde wegen der mit \b festgelegten Wortgrenzen zwar auf "print" anschlagen, nicht jedoch auf "printf".
Während mehrere Leerzeichen, Tabulatoren und Zeilenumbrüche normalerweise gleichermaßen, nämlich als ein Leerzeichen, behandelt werden, weist der auf 1 gesetzte Schalter -litspace den Matcher an, Leerzeichen im Query-String wörtlich zu nehmen.
Und während das NEAR-Konstrukt im advanced_text-Modus üblicherweise Übereinstimmungen meldet, falls die kombinierten Worte nicht mehr als zehn Worte auseinander liegen, stellt die -near-Option beliebige Zahlenwerte ein.
Soviel zur Syntax von Text::Query -- Zeit für eine richtige Anwendung: findsearch.pl findet passende Textdateien unterhalb eines eingestellten Verzeichnisses.
Listing 1: findsearch.pl |
01 #!/usr/bin/perl -w 02 03 use strict; 04 use Getopt::Std; 05 use IO::File; 06 use File::Find; 07 use Text::Query; 08 09 my %opts; 10 getopts('a', \%opts); 11 12 my $mode = $opts{a} ? "advanced_text" : "simple_text"; 13 14 my ($dir, $query) = ARGV; 15 16 if(! defined $dir or ! -d $dir) { 17 usage("Directory not specified or unreadable"); 18 } 19 20 if(! defined $query) { 21 usage("No query given"); 22 } 23 24 my $q=Text::Query-new($query, 25 -mode = $mode, 26 ); 27 28 find(sub { search_file($q) }, $dir); 29 30 ################################################## 31 sub search_file { 32 ################################################## 33 my ($q) = _; 34 35 my $file = $_; 36 37 return unless -T $file; 38 39 my $fh = IO::File-new(" $file"); 40 41 if(! $fh) { 42 warn "Cannot open '$File::Find::dir/$file'"; 43 return 1; 44 } 45 46 my $data = join '', ; 47 48 $fh-close; 49 50 if($q-match($data)) { 51 print "$File::Find::dir/$file\n"; 52 } 53 54 $_ = $file; 55 } 56 57 ################################################## 58 sub usage { 59 ################################################## 60 my ($message) = _; 61 (my $prog = $0) =~ s#.*/##g; 62 63 print EOT; 64 $message 65 usage: $prog [-a] dir query 66 dir: Directory to start search in 67 query: Query string 68 simple: [+-]ausdruck [+-]ausdruck ... 69 -a: ausdruck AND|OR|NEAR [NOT] ausdruck ... 70 EOT 71 72 exit 1; 73 } |
Das Textstück in Listing 2, das aus der Text::Query-Dokumentation stammt, soll für die nachfolgenden Untersuchungen herhalten.
findsearch.pl erwartet als Parameter das Verzeichnis, in dem es die Suche nach passenden Dateien starten soll und den Query-Ausdruck, der entscheidet, ob eine Datei passt oder nicht. Mit der Option -a schaltet findsearch.pl vom "simple_text" in den "advanced_text"-Modus, verträgt dann also auch boolsche Operationen wie AND und OR wie oben beschrieben. Ist der Beispieltext von Listing 2 zum Beispiel irgendwo in testdir/test.dat begraben, gibt
findsearch.pl -a testdir \ 'object AND NOT cowboy'
schnell "testdir/test.dat" aus, denn testdir/test.dat erfüllt den angegebenen Query. Es enthält den Ausdruck "object" in der ersten Zeile und nirgendwo den Ausdruck "cowboy". Tabelle 1 zeigt einige Kommandozeilenoptionen im Einsatz und deckt in der zweiten Spalte auf, ob der Matcher das Textstück aus Listing 2 unter diesen Bedingungen als passend ansah. Tabelle 1 startet im simple_text-Modus und arbeitet sich dann über den advanced_text-Modus zu komplizierteren Queries weiter.
Listing 2: test.dat |
This module provides an object that parses a string containing a Boolean query expression similar to an AltaVista "simple query". |
findsearch.pl setzt das korrekt installierte Modul Text::Query vom CPAN voraus. Es wird am einfachsten mit der CPAN-Shell und
perl -MCPAN -eshell cpan install Text::Query
von dort abgeholt und auf die heimische Festplatte kopiert. Zeile 3 in findsearch.pl weist das Skript mit use strict an, keine schlampigen Konstruktionen durchgehen zu lassen und alle Variablen ordentlich mit my zu lokalisieren. Anschließend kommen wichtige Module hinzu: Getopt::Std zum Erkennen von Kommandozeilenoptionen, IO::File für moderne Filehandles, File::Find zum Durchsuchen von Unterverzeichnissen und das frisch vom CPAN geholte Text::Query zum Erkennen von AltaVista-ähnlichen Patterns in Textstücken.
Zeile 10 legt -a als gültigen Kommandozeilenschalter fest und setzt den Eintrag 'a' im Hash %opts auf 1, falls -a gesetzt wurde. Anschließend entfernt es den Schalter auch noch aus ARGV, um dem restlichen Programm die Arbeit zu erleichtern. Zeile 14 muss danach nur noch die verbliebenen Parameter abholen und diese als das Startverzeichnis und den Query-Ausdruck interpretieren.
Falls das Startverzeichnis oder der Query-Ausdruck fehlen, verzweigen die Zeilen 17 oder 21 zur Funktion usage, die ab Zeile 58 definiert ist, eine kurze Bedienungsanleitung ausgibt und das Programm daraufhin terminiert.
Andernfalls erzeugt Zeile 24 ein neues Text::Query-Objekt und gibt diesem den in Zeile 12 gesetzten mit, der entweder auf simple_text oder advanced_text steht.
Zeile 28 startet die Suche im angegebenen Unterverzeichnis und in beliebigen Stockwerken darunter. Die find-Funktion aus dem File::Find-Modul nimmt als Parameter eine Referenz auf eine Funktion und das Startverzeichnis entgegen, ruft für jeden gefundenen Eintrag (also nicht nur für Dateien, sondern auch Verzeichnisse) die angegebene Funktion auf und setzt dort die Spezial-Variable auf den Namen des Eintrags und auf den Verzeichnispfad dorthin. Damit der Finder an die Callback-Funktion auch noch das Query-Objekt übergibt, definiert Zeile 24 einfach eine Subroutine um den Aufruf von search_file, die jedesmal schnell dazuschmuggelt.
search_file ab Zeile 31 holt das Query-Objekt ab, setzt auf den auf dem -Weg hereingereichten Eintrag und, falls es sich bei diesem nicht um eine Textdatei handelt, bricht es die Funktion in Zeile 37 ab und kehrt zurück, damit der Finder mit dem nächsten gefundenen Eintrag fortfahren kann.
Wurde tatsächlich eine Textdatei gefunden, öffnet Zeile 39 diese, und Zeile 46 liest deren Inhalt auf einen Schlag in den Skalar ein. Falls im System irgendwelche 10-Gigabyte-Textdateien herumlungern, kann dies zu Problemen führen. Falls dies der Fall ist, sollte man noch einen Test der Art
return if -s $file 1000000;
anbringen, um Textdateien, die größer als ein Megabyte sind, zu ignorieren. Zeile 50 prüft, ob der Matcher den Daumen für die Datei nach oben oder unten hält -- und falls der Ausdruck passt, gibt Zeile 51 schlicht den Dateinamen mitsamt dem Pfad aus, und der Anwender weiß, dass dies eventuell die seit Ewigkeiten verschollene Datei ist -- endlich gefunden! Zeile 54 setzt wieder auf den ursprünglichen Wert zurück, worauf das File::Find-Modul besteht, sonst kracht's.
Und fertig -- dank raffinierter Modultechnik! Viel Spass beim Stöbern, bis zum nächsten Mal! (hmi)
Copyright © 2000 Linux New Media AG