Im Dschungel des Amazon (Linux-Magazin, Juli 2003)

Dem Beispiel des famosen Suchdienstes Google folgend, bietet nun auch das weltgrößte virtuelle Kaufhaus Amazon.com seinen umfangreichen Katalog programmatisch per Webservice an.

Nicht jeder möchte mit einem Browser herumhantieren, um im Amazon-Katalog zu wühlen. Wie wär's damit, durch alle angebotenen Perl-Bücher zu gehen und dasjenige herauszusuchen, auf das Amazon.com den höchsten Preisnachlass gibt? Was tun, wenn eine Website das Coverbild einer beliebigen CD darstellen möchte, deren ASIN (Amazon.com Standard Item Number) sie kennt? Oder alle Rolling-Stones-CDs auszugeben, die's für weniger als $13.99 gibt?

Amazon.com bietet seit einiger Zeit den programmierbaren Zugriff auf seine Datenbank an. Sowohl eine SOAP-Schnittstelle wie auch eine vereinfachte XML-über-HTTP-Anbindung ist möglich. Für simple Anfragen mit einigen Parametern, auf die der Server mit Datensätzen antwortet, ist SOAP fast schon überdimensioniert. Aus diesem Grund bürgert es sich in der Webservice-Welt immer mehr ein, dass ein Webserver einfache parametrisierte HTTP-Requests entgegennimmt und darauf mit XML antwortet. Zwar kann man mit Perls SOAP::Lite kinderleicht SOAP programmieren, aber mit der heute verwendeten XML/HTTP-Schnittstelle geht's noch einfacher.

Leichter als SOAP

Nehmen wir an, wir wollten das Buch mit der ISBN-Nummer 0201360683 finden. Hierzu reicht einfach ein HTTP-Request mit folgender URL:

    http://xml.amazon.com/onca/xml2?
        t=xxx&dev-t=yyy&AsinSearch=0201360683&type=lite&f=xml

Im Request stehen folgende Parameter:

t
Die Amazon-Partner ID vom Amazon-Associates-Programm, die nur dazu dient, vom Webservice zurückgesandte Links auf die Waren mit der Partner-ID zu bestücken. Wer keine hat, lässt den Parameter einfach weg.

dev-t
Der Amazon-Developer-Token, den man sich einfach bei amazon.com unter [3] abholen kann, wenn man seine Email-Adresse hinterlässt und den Nutzungsbedingungen zustimmt. Mit dem Token (und, während dieser Artikel entstand, sogar noch mit Zufallswerten) kann man dann laut den Bestimmungen beliebig viele Anfragen senden, solange man unter der Schmerzgrenze von 1 Request/Sekunde bleibt.

AsinSearch
Die eindeutige Produktnummer auf amazon.com, die bei Büchern identisch mit deren ISBN-Nummer ist.

type
Legt mit lite oder heavy fest, ob die Kurz- oder Langform der XML-Antwort gewünscht wird.

f
Setzt mit xml das Ausgabeformat auf XML.

Der amazon.com Webservice wird daraufhin sofort eine XML-Antwort nach Abbildung 1 zurückschicken, die alle notwendigen Produktdaten des angeforderten Artikels enthält, Links auf die von amazon.com angebotenen Abbildungen, den Amazon-Preis, den Listenpreis und vieles andere mehr.

Abbildung 1: Die XML-Antwort auf die ASIN-Web-Anfrage

Dergleichen Daten lassen sich natürlich mit Perl und einem Modul wie XML::Simple hervorragend extrahieren und für weitere Forschungen ummodeln und zurechtbiegen. Damit auch das noch einfacher wird, steht unter [2] (und auch auf dem CPAN) das brandneue Modul Net::Amazon zur Verfügung, mit dem man, wie in Listing asin_fetch gezeigt, schön objektorientiert mit dem Amazon-Service spielen kann.

