Ein Perl-Skript verfolgt für Schnäppchenjäger die Preisentwicklung auf Amazon und schlägt per E-Mail freudig Alarm, falls sich überwachte Produkte plötzlich verbilligen.
Soll ich die Digitalkamera, die ich mir so sehnlichst wünsche, gleich heute kaufen? Oder lieber noch darauf warten, dass sie morgen vielleicht billiger zu haben ist? Keine leichte Frage, aber ein Blick auf die Kamerapreise der letzten Monate verrät mir vielleicht, mit welcher Preisentwicklung ich wahrscheinlich rechnen kann.
Böte Amazon nach dem Vorbild der Aktienkurse auf großen Finanzseiten die Preisentwicklung der gehandelten Produkte, dann würde sich der Kunde unter Umständen ärgern, wenn er ein Angebot verpasst hat. Oder aber er spekuliert dann erst recht darauf, dass sich der Preisverfall noch fortsetzt und ihm bald günstigere Konditionen beschert. Einige Preisvergleichs-Seiten bieten eine solche Historie-Funktion, mitteln dabei allerdings den Preis aller Anbieter und verraten an dieser Stelle nicht, wer den Tiefpreis-Rekord hält.
Nun bietet aber der Versandhändler Amazon noch keine Statistik der Preisentwicklung an und wird das womöglich auch niemals tun. Das macht nichts – es ist nämlich nicht übermäßig schwer, sich diese Funktion mit einem kleinen Perl-Skript selber nachzurüsten.
Das hier vorgestellte Skript »amtrack« (Listing 1) liest eine Konfigurationsdatei »~/.amtrack-rc« nach Abbildung 1 aus, um herauszufinden, für welche Artikel der Benutzer sich interessiert. Ein Cronjob ruft es in regelmäßigen Abständen auf, wobei es sich jedes Mal mit dem Amazon-Webservice verbindet, die aktuellen Preise aller konfigurierten Artikel abfragt und sie in einer lokal angelegten SQLite-Datenbank speichert.

Abbildung 1: Die Konfigurationsdatei »~/.amtrack-rc« listet alle zu überwachenden Produkte samt ihren Amazon-eigenen ASIN-Nummern auf.
Alarm bei Preisverfall
Sinkt der Preis eines Artikels, schickt das Skript eine E-Mail mit der Produkt-URL und dem derzeit aktuellen Preis an die in Zeile 79 voreingestellte E-Mail-Adresse. Der erfreute Schnäppchenjäger braucht dann nur noch in seinem E-Mail-Client auf die angegebene URL zu klicken, um sofort danach im Browser einen letzten Blick auf das Produkt zu werfen und es anschließend spontan zu kaufen.
Da die Preise in einer Datenbank liegen, kann das Skript selbst historische Daten ohne Weiteres abfragen und darstellen. Der Aufruf »amtrack -l« liefert die zuletzt eingeholten Preise aller überwachten Produkte (Abbildung 2). Den gesamten Inhalt der Datenbank gibt die Option »-a« aus, das kann sich nach längerer Laufzeit des Preiswächters allerdings etwas in die Länge ziehen.

