Jeder der Rechnungen an seine Kunden schreibt hat die Herausforderung den Zahlungseingang der selbigen zu überwachen und im schlimmsten Fall Zahlungserinnerungen, Mahnungen oder sonstige nächste Schritte einleiten zu müssen. Damit dies möglich ist muss im Idealfall mehrmals täglich das Konto auf neue Zahlungseingänge geprüft werden. Betreiber von reinen Onlinesystemen / Onlineshops stehen vor der selben Herausforderung nur mit der Ausnahme, dass die Anzahl der auf Zahlungseingang wartenden Bestellungen deutlich höher ist. Der manuelle Aufwand zur Prüfung auf offene Zahlungen verschlingt eine Menge Zeit – also muss eine möglichst einfache Softwarelösung her.
Auch ich stand nach dem Wechsel meiner Hausbank erneut vor der Herausforderung mein eigenes Rechungs- / Buchhaltungssystem an die neuen Gegebenheiten anzubinden. Also los gehts …
Der Standardweg: Umsätze per HBCI abrufen
In der Theorie ist das automatisierte Prüfen auf Zahlungseingänge ein Kinderspiel – immerhin unterstützen die meisten Banken das HBCI-Protokoll welches mit Hilfe der freien Library HBCI4Java angebunden und z.B. zum Abrufen der Umsatzliste genutzt werden kann. Wir schreiben also eine kleine Java-Klasse, loggen uns automatisiert in der Bank ein, rufen die Umsatzliste ab und schon haben wir was wir brauchen um Weiterzuarbeiten. Ein Beispiel hierfür werde ich mir an dieser Stelle sparen – das Internet ist voll davon. Ein guter Einstieg in die Thematik bringt Ihnen aber die Informationsseite zur Open-Source-Bibliothek HBCI4Java.
Kein HBCI – Was nun?
Was tun wir nun aber wenn unsere Bank kein HBCI unterstützt? Wir vergessen die Idee die Umsätze automatisiert zu lesen und machen das ganze manuell. Kein Problem, dauert pro Tag ja nur eine halbe Stunde – die Zeit habe ich. Nun gut, pro Tag 30 Minuten – in der Woche also „nur“ 4 1/2 Stunden. Haben wir so viel Zeit? Nein, das muss anders gehen.
Und ja, es geht anders – warum bringen wir unserer Software nicht einfach bei genau das zu tun, was wir selbst händisch tun? Wir loggen uns über den Browser ein, klicken ein wenig herum und lesen die Umsätze manuell – wenn wir das können dann kann das auch der Computer. Die Idee für einen kleinen Webcrawler ist geboren.
Welches Framework und warum?
Die ersten Versuche zum Abruf der Umsätze wurde auf Basis mit dem Selenium Testframework durchgeführt. Dies hat den Vorteil dass die Benutzeraktionen direkt im Browser (über Firefox) aufgezeichnet und anschliessend in eine Java-Klasse exportiert werden können. Somit haben wir schnell eine erste Testklasse mit welcher wir das Konzept des Webcrawlers testen können. Der Nachteil ist, dass während der Ausführung des Codes ein eigener Browser gestartet wird. Zwar kann der Start des Browsers über die zusätzliche Installation eines Testservers (siehe Webseite von Selenium) umgangen werden, dennoch erscheint mir diese Lösung als etwas zu übertrieben für eine solch kleine Lösung.
Also erinnerte ich mich an ein Gespräch mit einem Kollegen welcher mir von einer Java-Library namens „Jsoup“ erzählte. Diese Library ermöglicht es dem Benutzers Webseiten direkt aus dem Code zu lesen und unterstützt neben dem einfachen HTML auch Features wie CSS oder JQuery. Nachdem die ersten Tests mit Jsoup erfolgreich und die Laufzeit deutlich besser als die vorherige war, wurde die Entscheidung zum Einsatz von JSoup getroffen.
Los geht´s
Bitte haben Sie Verständnis dafür, dass ich in meinem Beispiel keinen Code veröffentliche welcher direkt ausführbar und funktional zum Zugriff auf eine Bank ist. Vielmehr soll diese Seite einen technischen groben Überblick darüber geben, wie einfach es ist eine Webseite zu lesen und deren Inhalte auszuwerten. Wer diese Punkte nachvollziehen kann ist mit Sicherheit in der Lage die notwendigen Schritte selbst zu implementieren und in die eigene Softwarelösung einzubinden.
Die hier gewählte Implementierung setzt den Focus auf Lesbarkeit. Jsoup ermöglicht auch den verschachtelten Aufruf mit der direkten Angabe aller Page- / Form-Parameter in nur einer einzigen Zeile. Mir persönlich ist das zu unlesbar und für spätere Wartungen / Fehlersuchen zu kompliziert. Aus diesem Grund besteht mein Code aus einigen Codezeilen mehr als in anderen Beispielen zu finden.
Bitte beachten Sie dass der Beispielcode in keinem Fall so eingesetzt werden sollte sondern nur das Vorgehen demonstrieren soll. Speziell das Thema des Exception-Handlings und eventuelle Null-Checks sollten zwingend auf die eigenen Bedürfnisse angepasst und überdacht werden.
erster Aufruf und Login
Der erste Request in meinem Beispiel dient dazu, eine Webseite vom Code aus aufzurufen und diese mittels GET (also dem Befehl welcher der Browser beim Besuch verwendet) zu lesen. Hierbei gehe ich davon aus, dass die Zielseite eine Form mit dem Namen „login-form“ beinhaltet. Innerhalb dieser Form werden 2 Elemente mit dem Namen „username“ und „password“ erwartet:
public Response getLoggedInSession(String url, String username, String password) { try { Document doc = Jsoup.connect(url).get(); FormElement loginForm = (FormElement) doc.select( "[name=login-form]").first(); loginForm.getElementById("username").attr("value", username); loginForm.getElementById("password").attr("value", password); Response response = loginForm.submit().execute(); return response; } catch (Exception e) { e.printStackTrace(); return null; } }
Diese Funktion führt die folgenden Schritte durch:
- HTTP-GET auf die übergebene URL
- Befüllung des Feldes „username“ mit dem übergebenen Username
- Befüllung des Feldes „password“ mit dem übergebenen Passwort
- Submit der Form über Submit
- Rückgabe der Page welche nach dem Loginversuch vom Server zurückgegeben wurde
Anklicken eines Links innerhalb der eingeloggten Session
Im ersten Schritt wurde bereits eine Internetseite aufgerufen und ein auf der Seite befindliches Formular mit 2 Felder ausgefüllt. Dieses Formular wurde mittels dem HTTP-POST an den Server übertragen und das Ergebnis dieses Posts zurück gegeben.
Gehen wir nun also davon aus dass der Login erfolgreich war und wir uns nun auf der gleichen Seite befinden, wie wenn wir uns selbst über den Browser auf der gewünschten Seite einloggen. Als nächstes klicken wir im Menübaum einen bestimmten Link an um z.B. in die Umsatzliste unseres Kontos zu gelangen. Wichtig hierbei ist, dass wir dem System irgendwie mitteilen, dass wir uns bereits in einer authentifizierten Session befinden. Hierzu übergeben wir einfach die aktuelle Session-ID beim Aufruf des gewünschten Links und rufen die Page über den HTTP-GET auf:
public Document getUmsatzDocument(Response loggedInResponse, String umsatzDocumentUrl) { try { String sessionId = loggedInResponse.cookies().get("JSESSIONID"); Document umsatzDoc = Jsoup.connect(umsatzDocumentUrl) .cookie("JSESSIONID", sessionId).get(); return umsatzDoc; } catch (Exception e) { e.printStackTrace(); return null; } }
Interpretation der Ergebnisse
Wenn alles gut gelaufen ist haben wir nun das Dokument in der Hand, welches die uns bekannte Seite mit den Kontoumsätzen beinhaltet. Diese Seite muss nun interpretiert werden, ich gehe im Beispiel davon aus, dass die Seite folgende Inhalte enthält:
- ein DIV-Element mit der ID transactionList
- Dieses DIV enthält eine Liste von weiteres DIV mit der ID transaction
- jedes DIV mit der ID transaction enthält einen einzelnen Umsatz
- in jedem DIV existieren DIVs mit den eigentlichen Inhalten
- ID date = Datum
- ID amount = Betrag
- ID topic = Verwendungszweck
Eine solche Beispielseite könnte wie folgt gelesen und interpretiert werden:
private void readUmsaetze(Document umsatzDoc) { try { Element umsatzDiv = umsatzDoc.select("[id=transactionList]") .first(); Elements umsaetze = umsatzDiv.select("[id=transaction]"); for (Element umsatz : umsaetze) { String datum = umsatz.getElementById("date").text(); String betrag = umsatz.getElementById("amount").text(); String verwendungszweck = umsatz.getElementById("topic").text(); // behandle Umsatz wie gewünscht } } catch (Exception e) { e.printStackTrace(); } }
Was nun? Wie sieht der Praxiseinsatz aus?
Wie am obigen Beispiel zu sehen ist es mit nur wenigen Codezeilen möglich einen Login auf einer Seite durchzuführen, interne Seiten anzusurfen und deren Inhalte zu lesen und zu interpretieren. Im Grunde ist dieses Vorgehen für sämtliche Browserbasierten Anwendungen adaptierbar und kann in beliebiger Art und Weise eingesetzt werden.
Problematisch bei dem Ansatz eines Webcrawlers ist allerdings die Tatsache, dass wir in unserem Code auf eine gewisse Beständigkeit des Layouts der zu lesenden Internetpräsenz angewiesen sind. Ständige Veränderungen der Internetseite auf der anderen Seite führen dazu, dass unsere eigene Software mehr oder weniger stabil ist und ständigen Anpassungen unterliegt.
Bei mir persönlich hat sich dieses Vorgehen im Praxiseinsatz sowohl bei mir als auch bei einigen meiner Kunden inzwischen bewährt. Allerdings ist mein Praxiscode hierbei natürlich deutlich stabiler als der hier beispielhaft veröffentlichte und geht auf Themen wie die Fehlerbehandlung, Vermeidung von Doppelmeldungen oder Erkennung von Änderungen in der zugrunde liegenden Seitenstruktur ein.
Jeder sollte für sich selbst herausfinden ob der Einsatz eines eigenen Webcrawlers eine Lösung für die vorliegende Problematik sein kann oder ob eine andere Alternativ gesucht werden muss. Allerdings ist dieses Vorgehen ein gutes Beispiel dafür, dass nicht immer der Aussage „geht nicht“ zu vertrauen ist – mit ein wenig nachdenken geht, sofern man bereit ist andere Wege zu bestreiten, so einiges …