Listing 1: asin_fetch

    01 #!/usr/bin/perl
    02 ###########################################
    03 # asin_fetch
    04 # Mike Schilli, 2003 (m@perlmeister.com)
    05 # Fetch book info by ASIN
    06 #     asin_fetch 0201360683
    07 ###########################################
    08 use warnings;
    09 use strict;
    10 
    11 use Net::Amazon;
    12 use Net::Amazon::Request::ASIN;
    13 
    14 my $ua = Net::Amazon->new(
    15     token       => 'YOUR_AMZN_TOKEN',
    16 );
    17 
    18 die "usage: $0 asin\n(use 0201360683 as " .
    19     "an example)\n" unless defined $ARGV[0];
    20 
    21 my $req = Net::Amazon::Request::ASIN->new(
    22     asin  => $ARGV[0],
    23     #type  => 'heavy',
    24 );
    25 
    26   # Response is Net::Amazon::ASIN::Response
    27 my $resp = $ua->request($req);
    28 
    29 if($resp->is_success()) {
    30     print $resp->as_string(), "\n";
    31 } else {
    32     print "Error: ", 
    33           $resp->message(), "\n";
    34 }

Produktsuche per Katalognummer

Am Anfang zieht das Skript die Module Net::Amazon und Net::Amazon::Request::ASIN herein -- Net::Amazon funktioniert ähnlich wie die bekannte LWP-Library, die zunächst einen UserAgent definiert, dann einen Request und diesen anschließend an den UserAgent weiterreicht. Das Net::Amazon-Objekt nimmt den Developer-Token entgegen und führt anschließend beliebig viele Anfragen aus. Das Skript asin_fetch erwartet die ISBN-Nummer auf der Kommandozeile. Falls diese fehlt, bricht Zeile 18 den Reigen mit einer usage-Meldung ab. Zeile 21 definiert die ASIN-Anfrage und setzt den asin-Parameter auf die angegebene Nummer. Zeile 26 führt dann den eigentlichen HTTP-Request an den Amazon-Server aus, schluckt das zurückkommende XML, analysiert es und legt es in einer Hashstruktur im Antwortobjekt $resp vom Typ Net::Amazon::Response::ASIN ab. Alle Net::Amazon::Response::*-Objekte verfügen über die Methoden is_success(), is_error(), message() (etwaige Fehlermeldung) und as_string() (fasst die wichtigsten Ergebnisparameter lesbar zusammen).

Der Aufruf von asin_fetch mit der ASIN 0596000278 fördert folgendes zutage:

    $asin_fetch 0596000278
    Larry Wall/Tom Christiansen/Jon Orwant, 
    "Programming Perl (3rd Edition)", 2000, 
    $34.97, 0596000278

Die gefundenen Objekte (Net::Amazon nennt sie Properties) sind entweder Bücher, CDs, Elektrogeräte etc. -- zur Zeit werden Net::Amazon::Property::Book und Net::Amazon::Property::Music explizit unterstützt, der Rest strandet als generisches Net::Amazon::Property. Die properties()-Methode eines Response-Objektes gibt eine Liste aller gefundenen Objekte zurück.

Allgemein bietet Net::Amazon::Property (und damit auch die davon abgeleiteten Klassen) für jeden von Amazon.com im zurückgelieferten XML definierten Produktparameter eine Methode an, unter anderem:

Asin()
ASIN-Nummer

ReleaseDate()
Erscheinungsdatum

ListPrice()
Listenpreis

OurPrice()
Preis bei Amazon

Manufacturer()
Hersteller (Verlag, Plattenlabel)

Catalog()
Produkttyp (Book, Music, Classical, Electronics ...)

Artist()
Interpret bei Pop-CDs

Authors()
Gibt bei Büchern einen Sub-Hash mit einem Artist-Eintrag zurück.

ImageUrlLarge()
URL zum Produktbild auf der Amazon-Website (statt Large auch: Medium/Small).

ProductName()
Produktname oder Buch/CD-Titel

