Aus Linux-Magazin 05/2008

Perl-Skript überwacht Preise bei Amazon

© philiph, photocase.com

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.

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.

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.

Abbildung 3: Die SQLite-Datenbank lässt sich auch mit dem Kommandozeilen-Client »sqlite3« abfragen.

Abbildung 3: Die SQLite-Datenbank lässt sich auch mit dem Kommandozeilen-Client »sqlite3« abfragen.

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.

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.

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.

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:
»amtrack«

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


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.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben