Ein kleiner Altavista für die Festplatte gefällig?
Das Modul Text::Query
sucht in Text-Dokumenten
nach vorgegebenen Ausdrücken.
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 Perls File::Find
-Modul problemlos, nur Pattern Matching,
das über reguläre Ausdrücke hinausgeht, war bislang noch mit
Aufwand verbunden. Das Text::Query
-Modul 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
-Moduls 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. Und "hello world"
(in doppelten Anführungszeichen)
passt nur auf Dokumente, in denen wörtlich irgendwo hello world
steht.
Standardmäßig 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 dem Advanced Search auf AltaVista.com. hello AND world
passt auf Dokumente, die hello
und world
irgendwo enthalten,
hello OR world
ist zufrieden, wenn auch 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
suchte 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 $query_string
den Query-String enthält
(z. B. "hello AND world"
)
und $mode
entweder auf "simple_text"
oder "advanced_text"
gesetzt ist. Anschließend stellt
$yesno = $query->match($text);
fest, ob der Text $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 C<simple_text>- oder C<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
gesetzter 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 10 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.
Das Textstück in Listing test.dat
, 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 test.dat
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 test.dat
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.
Kommandozeilenparameter | Ausdruck passt auf Listing test.dat oder nicht? |
---|---|
'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 |
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 C<findsearch.pl> weist das Skript mit C<use strict> an, keine schlampigen Konstruktionen durchgehen zu lassen und alle Variablen ordentlich mit C<my> zu lokalisieren. Anschließend kommen wichtige Module hinzu: C<Getopt::Std> zum Erkennen von Kommandozeilenoptionen, C<IO::File> für moderne Filehandles, C<File::Find> zum Durchsuchen von Unterverzeichnissen und das frisch vom CPAN geholte C<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 $mode
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 $File::Find::dir
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 $q
dazuschmuggelt.
search_file
ab Zeile 31 holt das Query-Objekt ab, setzt $file
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
$data
ein. Falls im System irgendwelche 10-Gigabyte-Textdateien
herumlungern, kann dies zu Problemen führen und, falls dies der
Fall ist, sollte man noch einen Test der Art
return if -s $file > 1_000_000;
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 vom CPAN! Viel Spass beim Stöbern, bis zum nächsten Mal!
test.dat
This module provides an object that parses a string containing a Boolean query expression similar to an AltaVista "simple query".
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 '', <$fh>; 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 }
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |