Es ist angezapft! (Linux-Magazin, Juni 2005)

Neben Google, Amazon und Ebay bietet neuerdings auch Yahoo! eine Programmierschnittstelle zu einigen seiner Dienste an. Drei einfache Perl-Skripts stöbern heute damit Tippfehler, Internet-Bildchen und verschollen geglaubte Klassenkameraden auf.

Immer mehr Internet-Anbieter stellen ihre Dienste nicht nur über eine Browser-Schnittstelle ins Netz, sondern erlauben Entwicklern, deren eigene Applikationen mittels Web-APIs einzuklinken. Yahoo! bietet seit neuestem ein REST-(URLs hin, XML zurück)-Interface zu seinem Such-Engine an, zu dem es natürlich auch ein passendes Perl-Modul gibt: ``Yahoo::Search'', erhältlich vom CPAN und entwickelt vom großen Regex-Zampano Jeffrey Friedl.

Es vereinfacht die Suche nach Dokumenten, Bildern, Videos und einiges mehr, denn HTTP-Zugriffe und XML-Extraktion sind sauber hinter einfachen Methodenaufrufen versteckt. Wer eine Applikation entwickeln möchte, holt sich einfach eine eigene Application-ID ab. Sie berechtigt zu 5.000 Aufrufen der in diesem Artikel vorgestellten Dienste pro Tag. Die Registrierung erfordert auf [2] lediglich eine Yahoo-ID, die es ebenfalls kostenlos gegen die Angabe einer gültigen Email-Adresse gibt.

Demokratische Rechtschreibung

Falls man nicht genau weiss, wie ein bestimmtes Wort geschrieben wird, bietet sich der Duden oder ein Englisch-Wörterbuch an. Neuerdings kann man aber auch einen Such-Engine zur Rechtschreibprüfung heranziehen.

Im Wildwuchs des Internets finden sich zwar haufenweise Rechtschreibfehler, doch erstaunlicherweise folgen die Mehrzahl aller Webseiten der korrekten Rechtschreibung. Such-Engines bemerken die Diskrepanz und bieten üblicherweise ``Did you mean ...?''-Funktionen an. Das Programm typo ruft über die Methode Term() und dem Parameter Spell den Korrektur-Webservice bei Yahoo! auf:

    $ typo miscelaneous
    Corrected: miscellaneous

Und der Vorteil eines Search-Engines gegenüber einem herkömmlichen Lexikon ist freilich, dass ersterer auch populärwissenschaftlich auf dem Laufenden ist:

    $ typo foo foughters
    Corrected: foo fighters

Wer also ein Lied im Radio hört, aber den Ansager beim Bekanntgeben des Bandnamens anschließend nur undeutlich versteht, gibt einfach das Gehörte in die Suchmaschine ein. Falls die Gruppe recht bekannt ist, wird's die Korrektur schon richten. Und sogar einige deutsche Wörter kennt man bei Yahoo!:

    $ typo hotzenplutz
    Corrected: hotzenplotz

Listing typo zeigt die kurze Implementierung: Die use-Anweisung in Zeile 13 zieht das vorher mit einer CPAN-Shell installierte Modul Yahoo::Search herein und übergibt ihm die vorher auf [2] abgeholte Application-ID. Die ID linux_magazin ist gültig und sollte für kurze Tests reichen. Wer eines der heute vorgestellten Skripts häufiger benutzt, sollte sich eine eigene holen.

Die Methode Terms() nimmt mit dem Parameter Spell ein Wort oder eine Wortfolge entgegen, schickt sie an den Service auf der Yahoo!-Seite, untersucht das zurückgeschickte XML und fieselt eine etwaige Antwort heraus. Zeile 15 legt sie in der Variablen $suggestion ab. Das anschließende if-else-Konstrukt gibt sie entweder die Anwort aus, oder, falls Such-Engine nichts außergewöhnliches zum untersuchten Begriff eingefallen ist, ``No corrections''.

Leider hinkt die Internationalisierung des Service noch etwas hinterher, und zu Wörtern mit Umlauten fallen ihm (noch) keine Korrekturen ein.

Listing 1: typo

    01 #!/usr/bin/perl -w
    02 ###########################################
    03 # typo - Get Yahoo! spelling corrections
    04 # Mike Schilli, 2005 (m@perlmeister.com)
    05 ###########################################
    06 use strict;
    07 
    08 my $term = "@ARGV";
    09 
    10 die "usage: $0 word/phrase ..." 
    11     unless length $term;
    12 
    13 use Yahoo::Search AppId => "your_yahoo_token";
    14 
    15 my($suggestion) = Yahoo::Search->Terms(
    16     Spell => $term);
    17 
    18 if(defined $suggestion) {
    19     print "Corrected: $suggestion\n";
    20 } else {
    21     print "No suggestions\n";
    22 }

Auf der Suche nach der verlorenen Zeit

Die Spiders der Search-Engines grasen ständig das bekannte Internet nach neu auftauchenden Informationen ab. Entsprechend ändern sich die Suchresultate zu bestimmten Begriffen. Wer einmal am Tag eine Abfrage mit den Namen seiner alten Schulkameraden abfeuert, bekommt mit, falls diese neue Homepages aufziehen oder zu Rang und Namen kommen. Natürlich hat niemand die Zeit, das jeden Tag manuell durchzuführen und sich die Resultate zu merken.

Das Skript buddy wird deshalb einmal am Tag per Cronjob aufgerufen und holt jeweils die ersten 25 Ergebnisse zu einer Liste von Namen ein, die in der Konfigurationsdatei ~/.buddy liegen. Alle bis Dato unbekannten URLs und einen kurzen Ausschnitt aus dem zur Suchabfrage passenden Textteil der gefundenen Webseite schickt buddy dann an eine vorgegebene Email-Adresse. So bleibt der Empfänger auf dem Laufenden und bekommt mit, wenn sich ein bekannter Name irgendwo auftaucht -- vielleicht sogar bei einer Nobelpreisnominierung?

buddy hält die so gefundenen URLs in einem Cache vorrätig, der sich die Einträge einen Monat lang merkt und wiedergefundene Treffer so markiert, als wären sie gerade zum ersten Mal aufgetaucht. So simuliert der Cache ein ``schlechtes'' Gedächtnis, das sich wieder freut, falls ein alter Name nach einem Monat der Abwesenheit wieder auftaucht.

Abbildung 1: Nachricht per Email: Alte Klassenkameraden sind auf dem Web aufgetaucht.

In Zeile 10 erwartet das Skript die Email-Adresse, an die es die gefundene Änderungen verschickt. Wird buddy von der Kommandozeile mit der Option -v (für verbose) aufgerufen, initialisiert Zeile 22 das Log4perl-Framework mit dem Log-Level $DEBUG, und macht es damit gesprächiger. Andernfalls werden nur Log-Messages der Priorität WARN und höher ausgegeben.

Zeile 24 deklariert die weiter unten definierte Funktion mailadd, damit Perl weiß, dass es sich um eine Funktion handelt und Aufrufe schon vor der Definition ohne lästige Klammern erfolgen können. mailadd unterhält eine our-Variable $maildata, die es sich mit der ab Zeile 88 definierten Funktion mailsend teilt. Aufrufe von mailadd hängen nur Text an $maildata an, der dann beim Aufruf von mailsend per Mail::Send an die angegebene Email-Adresse abgeschickt wird.

Die aus dem Modul Sysadm::Install importierte Funktion plough (Englisch: pflügen), nimmt in Zeile 28 eine Callback-Funktion und einen Dateinamen entgegen. Sie liest die Datei ein, ruft den Callback nach jeder gelesenen Zeile auf und übergibt den Zeileninhalt in der Variablen $_. Zeile 29 verwirft mit # beginnende Kommentarzeilen und der chomp-Befehl sägt den Zeilenumbruch ab. Zeile 31 schiebt gefundene ``Buddies'' ans Ende des stetig wachsenden Arrays @buddies.

