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