|
|
Mit einem Perl-Agenten und Jabber über Ebay-Auktionen informierenE-BaywatcherMichael Schilli |
Das Online-Auktionshaus Ebay bietet nach eigenen Angaben allein in Deutschland Zugriff auf mehr als eine Million Artikel. Um so schwerer wird es bei dieser Angebotsdichte, eine bestimmte Auktion zu finden. Das hier vorgestellte Skript »ebaywatch« (siehe Listing 1) sendet in regelmäßigen Abständen definierbare Suchanfragen an den Ebay-Server und wertet die gefundenen Auktionen nach ihrem Abschlussdatum aus.
Nähert sich eine Auktion ihrem Ende, verpackt das Skript eine kurze Beschreibung mit der URL zur Auktion in eine Instant-Messenger-Nachricht (IM) und schickt diese über den Jabber-Server an einen Gaim-Client[5]. Gaim lässt die Nachricht daraufhin dem interessierten Benutzer auf den Bildschirm flattern (siehe Abbildung 1). Ein Mausklick auf die URL genügt dann, um einzuschreiten und mitzusteigern.
Die Datei ».ebaywatchrc« im Homeverzeichnis des Anwenders legt fest, nach welchen Stichworten der Agent auf der Ebay-Website suchen soll. Jede nicht auskommentierte Zeile steht für eine Suchanfrage. Die Zeilen
# ~/.ebaywatchrc dwl 650 nikon
definieren zum Beispiel Anfragen nach der D-Link-Netzwerkkarte DWL-650 und nach allen möglichen Produkten der Firma Nikon.
Das Skript durchforstet die Titelfelder der einzelnen Auktionen und unterstützt mit den in[2] beschriebenen Kürzeln sogar erweiterte Suchfunktionen. So findet zum Beispiel die Zeile »foto -nikon« alle Fotoartikel, aber keine Produkte der Firma Nikon. Der Suchstring »beatles (dvd,cd)« fördert alle Angebote von Beatles-CDs und -DVDs ans Licht.
Wie so oft hat sich bereits jemand nach dem Motto "I wrote code, so you don't have to" (siehe[6]) daran gemacht, die Suche in den Ebay-Beständen als Perl-Modul zu realisieren. In den CPAN- Archiven findet sich Martin Thurns »WWW::Search::Ebay« mit dem Modul »WWW::Search::Ebay::ByEndDate«. Es generiert die vom Benutzer vorgegebenen Suchabfragen und sortiert die eintrudelnden Ergebnisse nach dem Enddatum - perfekt!
Das ebenfalls beim CPAN erhältliche »Net::Jabber«-Modul von Ryan Eatmon enthält ein vollständiges API, um funktionsfähige Jabber-Clients zu schreiben. Ebaywatch nutzt lediglich einen kleinen Teil davon: Es muss sich im Bedarfsfall nur schnell mit dem Jabber-Server »jabber.org« auf Port 5222 verbinden, ihm seine Anwesenheit mitteilen, eine Nachricht an den Mandanten abschicken und sich dann gleich wieder verabschieden. Wer mehr Funktionen nutzen will, findet sie in dem Buch des mächtigen Jabberers DJ Adams (siehe[3]).
Um den Ablauf zu vereinfachen, benutzen sowohl das Überwachungsskript als auch der Empfänger den gleichen Jabber-Account. Das ist möglich, da der Jabber-Server es erlaubt, sich mit einem einzigen Benutzernamen von mehreren IM-Clients aus gleichzeitig einzuloggen. Damit der Server die verschiedenen eingeloggten Benutzer voneinander unterscheiden kann, qualifizieren diese sich zusätzlich zu ihrem Benutzernamen noch mit einer so genannten Resource. Sie besteht aus einem String, der jeden Client in Verbindung mit dem Benutzernamen eindeutig identifiziert. Das Überwachungsskript benutzt den Resource-Namen »ebaywatcher«, während Gaim seinen eigenen Resource-String definiert. Neuere Gaim-Versionen erlauben es auch, die Resource über ein Dialogfenster einzustellen.
Die Konfigurationssektion am Anfang von »ebaywatch« definiert in Zeile 14 (Listing 1) die Variable »$EBAY_HOST«. Sie gibt an, welchen der vielen internationalen Ebay-Server das Skript kontaktieren soll. Im Listing ist »http://search .ebay.de« festgelegt. Wer das amerikanische Original bevorzugt, setzt »$EBAY _HOST« auf »http://search.ebay.com«. Die Variable »$MINS_TO_END« bestimmt, wie viele Minuten vor Ende einer Auktion die Blitznachricht eintreffen soll - voreingestellt sind zehn.
In der Datei aus »$SEEN_DB_FILE« legt Ebaywatch einen persistenten Hash ab. Dort speichert das Programm Statusinformationen, die es nicht nur innerhalb eines Laufs, sondern über mehrere Aufrufe hinweg benötigt. In der Zeile 28 bindet der »tie«-Befehl den globalen Hash »%SEEN« an die konfigurierte Datei. Das Modul »DB_File« sorgt dafür, dass jede Änderung im Hash auch in diesem File landet - vergleichbar einer einfachen Datenbank.
Die Option »O_ RDWR« sorgt für Lese- und Schreibrechte, während »O_CREAT« den »tie()« dazu veranlasst, die Datei bei Bedarf neu anzulegen. Zeile 32 sorgt dann dafür, dass der Hash sich auch wieder ordnungsgemäß von der Datei abkoppelt, falls das Programm abbricht.
Programme, die wie Ebaywatch im Hintergrund laufen, geben Statusmeldungen am besten in eine Logdatei aus. Dafür sorgen die drei Funktionen »DEBUG()«, »INFO()« und »LOGDIE()« aus dem Log::Log4perl-Fundus. Im Listing ist »/tmp/ebaywatch.log« als Logfile ausgewählt.
Die Konstruktion des Ebay-Objekts in Zeile 34 ist etwas ungewöhnlich, sie erfolgt über die »new«-Methode der »WWW::Search«-Klasse, die den String »'Ebay::ByEndDate'« als Parameter erhält. Die anschließende While-Schleife ab Zeile 39 iteriert durch die Zeilen der Datei »~/.ebaywatchrc«, eliminiert die Kommentare und Leerzeilen und merkt sich den jeweils geforderten Suchausdruck in »$term«.
Der persistente Hash »%SEEN« speichert unter den Schlüsseln »"url/$url"« die URLs von Auktionen, die Ebaywatch schon gemeldet hat und daher nicht erneut per IM-Nachricht senden soll. Es kann vorkommen, dass Ebay für einige Suchbegriffe nur Auktionen anzeigt, die so weit in der Zukunft liegen, dass sich eine weitere Anfrage in nächster Zeit nicht lohnen würde. Schließlich soll das Skript die Ebay-Betreiber nicht mit zu vielen Anfragen verärgern, die sowieso keine neuen Informationen enthalten können. Ebaywatch speichert diese Suchbegriffe unter den Schlüsseln »"notuntil/$term"« in »%SEEN« und legt als zugehörigen Wert die lokale Unix-Zeit der nächsten Abfrage fest.
Zeile 58 wandelt Sonderzeichen in URL-kompatible Sequenzen um und Zeile 60 bereitet die gesamte URL der Suchanfrage nach den Ebay-Richtlinien vor. Die While-Schleife ab Zeile 63 holt mit der »next_result()«-Methode die Ergebnisse. Folgende Methoden liefern wichtige Auktions-Informationen:
Wegen der unterschiedlichen Zeitanzeige bei »change_date()« zwischen dem amerikanischen und dem deutschen Format wandelt die ab Zeile 99 definierte Funktion »minutes()« einfach beide Darstellungen in Minuten um. Sie nutzt dazu eine simple Mustererkennung mit regulären Ausdrücken.
In der Ergebnisschleife erscheinen die zeitlich nächstgelegenen Auktionen immer zuerst, das garantiert »WWW:: Search::Ebay::ByEndDate«. Wenn also Zeile 77 feststellt, dass die nächste Auktion in der Ergebnisliste mehr als zehn Minuten in der Zukunft liegt und sie die nächste Untersuchung zehn Minuten vor Ende dieser Auktion anberaumt, kann das Skript getrost alle späteren Auktionen außer Acht lassen und mit »last« die Schleife abbrechen. In diesem Fall zieht es zehn Minuten von der Endzeit der Auktion ab, wandelt diesen Wert in die lokale Unix-Uhrzeit in Sekunden um und speichert diese unter »"notuntil/$term"« im permanenten Hash ab.
Steht der Ergebniszähler »$hits« am Ende der Schleife immer noch auf »0«, gab es zum angegebenen Suchbegriff keinen Treffer und die nächste Suche verschiebt sich um einen Tag.
Um eine Jabber-Nachricht abzuschicken, baut Ebaywatch in Zeile 86 den HTML-Code für einen Link und die verfügbaren Auktionsinformationen zusammen und eliminiert in Zeile 90 mit einem regulären Ausdruck alle nicht druckbaren Zeichen. Die Funktion »jabber _send()« nimmt dann die String-Nachricht als Parameter entgegen und erstellt ein neues »Net::Jabber::Client«-Objekt. Nach einer erfolgreichen Kontaktaufnahme mit »jabber.org« via »Connect()« sendet der Client seinen Benutzernamen und das Passwort für den Jabber-Account (siehe Kasten "Voraussetzungen für Ebaywatch"). Den »resource«-Parameter setzt das Skript wie oben beschrieben auf »ebaywatcher«.
In Zeile 138 teilt die Methode »PresenceSend()« dem Jabber-Server mit, dass der Skript-Client anwesend ist. Dieser Client interessiert sich nicht für die Präsenz anderer Clients, daher setzt er in Zeile 119 die korrespondierende Callback-Funktion auf Ignorieren:
$c->SetCallBacks( presence => sub {} );
Folglich ignoriert das Skript die Presence-Mitteilungen anderer Clients.
Die Jabber-ID des Mandanten setzt sich zusammen aus dem Benutzernamen und dem als »@jabber.org« angehängten Jabber-Server. Die Send-Methode schickt die als »Net::Jabber::Message«-Objekt verkleidete Nachricht an den Server, der sie auch dann entgegennimmt, wenn der Mandant gar nicht online ist. Das angehängte »/GAIM« bestimmt, dass die Methode »SendTo()« die Nachricht nicht an den Jabber-Client im Skript »ebaywatch« schickt (der ja unter der Resource »ebaywatcher« eingeloggt ist), sondern an den unter derselben Benutzer-ID (im Beispiel »mikes-ebay-watcher«) eingeloggten Gaim-Client, der daraufhin automatisch den Resource-Namen »GAIM« definiert.
Wenn das Skript von der Kommandozeile aus einwandfrei läuft (ein »tail -f Logdatei« hilft beim Überwachen), startet folgende Zeile in der Cron-Datei das Skript alle fünf Minuten:
*/5 * * * * /home/mschilli/bin/ebaywatch
Wer den ganzen Tag lang über einen Instant Messenger kommuniziert, wird sich über ein paar zusätzliche Nachrichten freuen, die der virtuelle Freund schickt. Und dann einfach draufgeklickt und mitgesteigert! (mwe/fjl)
Infos |
[1] Listings zu diesem Artikel: [ftp://ftp.linux-magazin.de/pub/listings/magazin/2004/01/Perl/] [2] David A. Karp, "eBay Hacks: 100 Industrial-Strength Tips and Tools": O'Reilly 2003, ISBN 0-59600-564-4 [3] DJ Adams, "Programming Jabber": O'Reilly 2002, ISBN 0-59600-202-5 [4] Logdateien automatisch begrenzen und rotieren: [http://log4perl.sourceforge.net/releases/Log-Log4perl/docs/html/Log/Log4perl/FAQ.html#how_can_i_roll_over_my_logfiles_automatically_at_midnight] [5] Gaim, ein Instant-Messenging-Client: [http://gaim.sourceforge.net] [6] T-Shirt "I wrote code so you don't have to": [http://www.thinkgeek.com/interests/oreilly/tshirts/6067] |
Der Autor |
Michael Schilli arbeitet als Web-Engineer für AOL/Netscape in Mountain View, 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 ist [http://perlmeister.com].
|
Listing 1: Ebay-Auktionen per Jabber überwachen |
001 #!/usr/bin/perl 002 ########################################### 003 # ebaywatch 004 # Mike Schilli, 2003 (m@perlmeister.com) 005 ########################################### 006 use warnings; 007 use strict; 008 009 our $JABBER_ID = "mikes-ebay-watcher"; 010 our $JABBER_PASSWD = "*******"; 011 our $JABBER_SERVER = "jabber.org"; 012 our $JABBER_PORT = 5222; 013 our $SEEN_DB_FILE = "/tmp/ebaywatch"; 014 our $EBAY_HOST = "http://search.ebay.de"; 015 our $MINS_TO_END = 10; 016 our $RC_FILE = "$ENV{HOME}/.ebaywatchrc"; 017 our %SEEN; 018 019 use Net::Jabber qw(Client); 020 use DB_File; 021 use Log::Log4perl qw(:easy); 022 use WWW::Search::Ebay; 023 024 Log::Log4perl->easy_init( 025 { level => $DEBUG, 026 file => ">>/tmp/ebaywatch.log" }); 027 028 tie %SEEN, 'DB_File', $SEEN_DB_FILE, 029 O_CREAT|O_RDWR, 0755 or 030 LOGDIE "tie: $SEEN_DB_FILE ($!)"; 031 032 END { untie %SEEN } 033 034 my $search = WWW::Search->new( 035 'Ebay::ByEndDate'); 036 open FILE, "<$RC_FILE" or 037 LOGDIE "Cannot open $RC_FILE"; 038 039 while(<FILE>) { 040 # Discard comment and empty lines 041 s/^\s*#.*//; 042 next if /^\s*$/; 043 chomp; 044 045 my $term = $_; 046 my $hits = 0; 047 048 if(exists $SEEN{"notuntil/$term"} and 049 time() < $SEEN{"notuntil/$term"}) { 050 DEBUG "Not checking '$term' until ", 051 scalar localtime 052 $SEEN{"notuntil/$term"}; 053 next; 054 } 055 056 DEBUG "Searching for '$term'"; 057 058 my $q = WWW::Search::escape_query($term); 059 060 $search->native_query($q, 061 { ebay_host => $EBAY_HOST } ); 062 063 while (my $r = $search->next_result()) { 064 $hits++; 065 DEBUG "Result: ", $r->url(), 066 " ", $r->title(), 067 " ", $r->description(), 068 " ", $r->change_date(); 069 070 if($SEEN{"url/" . $r->url()}) { 071 DEBUG "Already notified"; 072 next; 073 } 074 075 my $mins = minutes($r->change_date()); 076 077 if($mins > $MINS_TO_END) { 078 $SEEN{"notuntil/$term"} = 079 time + ($mins - $MINS_TO_END) * 60; 080 last; 081 } 082 083 INFO "Notify for ", $r->description; 084 $SEEN{"url/" . $r->url()}++; 085 086 my $msg = "<A HREF=" . $r->url() . 087 ">" . $r->title() . "</A> " . 088 "(${mins}m) " . $r->description; 089 090 $msg =~ s/[^[:print:]]//g; 091 jabber_send($msg); 092 } 093 # Pause for 1 day on no results 094 $SEEN{"notuntil/$term"} = 095 time + 24*3600 unless $hits; 096 } 097 098 ########################################### 099 sub minutes { 100 ########################################### 101 my($s) = @_; 102 103 my $min = 0; 104 105 $min += 60*24*$1 if $s =~ /(\d+)[dT]/; 106 $min += 60*$1 if $s =~ /(\d+)[hS]/; 107 $min += $1 if $s =~ /(\d+)[mM]/; 108 109 return $min; 110 } 111 112 ########################################### 113 sub jabber_send { 114 ########################################### 115 my($message) = @_; 116 117 my $c = Net::Jabber::Client->new(); 118 119 $c->SetCallBacks(presence => sub {}); 120 121 my $status = $c->Connect( 122 hostname => $JABBER_SERVER, 123 port => $JABBER_PORT, 124 ); 125 126 LOGDIE "Can't connect: $!" 127 unless defined $status; 128 129 my @result = $c->AuthSend( 130 username => $JABBER_ID, 131 password => $JABBER_PASSWD, 132 resource => 'ebaywatcher', 133 ); 134 135 LOGDIE "Can't log in: $!" 136 unless $result[0] eq "ok"; 137 138 $c->PresenceSend(); 139 140 my $m = Net::Jabber::Message->new(); 141 my $jid = "$JABBER_ID" . '@' . 142 "$JABBER_SERVER/GAIM"; 143 $m->SetBody($message); 144 $m->SetTo($jid); 145 DEBUG "Jabber to $jid: $message"; 146 my $rc = $c->Send($m, 1); 147 148 $c->Disconnect; 149 } |