home
lars.st0ne.at
Linux And Related Stuff
[BLOG] |ARCHIVE| |TAG MAP| |OTHER STUFF| |ME|
  • BankAustria Onlinebanking Interface


    #bankaustria #hbci #kmymoney #python

    !ACHTUNG!: Nach der EDV Umstellung der CA, die am 30.10.2012 abeschlossen wurd, funktioniert das Script nicht mehr! ... Ich arbeite an einer neuen Version!

    Beacause this is an "austria only" related topic, the article is written in german.

    Vorwort: Den folgenden Code habe ich für mich persönlich entwickelt, er erhebt keinen Anspruch auf Vollständigkeit/Richtigkeit. Ich übernehme keinerlei Verantwortung für Folgen und eventuelle Schäden! Lest den Code bevor ihr ihn ausführt!!!!!!

    Ich verwende KMyMoney um meine persönlichen Finanzen zu verwalten. Laut BankAustria besteht, außer über die Webseite, keine Möglichkeit zu meinen Buchungszeilen zu kommen. Leider wird der in Deutschland weit verbreitete HBCI Standard von keiner österreichischen Bank unterstützt!

    Da das Abtippen aller Buchungszeilen eine fade Angelegenheit ist, habe ich mich entschlossen ein Python Skript zu schreiben, das die Daten von der BankAustria Seite holt und in das QIF Format umwandelt.

    Die OnlineBanking Seite erlaubt den Download der Transaktionen im XML Format. Leider ist in dem Format nicht vorgesehen den Empfänger/Sender in einem eigenen Feld auszuweisen. Diese Information ist nur im Memo Text verwurschtet. ... Ein Großteil der Empfänger/Sender können aber mit etwas Aufwand extrahiert werden.

    KMyMoney kann das QIF Format dann importieren. Hab es nicht probiert, aber ich denke das QIF kann auch von allen anderen Programmen ( z.B MSMoney ), die das Format unterstützen importiert werden.

    Im Detail macht das Skript folgendes:
    - ladet das aktuelle XML File von der Seite
    - archiviert das XML File
    - extrahiert die Empfänger aus dem Memo Text
    - archiviert alle Transaktionen ( mit Empfänger ) im File transactions.csv
    - erstellt ein QIF File, wenn neue Transaktionen vorhanden sind

    INFO!: Alle folgenden Kennwörter, Anmeldeinformationen sowie alle Transaktionen sind fiktiv und sind gegebenen Falls durch eigene Anmeldeinformationen zu ersetzen.

    Beispiel-Aufruf mit Ausgabe:

    $ ./ba_export_xml.py 02891238 09605624500
    Password:
    STEP 1 ... get session cookie
    STEP 2 ... perform login
    STEP 3 ... extract session arguments
      OK ... got session_args
    STEP 4 ... get transaction xml file
      OK: got 200 transactions
    STEP 5 ... exctractin Payees
      WARNING: no payee for : UEBERW. REF.FBADT120701083434 Auslandsüberweisung Ref. FBADT120701083434, ...
      OK: last transaction found
        last transaction was: "30.07.2012";" 30.07.2012";"-22,90";"EUR";"HOFER DANKT 0531K9 27.06.UM 10.30O";"HOFER DANKT"
    STEP 6 ... generate QIF file
      OK: QIF file import_20120730_1516.qif was written with 2 transactions
    STEP 7 ... logout
    

    02891238 ... ist die 8 stellige Verfügernummer
    09605624500 ... ist die Kontonummer
    Nicht erkannte Empfänger werden als UNBEKANNT im CSV File ausgewiesen.
    Der Pincode kann entweder interaktiv eingegeben werden, oder in der Datei pin.txt im selben Verzeichnis abgelegt werden.

    CSV Struktur:

    "Buchungsdatum";"Valutadatum";"Währung";"Betrag";"Buchungstext";"Empfänger/Sender"
    "23.02.2012";" 23.02.2012";"-150,00";"EUR";"AUTOMAT 00043189 K9 23.02.UM 15.40 O";"AUTOMAT"
    "24.02.2012";" 24.02.2012";"-6,99";"EUR";"Lastschrift a/CARD COMPLETE SERVICE BANK AG VISA-RECHNUNG 11917956419";"CARD COMPLETE SERVICE BANK AG"
    "28.02.2012";" 28.02.2012";"4801,31";"EUR";"Gutschrift a/TUAMEISTNIX AG LOHN/GEHALT 00013999/201202";"TUAMEISTNIX AG"
    "01.03.2012";" 01.03.2012";"-111,00";"EUR";"DA-NR.: 003 Empfänger: Hans Wurst Kontonummer: 00301 867 197 BLZ: 39822 Spende";"Hans Wurst"
    

    QIF Struktur:

    !Account
    NBA Konto
    TBank
    ^
    !Type:Bank
    D23.02.2012
    PAUTOMAT
    T-150,00
    MAUTOMAT 00043189 K9 23.02.UM 15.40 O
    ^
    D24.02.2012
    PCARD COMPLETE SERVICE BANK AG
    T-6,99
    MLastschrift a/CARD COMPLETE SERVICE BANK AG VISA-RECHNUNG 11917956419 10/12000/00104
    ^
    D28.02.2012
    PTUAMEISTNIX AG
    T4801,31
    MGutschrift a/TUAMEISTNIX AG LOHN/GEHALT 00013999/201202
    ^
    D01.03.2012
    PHans Wurst
    T-111,00
    MDA-NR.: 003 Empfänger: Hans Wurst Kontonummer: 00301 867 197 BLZ: 39822 Spende
    ^
    

    download Skript


    Skript:

    #!/usr/bin/python
    # -*- coding: utf-8 -*-
    # script name: ba_export_xml.py
    # This scripts are Copyrighted by Robert Steininger,
    # and licensed under the Creative Commons Attribution-Share Alike 3.0 License.
    # see http://www.creativecommons.org/licenses/by-sa/3.0/
    # Please respect my wish and meet the license requiremnts.
    #
    # !!! THIS SCRIPT COMES WITH ABSOLUT NO WARRANTY !!!
    #
    
    import urllib2, cookielib
    import os, sys, time, re, datetime
    import codecs
    import getpass
    from lxml import etree
    
    if len( sys.argv ) != 3:
       print "usage: %s <verfueger_nummer> <konto_nummer>"
       sys.exit(1)
    
    verfueger_nummer = sys.argv[1]
    konto_nummer     = sys.argv[2]
    
    if os.path.exists( "pin.txt" ):
       fh = open( "pin.txt" )
       pin = re.sub( '(\n|\r)', '', fh.readline() )
    else:
       pin = getpass.getpass()
    
    start_url       = 'https://online.bankaustria.at//bach/de/login/login.html'
    login_url       = 'https://online.bankaustria.at/servlet/SSOLogin'
    giro_url        = 'https://online.bankaustria.at/servlet/GiroKontoDetail'
    detail_url      = 'https://online.bankaustria.at/servlet/GiroBuchungDetail'
    logout_url      = 'https://online.bankaustria.at/servlet/Logout'
    
    cj = cookielib.CookieJar()
    
    if os.environ.has_key('HTTPS_PROXY'):
       proxy_handler = urllib2.ProxyHandler({ 'https': os.environ['HTTPS_PROXY'] })
    
       # uncomment the following 2 lines if you proxy needs basic auth !!! ... not tested
       #proxy_auth_handler = urllib2.ProxyBasicAuthHandler()
       #proxy_auth_handler.add_password('realm', 'host', 'username', 'password')
       #opener = urllib2.build_opener(proxy_handler, proxy_auth_handler)
    
       # no proxy auth
       opener = urllib2.build_opener( proxy_handler, urllib2.HTTPCookieProcessor(cj) )
    else:
       opener = urllib2.build_opener( urllib2.HTTPCookieProcessor(cj) )
    
    # fake User-Agent
    opener.addheaders = [('User-agent', 'Mozilla/5.0')]
    urllib2.install_opener(opener)
    
    print >> sys.stderr, "STEP 1 ... get session cookie"
    req = urllib2.Request( start_url )
    r = urllib2.urlopen(req)
    
    if r.code != 200:
       print >> sys.stderr, "ERROR: got status code %d" % r.code
       sys.exit( 1 )
    
    
    print >> sys.stderr, "STEP 2 ... perform login"
    req = urllib2.Request( login_url )
    req.add_data( 'timestamp=%d&JSOS=Linux i686&yzbks=%s&jklwd=%s&JSBROWSER=mozilla' % (
                                            int( time.time() ), # unix epoch time
                                            verfueger_nummer,
                                            pin,                # passwort
                                           ) )
    r = urllib2.urlopen(req)
    
    if r.code != 200:
       print >> sys.stderr, "ERROR: got status code %d" % r.code
       sys.exit( 1 )
    
    print >> sys.stderr, "STEP 3 ... extract session arguments"
    session_args = None
    regex = re.compile( '^.*MenuHead\?(sessionid=.*?)".*$')
    for line in r.readlines():
       match_object = regex.match( line )
       if not match_object:
          continue
       if len( match_object.groups() ) > 0:
          session_args = match_object.groups()[0]
          break
    
    if not session_args:
       print >> sys.stderr, "unable to get session arguments"
       sys.exit(1)
    
    print >> sys.stderr, "   OK ... got session_args"
    
    print >> sys.stderr, "STEP 4 ... get transaction xml file"
    req = urllib2.Request( giro_url )
    arguments = []
    arguments.append( session_args )
    arguments.append( "language=DE" )
    arguments.append( "mode=bno" )
    arguments.append( "gknSel=01%sEUR12000*" % konto_nummer )
    arguments.append( "gknDefault=yes" )
    arguments.append( "gknRadio=gknRadioPeriode" )
    arguments.append( "gknPeriodeSel=gknPeriodeSelLetzte6monate" )
    arguments.append( "downloadmode=ifxxmlyes" )
    arguments.append( "downloadfilename=export.xml" )
    
    req.add_data( '&'.join( arguments ) )
    r = urllib2.urlopen(req)
    
    if r.code != 200:
       print >> sys.stderr, "ERROR: got status code %d" % r.code
       sys.exit( 1 )
    
    
    xml_data = r.read()
    
    # archive xml file
    fname = "ba_%s.xml" % datetime.datetime.now().strftime('%Y%m%d_%H%M')
    fh = codecs.open( fname, encoding='utf-8', mode='w+' )
    fh.write( xml_data )
    fh.close()
    xmltree =  etree.fromstring( xml_data )
    transactions = []
    
    # extrace transactions from xml
    for t in list( xmltree.iter( "BankAcctTrnRec") ):
       transactions.append( [ unicode( t.find("PostedDt").text.decode('utf-8')),
                              unicode( t.find("EffDt").text.decode('utf-8')),
                              unicode( t.find("CurAmt").find("Amt").text.decode('utf-8')),
                              unicode( t.find("CurAmt").find("CurCode").text.decode('utf-8')),
                              unicode( t.find("Memo").text.encode("utf-8").decode('utf-8'))
                          ] )
    
    print >> sys.stderr, "   OK: got %s transactions" % len( transactions )
    
    print >> sys.stderr, "STEP 5 ... exctractin Payees"
    
    # regex for transaction with the style:
    # DA-NR.: 003 ... Empfänger:    Hans Wurst ... Kontonummer: 0187247773    BLZ: 21357      Dauerwurstauftrag
    empf_regex = re.compile( '^.{58}Empf.nger:\s+(.*)\s+(?<=^.{130})Kontonummer:.*$', re.U )
    
    # regex for transaction with the style:
    # BAUHAUS 2231 0298  K9 12.01.UM 18.25     O
    O_regex = re.compile( '^(.*?)\s+\d+\s+K\d+\s+\d\d\.\d\d\.UM\s+\d\d\.\d\d\s+(?<=^.{41})O$', re.U )
    
    # regex for transaction with the style:
    # AT  1249,98 MAESTRO POS 20.06.12 14.32K9 O                WUERSCHTLBUDE          AM GRABEN
    Oplus_regex = re.compile( '^.*(?<=^.{41})O\s+(?<=^.{58})(.{23}).*$', re.U )
    
    # regex for transaction with the style:
    # ABHEBUNG AUTOMAT NR. 19999 AM 18.01. UM 12.42 UHR NEUSIEDL PK BANKCARD 9
    abh_regex = re.compile( '^ABHEBUNG AUTOMAT.*', re.U )
    
    # regex for transaction with the style:
    # EZE-Lastschrift a/TUAMEISTNIX AG                          TUAMEISTNIX-Vtkonto 13088418 011384105403                 ....
    schrift_regex = re.compile( '^(?:SEPA-Last|EZE-Last|Gut|Last)schrift.*a/(.*?)(?<=^.{58}).*$', re.U )
    
    # regex for transaction with the style:
    # PILLA DANKT  4222P K9 01.05.UM 11.34"
    sonstige_regex = re.compile( '^(.*)(?<=^.{13})..*K\d \d\d\.\d\d\.UM \d\d\.\d\d$', re.U )
    
    # regex for transaction with the style:
    # BARAUSZAHLUNG NIRGENDWO
    bara_regex = re.compile( '^(BARAUSZAHLUNG).*$', re.U )
    bare_regex = re.compile( '^(BAREINZAHLUNG).*$', re.U )
    
    
    konto_regex = re.compile( '^(Kontopaket|Porto|KEST|Habenzinsen|Sollzinsen)$', re.U )
    
    
    # remove leading and tailing spaces
    space_regex = re.compile( '(^\s+|\s+$)', re.U )
    
    shorten_regex = re.compile( '\s{2,}', re.U )
    
    for t in transactions:
       #text = t[4].decode('utf-8')
       text = t[4]
       t[4] = space_regex.sub( '; ', t[4] )
       t[4] = shorten_regex.sub( ' ', t[4] )
    
    
       match = schrift_regex.match( text )
       if match != None:
          t.append( space_regex.sub( '', match.groups()[0] ) )
          continue
    
       match = empf_regex.match( text )
       if match != None:
          t.append( space_regex.sub( '', match.groups()[0] ) )
          continue
    
       match = O_regex.match( text )
       if match != None:
          t.append( space_regex.sub( '', match.groups()[0] ) )
          continue
    
       match = Oplus_regex.match( text )
       if match != None:
          t.append( space_regex.sub( '', match.groups()[0] ) )
          continue
    
       match = sonstige_regex.match( text )
       if match != None:
          t.append( space_regex.sub( '', match.groups()[0] ) )
          continue
    
       match = abh_regex.match( text )
       if match != None:
          t.append( 'AUTOMAT' )
          continue
    
       match = konto_regex.match( text )
       if match != None:
          t.append( match.groups()[0] )
          continue
    
       match = bare_regex.match( text )
       if match != None:
          t.append( match.groups()[0] )
          continue
    
       match = bara_regex.match( text )
       if match != None:
          t.append( match.groups()[0] )
          continue
    
       # else UNKNOWN
       t.append( 'UNBEKANNT' )
       print '   WARNING: no payee for : %s' % text
    
    
    
    
    
    fh = codecs.open( "transactions.csv", encoding='utf-8', mode='a+' )
    # seek to the end
    fh.seek(0, os.SEEK_END)
    file_length = fh.tell()
    
    # seek to the file begin
    fh.seek(0)
    if file_length == 0:
       last_transaction = ''
       fh.write( u'"Buchungsdatum";"Valutadatum";"Währung";"Betrag";"Buchungstext";"Empfänger/Sender"\n' )
    else:
       last_transaction = re.sub( '[\r\n]', '', fh.readlines()[-1] )
    
    found_last = False
    import_list = []
    for t in list( reversed( transactions ) ):
       if last_transaction == '':
          fh.write( '"' + '";"'.join( t ) + '"\n' )
          import_list.append( t )
          continue
    
       if found_last == True:
          fh.write( '"' + '";"'.join( t ) + '"\n' )
          import_list.append( t )
          continue
    
       if last_transaction == '"' + '";"'.join( t ) + '"':
          print >> sys.stderr, "   OK: last transaction found"
          print >> sys.stderr, "     last transaction was: %s" % last_transaction
          found_last = True
    fh.close()
    
    # generate qif
    print >> sys.stderr, "STEP 6 ... generate QIF file"
    if len( import_list ) > 0:
       fname = "import_%s.qif" % datetime.datetime.now().strftime('%Y%m%d_%H%M')
       fh = codecs.open( fname, encoding='utf-8', mode='w+' )
       fh.write( "!Account\nNBA Konto\nTBank\n^\n!Type:Bank\n" )
       for t in import_list:
          fh.write( "D" + t[0] + "\n" )
          fh.write( "P" + t[5] + "\n" )
          fh.write( "T" + t[2] + "\n" )
          fh.write( "M" + t[4] + "\n" )
          fh.write( "^" + "\n" )
       fh.close()
       print >> sys.stderr, "   OK: QIF file %s was written with %d transactions" % ( fname, len( import_list ) )
    else:
       print >> sys.stderr, "   nothing to do"
    
    
    # logout
    print >> sys.stderr, "STEP 7 ... logout"
    req = urllib2.Request( logout_url )
    req.add_data( '%s&language=DE&mode=no' % session_args )
    r = urllib2.urlopen(req)
    
    if r.code != 200:
       print >> sys.stderr, "ERROR: got status code %d" % r.code
       sys.exit( 1 )
    

    Jul 27 2012 19:07
    by st0ne
    ? hits
    • comment @Twitter
    • share on Twitter
©2012 Robert Steininger aka st0ne
CC-BY-SA