Zeile 48 kontaktiert dann über die Results()-Methode den Yahoo!-Dienst. Der Name eines Klassenkameraden aus der Konfigurationsdatei wird in doppelte Anführungszeichen eingerankt und dann als String (qq{"$buddy"}) mit dem Doc-Parameter übergeben, denn es handelt sich um eine Web-Suche. Die als Liste zurückkommenden Ergebnisobjekte geben über die Methoden Url() und Summary() URL und Kurzbeschreibung der Treffer aus.

Der in Zeile 34 angelegte File-Cache wird üblicherweise hinter den Kulissen in /tmp/FileCache angelegt und speichert abgelegte Einträge wegen des auf 30 Tage gesetzten Parameters default_expires_in einen Monat lang.

Listing 2: .buddy

    1 # Meine Klassenkameraden
    2 Bodo Ballermann
    3 Rudi Ratlos
    4 Rambo Zambo
    5 Elli Pirelli

Kodierung

Da sich der Webservice bei Anfragen und Antworten strikt an UTF-8 hält, müssen die Namen in UTF-8 in ~/.buddy stehen. Wer eine neuere Linux-Distribution fährt, dessen Editor wird die eingetippten Namen gleich in UTF-8 abspeichern. Wer noch hinterher hinkt, und mit Latin1 arbeitet, kann die Daten mit einem kleinen Programm wie

    # toutf8
    use Text::Iconv;
    my $conv = Text::Iconv->new("Latin1", "UTF-8");
    print $conv->convert(join '', <>);

und einem Kommandoaufruf wie

    $ toutf8 buddy.latin1 >~/.buddy

schnell von Latin1 nach UTF-8 konvertieren.

Im Skript ist lediglich die Email-Adresse in Zeile 10 an die lokalen Gegebenheit anzupassen. Ein Cron-Eintrag der Form

    0 5 * * * $HOME/bin/buddy

ruft das Skript dann einmal täglich um fünf Uhr früh auf, klappert den Such-Engine ab, frischt den Cache auf und schickt eine Email, falls sich etwas getan hat.

Listing 3: buddy

    001 #!/usr/bin/perl -w
    002 ###########################################
    003 # buddy - Keep track of buddies with
    004 #         Yahoo! Search
    005 # Mike Schilli, 2005 (m@perlmeister.com)
    006 ###########################################
    007 use strict;
    008 
    009 my $BUDDY_FILE = "$ENV{HOME}/.buddy";
    010 my $EMAIL_TO   = 'meldung@ich.lausche.com';
    011 
    012 use Sysadm::Install qw(:all);
    013 use Yahoo::Search;
    014 use Text::Wrap;
    015 use Cache::FileCache;
    016 use Log::Log4perl qw(:easy);
    017 use Getopt::Std;
    018 use Mail::Send;
    019 
    020 getopts("v", \my %o);
    021 
    022 Log::Log4perl->easy_init($o{v} ? 
    023                          $DEBUG : $WARN);
    024 sub mailadd;
    025 
    026 my @buddies = ();
    027 
    028 plough sub { 
    029          return if /^\s*#/;
    030          chomp;
    031          push @buddies, $_;
    032        }, $BUDDY_FILE;
    033 
    034 my $cache = Cache::FileCache->new({
    035     namespace           => "Buddy",
    036     default_expires_in  => 3600*24*30,
    037 });
    038 
    039 my $search = Yahoo::Search->new(
    040     AppId      => "linux_magazin",
    041     Count      => 25,
    042 );
    043 
    044 for my $buddy (@buddies) {
    045 
    046   DEBUG "Search request for '$buddy'";
    047 
    048   my @results = $search->Results(
    049       Doc => qq{"$buddy"}
    050   );
    051 
    052   my $buddy_printed = 0;
    053 
    054   DEBUG scalar @results, " results";
    055 
    056   for my $result (@results) {
    057         
    058     if($cache->get($result->Url())) {
    059       DEBUG "Found in cache: ", 
    060             $result->Url();
    061           # Refresh if found
    062         $cache->set($result->Url(), 1);
    063         next;
    064     }
    065 
    066     mailadd "\n\n### $buddy ###" 
    067       unless $buddy_printed++;
    068 
    069     mailadd $result->Url();
    070 
    071     $cache->set($result->Url(), 1);
    072 
    073     mailadd fill("    ", "    ", 
    074           $result->Summary()), "";
    075   }
    076 }
    077 
    078 mailsend();
    079 
    080 ###########################################
    081 sub mailadd {
    082 ###########################################
    083     our $maildata;
    084     $maildata .= "$_\n" for @_;
    085 }
    086 
    087 ###########################################
    088 sub mailsend {
    089 ###########################################
    090     our $maildata;
    091 
    092     return unless defined $maildata;
    093 
    094     DEBUG "Sending email: $maildata";
    095 
    096     my $msg = Mail::Send->new();
    097     $msg->to($EMAIL_TO);
    098     $msg->subject("Buddy Watch News");
    099     my $fh = $msg->open;
    100     print $fh $maildata;
    101     close $fh;
    102 }

