Wer in letzter Sekunde in laufende Versteigerungen eingreifen will, nutzt einen Perl-Agenten, der auf ebay.de fortlaufend nach Stichworten sucht und seinem Mandanten sofort per Instant Message Bescheid gibt, wenn sich eine Auktion dem Ende nähert.
Das heute vorgestellte Skript ebaywatch
sendet in regelmäßigen
Abständen definierbare Suchanfragen an den Ebay-Versteigerungsserver und
wertet die gefundenen Auktionen nach ihrem Enddatum aus. Nähert sich
eine Auktion dem Ende, verpackt es eine kurze Beschreibung mit einem
fertigen URL in eine IM-Nachricht, und schickt diese über den
Jabber-Server an einen gaim
-Client ([5]), der sie dem interessierten
Benutzer auf den Bildschirm flattern lässt (Abbildung 1).
Letzterer kann dann mit einem Mausklick einschreiten und mitsteigern.
In der Datei .ebaywatchrc
im Heimatverzeichnis legt der Anwender fest,
nach welchen Stichworten der Agent auf
der Ebay-Website suchen soll. Jede nicht-auskommentierte Zeile
zeigt eine Suchanfrage:
# ~/.ebaywatchrc dwl 650 nikon
definiert zum Beispiel Anfragen nach der DLink-Netzwerkkarte DWL-650
und nach allen möglichen Produkten der Firma Nikon.
Die Suche erfolgt im Titelfeld der laufenden Auktionen und unterstützt
mit den in [2] beschriebenen Kürzeln sogar
erweiterte Suchfunktionen. So findet zum Beispiel
foto -nikon
alle Fotoartikel außer Nikon-Produkten oder
"beatles (dvd,cd)"
alle Beatles-CDs und -DVDs aber keine
Vinyl-Schallplatten.
Abbildung 1: Der Perl-Agent meldet pünktlich drei Auktionen, die in wenigen Minuten zuende gehen. |
Um automatisch mit den Ebay-Seiten zu kommunizieren, könnte man freilich
einen Screen-Scraper schreiben -- aber wie fast immer hat sich
schon jemand nach dem Motto ``I wrote code so you don't have to''
(http://www.thinkgeek.com/interests/oreilly/tshirts/6067)
daran gemacht:
Auf dem CPAN findet sich die Distribution WWW::Search::Ebay
von
Martin Thurn mit dem Modul WWW::Search::Ebay::ByEndDate,
das vorgegebene Suchabfragen absetzt und eintrudelnde Ergebnisse
nach dem Enddatum sortiert -- perfekt!
Das ebenfalls vom CPAN erhältlich Net::Jabber-Modul von Ryan Eatmon enthält
eine vollständige API, um einen funktionsfähigen Jabber-Client zu schreiben.
ebaywatch
nutzt nur einen kleinen Teil davon: Es muss im
Bedarfsfall nur schnell eine Verbindung zum Jabber-Server
jabber.org auf Port 5222 aufnehmen,
ihm seine Anwesenheit mitteilen, eine Nachricht an den Mandanten abfeuern und
sich dann verabschieden. Wer mehr Funktionalität sucht, der findet sie
im Buch des mächtigen Jabberers DJ Adams ([3]).
Jabber erlaubt es sogar, dass man sich mit einem einzigen
Benutzernamen von mehreren IM-Clients aus gleichzeitig einloggt. Damit der
Server am Ende nicht durcheinanderkommt, qualifiziert man den Benutzernamen
noch mit einer sogenannten ``Resource'', einem String, der einen Client
vom anderen unterscheidet. Während gaim
seinen
eigenen Resource-String definiert, wählen wir für das Überwachungsskript
einfach den Resource-Namen ebaywatcher
. So kommen wir mit nur einem
Jabber-Account zurande, den sowohl das sendende Skript als auch die
empfangende IM-Applikation nutzen.
Die Konfigurationssektion in ebaywatch
definiert in $EBAY_HOST
,
welchen der vielen internationalen Ebay-Server das Skript kontaktieren soll.
Im Listing ist http://search.ebay.de
festgelegt. Wer lieber das
amerikanische Original will, setzt $EBAY_HOST
auf
http://search.ebay.com
. $MINS_TO_END
bestimmt, wieviele Minuten
vor Ende einer Auktion die Blitznachricht kommt -- voreingestellt sind 10.
In der Datei $SEEN_DB_FILE legt ebaywatch
einen persistenten Hash ab,
um Statusinformationen zwischen verschiedenen Aufrufen zu sichern. In
Zeile 28 bindet der tie
-Befehl den globalen Hash %SEEN
mittels
des DB_File-Moduls an die konfigurierte Datei.
Die Optionen O_RDWR und O_CREAT sichern Schreib/Leserechte und lassen
tie()
die Datei anlegen, falls diese noch nicht existiert.
Zeile 32 mit dem untie
-Aufruf in der END-Klausel sorgt dafür, dass
der Hash sich auch wieder ordnungsgemäß von der Datei abkoppelt,
falls das Programm abbricht.
Was Programme wie ebaywatch
treiben, die im Hintergrund laufen, erfährt
man am sichersten über eine Logdatei, in die die Funktionen DEBUG(), INFO(),
LOGDIE()
etc. aus dem Log::Log4perl-Fundus schreiben. Im Listing
wurde /tmp/ebaywatch.log
als Logfile gewählt. Weitere Hinweise
zur Konfiguration des Logsystems finden sich im Abschnitt ``Installation''.
Die Konstruktion des Ebay-Objektes in Zeile 34 ist etwas ungewöhnlich,
da sie über die new
-Methode der WWW::Search
-Klasse mit
dem String Ebay::ByEndDate
als Parameter erfolgt.
Die while-Schleife ab Zeile 39 iteriert durch
die Zeilen der Datei ~/.ebaywatchrc
, eliminiert Kommentare und Leerzeilen
und merkt sich den jeweils geforderten Suchausdruck in $term
.
Der persistente Hash %SEEN
speichert unter den Schlüsseln
"url/$url"
URLs von Auktionen, die ebaywatch
schon gemeldet hat
und deswegen nicht nochmal senden möchte.
Und dann gibt es Suchbegriffe aus ~/.ebaywatchrc
, für die Ebay
nur Auktionen lieferte, deren Enddatum so weit in der Zukunft lag,
dass sich eine Anfrage an Ebay für einige Zeit nicht mehr lohnt.
Schließlich wollen wir nicht die Ebay-Leute verärgern, indem wir
alle 5 Minuten Abfragen schicken, von denen bereits klar ist, dass
sie keine neuen Informationen liefern.
ebaywatch
speichert diese Suchbegriffe unter den Schlüsseln
"notuntil/$term"
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 den gesamten URL der Suchanfrage nach den Ebay-Richtlinien
vor. Die while-Schleife ab Zeile 63 holt mit der next_result()
-Methode
Ergebnisse von Ebay ein, folgende Methoden liefern wichtige
Auktions-Informationen:
url()
title()
description()
change_date()
2T 02Std 29Min
oder 2d 02h 29m
)
Wegen der unterschiedlichen Zeit-Anzeige bei change_date()
zwischen
amerikanischem und deutschem Format wandelt die ab Zeile 99 definierte
Funktion minutes()
beide Darstellungen einfach über eine simple
Mustererkennung mit regulären Ausdrücken in Minuten um.
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 mehr als 10 Minuten
in der Zukunft liegt, und sie die nächste Untersuchung 10 Minuten
vor Ende dieser Auktion anberaumt, kann sie getrost alle späteren Auktionen
vergessen und mit last
die Schleife abbrechen. In diesem Fall
zieht sie 10 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, wobei sie $term
mit dem angegebenen Suchausdruck
ersetzt.
Steht der Ergebniszähler $hits
am Ende der Schleife immer noch auf 0,
fanden sich zum Suchbegriff keine Treffer und die nächste Untersuchung
des Begriffs verschiebt sich um einen Tag in die Zukunft.
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 $MINS_TO_END = 10; 015 our $RC_FILE = "$ENV{HOME}/.ebaywatchrc"; 016 our $EBAY_HOST = "http://search.ebay.de"; 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 }
Um eine Jabber-Nachricht abzufeuern, baut ebaywatch
in Zeile 86 den
HTML-Code für einen Link und die verfügbaren Auktions-Informationen
zusammen und eliminiert in Zeile 90 mit der [:print:]
-Klasse in einem
regulären Ausdruck alle nicht-druckbaren Zeichen.
Die Funktion jabber_send()
nimmt dann
die String-Nachricht als Parameter entgegen
und kreiert 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 im Abschnitt
``Jabber-Geschnatter'' neuangelegten Account. Den resource
-Parameter setzt
ebaywatch
auf ebaywatch
, wie oben besprochen. Zeile 138 teilt dem
Jabber-Server die Anwesenheit des Skript-Clients
mit, die korrespondierende Callback-Funktion
für andere Clients wurde vorher in Zeile 119 schon mit
$c->SetCallBacks( presence => sub {} );
auf ``Ignorieren'' gesetzt. 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 dann an den Server, der sie auch dann entgegennimmt,
wenn der Mandant gar nicht Online ist.
Und wichtig: Das hintenangehängte /GAIM
bestimmt, dass SendTo()
die
Nachricht nicht an den Jabber-Client im Skript ebaywatch
schickt (der
bekanntlich unter der Resource ebaywatch
eingeloggt ist),
sondern an den unter
derselben BenutzerID (im Beispiel mikes-ebay-watcher
) eingeloggten
gaim-Client, der automatisch den Resource-Namen GAIM
definiert.
Wie üblich sind die geforderten Zusatzmodule
WWW::Search::Ebay
, Net::Jabber
und Log::Log4perl
über eine CPAN-Shell zu installieren:
perl -MCPAN -eshell cpan> install WWW::Search::Ebay cpan> install Net::Jabber cpan> install Log::Log4perl
Die ersten beiden
fordern weitere Module vom CPAN an. Glücklich ist, wer
die CPAN-Shell-Option prerequisites_policy
auf follow setzte
und nicht den Defaulwert ask beibehalten hat.
Je nachdem, wie detailliert Log4perl die Ereignisse in der Logdatei
protokollieren soll, kann man den Loglevel in Zeile 25 entweder bei
$DEBUG
belassen, oder auf $INFO
heraufsetzen oder gar auf
$ERROR
, wenn nur schwere Fehler erscheinen sollen. Wer Angst hat,
dass die Logdatei zu lang wird und die Platte überläuft, kann die
Log::Log4perl-Konfiguration mit einem RollingFileAppender erweitern,
der Dateien nur bis zu einer vordefinierten Größe vollschreibt, eine
einstellbare maximale Zahl von Dateien anlegt und die erste wieder
überschreibt, wenn die Maximalzahl erreicht ist ([4]).
Einen Jabber-Account legt man am einfachsten mit gaim
an, einem
anpassungsfähigen IM-Client, der alle gängigen Instant-Message-Protokolle
zugleich spricht. Unter [5] gibt's das fabelhafte Teil zum Herunterladen.
Auch Jabber beherrscht es, wenn man im Menüpunkt Tools/Plugins
die jabberlib.so
auswählt und hinzulädt (Abbildung 2).
Abbildung 2: gaim versteht das Jabber-Protokoll, wenn libjabber geladen wurde. |
Dann schnell unter Tools->Accounts
auf New Account
geklickt und das Formular
nach Abbildung 3 ausgefüllt -- schon legt gaim
nicht nur einen neuen
Account auf dem Jabber-Server jabber.org an, sondern konfiguriert den
Client auch noch so, das er sich nach einem Neustart automatisch
mit dem gemerkten Passwort einloggt.
Abbildung 3: gaim legt einen neuen Jabber-Account an |
Wenn das Skript von der Kommandozeile aus einwandfrei läuft (ein
tail -f
auf die Logdatei zeigt, was abgeht), wird der Cronjob
folgendermaßen aufgesetzt:
05,10,15,20,25,30,35,40,45,50,55 * * * * /home/mschilli/bin/ebaywatch
Dies startet das Skript alle fünf Minuten, aber entsprechend der implementierten Vorsichtsmaßnahmen wird es sich meist sofort wieder schlafen legen, wenn es einmal festgestellt hat, dass die nächste relevante Auktion erst nach einiger Zeit endet.
Wer eh den ganzen Tag über IM-Messages kommuniziert, wird sich über ein paar zusätzliche Nachrichten freuen, die ein ``virtueller'' Freund schickt. Und dann einfach draufgeklickt und mitgesteigert!
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. |