Diese variieren je nach Art des Produkts. So gibt es bei Büchern kein Artist-Feld wie bei CDs, sondern einen Authors-Eintrag, der wiederum einen Unter-Hash enthält, der unter einem Author-Schlüssel entweder einen Einzeleintrag oder eine Referenz auf eine Liste mit Autoren enthält. Um diesen Wirrwarr zu vereinfachen, bietet Net::Amazon in spezialisierten Net::Amazon::Property-Objekten wie Net::Amazon::Property::Book bequemere Methoden an. Ist eine Property zum Beispiel von diesem Typ, gibt eine zusätzliche Methode authors() die Liste der Autorennamen zurück.

Als weiteres Beispiel zeigt Listing keyword, wie man auf Amazon.com nach einem Schlüsselwort in einem Produktbereich sucht. Der Konstruktor des Net::Amazon-Objekts nimmt ab Zeile 17 nicht nur den Developer-Token entgegen, sondern auch den Parameter max_pages, der dort auf 5 gesetzt ist (entspricht der Standardeinstellung). Liefert eine Suchanfrage mehrere Treffer, liefert Amazon immer nur 10 auf einmal, und weitere Web-Requests müssen eventuell folgende Seiten nachholen. Wenn man die Seitenzahl max_pages setzt, abstrahiert Net::Amazon dies und holt automatisch solange Ergebnisse nach, bis die maximale Seitenzahl erreicht ist, oder die Treffer ausgehen.

Zeile 11 zieht das Net::Amazon::Request::Keyword-Modul herein, dessen Konstruktoraufruf ab Zeile 22 das auf der Kommandozeile bereitgestellte Schlüsselwort (zum Beispiel perl) und den Produktbereich (z.B. books, music, classical, electronics) mit dem mode-Parameter setzt.

Zeile 31 bekommt mit der properties()-Methode des Response-Objekts eine Liste gefundener Property-Objekte und iteriert über diese. Auch bei explizit angegebenem Suchbereich liefert Amazon manchmal Produkte aus anderen Bereichen, also stellt Zeile 33 sicher, dass es sich nur um Bücher handelt, indem es den Catalog()-Eintrag mit dem String Book vergleicht (man beachte die Namensinkonsistenz mit dem vorher mit books festgelegten mode-Parameter) und bei Abweichungen den Treffer einfach überspringt. Ab Zeile 35 kommen dann sowohl spezielle Methoden der Net::Amazon::Property::Book-Klasse (authors(), title()), als auch generische Methoden der Net::Amazon::Property-Klasse (Asin(), OurPrice()) zum Einsatz, um den Treffer anzuzeigen.

Listing 2: keyword

    01 #!/usr/bin/perl
    02 ###########################################
    03 # keyword - search by keyword
    04 #     keyword what_to_search_for mode
    05 # Mike Schilli <mschilli1@aol.com>, 2003
    06 ###########################################
    07 use strict;
    08 use warnings;
    09 
    10 use Net::Amazon;
    11 use Net::Amazon::Request::Keyword;
    12 
    13 die "usage: $0 keyword what mode\n(use " .
    14     "perl/books as an example)\n" 
    15     unless defined $ARGV[1];
    16 
    17 my $ua = Net::Amazon->new(
    18     token       => 'YOUR_AMZN_TOKEN',
    19     max_pages   => 5,
    20 );
    21 
    22 my $req = Net::Amazon::Request::Keyword
    23           ->new(
    24     keyword   => $ARGV[0],
    25     mode      => $ARGV[1],
    26 );
    27 
    28  # Response: Net::Amazon::Keyword::Response
    29 my $resp = $ua->request($req);
    30 
    31 for ($resp->properties) {
    32 
    33    next unless $_->Catalog() eq "Book";
    34 
    35    print join(", ", $_->Asin(),
    36          join("/", $_->authors()),
    37          $_->title(),
    38          $_->OurPrice()), "\n";
    39 }