Bilderreigen

Auch die Suche nach Bildern unterstützt der neue Service. Zu einem Begriff findet der Search-Engine eine Reihe passender Bilder, die das Skript slideshow dann im 5-Sekunden-Takt wie in einer Diashow nach und nach im Browser darstellt.

Wie Abbidlung 2 zeigt, stellt das Skript beim ersten Aufruf zunächst ein einfaches Suchformular dar. Trägt der Benutzer einen Suchbegriff ein (z.B. ``San Francisco''), und klickt auf den ``Search''-Knopf, ist der CGI-Parameter q gesetzt und Zeile 40 ruft die Methode Results() des Pakets Yahoo::Search auf. Der Parameter Image übergibt den Suchbegriff, ein Count von 50 limitiert das Ergebnis auf 50 Treffer, und ein auf 0 gesetztes AllowAdult verhindert mehr oder weniger erfolgreich, dass sich plötzlich nackte Menschen auf dem Bildschirm tummeln.

Da der Text der Bildunterschrift in UTF-8 daherkommt, zeigt die Methode header() in Zeile 21 dem Browser neben den üblichen Header-Zeilen an, das die dynamisch erzeugte Webseite in UTF-8 kodiert ist.

Das Skript slideshow speichert das Ergebnis einer Suchabfrage, also die Image-URLs und deren Summary-Texte, als Array von Arrays in einem persistenten Datei-Cache ab, damit der Dia-Projektor nicht bei jedem neuen Bild wieder zum Such-Engine zurückkehren muss.

Der persistente Datei-Cache Cache::FileCache speichert aber Key-Value Paare ab, wobei als Werte nur einfache Skalare und keine verschachtelten Strukturen erlaubt sind. Das Modul Storable hilft hier aus der Patsche, denn dessen Funktion freeze() kann eine Datenstruktur serialisieren, bevor sie in den Cache wandert. Kommt der serialisierte Datensalat wieder aus dem Cache zurück, wandelt ihn der De-Serialisierer thaw() wieder in eine verschachtelte Perl-Datenstruktur um.

Damit das CGI-Skript nicht versehentlich hereinkommende Daten ungeprüft für Systemaufrufe verwendet und damit Sicherheitslöcher in die Applikation reißt, steht in der She-Bang-Zeile hinter dem Aufruf des Perl-Interpreters die Option -T.

Der erste if-Block ab Zeile 23 kommt zum Einsatz, falls das Skript sowohl mit dem Query-String also auch mit der fortlaufenden Nummer des aktuellen Bildes aufgerufen wurde. Ist dies ist der Fall, steht im Cache die Folge der Bild-URLs und deren Beschriftungen. Zeile 24 taut den Array von Arrays auf und der Modulo-Operator in Zeile 26 sorgt dafür, dass die fortlaufend hochgezählte Bildnummer immer in den Array hinein zeigt und nie darüber hinausschießt.

