Meine Bank bietet mir den Download der Kontoumsätze als CSV-Datei an. Mich nervt allerdings immer das Einloggen und die ganze Prozedur, bis ich endlich bei den Umsätzen bin. Ich möchte daher den Download weitestgehend automatisieren. Leider bietet die Bank auch keine API dafür an.
Die Bank bietet zwar eine Software dafür an, allerdings läuft diese nur unter Windows und auch sonst ist die nicht so grandios. Also dachte ich dran, das ganze mit PHP oder besser noch mit Python zu machen.
Der Login erfolgt mit Kontonummer und PIN. Beides geht über eine SSL-Verbindung zum Webserver. Die anschließenden Formulare sind immer gleich. Diese lassen sich also faken. Ich erhalte aber bei jedem Login eine andere Session-ID, die immer als hidden-Field in den HTML-Formularen steckt.
Es gilt also zunächst, den Login automatisiert per HTTP-POST-Request durchzuführen. Anschließend kann ich aus dem Seitenquelltext via RegEx die Session-ID extrahieren.
Wie gestaltet man in Python einen HTTP-POST-Request?
Fake-Formular per POST an Bank verschicken
-
- Python-Forum Veteran
- Beiträge: 16025
- Registriert: Freitag 20. Juni 2003, 16:30
- Kontaktdaten:
Das geht über den Data-Parameter von urllib2.urlopen(). Hier mal ein Beispiel, wie ich so etwas ähnliches gemacht habe (ohne Cookielib). Dabei habe ich aber BeautifulSoup statt regulären Ausdrücken verwendet um mit mein Ticket, in deinem Fall eine Session-ID, rauszuholen.droptix hat geschrieben:Wie gestaltet man in Python einen HTTP-POST-Request?
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
Ich habe mir für solche und ähnliche Sachen einen "FakeBrowser" geschrieben:
libxml2dom musst du vermutlich erst installieren, aber es vereinfacht die Verarbeitung doch ziemlich (wie z.B. das Übernehmen der hidden Fields, was du ja auch brauchst)
Noch ein konkretes Bsp:
Code: Alles auswählen
#! /usr/bin/env python
# -*- coding: UTF-8 -*-
import cookielib, urllib2, socket, urlparse, libxml2dom
from urllib import urlencode
class FakeBrowser:
HEADERS = {'User-agent' : 'Opera/9.10 (X11; Linux i686; U; en)'}
def __init__(self):
socket.setdefaulttimeout(30) # seconds
self.cj = cookielib.LWPCookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.cj))
urllib2.install_opener(opener)
self.standardParser = None
self.url = None
def _getAbsoluteURL(self, url):
if url.startswith('http://') or url.startswith('https://'):
return url
else:
if self.url is None:
raise Exception, "History is empty - cannot open relative url"
else:
return urlparse.urljoin(self.url, url)
def GET(self, url, getDomNode=False, parse=True):
return self.POST(url, None, getDomNode, parse)
def POST(self, url, params={}, getDomNode=False, parse=True):
if params != None:
params = urlencode(params)
url = self._getAbsoluteURL(url)
req = urllib2.Request(url, params, self.HEADERS)
retries = 0
while retries < 5:
try:
handle = urllib2.urlopen(req)
data = handle.read()
parse and self.standardParser != None and self.standardParser(data)
self.url = handle.geturl()
if getDomNode:
return libxml2dom.parseString(data, html=True)
else:
return data
except (socket.timeout, urllib2.URLError):
retries += 1
raise Exception, "5 retries - aborting..."
def getHiddenFields(self, formNode, params=None):
if params is None:
params = {}
for h in formNode.xpath("//input[@type='hidden']"):
params[h.getAttribute('name')] = h.getAttribute('value')
return params
def printCookies(self):
for index, cookie in enumerate(self.cj):
print index, ' : ', cookie
Noch ein konkretes Bsp:
Code: Alles auswählen
#! /usr/bin/python
# -*- coding: UTF-8 -*-
import os.path
from fakeBrowser import FakeBrowser
LOGIN_URL = 'https://rechnung.bob.at/login.do'
DL_DIR = '/mnt/guardian/shared/bob.rechnung/'
def login(browser, user, pwd):
doc = browser.GET(LOGIN_URL, getDomNode=True)
form = doc.xpath("//form")[0]
params = browser.getHiddenFields(form)
params['username'] = user
params['password'] = pwd
response = browser.POST(form.getAttribute('action'), params, getDomNode=True)
if browser.url == LOGIN_URL:
raise Exception, "Login failed..."
return response
def downloadBills(browser, docNode):
links = docNode.xpath("//a[@title='Rechnung als PDF anzeigen']")
for link in links:
href = link.getAttribute('href')
filename = href[href.rfind('/')+1 : href.find('?')]
if not os.path.exists(os.path.join(DL_DIR, filename)):
bill = browser.GET(href)
f = open(os.path.join(DL_DIR, filename), 'w')
f.write(bill)
f.close()
if __name__ == '__main__':
fb = FakeBrowser()
response = login(fb, 'XXX', 'YYY')
downloadBills(fb, response)
[url=http://www.leckse.net/artikel/meta/profilieren]Profilieren im Netz leicht gemacht[/url]
OK, das klappt schonmal ganz gut. Ich besuche zuerst die Startseite, um eine Pre-Session-ID zu erhalten. Außerdem ist das Feld für das Passwort jedesmal generiert, damit der Login nur im Zusammenspiel aus Pre-Session-ID und Passwortfeld funktioniert.
Ich habe also beides. Nun erstelle ich ein Dictionary, dessen Keys den Feldnamen des Login-Formulars entsprechen:
Nun schicke ich das alles ab:
Es geht! Aber: Wo stelle ich ein, ob ich die Daten in data per GET oder POST abschicken möchte? Müsste man die bei GET gleich direkt an die URL hängen?
Ich habe also beides. Nun erstelle ich ein Dictionary, dessen Keys den Feldnamen des Login-Formulars entsprechen:
Code: Alles auswählen
fields = {}
fields['KtoNr'] = "123456789"
fields['d682ca'] = "passwort" # feldname wird immer neu generiert
fields['pre_sess'] = "3dbf2c29f472daad4e232fd387943c20p000_00005078"
fields['Login'] = "Login" # login button
Code: Alles auswählen
url = "http://www.bank.de/login.php"
request = urllib2.Request(url)
data = urllib.urlencode(fields)
try:
handle = urllib2.urlopen(request, data)
content = handle.read()
handle.close()
return content
except urllib2.HTTPError:
print "error 404: %s" % url
Wenn data angegeben ist, wird es laut Doku per POST abgeschickt, d.h. zu der zweiten Frage würde ich mal ja sagen.droptix hat geschrieben:Es geht! Aber: Wo stelle ich ein, ob ich die Daten in data per GET oder POST abschicken möchte? Müsste man die bei GET gleich direkt an die URL hängen?
[url=http://www.leckse.net/artikel/meta/profilieren]Profilieren im Netz leicht gemacht[/url]
Jippieh, alles klappt! Nun kriege ich meine Kontoauszüge automatisiert per Python-Skript
Jetzt muss ich die Transaktionen aus der CSV-Datei nur noch in eine Datenbank überführen...
EDIT: Ich stelle fest, dass die CSV-Datei nach dem Runterladen immer eine Leerzeile nach jeder Zeile hat. Wenn ich sie manuell über die Website runterlade, dann ist das nicht der Fall. Muss ich da noch irgend einen Header mitsenden, um den Zeilenumbruch wegzukriegen oder passiert das während des Speicherns? Ich suche mir nach dem Login die URL der Datei raus und fordere die URL an. Als Ergebnis erhalte ich den Inhalt der Datei als String, den ich anschließend speichere. Muss ich vorher vielleicht noch Zeilenumbrüche umwandeln?

Jetzt muss ich die Transaktionen aus der CSV-Datei nur noch in eine Datenbank überführen...
EDIT: Ich stelle fest, dass die CSV-Datei nach dem Runterladen immer eine Leerzeile nach jeder Zeile hat. Wenn ich sie manuell über die Website runterlade, dann ist das nicht der Fall. Muss ich da noch irgend einen Header mitsenden, um den Zeilenumbruch wegzukriegen oder passiert das während des Speicherns? Ich suche mir nach dem Login die URL der Datei raus und fordere die URL an. Als Ergebnis erhalte ich den Inhalt der Datei als String, den ich anschließend speichere. Muss ich vorher vielleicht noch Zeilenumbrüche umwandeln?
-
- Python-Forum Veteran
- Beiträge: 16025
- Registriert: Freitag 20. Juni 2003, 16:30
- Kontaktdaten:
Dazu musst du uns wohl den Code zeigen, der die CSV-Datei speichert. Ich tippe mal drauf, dass writeline() aufgerufen wird, die Datei aber an den einzlenen Zeilen schon ein \n hat. Irgendwie so wird es sein, dass die Zeilenumbrüche dann doppelt sind. Lässt sich sicher leicht beheben.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
Wie hast Du das mit der Leerzeile festgestellt? Ist da irgendwo ``print`` involviert? Das gibt ja selbst nochmal einen Zeilenumbruch aus, auch wenn die Zeichenkette schon einen am Ende hatte.
Na indem ich die runtergeladene Datei in einem Editor oder in OpenOffice.org Calc öffne.BlackJack hat geschrieben:Wie hast Du das mit der Leerzeile festgestellt?
Hier der Code...
Prinzipiell bastel ich mir die Download-URL analog zu Leonidas' Code-Snippet mittels urllib2 und BeautifulSoup zusammen. Die URL wird dann in getPage(url) angefordert. Als Ergebnis erhalte ich den Inhalt der Datei als String, den ich mit write() in eine Datei schreibe.
Code: Alles auswählen
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import urllib
import urllib2
import urlparse
import os.path
from BeautifulSoup import BeautifulSoup
class Object:
pass
class AccountTurnover:
def __init__(self):
# configuration
self.config = Object()
# front page
self.config.frontpage = Object()
self.config.frontpage.url = None
# login page
self.config.login = Object()
self.config.login.url = None
self.config.login.user = None
self.config.login.passwd = None
# download page
self.config.download = Object()
self.config.download.url = None
# logout action
self.config.logout = Object()
self.config.logout.url = None
# session
self.session = None
# file content and filename
self.fileContent = ""
def getPage(self, url, data=None, headers=None):
request = urllib2.Request(url)
if data is not None:
data = urllib.urlencode(data)
try:
handle = urllib2.urlopen(request, data)
content = handle.read()
handle.close()
return content
except urllib2.HTTPError:
print "error 404: %s" % url
def getElement(self, html, name, attrs):
soup = BeautifulSoup(html)
tag = soup.find(name, attrs)
return tag
def login(self):
# get pre-session
html = self.getPage(self.config.frontpage.url)
frameSrc = self.getElement(html, "frame", {'scrolling' : "yes"})['src']
# get password field
html = self.getPage(urlparse.urljoin(self.config.login.url, frameSrc))
fields = {}
fields['KtoNr'] = self.config.login.user
fields['pre_sess'] = self.getElement(html, "input", {'name' : "pre_sess"})['value']
fields['Direkt'] = "120401"
fields['screenres'] = ""
fields['Login'] = "Login"
passwdField = self.getElement(html, "input", {'type' : "password"})['name']
fields[passwdField] = self.config.login.passwd
# get session
html = self.getPage(self.config.login.url, fields)
self.session = self.getElement(html, "input", {'name' : "sess"})['value']
def logout(self):
fields = {}
fields['sess'] = self.session
fields['Logout'] = "Logout"
self.getPage(self.config.logout.url, fields)
print "logged out"
def download(self):
fields = {}
fields['Konto'] = self.config.login.user
fields['Art'] = "99"
fields['von'] = "01.12.2006"
fields['bis'] = "31.12.2006"
fields['ungebucht'] = "1"
fields['sess'] = self.session
fields['Weiter'] = "Weiter"
html = self.getPage(self.config.download.url, fields)
# get downloads
downloads = self.getElement(html, "ul", {'class' : "downloads"})
csv = self.getElement(html, "a", {'target' : "_blank"})['href']
url = urlparse.urljoin(self.config.download.url, csv)
url = url.split("&")
url = "&".join(url)
self.fileContent = self.getPage(url)
def saveToDisk(self, filename):
print "saving to %s..." % filename
try:
handle = file(filename, "w")
handle.write(self.fileContent)
handle.close()
print "saved successfully"
except:
print "error while saving"
if __name__ == "__main__":
at = AccountTurnover()
at.config.frontpage.url = "https://meine.bank.de/index.php"
at.config.login.url = "https://meine.bank.de/login.php"
at.config.logout.url = "https://meine.bank.de/logout.php"
at.config.download.url = "https://meine.bank.de/umsaetze.php"
at.config.login.user = "1234567890"
at.config.login.passwd = raw_input("%s: " % at.config.login.user)
at.login()
at.download()
filename = None
while not filename:
filename = raw_input("save to:")
if os.path.exists(filename):
print "%s already exists... choose again" % filename
filename = None
at.saveToDisk(filename)
at.logout()
http://meine.bank.de/download.php?file= ... c78ff8daaa...
Also anstatt dem normalen & als Variablen-Trenner bekomme ich die HTML-Entity & Wenn ich die nicht umwandle (s. download()), ist die URL ungültig und die Datei kann nicht runtergeladen werden. Ich behelfe mir momentan mit einem split("&") und einem anschließenden "&".join().
Damit siehst Du normalerweise nicht wirklich was drinsteht. Also Byte für Byte. Wenn man genau wissen will, was da drin steht, braucht man entweder ``print repr(data)`` oder einen Hexeditor.droptix hat geschrieben:Na indem ich die runtergeladene Datei in einem Editor oder in OpenOffice.org Calc öffne.BlackJack hat geschrieben:Wie hast Du das mit der Leerzeile festgestellt?
Du öffnest die Datei als Textdatei. Wenn die Daten die Du runterlädtst schon Zeilenenden im Windowsformat haben ('\r\n'), wird beim schreiben in eine Textdatei unter Windows aus dem '\n' nochmal ein '\r\n', also steht in der Datei letztendlich '\r\r\n' am Ende jeder Zeile. Oder '0D 0D 0A' für den Hexeditor.
Verschwindet das Problem wenn Du die Datei im Binärmodus ('wb') öffnest?
Der Grund wird wohl sein, dass das gültiges HTML ist. Du kannst natürlich die `replace()`-Methode von Zeichenketten benutzen, aber sauberer wäre es wohl, `BeautifulSoup` zu sagen das HTML-Entities ersetzt werden sollen:Ach ja nochwas: Ich suche sowas wie str_replace() aus PHP. Aus irgend einem Grund krieg ich als Download-URL sowas wie
http://meine.bank.de/download.php?file= ... c78ff8daaa...
Code: Alles auswählen
soup = BeautifulSoup(source,
convertEntities=BeautifulSoup.HTML_ENTITIES)
Jepp, danke!BlackJack hat geschrieben:Verschwindet das Problem wenn Du die Datei im Binärmodus ('wb') öffnest?
Klappt nicht, also url.replace("&", "&"). Funzt!BlackJack hat geschrieben:aber sauberer wäre es wohl, `BeautifulSoup` zu sagen das HTML-Entities ersetzt werden sollen:
Code: Alles auswählen
soup = BeautifulSoup(source, convertEntities=BeautifulSoup.HTML_ENTITIES)