Ein Screenscraper und eine Applikation für das offizielle Ebay-API lösen Alarm bei neu eingegangenem Kunden-Feedback aus und spüren Fehler in der monatlichen Ebay-Abrechnung auf.
Wenn ich irgendwann meine Memoiren schreibe, bette ich dort eine längliche Abhandlung über meinen Lebensleitsatz ein, der da heißt: “Was du nicht ständig kontrollierst, läuft irgendwann garantiert unbemerkt schief.” Getreu diesem Motto möchte ich sofort eine E-Mail erhalten, wenn einer meiner Ebay-Kunden sein Feedback über eine Transaktion hinterlassen hat.
Einfach abspachteln
Um mich zunächst nicht bei Ebay als Developer anmelden zu müssen, schrieb ich einen Screenscraper, der aus dem HTML-Salat der Feedback-Seite die aktuelle Anzahl der Feedbacks herausfieselt, abspeichert und bei späteren Tests Alarm schlägt, falls sich der Zähler erhöht hat.
Online PLUS
Im Screencast demonstriert Michael Schilli das Beispiel: https://www.linux-magazin.de/Ausgaben/2016/06/plus
Klicke ich auf Ebay.com meinen Usernamen an, lande ich auf der Feedback-Seite in Abbildung 1. »view-source« im Browser bringt den HTML-Code aus Abbildung 2 zum Vorschein, und eine Textsuche nach dem String »362« (die aktuelle Anzahl der Feedbacks) zeigt, dass diese sich in einem Markup-Tag der Klasse »mbg-l« befindet:
<span class="mbg-l">( 362<img src="...</span>
Ein XPath-Prozessor wie HTML::TreeBuilder::XPath kann den Inhalt dieses Tag einfach hervorholen, die Abfrage
/html/body//span[@class="mbg-l"]
findet alle »span« -Elemente im HTML-Body, die ein »class« -Attribut mit dem Wert »mbg-l« führen. Der doppelte Schrägstrich im Ausdruck gibt an, dass sich die gesuchten Elemente in beliebiger Verschachtelungstiefe unterhalb des HTML-Body-Tag befinden dürfen.
Der XPath-Query spuckt dann einen String wie etwa »( 362) (…« aus. Daraus mittels eines regulären Ausdrucks in Perl die erste Zahl herauszulösen ist ein Kinderspiel. Listing 1 tut genau dies in der Funktion »feedback_fetch« ab Zeile 70, speichert dann die gefundene Zahl in einer Cachedatei, vergleicht den Wert dort beim nächsten Aufruf mit dem aktuell eingeholten und feuert eine E-Mail ab, wenn der Zähler sich erhöht hat.
Listing 1
ebay-feedback
01 #!/usr/local/bin/perl -w
02 use strict;
03 use Sysadm::Install qw(:all);
04 use LWP::Simple ;
05 use HTML::TreeBuilder::XPath;
06 use Log::Log4perl qw(:easy);
07
08 my $nick = "my-ebay-name";
09 my $ebay_url =
10 "http://feedback.ebay.com" .
11 "/ws/eBayISAPI.dll?ViewFeedback2" .
12 "&userid=$nick";
13
14 my( $home ) = glob "~";
15 my $data_dir = "$home/logs";
16 my $cache =
17 "$data_dir/ebay-feedback.cache";
18 my $log_file =
19 "$data_dir/ebay-feedback.log";
20
21 # mail prefs
22 my $mailer = "/usr/bin/mail";
23 my $mail_to = 'my@email.com';
24
25 mkd $data_dir if !-d $data_dir;
26
27 Log::Log4perl->easy_init( {
28 level => $DEBUG,
29 file => ">>$log_file" } );
30
31 my $last_feedback;
32
33 if( -f $cache ) {
34 $last_feedback = slurp $cache;
35 }
36
37 my $feedback = feedback_fetch();
38
39 if( !defined $last_feedback or
40 $last_feedback != $feedback ) {
41
42 $last_feedback ||= 0;
43
44 INFO "New feedback: $feedback";
45 INFO "Sending mail to $mail_to";
46
47 open PIPE,
48 "| $mailer -s 'New Ebay Feedback: " .
49 "$feedback' $mail_to";
50
51 print PIPE <<EOT;
52 Ebay feedback changed: It's $feedback now
53 and was $last_feedback yesterday:
54
55 $ebay_url
56
57 Greetings!
58
59 Your faithful Ebay feedback scraper.
60 EOT
61 close PIPE;
62
63 blurt $feedback, $cache;
64
65 } else {
66 INFO "Feedback unchanged ($feedback).";
67 }
68
69 ###########################################
70 sub feedback_fetch {
71 ###########################################
72 INFO "Fetching $ebay_url";
73
74 my $content = get $ebay_url;
75
76 if( !defined $content ) {
77 ERROR "Fetching $ebay_url failed";
78 return undef;
79 }
80
81 my $tree= HTML::TreeBuilder::XPath->new;
82 $tree->parse( $content );
83
84 my( $text ) = $tree->findvalue(
85 '/html/body//span[@class="mbg-l"]');
86
87 if( $text =~ /\s*(\d+)/ ) {
88 return $1;
89 }
90
91 ERROR "Pattern in page not found";
92 return undef;
93 }
E-Mail als Kommando
Das Schreiben und Lesen der Cachedatei, die als einzigen Wert die beim letzten Aufruf festgestellte Anzahl der Feedbacks speichert, erledigen die Funktionen »slurp« und »blurt« aus dem CPAN-Modul Sysadm::Install, die diese in Zeile 3 exportieren. Zum Einholen der E-Bay-Seite nutzt das Skript das abgespeckte Modul LWP::Simple, dessen »get« -Methode schlicht einen HTTP-Request auf die angegebene URL absetzt und den Inhalt der gefundenen Seite im Erfolgsfall zurückgibt.
Unter Linux eine Mail abschicken lässt sich oft ganz einfach mit dem Utility »/usr/bin/mail« erledigen, wer mehr Flexibilität wünscht, dem sei das CPAN-Modul Mail::DWIM empfohlen. Da ich auf meiner Linux-Kiste daheim »/usr/bin/mail« bereits durch ein Perl-Skript ersetzt habe, das mit den Erfordernissen meines ISP zurechtkommt, ließ ich es dabei. Abbildung 3 zeigt die eingetroffene E-Mail in meinem Gmail-Account.
Vor der Inbetriebnahme sind noch die Parameter am Skriptkopf an die lokalen Verhältnisse anzupassen. In Zeile 8 erhält die Variable »$nick« den Namen des Ebay-Users zugewiesen, dessen Feedback-Zähler »ebay-feedback« überwachen soll. Die URL gilt für US-Ebay-Accounts, deutsche Accounts laufen statt dessen unter http://ebay.de. Und schließlich benötigt Zeile 23 in der Variablen »$mail_to« noch die Mailadresse des Users, an den die Alarm-E-Mails gehen sollen.
Kontrolle des Kontrolleurs
Wie lässt sich kontrollieren, ob das Skript immer noch funktioniert oder ob Ebay sein Seitenlayout geändert hat? Listing 1 protokolliert dazu alle Vorgänge mittels Log::Log4perl in die Logdatei »~/logs/ebay-feedback.log« und ein Nagios-Skript könnte zum Beispiel jeden Tag die dort eintrudelnden Einträge nach einem Muster wie “OK” unter dem aktuellen Datum durchforsten und Alarm schlagen, falls es nicht fündig wird.
E-Bay kann nicht addieren
Eigentlich sollte man nicht meinen, dass es Leute gibt, die Einzelposten einer Computer-genierten Abrechnung zusammenzählen und nachprüfen, ob die Gesamtsumme stimmt. Aber ich muss gestehen, dass ich da auch manchmal nicht widerstehen kann. Besonders bei der monatlichen E-Bay-Abrechnung, wenn mal wieder nur vier Posten draufstehen wie in Abbildung 4.
Jeder Zweitklässler mit einem Taschenrechner könnte mühelos verifizieren, dass 4,40 + 0,45 + 1,00 + 0,39 die Gesamtsumme von 6,24 US-Dollar ergibt. Doch Ebays Java-Jockeys kennen wohl den Artikel zur Fließkomma-Arithmetik, den jeder Programmierer [2] gelesen haben sollte, noch nicht. Sonst wüssten sie, dass CPUs Fließkommazahlen ungenau abspeichern und deswegen beim Addieren erstaunliche Rundungsfehler auftreten können.
So stellten mir die Hafenmeister im März frecherweise 6,25 US-Dollar in Rechnung, obwohl die Gesamtsumme der Einzelposten zweifelsohne 6,24 beträgt. Beim Kundencenter hatte ich vor einiger Zeit schon angerufen und hing eine halbe Stunde mit einem Repräsentanten aus Übersee in der Leitung, um eine Diskrepanz von ein paar Cents berichtigen zu lassen, aber die 1-Cent-Beschwerde hebe ich mir für einen ganz besonderen Tag auf.
Buchprüfer
Das Skript in Listing 2 hilft dabei, diese Buchprüfung jeden Monat automatisch durchzuführen. Es stützt sich auf das offizielle Ebay-API, weist sich mit einem Token gegenüber dem Ebay-Webservice aus und holt die Abrechnung des vergangenen Monats als XML ein. Dort stehen die Einzelposten (jeweils unter dem Tag »NetDetailAmount« ) sowie die Gesamtsumme (unter »InvoiceNewFee« ). Das darauf folgende »invoice-check« in Listing 3 kann dann prüfen, ob Ebay richtig gerechnet hat (Abbildung 5).
Listing 2
ebay-invoice
01 #!/usr/local/bin/perl -w
02 use strict;
03 use LWP::UserAgent;
04 use DateTime;
05 use Path::Tiny;
06
07 my $dt_today = DateTime->today;
08 my $dt = DateTime->new(
09 year => $dt_today->year,
10 month => $dt_today->month,
11 day => 1,
12 );
13 my $invoice_date =
14 $dt->subtract( days => 1 )->ymd;
15
16 my $ua = LWP::UserAgent->new;
17
18 $ua->default_header(
19 "X-EBAY-API-CALL-NAME", "GetAccount" );
20 $ua->default_header(
21 "X-EBAY-API-COMPATIBILITY-LEVEL", 863 );
22 $ua->default_header(
23 "Content-Type", "text/xml" );
24 $ua->default_header(
25 "X-EBAY-API-SITEID", "0" );
26
27 my $token = path( "token" )->slurp;
28 chomp $token;
29
30 my $body = <<EOT;
31 <?xml version="1.0" encoding="utf-8"?>
32 <GetAccountRequest
33 xmlns="urn:ebay:apis:eBLBaseComponents">
34 <RequesterCredentials>
35 <eBayAuthToken>$token</eBayAuthToken>
36 </RequesterCredentials>
37 <AccountHistorySelection>SpecifiedInvoice</AccountHistorySelection>
38 <InvoiceDate>$invoice_date</InvoiceDate>
39 </GetAccountRequest>
40 EOT
41
42 my $resp = $ua->post(
43 "https://api.ebay.com/ws/api.dll",
44 Content => $body );
45
46 if( $resp->is_error ) {
47 die $ua->message;
48 }
49
50 print $resp->decoded_content;
Listing 3
invoice-check
01 #!/usr/local/bin/perl -w
02 use strict;
03 use XML::Simple;
04 use Math::Currency;
05
06 my $xml = join "", <>;
07 my $ref = XMLin( \$xml );
08 my $total = 0;
09 my @items = ();
10
11 for my $entry ( @{ $ref->{
12 "AccountEntries" }->{ AccountEntry } } ) {
13
14 # ignore payments
15 next if $entry->{ ItemID } == 0;
16
17 my $amount = $entry->{
18 "NetDetailAmount" }->{ content };
19
20 # ignore free items
21 next if $amount == 0;
22
23 my $mc = Math::Currency->new( $amount );
24 print "$mc\n";
25 $total += $mc;
26 }
27
28 my $invoice_amount =
29 $ref->{ AccountSummary }->{
30 "InvoiceNewFee" }->{ content };
31
32 print "Total: $total\n";
33 print "Invoice: \$$invoice_amount\n";
Schlüssel zum Datenreich
Eine Applikation, die Ebay-Nutzerdaten liest oder schreibt, muss sich gegenüber der Plattform mit einem Token ausweisen. Hierzu muss sich der Entwickler beim Ebay Developers Program registrieren und eine Applikation anmelden. Danach führt die Applikation den User durch den Login-Prozess, der so einen Token erhält. Falls der Entwickler den Token nur zu Testzwecken und für seinen eigenen Account benötigt, kann er, wie Abbildung 6 zeigt, den Knopf »Sign in to Production« drücken [3].
Dieser Token gilt dann für den tatsächlichen Account in der Live-Produktion und nicht etwa für die so genannte Sandbox, in der Entwickler ebenfalls ihre Apps ausprobieren können, bis sie sicher sind, dass diese Produktionsreife erlangt haben (Abbildung 7).
Abfragen auf persönliche Daten benötigen einen vom User autorisierten Token, der in Listing 2 in Zeile 35 sichtbar unter dem Tag »RequesterCredentials« im XML des Requests stehen muss. Es handelt sich um einen 872 Zeichen langen Hex-String, der ohne Zeilenumbruch zwischen den dazwischen eingebetteten »eBayAuthToken« -Tags stehen muss.
Sankt Bürokratius
Die Dokumentation auf den Seiten des Ebay Developers Program ist leicht chaotisch angeordnet, die einzelnen APIs überlappen wohl aus historischen Gründen. Auf [3] erhalten Entwickler ihre Tokens und unter [4] steht, welche Parameter die Abfrage »GetAccount« benötigt, um eine an einem bestimmten Datum erstellte Abrechnung einzuholen. Wie Listing 2 zeigt, benötigt der an das Ebay-API abgesetzte Post-Request neben dem XML-Body auch noch einige HTTP-Headerwerte. Der Wert des Headers »X-EBAY-API-SITEID« bestimmt, an welche Ebay-Abteilung in welchem Land die Abfrage geht, und der Wert »0 « gilt für Ebay.com in den USA, während Applikationen Ebay.de in Deutschland unter der Zahl »77« erreichen.
Der Header »X-EBAY-API-COMPATIBILITY-LEVEL« spezifiziert die Mindestversion des Web-API, mit der der Client noch arbeiten kann. Gegenwärtig ist Version 959 aktuell, der im Skript angegebene Wert von 863 hat lediglich akademischen Status. Um das Ergebnis in XML zu erhalten, setzt Zeile 23 den Header »Content-Type« auf »text/xml« . Und dass die Applikation die Servermethode »GetAccount« aufruft, steht zwar schon im XML-Body des Post-Request, aber St. Bürokratius verlangt, dass sie noch einmal im Header »X-EBAY-API-CALL-NAME« in Zeile 19 steht.
Ebay erstellt Rechnungen immer zum Monatsende, und um vom heutigen Datum auf den letzten Tag im vergangenen Monat zu kommen, also das letzte Rechnungsdatum, setzt Listing 2 in Zeile 11 den Tageswert des heutigen Datums auf »1« (also den ersten des aktuellen Monats) und zieht dann einen Tag davon ab. Es landet also auf dem letzten Tag des vorherigen Monats.
Die »DateTime« -Funktion »ymd« modelt das Datum zum Beispiel für die Märzabrechnung 2016 ins Format »20160331« um, das das Ebay-API versteht, falls in der Abfrage unter »AccountHistorySelection« der Wert »SpecifiedInvoice« angegeben ist. Das Datum dieser Abrechnung muss dann unter »InvoiceDate« stehen.
Die sensitiven Token-Daten erwartet das Skript in einer Datei »token« , von wo es sie in Zeile 27 ausliest, in Zeile 28 ein eventuelles Newline-Zeichen am Datei-Ende entfernt und den Schlüssel dann über die Variable »$token« ins XML ab Zeile 31 einbaut. Das Datum der Abrechnung liegt in der Variablen »$invoice_date« und fließt ebenfalls ins XML ein, Zeile 38 interpoliert den Wert.
Tritt ein Fehler beim Holen der Daten auf, bricht Zeile 47 das Skript ab und druckt die von Ebay kommende Fehlermeldung. Typische Probleme sind unerwünschte Leerzeichen und Zeilenumbrüche in den XML-Daten, auf die das Portal sauer reagiert, sie aber zum Glück in den Fehlermeldungen detailliert beschreibt. Geht alles glatt, druckt Zeile 50 das zurückkommende XML auf der Standardausgabe aus, wo die nächste Stufe der Verarbeitungs-Pipeline die Daten aufgreift.
Addieren, aber richtig
Das Skript in Listing 3 schnappt sich den von Listing 2 kommenden XML-Salat, sucht darin mittels des CPAN-Moduls XML::Simple nach den Einzelposten (Abbildung 8) sowie der Gesamtsumme und verifiziert, ob die Addition der Einzelposten auch tatsächlich die Gesamtsumme ergibt. Das Schöne an XML::Simple ist, dass es jedes XML in eine riesige Perl-Datenstruktur verwandelt, in der ein Skript nach Belieben herumfuhrwerken kann. Tags verwandeln sich in Hashschlüssel und aus Listen von Einzeleinträgen macht es Perl-Arrays. Für den Text, der zwischen zwei XML-Tags als deren Inhalt steht, erfindet XML::Simple einen Hash-Key namens »content« , unter dem zum Beispiel Zeile 30 in Listing 3 die Gesamtsumme der Rechnung aus dem Tag »InvoiceNewFee« extrahiert.
Wie gesagt, in [2] wird dringend davon abgeraten, Dollar- (oder Euro-) und Cent-Beträge einfach als Fließkommawerte zu addieren. Vielmehr sollten sorgfältige Programmierer alles in Cents umrechnen und diese als Integer weiterverarbeiten, um vom Ergebnis anschließend die letzten zwei Ziffern wieder abzutrennen und als Cent-Beträge auszuweisen.
Das CPAN-Modul Math::Currency addiert Beträge und formatiert das Ergebnis ansprechend, auch wenn die Eingaben wie im XML von Ebay mit ihren einstelligen Nachkommabeträgen (etwa »$1,6« ) nicht ganz korrekt vorliegen. Allerdings hat der Zahn der Zeit an dem Modul genagt, die letzte Release ist sechs Jahre alt und die beiliegende Test-Suite rasselt mit Pauken und Trompeten durch die Prüfung. Doch das Modul funktioniert, also zwang ich die CPAN-Shell mit
cpanm --force Math::Currency
dazu, es trotzdem zu installieren. Die XML-Abrechnung enthält nicht nur die Einzelposten und die Gesamtsumme, sondern auch noch die Beträge, die der Kunde aus vorherigen Rechnungen bereits an Ebay überwiesen hat. Zeile 15 erkennt diese an ihrer »ItemID« , die anders als bei fälligen Gebühren für Verkaufsobjekte den Wert »0« hat.
Kassierer kontrollieren
Der Fehler bei der Ebay-Abrechnung brachte mich auf die Idee, beim nächsten Supermarktbesuch auch die Einzelposten auf dem Kassenbon zu kontrollieren. Wem würde es auffallen, wenn die ausgewiesene Gesamtsumme um ein paar Cent von der Summe der Einzelposten abwiche? Niemandem, außer mir wahrscheinlich, der die Kontrolle selbstverständlich durch ein Skript ausführen würde, das die OCR-Daten des Kassenbons fräße. Vielleicht bald auf diesem Kanal.
Infos
- Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2016/06/Perl
- “What Every Computer Scientist Should Know About Floating-Point Arithmetic”: http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
- Ebay Developers Program Application Keys: https://developer.ebay.com/my/keys
- API-Dokumentation zu »GetAccount« , um die monatliche Ebay-Rechnung eines Accounts abzurufen: http://developer.ebay.com/Devzone/xml/docs/Reference/ebay/GetAccount.html