Die in Zeile 27 mit dem Parameter 5 (Sekunden) aufgerufene Funktion refresh() ist ab Zeile 67 definiert. Sie gibt HTML-Sequenzen zurück, die den Browser mittels eines META-Tags veranlassen, nach Ablauf der übergebenen Anzahl von Sekunden eine neue Seite zu laden, die eine um Eins erhöhte Bildnummer aufweist.

Ein weiterer Parameter, $reset, legt fest, ob der nachzuladende URL das nächste Bild anzeigt (next_url zählt einfach den Nummern-Parameter s um eins Hoch) oder ob es mit dem Ursprungs-URL (hervorgeholt von der Funktion url() des CGI-Moduls) zurück zur Startseite mit dem Eingabefeld geht.

Um das Skript zu installieren, verfrachtet man es einfach ins Verzeichnis cgi-bin des Webservers und stellt den Web-Browser darauf ein. Willkommen zum unschlagbaren Charme von Privatfotos!

Listing 4: slideshow

    01 #!/usr/bin/perl -wT
    02 ###########################################
    03 # slideshow
    04 # Mike Schilli, 2005 (m@perlmeister.com)
    05 ###########################################
    06 use strict;
    07 
    08 use CGI qw(:all);
    09 use Yahoo::Search AppId => "linux_magazin";
    10 use Cache::FileCache;
    11 use Storable qw(freeze thaw);
    12 
    13 my $cache = Cache::FileCache->new({
    14     namespace          => 'slideshow',
    15     default_expires_in => 3600,
    16     auto_purge_on_set  => 1,
    17 });
    18 
    19 my $data;
    20 
    21 print header(-charset => "utf-8");
    22 
    23 if(param('q') and defined param('s')) {
    24     $data = thaw $cache->get(param('q'));
    25     my $seq = param('s');
    26     $seq %= scalar @$data;
    27     print refresh(5);
    28     print center(
    29        a({href => url()}, "Stop"), 
    30        a({href => next_url()}, "Next"), 
    31        p(),
    32        b(param('q')), ":", 
    33        i($data->[$seq]->[1]), p(),
    34        img({src => $data->[$seq]->[0]}),
    35        p(), a({href => $data->[$seq]->[0]}, 
    36                $data->[$seq]->[0]),
    37     );
    38 } elsif(param('q')) {
    39     my @results = 
    40         Yahoo::Search->Results(
    41           Image        => param('q'),
    42           Count        => 50,
    43           AllowAdult   => 0,
    44         );
    45     if(@results) {
    46         for(@results) {    
    47             push @$data, 
    48                  [$_->Url(), $_->Summary()];
    49         }
    50         print refresh(0);
    51         $cache->set(param('q'), 
    52                     freeze($data));
    53     } else {
    54         print refresh(0, 1);
    55     }
    56 } else {
    57     print h2("Slideshow Search"),
    58           start_form(),
    59           textfield(-name => 'q'), 
    60           submit(-value => "Search"),
    61 	  end_form(),
    62           font({size => 1}, 
    63                "Powered by Yahoo! Search");
    64 }
    65     
    66 ##########################################
    67 sub refresh {
    68 ##########################################
    69     my($sleep, $reset) = @_;
    70 
    71     return start_html(
    72       -title => "Slideshow",
    73       -head  => meta({
    74         -http_equiv => "Refresh", 
    75         -content    => "$sleep, URL=" . 
    76              ($reset ? url() : next_url())
    77     }));
    78 }
    79 
    80 ##########################################
    81 sub next_url {
    82 ##########################################
    83     my $s   = param('s');
    84     $s    ||= 0;
    85 
    86     return sprintf "%s?q=%s&s=%d", url(), 
    87                  param('q'), $s+1; 
    88 }

Abbildung 2: Eine Anfrage nach "San Francisco" im Suchfeld der Applikation ...

Abbildung 3: ... liefert eine spannende Diashow, hauptsächlich mit Urlaubsfotos von Privatleuten.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2005/06/Perl

[2]
Yahoo!s Developer API homepage: http://developer.yahoo.com

Michael Schilli

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