Mit eines Screenscrapers oder einer Applikation mittels der offiziellen Ebay-API gelingt es, bei neu eingegangenem Kunden-Feedback Alarm auszulösen sowie Bugs in der monatlichen Ebay-Abrechnung aufzuspüren.
Wenn ich demnächst meine Memoiren schreibe, werde ich sicher eine langatmige Abhandlung über meinen Lebensleitsatz einbetten, der da heißt: "Was du nicht ständig kontrollierst, läuft irgendwann garantiert unbemerkt schief". Getreu diesem Motto möchte ich zum Beispiel sofort eine Email erhalten, wenn einer meiner Ebay-Kunden ein sogenanntes Feedback über eine zwischen uns abgewickelte Transaktion hinterlassen hat. Da ich mich zunächst nicht lange bei Ebay als Developer anmelden wollte, schrieb ich kurzerhand einen Screenscraper, der aus dem HTML-Salat der Feedback-Seite die aktuelle Zahl der Feedbacks herausfieselt, abspeichert, und bei späteren Tests Alarm schlägt, falls sich der Zähler erhöht hat.
Abbildung 1: Auf der Feedback-Seite bei Ebay steht die Anzahl der bisher eingegangenen Kundenkommentare (hier 362). |
Klicke ich auf Ebay.com auf meinen Usernamen, 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="...
Ein XPath-Prozessor wie zum Beispiel das CPAN-Modul HTML::TreeBuilder::XPath kann nun den Inhalt dieses Tags sehr 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 dabei an,
dass sich die gesuchten Elemente in beliebiger Verschachtelungstiefe in
Elementen unterhalb des HTML-Body-Tags befinden können. Der XPath-Query spuckt
dann einen String wie etwa "( 362) (..."
aus, und daraus mittels eines regulären Ausdrucks in Perl die erste Zahl
herauszufieseln ist ein Kinderspiel. Listing 1 tut genau dies in der
Funktion feedback_fetch
ab Zeile 70, speichert
gefundene Zahlen in einer Cache-Datei, vergleicht den Wert dort beim nächsten
Aufruf mit dem aktuell eingeholten, und feuert eine Email ab, wenn der Zähler
sich erhöht hat.
Abbildung 2: In einem HTML-Element der Klasse "mbg-l" steht die Anzahl der Feedbacks (362). |
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 }
Das Schreiben und Lesen der Cache-Datei, 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 exportiert. Zum Einholen der Ebay-Seite nutzt das Skript
das abgespeckte Modul LWP::Simple
, dessen get
-Methode schlicht
einen HTTP-Request auf den angegebenen URL absetzt und den Inhalt der
gefundenen Seite im Erfolgsfall zurückgibt.
Unter Linux eine Mail abzuschicken, lässt sich oft ganz einfach mittels der
Utilty /usr/bin/mail
erledigen, wer mehr Flexibilität wünscht, dem sei das
CPAN-Modul Mail::DWIM empfohlen. Da ich aber auf meiner Linux-Kiste daheim
/usr/bin/mail
bereits durch ein Perl-Skript ersetzt habe, das mit den
Erfordernissen meines ISPs zurechtkommt, beließ ich es einfach dabei.
Abbildung 3 zeigt die eingetroffene Email 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 ebay.de. Und schließlich benötigt Zeile 23 in der Variablen
$mail_to
noch die Emailadresse des Users, an den die Alarmemails gehen
sollen.
Abbildung 3: Eine Email benachrichtigt den User über das eingetroffene Feedback. |
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.
Abbildung 4: Der Scraper hat festgestellt, dass die Zahl der Feedbacks seit dem letzten Test von 361 auf 362 hochgeschnellt ist. |
Abbildung 5: Bei Ebay ergeben $4.40 + $0.45 + $1.00 + $0.39 nicht $6.24 sondern $6.25. |
Eigentlich man sollte nicht meinen, dass es Leute gibt, die die Einzelposten einer computergenierten 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 Ebay-Abrechnung, wenn mal wieder nur vier Posten draufstehen, wie in Abbildung 5. Jeder Erstklässler mit einem Taschenrechner könnte mühelos verifizieren, dass $4.40 + $0.45 + $1.00 + $0.39 die Gesamtsumme von $6.24 ergeben. Doch Ebays Java-Jockeys haben anscheinend den Artikel zur Fließkommaarithmetik, den jeder Programmierer [2] gelesen haben sollte, noch nicht gelesen. Sonst wüssten sie nämlich, dass CPUs Fließkommazahlen ungenau abspeichern und deswegen beim Addieren erstaunliche Rundungsfehler auftreten können, wenn man nicht aufpasst wie ein Luchs.
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;
So stellten mir diese Freunde der Sonne im März frecherweise $6.25 in Rechnung, obwohl die Gesamtsumme der Einzelposten zweifelsohne $6.24 beträgt. Beim Kundencenter hatte ich vor einiger Zeit schon einmal 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.
Das Skript in Listing 2 hilft dabei, diese Buchprüfung jeden Monat
automatisch durchzuführen. Es stützt sich auf die 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 sowohl die
Einzelposten (jeweils unter dem Tag NetDetailAmount
) als auch
die Gesamtsumme (unter InvoiceNewFee
) und das nachgeschaltete invoice-check
in Listing 3 kann dann prüfen, ob Ebay richtig gerechnet hat.
Abbildung 6: Zwei Skripts holen die letzte Abrechnung und prüfen sie auf Richtigkeit. |
Eine Applikation, die Ebay-Nutzerdaten liest oder schreibt, muss sich gegenüber Ebay mit einem Token ausweisen. Hierzu muss sich der Entwickler beim Ebay-Developer-Program registrieren und eine Applikation anmelden. Anschließend kann dann entweder die Applikation den User durch den Login-Prozess führen und so einen Token erhalten, oder, falls der Entwickler den Token nur zu Testzwecken und für seinen eigenen Account benötigt, in Abbildung 7 einfach den Knopf "Sign in to Production" drücken ([3]). Dieser gilt dann für den tatsächlichen Account in der Live-Produktion, und nicht etwa für die sogenannte "Sandbox" in der Entwickler ebenfalls ihre Apps ausprobieren können, bis sie sicher sind, dass diese Produktionsreife erlangt haben.
Abbildung 7: Entwickler können bei Ebay zu Testzwecken einen Token für ihren eigenen Ebay-Account abholen. |
Alle 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 darunter eingebetteten eBayAuthToken
-Tags im XML stehen muss.
Abbildung 8: Nach dem Einloggen fragt Ebay noch einmal nach, ob der Entwickler seiner App auch wirklich traut. |
Die Dokumentation auf den Seiten des Ebay-Developers-Program ist leicht
chaotisch angeordnet und 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 die 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
der 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 Wert. 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-Requests, aber St. Bürokratius verlangt, dass sie ebenfalls nochmal
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 demnach auf dem letzten Tag
des vorhergehenden Monats und die DateTime-Funktion ymd
modelt das
Datum z.B. für die Märzabrechnung 2016 ins Format 20160331
um,
das die Ebay-API versteht, falls in der Abfrage
unter AccountHistorySelection
der Wert SpecifiedInvoice
angegeben wurde. Das Datum dieser Abrechung 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 Dateiende entfernt und den Schlüssel dann
über die Variable $token
ins XML ab Zeile 31 einbaut. Das Datum
der gewünschten Abrechung liegt in der Variablen $invoice_date
und
fließt ebenfalls ins XML ein, Zeile 38 interpoliert den Wert.
Tritt ein Fehler beim Einholen der Daten auf, bricht Zeile 46 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 Ebay sauer reagiert, 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.
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 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 jegliches 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.
Abbildung 9: Die Online-Abrechnung in XML listet auch die Einzelbeträge auf, sodass ein Skript sie nachkontrollieren kann. |
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 "InvoiceBalance" }->{ content }; 31 32 print "Total: $total\n"; 33 print "Invoice: \$$invoice_amount\n";
Wie gesagt, wird in [2] dringend davon abgeraten, Dollar (oder Euro) und Centbeträge einfach als Fließkommawerte aufzuaddieren. Vielmehr sollten sorgfältige Programmierer alles in Cents umrechnen und diese als Integer weiter verarbeiten, um vom Ergebnis anschließend die letzten zwei Ziffern wieder abzutrennen und als Centbeträge auszuweisen.
Das CPAN-Modul Math::Currency addiert Beträge ordentlich auf und formatiert das Ergebnis ansprechend, auch wenn die Eingaben wie im XML von Ebay mit ihren einstelligen Nachkommabeträgen (z.B. "$1.6") nicht ganz korrekt vorliegen. Allerdings hat der Zahn der Zeit schon sehr an dem Modul genagt, der letzte Release ist 6 Jahre alt und die beiliegende Test-Suite rasselt mit Pauken und Trompeten durch die Prüfung. Funktionieren tut das Modul aber, und das Rad neu zu erfinden war mir zu umständlich, 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 vorhergegangenen 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.
Wie wird es weitergehen? Dieser Fehler bei der Ebay-Abrechnung brachte mich auf die Idee, beim nächsten Supermarktbesuch auch die Einzelposten auf dem Kassenbon nachzukontrollieren. Wem würde es schon auffallen, wenn die dort ausgewiesene Gesamtsumme um ein paar Cent von der Summe der Einzelposten abwiche? Niemand außer mir wahrscheinlich, der die Kontrolle selbstverständlich nicht von Hand sondern durch ein Skript ausführen würde, das die OCR-Daten des Kassenbons fräße. Vielleicht bald auf diesem Kanal.
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