Abbildung 2: Beim Aufruf mit der Option »-l« listet Amtrack die aktuellen Preise aller zur Überwachung eingetragenen Produkte.
Ganz ohne Optionen aufgerufen, arbeitet sich »amtrack« durch die in der Konfigurationsdatei »~/.amtrack-rc« definierten Produktkürzel und frischt die Datenbank mit den aktuellen Preisen auf.
Die Konfigurationsdatei besteht aus zwei Spalten: In der ersten steht die ASIN-Nummer des gewünschten Produkts und rechts, durch ein oder mehrere Leerzeichen getrennt, eine kurze Produktbeschreibung. Die ist zwar für die Datenbank unwesentlich, erweist sich aber für die übersichtliche Darstellung in den E-Mails als nützlich. Kommentarzeilen fangen mit »#« an, das Skript ignoriert sie ebenso wie Leerzeilen.
Die Funktion »config_read()« ab Zeile 92 in Listing 1 liest die Konfiguration ein und gibt zwei Referenzen zurück: eine auf einen Array »@config« und eine auf einen Hash »%config«. Während der Array Wertepaare aus ASIN und Beschreibungen enthält, weist der Hash den ASINs ihren zugehörigen Text zu.
Bei Amazon wühlen
Das CPAN-Modul Net::Amazon bildet eine objektorientierte Schnittstelle zu Amazons REST-basiertem Webservice. Der Anwender gibt einfach die so genannte ASIN-Nummer eines Produkts an – und schon kontaktiert das Modul Amazon und holt den Preis ein.
ASIN, die Universal-ISBN
Ursprünglich hatte Amazon ja nur Bücher im Angebot, die sich durch eine ISBN-Nummer eindeutig identifizieren lassen. Mit der Ausweitung des Produkt-Portfolios kam die ASIN-Nummer hinzu, die ähnlich aufgebaut ist, aber auch Buchstaben zulässt und damit weit mehr Artikel adressieren kann.
Die Methode »request()« der Klasse »Net::Amazon« nimmt eine Anforderung der Klasse »Net::Amazon::Request::ASIN« entgegen, deren Parameter die ASIN-Nummer eines Artikels ist. Anschließend wickelt sie die Kommunikation mit dem Amazon-Webserver ab und liefert ein Objekt der Klasse »Net::Amazon::Response::ASIN« zurück. Dessen Eigenschaft »is_success« zeigt an, ob alles klar ging.
Im Erfolgsfall gibt die Methode »properties()« ein Objekt der Klasse »Net::Amazon::Property« zurück, das das gefundene Produkt mit einer Produktbeschreibung, den Verbraucherbewertungen, Abbildungs-URLs und vielem anderen mehr sowie schließlich den Preis enthält. Amazon bietet auch andere Sucharten an (etwa nach Autor), dann kann »properties()« schon mal mehrere Einträge liefern. Die Methode »Our-Price()« einer Property gibt den aktuellen Preis des Produkts im Format »$ X.XX« oder »EUR X,XX« für das deutsche Locale (siehe weiter unten) aus.
Cache für die Historie
Das Skript nutzt auch das CPAN-Modul Cache::Historical. Es speichert Daten nicht nur unter einem Primärschlüssel ab, wie es ein normaler Cache auch kann, sondern nutzt als Sekundärschlüssel zusätzlich ein ebenfalls eingespeistes Datum. Das Skript legt die Produktpreise unter der ASIN als Primärschlüssel ab, zusätzlich wandert auch das Einholdatum in den Cache.
Hinter den Kulissen von Cache::Historical arbeitet die dateibasierte Datenbank SQLite, die das Modul als Zubehör auflistet und dank CPAN-Shell gleich mitinstalliert. Der Parameter »sqlite_file« des Konstruktors »new()« legt mit »~/.amzn-tracker-sqlite« die Datei fest, in der die Datenbank landet. Wer möchte, kann nach ein paar Testläufen die SQLite-Datenbank mit dem Clientprogramm »sqlite3« abfragen und die Daten wie in Abbildung 3 auflisten.
Die Methode »get_interpolated()« des Cache holt zu einem angegebenen Datum (im Skript ist es das aktuelle) und einem bestimmten Key (also der ASIN des jeweiligen Produkts) den zugehörigen Datenbankwert ein. Den legt das Skript in der Variablen »$last_price« ab und frischt die Datenbank mit dem neuesten Wert von der Amazon-Website auf. Danach holt es den aktuellen Preis sofort wieder aus der Datenbank und vergleicht ihn mit dem zuvor in »$last_price« gespeicherten.
Die Methode »values()« hingegen liefert eine Liste von Wertepaaren passend zu einem vorgegebenen Schlüssel. Jedes Paar ist eine Referenz auf einen Array, der das Datum als »DateTime«-Objekt sowie den Preis enthält. Ist der aktuelle Preis eines untersuchten Produkts niedriger als der letzte in der Datenbank gespeicherte, schickt das Skript in Zeile 78 eine E-Mail ab (Abbildung 4).
![Abbildung 4: Eine E-Mail trifft ein und meldet, dass der Preis des gesuchten Staubsaugerroboters Roomba [4] um zehn Dollar gefallen ist.](https://www.linux-magazin.de/wp-content/uploads/2008/04/abb4_jpg-300x121.jpg)
Abbildung 4: Eine E-Mail trifft ein und meldet, dass der Preis des gesuchten Staubsaugerroboters Roomba [4] um zehn Dollar gefallen ist.
Tu, was ich meine
Es gibt zwar eine ganze Reihe von CPAN-Modulen, die sich mit dem Versenden von E-Mail beschäftigen, aber Mail::DWIM (Do What I Mean) ist eines der einfachsten. Es exportiert die Funktion »mail«, die als Parameter einen Empfänger, eine Betreffzeile und den Mailtext entgegennimmt.
Für die restlichen Parameter wie Absender oder den Mailserver setzt es meist sinnvolle Defaults ein, im vorliegenden Fall etwa den aktiven User und die eingestellte Domain beziehungsweise den aktiven Sendmail-Daemon. Für einen anderen Mail-Transportmechanismus lässt sich auch der Parameter »smtp« zusammen mit der Angabe eines Mailhosts verwenden. Die Steuerung dieser Defaultwerte erfolgt über Parameter in einer lokalen ».maildwim«-Datei. Einzelheiten hierzu finden sich ausführlich in der Manpage von Mail::DWIM.
Die E-Mail enthält die URL zum Produkt, die sich einfach durch Anhängen von »/dp/$asin« an die Basis-URL der Amazon-Website ergibt. Um zur deutschen Amazon-Webseite zu gelangen, ist dabei »http://amazon.com« durch »http://amazon.de« zu ersetzen.
Damit der Anwender weiß, was das Skript treibt, loggt es alle Aktivitäten mit Log4perl. Die Datei »amtrack.l4p«, die Log4perl initialisiert, liegt im selben Verzeichnis wie das Skript selbst (Abbildung 5). Damit das Skript die Konfigurationsdatei auch dann findet, wenn es der Anwender aus einem anderen Pfad heraus aufgerufen hat (etwa mit »bin/amtrack« oder einfach nur »amtrack« aus dem Home-Directory), hilft das Modul »FindBin« mit der exportierten Variablen »$Bin« (Zeile 10), sodass »$Bin/amtrack.l4p« immer den absoluten Pfad zur Log4perl-Konfiguration ergibt.

Abbildung 5: Die Log4perl-Konfiguration des Skripts. Die Fehlermeldungen landen auf STDERR, den Rest leitet das Skript automatisch in ein Logfile.
Loggen professionell
Die Log4perl-Konfiguration ist nicht ganz ohne, denn das Skript soll reguläre Aktivitäten in eine Logdatei schreiben (Abbildung 6), aber Fehler auf die Konsole bringen. So schreibt ein regelmäßig aufgerufener Cronjob unauffällig die Logdatei weiter (Defaultmodus ist »append«), aber Fehler wie etwa eine nicht zustande gekommene Netzverbindung solle der »STDERR«-Kanal darstellen. So veranlassen Fehler als Nebeneffekt den aufrufenden Cron gleichzeitig noch dazu, eine E-Mail an den zuständigen Administrator zu senden.

Abbildung 6: Ausschnitt aus der Logdatei nach einem erfolgreichen Skriptlauf. Auch das Modul Net::Amazon ließe sich mit wenigen Handgriffen in das Logging integrieren.
Aktiv ist hier nur ein einziger Logger und nur die Kategorie »main« (also das Hauptprogramm) ist definiert. Net::Amazon ist ebenfalls bereits für Log4perl vorbereitet. Damit brächte ein weiterer Eintrag in der Konfigurationsdatei sofort Details über die Kommunikation mit Amazons Webserver auf den Schirm. Der Logger der Kategorie »main« steuert zwei Appender an, »Logfile« und »Screen«. Damit »Screen« nur Meldungen mit der Priorität »ERROR« und höher erhält, setzt
log4perl.appender.Screen.Threshold = ERROR
diesen Schwellenwert in der Appender-Definition. Wer sich näher mit dem nützlichen Log4perl-Framework auseinandersetzen möchte, der sei auf die Log4perl-Homepage [3] verwiesen, die eine ausführliche Dokumentation und eine FAQ mit Konfigurationsbeispielen bietet.
Nicht ohne Token
Amazon verlangt von Skripten, die in seinen Daten wühlen wollen, ein Token, das kostenlos und schnell erhält, wer sich unter [2] einschreibt und die Bedingungen akzeptiert. Danach ist »YOUR_AMZN_TOKEN« in Zeile 23 durch den richtigen Wert zu ersetzen.
Das Skript funktioniert sowohl mit der Website der amerikanischen Konzernmutter als auch mit der der deutschen Niederlassung. Für diese ist »locale => \’de\’« in Zeile 24 auszukommentieren. Die Preise erscheinen dann zwar im Format »EUR X,XX«, die Funktion »fix_price« wandelt sie aber ordentlich in Fließkommawerte um, die sich problemlos numerisch vergleichen lassen.
Da amerikanische Zahlen im Unterschied zu deutschen neben einem Punkt als Fließkomma auch noch Kommata verwenden, um Tausenderstellen abzutrennen, verwirft die Funktion »fix_price()« einfach alles außer den Ziffern und setzt anschließend ein Fließkomma vor die letzten beiden Ziffern.
Installation
Die oben im Skript angegebenen CPAN-Module installiert eine CPAN-Shell und löst dabei auch gleich alle Abhängigkeiten auf. Ein Crontab-Eintrag der Form
23 */6 * * * /Pfad/amtrack
ruft das Skript viermal täglich auf, und zwar 23 Minuten nach jeder durch 6 teilbaren Stunde. Das dürfte wohl genügen, um schnäppchentechnisch auf dem Laufenden zu bleiben. Das Modul Net::Amazon sorgt selbst dafür, dass das Skript die Nutzungsbedingungen von Amazon einhält.
Erweitern
Einige Erweiterungsvorschläge für das Skript: Jedem Preis ließe sich in der Konfigurationsdatei noch ein Limit zuordnen. Das Skript würde die Benachrichtigungs-E-Mail dann nur verschicken, falls der aktuelle Preis diese Grenze unterschreitet. Eine weitere Anwendung wäre beispielsweise die grafische Darstellung der Preisgestaltung über einen längeren Zeitraum. Für diesen Zweck böten sich die CPAN-Module RRDTool::OO oder Imager::Plot an. (jcb)
|
Listing 1: |
|---|
001 #!/usr/bin/perl -w
002 use strict;
003 use Getopt::Std;
004 use Net::Amazon;
005 use Net::Amazon::Request::ASIN;
006 use Log::Log4perl qw(:easy);
007 use Cache::Historical 0.02;
008 use DateTime;
009 use Mail::DWIM qw(mail);
010 use FindBin qw($Bin);
011
012 my ($home) = glob "~";
013 my $amzn_rc = "$home/.amtrack-rc";
014
015 Log::Log4perl->init("$Bin/amtrack.l4p");
016
017 my $cache = Cache::Historical->new(
018 sqlite_file =>;
019 "$home/.amtrack-sqlite"
020 );
021
022 my $UA = Net::Amazon->new(
023 token => 'YOUR_AMZN_TOKEN',
024 # locale => 'de',
025 );
026
027 my($config, $txt_by_asin) = config_read();
028
029 getopts("al", my %opts);
030
031 if($opts{l} or $opts{a}) {
032 for my $key (sort keys %$txt_by_asin) {
033 my $txt = $txt_by_asin->{$key};
034 for my $val ($cache->values( $key )) {
035 my($dt, $price) = @$val;
036 print "$dt $txt $pricen";
037 last if $opts{l};
038 }
039 }
040 } else {
041 update($config);
042 }
043
044 ###########################################
045 sub fix_price {
046 ###########################################
047 my($price) = @_;
048
049 if(defined $price) {
050 $price =~ s/[^d]//g;
051 $price =~ s/..$/.$&/g;
052 }
053 return $price;
054 }
055
056 ###########################################
057 sub update {
058 ###########################################
059 my($config) = @_;
060
061 for my $line (@$config) {
062
063 my($asin, $txt) = @$line;
064 my $now = DateTime->now();
065
066 my $last_price = fix_price($cache->
067 get_interpolated($now, $asin));
068
069 track($asin, $txt, $cache);
070
071 my $price_now = fix_price($cache->
072 get_interpolated($now, $asin));
073
074 if(defined $last_price and
075 defined $price_now) {
076
077 if( $price_now < $last_price) {
078 mail(
079 to => 'foo@bar.com',
080 subject => "[amtrack] " .
081 "$txt cheaper ($price_now < " .
082 "$last_price)",
083 text => "URL: " .
084 "http://amazon.com/dp/$asin",
085 );
086 }
087 }
088 }
089 }
090
091 ###########################################
092 sub config_read {
093 ###########################################
094
095 my @config = ();
096 my %config = ();
097
098 open AMZNRC, "$amzn_rc" or
099 die "Cannot open $amzn_rc";
100 while(<AMZNRC>) {
101 s/#.*//;
102 next if /^s*$/;
103 chomp;
104 my($asin, $txt) = split ' ', $_, 2;
105 push @config, [$asin, $txt];
106 $config{ $asin } = $txt;
107 }
108 close AMZNRC;
109
110 return @config, %config;
111 }
112
113 ###########################################
114 sub track {
115 ###########################################
116 my($asin, $txt, $cache) = @_;
117
118 INFO "Tracking asin $asin";
119
120 my $req =
121 Net::Amazon::Request::ASIN->new(
122 asin => $asin);
123
124 my $resp = $UA->request($req);
125
126 if($resp->is_success()) {
127 my($prop) = $resp->properties();
128 my $price = $prop->OurPrice();
129 INFO "Tracking $asin ",
130 "($txt): $price";
131 $cache->set(DateTime->now(),
132 $asin, $price) if $price;
133 } else {
134 ERROR "Can't fetch asin $asin: ",
135 $resp->message();
136 }
137 }
138
|
|
Infos |
|---|
|
[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2008/05/Perl] [2] Amazons Web-Services-Token: [http://www.amazon.com/soap] [3] Log4perl Homepage: [http://log4perl.com] [4] Staubsaugroboter Roomba: [http://usarundbrief.com/59/p6.html] |
|
Der Autor |
|---|
|
|