Folgender Aufruf, der nach Büchern sucht, auf die das Stichwort ``perl'' passt, fördert auch Produkte zutage, von denen man das nicht unbedingt erwarten würde:

    $ keyword perl books
    0596000480, David Flanagan, JavaScript: 
      The Definitive Guide, $31.47
    0596000278, Larry Wall/Tom Christiansen/Jon Orwant, 
      Programming Perl (3rd Editio
    ...

Schlüssel für Sparfüchse

Auf welche Perl-Bücher gibt Amazon den höchsten Rabatt? Listing cheapo sucht nach einem Stichwort (z.B. perl) und holt wegen dem auf 20 gesetzten Parameter max_pages gleich bis zu 20 mal 10 Treffer ein. Anschließend filtert es die von properties() zurückgelieferten Artikel mittels des grep-Kommandos und prüft, ob ein Treffer tatsächlich ein Buch ist und ob der Titel (mittels title()) tatsächlich das gesuchte Schlüsselwort enthält. Die sort-Funktion in Zeile 31 sortiert absteigend nach dem Ergebnis der Funktion saved, die ab Zeile 45 definiert ist und zu einem Buch-Objekt die prozentuale Ersparnis zwischen Listen- und Amazonpreis errechnet. Effizient ist das nicht, da könnte man eine Schwartz-Transformation zwischenschalten und auch nur die wirklich billigsten Bücher ständig bereithalten, falls man wirklich tausende von Treffern untersuchen wollte, aber für die gezeigte Menge macht's kaum einen Unterschied. Die Preisfelder enthalten übrigens beim amerikanischen Amazon ein Dollar-Zeichen, das die Zeilen 51 und 52 extrahieren müssen, bevor Arithmetik betrieben wird.

Die for-Schleife ab Zeile 36 gibt dann die fünf aufregensten Preisknüller mit prozentualer Ersparnis und Titel aus.

Listing 3: cheapo

    01 #!/usr/bin/perl
    02 ###########################################
    03 # maxauthors keyword
    04 # Mike Schilli <mschilli1@aol.com>, 2003
    05 ###########################################
    06 
    07 use strict;
    08 use warnings;
    09 
    10 use Net::Amazon;
    11 use Net::Amazon::Property;
    12 use Net::Amazon::Request::Keyword;
    13 
    14 die "usage: $0 keyword" unless 
    15     defined $ARGV[0];
    16 
    17 my $ua = Net::Amazon->new(
    18     token       => 'YOUR_AMZN_TOKEN',
    19     max_pages   => 20,
    20 );
    21 
    22 my $req = Net::Amazon::Request::Keyword->new(
    23     keyword   => $ARGV[0],
    24     mode      => "books"
    25 );
    26 
    27  # Response: Net::Amazon::Keyword::Response
    28 my $resp = $ua->request($req);
    29 
    30 my @books = 
    31    sort { saved($b) <=> saved($a) }
    32    grep { $_->Catalog eq "Book" &&
    33           $_->title =~ /$ARGV[0]/i }
    34     $resp->properties;
    35 
    36 for(0..4) {
    37     printf "%.2f%% (%s/%s) %s\n\n", 
    38           saved($books[$_]),
    39           $books[$_]->ListPrice,
    40           $books[$_]->OurPrice,
    41           $books[$_]->as_string;
    42 }
    43 
    44 ###########################################
    45 sub saved {
    46 ###########################################
    47     my($book) = @_;
    48 
    49     my $list = $book->ListPrice;
    50     my $our  = $book->OurPrice;
    51     $list =~ s/\$//;
    52     $our  =~ s/\$//;
    53 
    54     return ($list - $our)/$list*100;
    55 }

Oder wie wär's mit einem CGI-Skript, das zu einer vorgegeben ASIN ein Bild des Produkts anzeigt? Listing asin_img zeigt eine Implementierung, die einfach bei Amazon den URL abholt und dann einen Redirect ausführt. Nach der Installation im cgi-bin-Verzeichnis lässt der Aufruf

    http://localhost/cgi-bin/asin_img?asin=0201360683

den Browser das entsprechende Bild anzeigen. Zu beachten ist allerdings, dass Amazon verlangt, dass man keine Informationen aus deren Datenbestand anzeigt, ohne irgendwie zurück zu Amazon zu linken -- der Rubel muss schließlich Rollen, Amazon ist kein Wohlfahrtsunternehmen.

Listing 4: asin_img

    01 #!/usr/bin/perl
    02 ###########################################
    03 # asin_img - Fetch an ASIN's image
    04 # Mike Schilli, 2003 (m@perlmeister.com)
    05 ###########################################
    06 use warnings;
    07 use strict;
    08 
    09 use CGI qw(:all);
    10 use CGI::Carp qw(fatalsToBrowser);
    11 use Net::Amazon;
    12 use Net::Amazon::Request::ASIN;
    13 
    14 my $ua = Net::Amazon->new(
    15     token => 'YOUR_AMZN_TOKEN'
    16 );
    17 
    18 die "usage: $0 asin=0201360683\n" 
    19     unless param('asin');
    20 
    21 my $req = Net::Amazon::Request::ASIN->new(
    22     asin  => param('asin')
    23 );
    24 
    25 my $resp = $ua->request($req);
    26 
    27 print redirect(
    28      $resp->properties->ImageUrlLarge());

Und Listing roll findet schließlich alle Rolling-Stones-CDs unter $13.99. Die substr()-Funktion schneidet vom Preis das an erster Stelle stehende Dollarzeichen ab, bevor er mit einem Fließkommawert verglichen wird.

Zu beachten ist, dass es bei Amazon für klassische und Pop-CDs unterschiedliche Bereichte gibt (die modes heissen classical bzw. music). Objekte vom Typ Net::Amazon::Property::Music führen außer einer artist()-Methode für den Interpreten auch album() für den CD-Titel.

Listing 5: roll

    01 #!/usr/bin/perl
    02 ###########################################
    03 # All Rolling Stones CDs for < $13.99
    04 # Mike Schilli <mschilli1@aol.com>, 2003
    05 ###########################################
    06 
    07 use strict;
    08 use warnings;
    09 
    10 use Net::Amazon;
    11 use Net::Amazon::Request::Keyword;
    12 
    13 my $ua = Net::Amazon->new(
    14     token       => 'YOUR_AMZN_TOKEN',
    15     max_pages   => 10,
    16 );
    17 
    18 my $req = Net::Amazon::Request::Keyword->new(
    19     keyword   => "Rolling Stones",
    20     mode      => "music"
    21 );
    22 
    23  # Response: Net::Amazon::Keyword::Response
    24 my $resp = $ua->request($req);
    25 
    26 for ($resp->properties) {
    27     if($_->Catalog eq "Music" &&
    28        $_->OurPrice &&
    29        substr($_->OurPrice, 1) < 13.99) {
    30          print $_->album, " ",
    31          $_->OurPrice(), "\n";
    32     }
    33 }

Zur Zeit steht der Webservice nur für den amerikanischen amazon.com und den britischen amazon.co.uk zur Verfügung. Für den letzteren ist dem Konstruktor der Net::Amazon-Klasse zusätzlich das Wertepaar locale => uk zu übergeben. Die deutsche Schwester Amazon.de hatte leider bis Redaktionsschluss noch nicht nachgezogen, die Jungs und Mädels aus Hinterpfuideifel bei München hinken ja dem in den USA stationierten Mutterschiff immer ein wenig hinterher -- aber vielleicht wird's ja bald was, praktisch wär's! Viel Spass beim Stöbern!

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2003/07/Perl

[2]
Die Distribution des Perl-Moduls Net::Amazon: http://perlmeister.com/devel/#amzn

[3]
Die Amazon-SOAP-Seite mit Development Kits und Beschreibung: http://amazon.com/soap

[4]
brian d foy, ``Amazon.com wishlists'', The Perl Journal 02/2003 (nur per Abo und Online erhältlich)

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. Seine Homepage: http://perlmeister.com.