Abschnitt 19 · Lektion 7
Wie es weitergeht
Jetzt steht ein Werkzeug da, das einen Kontoauszug einliest und dir zeigt, wofür das Geld draufgegangen ist. Fünf Dateien und sechs Tests. Das war am Anfang des Kurses versprochen, und es ist kein Spielzeug. Es liest Exporte in zwei Kodierungen, lässt sich ohne Code an neue Läden anpassen und sagt beim Aufruf, was es braucht. Was noch fehlt, ist dein eigener Rechner mit deinem eigenen Export.
Das ganze Werkzeug
Umsätze Girokonto;DE12 3456 7890 1234 5678 90
Zeitraum;01.08.2026 bis 31.10.2026
Kontostand am 31.10.2026;3.214,55 EUR
Buchungstag;Wertstellung;Empfänger;Verwendungszweck;Betrag;Währung
30.10.2026;30.10.2026;STADTWERKE LINDENAU;Abschlag Strom 10/2026;-91,00;EUR
28.10.2026;28.10.2026;DRUCKEREI KRANICH GMBH;Gehalt Oktober;2.580,00;EUR
27.10.2026;27.10.2026;BUCHHANDLUNG SEITENWEISE;Kartenzahlung;-31,90;EUR
24.10.2026;26.10.2026;SUPERMARKT SONNENBLICK FIL. 12;Kartenzahlung;-71,36;EUR
20.10.2026;20.10.2026;BÄCKEREI KRUME;Kartenzahlung;-6,20;EUR
15.10.2026;15.10.2026;HAUSVERWALTUNG AHORN;Miete November;-1.120,00;EUR
12.10.2026;12.10.2026;HAUSVERWALTUNG AHORN;Erstattung Nebenkosten 2025;86,40;EUR
06.10.2026;06.10.2026;APOTHEKE AM MARKT;Kartenzahlung;-18,95;EUR
05.10.2026;05.10.2026;FITNESSSTUDIO KRAFTWERK;Beitrag Oktober;-39,00;EUR
02.10.2026;02.10.2026;SUPERMARKT SONNENBLICK FIL. 12;Bargeldauszahlung;-50,00;EUR
01.10.2026;01.10.2026;VERKEHRSBETRIEBE LINDENAU;Monatskarte;-58,00;EUR
30.09.2026;30.09.2026;STADTWERKE LINDENAU;Abschlag Strom 09/2026;-84,00;EUR
29.09.2026;29.09.2026;SUPERMARKT SONNENBLICK FIL. 12;Kartenzahlung;-63,47;EUR
28.09.2026;28.09.2026;DRUCKEREI KRANICH GMBH;Gehalt September;2.580,00;EUR
22.09.2026;22.09.2026;DROGERIE IM BAHNHOF;Kartenzahlung;-12,95;EUR
15.09.2026;15.09.2026;HAUSVERWALTUNG AHORN;Miete Oktober;-1.120,00;EUR
12.09.2026;14.09.2026;KINO LICHTSPIEL;Kartenzahlung;-24,00;EUR
10.09.2026;10.09.2026;BÄCKEREI KRUME;Kartenzahlung;-8,40;EUR
03.09.2026;03.09.2026;OLE BERGER;Anteil Konzertkarten;45,00;EUR
02.09.2026;02.09.2026;SUPERMARKT SONNENBLICK FIL. 12;Kartenzahlung;-52,18;EUR
01.09.2026;01.09.2026;VERKEHRSBETRIEBE LINDENAU;Monatskarte;-58,00;EUR
31.08.2026;31.08.2026;STADTWERKE LINDENAU;Abschlag Strom 08/2026;-84,00;EUR
28.08.2026;28.08.2026;DRUCKEREI KRANICH GMBH;Gehalt August;2.580,00;EUR
25.08.2026;25.08.2026;SUPERMARKT SONNENBLICK FIL. 12;Kartenzahlung;-58,12;EUR
19.08.2026;19.08.2026;BUCHHANDLUNG SEITENWEISE;Geschenkgutschein 25 €;-25,00;EUR
14.08.2026;14.08.2026;HAUSVERWALTUNG AHORN;Miete September;-1.120,00;EUR
10.08.2026;10.08.2026;KIOSK AM MARKT;Kartenzahlung;-4,50;EUR
03.08.2026;03.08.2026;VERKEHRSBETRIEBE LINDENAU;Monatskarte;-58,00;EUR
01.08.2026;03.08.2026;BÄCKEREI KRUME;Kartenzahlung;-7,80;EUR Stichwort;Kategorie
gehalt;Einkommen
bargeld;Bargeld
hausverwaltung;Wohnen
stadtwerke;Wohnen
versicherung;Versicherungen
supermarkt;Lebensmittel
bäckerei;Lebensmittel
drogerie;Haushalt
apotheke;Gesundheit
verkehrsbetriebe;Mobilität
bahnhof;Mobilität
kino;Freizeit
buchhandlung;Freizeit
fitness;Freizeit import csv
from dataclasses import dataclass
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from pathlib import Path
@dataclass
class Buchung:
datum: date
empfaenger: str
zweck: str
betrag: Decimal
kategorie: str = "Sonstiges"
def zahl_aus_text(text):
"""Macht aus einem deutschen Betrag wie "-1.234,56" ein Decimal."""
try:
return Decimal(text.replace(".", "").replace(",", "."))
except InvalidOperation:
raise ValueError(f"Kein Betrag: {repr(text)}")
def datum_aus_text(text):
"""Macht aus einem deutschen Datum wie "03.09.2026" ein date."""
return datetime.strptime(text, "%d.%m.%Y").date()
def lies_kontoauszug(pfad):
"""Liest den CSV-Export der Bank und gibt (buchungen, kodierung) zurück.
Die Datei ist in UTF-8 geschrieben oder in der alten Windows-Kodierung
cp1252, und kodierung ist dann "utf-8" oder "cp1252". Über der Zeile mit
den Spaltennamen, die mit Buchungstag anfängt, dürfen Zeilen ohne
Buchung stehen. Die Buchungen kommen in der Reihenfolge der Datei zurück.
"""
try:
text = Path(pfad).read_text(encoding="utf-8-sig")
kodierung = "utf-8"
except UnicodeDecodeError:
text = Path(pfad).read_text(encoding="cp1252")
kodierung = "cp1252"
zeilen = text.splitlines()
while not zeilen[0].startswith("Buchungstag"):
zeilen.pop(0)
buchungen = []
for zeile in csv.DictReader(zeilen, delimiter=";"):
buchungen.append(Buchung(
datum_aus_text(zeile["Buchungstag"]),
zeile["Empfänger"],
zeile["Verwendungszweck"],
zahl_aus_text(zeile["Betrag"]),
))
return buchungen, kodierung import csv
def lies_regeln(pfad):
"""Liest die Regeln aus einer CSV-Datei mit den Spalten Stichwort und Kategorie.
Gibt ein Dictionary zurück, Stichwort zu Kategorie, in der Reihenfolge der
Datei.
"""
regeln = {}
with open(pfad, encoding="utf-8-sig", newline="") as datei:
for zeile in csv.DictReader(datei, delimiter=";"):
if zeile["Stichwort"]:
regeln[zeile["Stichwort"]] = zeile["Kategorie"]
return regeln
def kategorisiere(buchung, regeln):
"""Gibt die Kategorie der Buchung zurück.
Es gilt die erste Regel, deren Stichwort im Empfänger oder im
Verwendungszweck steht, Groß und klein egal. Passt keine, ist es Sonstiges.
"""
text = (buchung.empfaenger + " " + buchung.zweck).lower()
for stichwort, kategorie in regeln.items():
if stichwort.lower() in text:
return kategorie
return "Sonstiges" from tabulate import tabulate
def euro(betrag):
"""Schreibt einen Betrag deutsch, mit Tausenderpunkt: 1.234,50 €."""
text = f"{betrag:,.2f}"
return text.replace(",", "_").replace(".", ",").replace("_", ".") + " €"
def excel_zahl(betrag):
"""Schreibt einen Betrag so, wie eine deutsche Tabelle ihn liest: 1234,50."""
return f"{betrag:.2f}".replace(".", ",")
def uebersicht(buchungen, monat):
"""Gibt die Übersicht über einen Monat als Text zurück, monat wie "2026-09"."""
im_monat = [buchung for buchung in buchungen if buchung.datum.strftime("%Y-%m") == monat]
if not im_monat:
return f"Übersicht {monat}: keine Buchungen"
ausgaben = {}
anzahl = {}
einnahmen = 0
for buchung in im_monat:
if buchung.betrag < 0:
ausgaben[buchung.kategorie] = ausgaben.get(buchung.kategorie, 0) - buchung.betrag
anzahl[buchung.kategorie] = anzahl.get(buchung.kategorie, 0) + 1
else:
einnahmen += buchung.betrag
zeilen = []
for kategorie, betrag in sorted(ausgaben.items(), key=lambda paar: paar[1], reverse=True):
zeilen.append([kategorie, anzahl[kategorie], euro(betrag)])
tabelle = tabulate(
zeilen,
headers=["Kategorie", "Buchungen", "Ausgaben"],
tablefmt="rounded_outline",
colalign=("left", "right", "right"),
)
gesamt = sum(ausgaben.values())
return "\n".join([
f"Übersicht {monat}: {len(im_monat)} Buchungen",
tabelle,
f"Einnahmen: {euro(einnahmen)}",
f"Ausgaben: {euro(gesamt)}",
f"Bleibt: {euro(einnahmen - gesamt)}",
]) from datetime import date
from decimal import Decimal
import pytest
from bericht import uebersicht
from einlesen import Buchung, lies_kontoauszug
from kategorien import kategorisiere
# Schreib hier Tests für die drei Teile des Werkzeugs, mindestens sechs:
# lies_kontoauszug() eine Datei in cp1252 mit Text und Kodierung, eine Zeile
# mit kaputtem Betrag
# kategorisiere() zwei passende Regeln, keine passende, Groß und klein
# uebersicht() ein Monat mit Einnahmen und Ausgaben
# Eine Datei für einen Test legst du in tmp_path an, wie im Artikel.
KOPF = "Buchungstag;Wertstellung;Empfänger;Verwendungszweck;Betrag;Währung\n"
REGELN = {"bargeld": "Bargeld", "supermarkt": "Lebensmittel", "kino": "Freizeit"}
def buchung(empfaenger, zweck="Kartenzahlung"):
return Buchung(date(2026, 10, 1), empfaenger, zweck, Decimal("-10.00"))
def test_cp1252(tmp_path):
datei = tmp_path / "auszug.csv"
datei.write_text(KOPF + "20.10.2026;20.10.2026;BÄCKEREI KRUME;Kartenzahlung;-6,20;EUR\n", encoding="cp1252")
buchungen, kodierung = lies_kontoauszug(datei)
assert kodierung == "cp1252"
assert buchungen[0].empfaenger == "BÄCKEREI KRUME"
def test_kaputter_betrag(tmp_path):
datei = tmp_path / "auszug.csv"
datei.write_text(KOPF + "20.10.2026;20.10.2026;BÄCKEREI KRUME;Kartenzahlung;6 Euro;EUR\n", encoding="utf-8")
with pytest.raises(ValueError):
lies_kontoauszug(datei)
def test_erste_passende_regel():
assert kategorisiere(buchung("SUPERMARKT SONNENBLICK", "Bargeldauszahlung"), REGELN) == "Bargeld"
def test_keine_passende_regel():
assert kategorisiere(buchung("KIOSK AM MARKT"), REGELN) == "Sonstiges"
def test_gross_und_klein():
assert kategorisiere(buchung("KINO LICHTSPIEL"), REGELN) == "Freizeit"
def test_einnahmen_extra():
buchungen = [
Buchung(date(2026, 10, 15), "HAUSVERWALTUNG AHORN", "Miete", Decimal("-1120.00"), "Wohnen"),
Buchung(date(2026, 10, 12), "HAUSVERWALTUNG AHORN", "Erstattung", Decimal("86.40"), "Wohnen"),
]
text = uebersicht(buchungen, "2026-10")
assert "Einnahmen: 86,40 €" in text
assert "Ausgaben: 1.120,00 €" in text import argparse
import csv
from bericht import excel_zahl, uebersicht
from einlesen import lies_kontoauszug
from kategorien import kategorisiere, lies_regeln
def schreibe_bericht(buchungen, monat):
"""Schreibt die Buchungen in bericht-JJJJ-MM.csv und gibt den Namen zurück."""
name = f"bericht-{monat}.csv"
with open(name, "w", encoding="utf-8-sig", newline="") as datei:
schreiber = csv.writer(datei, delimiter=";")
schreiber.writerow(["Datum", "Empfänger", "Verwendungszweck", "Kategorie", "Betrag"])
for buchung in buchungen:
schreiber.writerow([
buchung.datum.strftime("%d.%m.%Y"),
buchung.empfaenger,
buchung.zweck,
buchung.kategorie,
excel_zahl(buchung.betrag),
])
return name
def main():
parser = argparse.ArgumentParser(description="Wertet den CSV-Export eines Girokontos aus.")
parser.add_argument("datei", help="der Export der Bank, etwa kontoauszug.csv")
parser.add_argument("--monat", help="der Monat als JJJJ-MM, ohne Angabe der neueste")
args = parser.parse_args()
buchungen, kodierung = lies_kontoauszug(args.datei)
regeln = lies_regeln("regeln.csv")
for buchung in buchungen:
buchung.kategorie = kategorisiere(buchung, regeln)
monat = args.monat
if monat is None:
monat = max([buchung.datum for buchung in buchungen]).strftime("%Y-%m")
im_monat = [buchung for buchung in buchungen if buchung.datum.strftime("%Y-%m") == monat]
if not im_monat:
parser.error(f"im Monat {monat} gibt es keine Buchungen")
print(f"{args.datei}: {len(buchungen)} Buchungen, {kodierung}")
print(uebersicht(buchungen, monat))
print("Bericht:", schreibe_bericht(im_monat, monat))
if __name__ == "__main__":
main() Hier sind alle Teile auf einmal zu sehen, einlesen.py, kategorien.py, bericht.py und
main.py, dazu die Regeln in regeln.csv und die Tests. Gestartet wird es so, wie es auf jedem
Rechner gestartet würde, mit der Datei und dem Monat dahinter. In der Befehlsleiste stehen außerdem
der Aufruf ohne Monat, die Hilfe und die Tests.
Auf deinen Rechner
Wie Python auf den eigenen Rechner kommt, steht in Lektion 11.1, und wie eine eigene Umgebung
entsteht, in Abschnitt 13. Für das Werkzeug heißt das: ein Ordner mit den fünf Dateien und
regeln.csv, darin eine Umgebung mit python -m venv .venv, und eine requirements.txt mit zwei
Zeilen, tabulate und pytest, jeweils mit dem Bereich ihrer Hauptversion. Nach
pip install -r requirements.txt läuft python main.py kontoauszug.csv dort genauso wie hier.
Dann kommt der eigene Export dazu. Um die Kodierung kümmert sich lies_kontoauszug, um die Zeilen
über den Spaltennamen auch. Was sich von Bank zu Bank wirklich unterscheidet, sind die Namen der
Spalten. Und manchmal setzt eine Bank jedes Feld in Anführungszeichen.
"Buchungsdatum";"Valuta";"Name";"Verwendungszweck";"Betrag (EUR)"
"30.10.2026";"30.10.2026";"STADTWERKE LINDENAU";"Abschlag Strom 10/2026";"-91,00"
"28.10.2026";"28.10.2026";"DRUCKEREI KRANICH GMBH";"Gehalt Oktober";"2.580,00"
"27.10.2026";"27.10.2026";"BUCHHANDLUNG SEITENWEISE";"Kartenzahlung";"-31,90" import csv
from dataclasses import dataclass
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from pathlib import Path
@dataclass
class Buchung:
datum: date
empfaenger: str
zweck: str
betrag: Decimal
kategorie: str = "Sonstiges"
def zahl_aus_text(text):
"""Macht aus einem deutschen Betrag wie "-1.234,56" ein Decimal."""
try:
return Decimal(text.replace(".", "").replace(",", "."))
except InvalidOperation:
raise ValueError(f"Kein Betrag: {repr(text)}")
def datum_aus_text(text):
"""Macht aus einem deutschen Datum wie "03.09.2026" ein date."""
return datetime.strptime(text, "%d.%m.%Y").date()
def lies_kontoauszug(pfad):
"""Liest den CSV-Export der Bank und gibt (buchungen, kodierung) zurück.
Die Datei ist in UTF-8 geschrieben oder in der alten Windows-Kodierung
cp1252, und kodierung ist dann "utf-8" oder "cp1252". Über der Zeile mit
den Spaltennamen, die mit Buchungstag anfängt, dürfen Zeilen ohne
Buchung stehen. Die Buchungen kommen in der Reihenfolge der Datei zurück.
"""
try:
text = Path(pfad).read_text(encoding="utf-8-sig")
kodierung = "utf-8"
except UnicodeDecodeError:
text = Path(pfad).read_text(encoding="cp1252")
kodierung = "cp1252"
zeilen = text.splitlines()
while not zeilen[0].startswith("Buchungstag"):
zeilen.pop(0)
buchungen = []
for zeile in csv.DictReader(zeilen, delimiter=";"):
buchungen.append(Buchung(
datum_aus_text(zeile["Buchungstag"]),
zeile["Empfänger"],
zeile["Verwendungszweck"],
zahl_aus_text(zeile["Betrag"]),
))
return buchungen, kodierung import csv
from einlesen import Buchung, datum_aus_text, zahl_aus_text
# Welche Spalte dieser Bank welches Feld der Buchung füllt. Für die nächste
# Bank ändert sich nur dieses Dictionary.
SPALTEN = {
"datum": "Buchungsdatum",
"empfaenger": "Name",
"zweck": "Verwendungszweck",
"betrag": "Betrag (EUR)",
}
with open("andere-bank.csv", encoding="utf-8-sig", newline="") as datei:
for zeile in csv.DictReader(datei, delimiter=";"):
buchung = Buchung(
datum_aus_text(zeile[SPALTEN["datum"]]),
zeile[SPALTEN["empfaenger"]],
zeile[SPALTEN["zweck"]],
zahl_aus_text(zeile[SPALTEN["betrag"]]),
)
print(buchung) Hier heißt das Datum Buchungsdatum, der Empfänger Name und der Betrag Betrag (EUR), und um
jedes Feld stehen Anführungszeichen. Die Anführungszeichen nimmt csv von selbst weg. Für die Namen
reicht ein Dictionary, in dem steht, welche Spalte welches Feld füllt.
Wenn so etwas oben in einlesen.py steht, ist der Wechsel zu einer anderen Bank eine Änderung an
einer einzigen Stelle. Ob danach alles andere noch stimmt, siehst du an den Tests.
Dazu kommt die Zeile in lies_kontoauszug, an der die Zeile mit den Spaltennamen erkannt wird. Bei
dieser Bank fängt sie nicht mit Buchungstag an, sondern mit "Buchungsdatum", samt
Anführungszeichen. Beim Suchen dieser Zeile liest nämlich noch kein csv mit.
Und dann die Regeln. Beim ersten Lauf mit dem eigenen Export landet viel unter Sonstiges, und genau
da stehen die Regeln, die fehlen. Nach zwei, drei Monaten sind es nur noch die Ausreißer, und eine
neue Regel ist eine neue Zeile in regeln.csv.
Drei Richtungen
Daten
Wenn dir die Monatsübersicht Spaß gemacht hat, ist pandas das nächste Paket. Damit lassen sich Tabellen wie diese mit ein paar Zeilen gruppieren, verbinden und als Diagramm zeichnen. Dazu passt Jupyter, eine Umgebung, in der Code, Tabelle und Diagramm untereinander auf einer Seite stehen. Beides baut auf dem auf, was hier vorkam: Listen, Dictionaries, CSV, Datum und Geld.
Das Web
In Abschnitt 18 ging es darum, wie aus Python eine Seite im Browser wird. Mit Flask kommt man weit, auch mit Anmeldung und Datenbank. Django bringt für größere Anwendungen das meiste schon mit, von der Verwaltungsoberfläche bis zur Anmeldung, und FastAPI ist für Schnittstellen wie die des Haushaltsbuchs gemacht. Wer Seiten baut, braucht dazu HTML und CSS, und für beides gibt es auf dieser Lernplattform einen eigenen Kurs.
Automatisierung
Viele der nützlichsten Skripte sieht man nie. Sie laufen jeden Morgen von selbst, holen etwas ab,
räumen etwas auf und legen einen Bericht hin. Wie man ein Skript zu festen Zeiten startet, mit der
Aufgabenplanung von Windows, mit cron oder launchd, steht in Lektion 11.1. Das Werkzeug hier
braucht dafür nur noch den Export. Den holt bisher ein Mensch.
Was du dir als Nächstes bauen könntest
Umsätze Girokonto;DE12 3456 7890 1234 5678 90
Zeitraum;01.08.2026 bis 31.10.2026
Kontostand am 31.10.2026;3.214,55 EUR
Buchungstag;Wertstellung;Empfänger;Verwendungszweck;Betrag;Währung
30.10.2026;30.10.2026;STADTWERKE LINDENAU;Abschlag Strom 10/2026;-91,00;EUR
28.10.2026;28.10.2026;DRUCKEREI KRANICH GMBH;Gehalt Oktober;2.580,00;EUR
27.10.2026;27.10.2026;BUCHHANDLUNG SEITENWEISE;Kartenzahlung;-31,90;EUR
24.10.2026;26.10.2026;SUPERMARKT SONNENBLICK FIL. 12;Kartenzahlung;-71,36;EUR
20.10.2026;20.10.2026;BÄCKEREI KRUME;Kartenzahlung;-6,20;EUR
15.10.2026;15.10.2026;HAUSVERWALTUNG AHORN;Miete November;-1.120,00;EUR
12.10.2026;12.10.2026;HAUSVERWALTUNG AHORN;Erstattung Nebenkosten 2025;86,40;EUR
06.10.2026;06.10.2026;APOTHEKE AM MARKT;Kartenzahlung;-18,95;EUR
05.10.2026;05.10.2026;FITNESSSTUDIO KRAFTWERK;Beitrag Oktober;-39,00;EUR
02.10.2026;02.10.2026;SUPERMARKT SONNENBLICK FIL. 12;Bargeldauszahlung;-50,00;EUR
01.10.2026;01.10.2026;VERKEHRSBETRIEBE LINDENAU;Monatskarte;-58,00;EUR
30.09.2026;30.09.2026;STADTWERKE LINDENAU;Abschlag Strom 09/2026;-84,00;EUR
29.09.2026;29.09.2026;SUPERMARKT SONNENBLICK FIL. 12;Kartenzahlung;-63,47;EUR
28.09.2026;28.09.2026;DRUCKEREI KRANICH GMBH;Gehalt September;2.580,00;EUR
22.09.2026;22.09.2026;DROGERIE IM BAHNHOF;Kartenzahlung;-12,95;EUR
15.09.2026;15.09.2026;HAUSVERWALTUNG AHORN;Miete Oktober;-1.120,00;EUR
12.09.2026;14.09.2026;KINO LICHTSPIEL;Kartenzahlung;-24,00;EUR
10.09.2026;10.09.2026;BÄCKEREI KRUME;Kartenzahlung;-8,40;EUR
03.09.2026;03.09.2026;OLE BERGER;Anteil Konzertkarten;45,00;EUR
02.09.2026;02.09.2026;SUPERMARKT SONNENBLICK FIL. 12;Kartenzahlung;-52,18;EUR
01.09.2026;01.09.2026;VERKEHRSBETRIEBE LINDENAU;Monatskarte;-58,00;EUR
31.08.2026;31.08.2026;STADTWERKE LINDENAU;Abschlag Strom 08/2026;-84,00;EUR
28.08.2026;28.08.2026;DRUCKEREI KRANICH GMBH;Gehalt August;2.580,00;EUR
25.08.2026;25.08.2026;SUPERMARKT SONNENBLICK FIL. 12;Kartenzahlung;-58,12;EUR
19.08.2026;19.08.2026;BUCHHANDLUNG SEITENWEISE;Geschenkgutschein 25 €;-25,00;EUR
14.08.2026;14.08.2026;HAUSVERWALTUNG AHORN;Miete September;-1.120,00;EUR
10.08.2026;10.08.2026;KIOSK AM MARKT;Kartenzahlung;-4,50;EUR
03.08.2026;03.08.2026;VERKEHRSBETRIEBE LINDENAU;Monatskarte;-58,00;EUR
01.08.2026;03.08.2026;BÄCKEREI KRUME;Kartenzahlung;-7,80;EUR Stichwort;Kategorie
gehalt;Einkommen
bargeld;Bargeld
hausverwaltung;Wohnen
stadtwerke;Wohnen
versicherung;Versicherungen
supermarkt;Lebensmittel
bäckerei;Lebensmittel
drogerie;Haushalt
apotheke;Gesundheit
verkehrsbetriebe;Mobilität
bahnhof;Mobilität
kino;Freizeit
buchhandlung;Freizeit
fitness;Freizeit import csv
from dataclasses import dataclass
from datetime import date, datetime
from decimal import Decimal, InvalidOperation
from pathlib import Path
@dataclass
class Buchung:
datum: date
empfaenger: str
zweck: str
betrag: Decimal
kategorie: str = "Sonstiges"
def zahl_aus_text(text):
"""Macht aus einem deutschen Betrag wie "-1.234,56" ein Decimal."""
try:
return Decimal(text.replace(".", "").replace(",", "."))
except InvalidOperation:
raise ValueError(f"Kein Betrag: {repr(text)}")
def datum_aus_text(text):
"""Macht aus einem deutschen Datum wie "03.09.2026" ein date."""
return datetime.strptime(text, "%d.%m.%Y").date()
def lies_kontoauszug(pfad):
"""Liest den CSV-Export der Bank und gibt (buchungen, kodierung) zurück.
Die Datei ist in UTF-8 geschrieben oder in der alten Windows-Kodierung
cp1252, und kodierung ist dann "utf-8" oder "cp1252". Über der Zeile mit
den Spaltennamen, die mit Buchungstag anfängt, dürfen Zeilen ohne
Buchung stehen. Die Buchungen kommen in der Reihenfolge der Datei zurück.
"""
try:
text = Path(pfad).read_text(encoding="utf-8-sig")
kodierung = "utf-8"
except UnicodeDecodeError:
text = Path(pfad).read_text(encoding="cp1252")
kodierung = "cp1252"
zeilen = text.splitlines()
while not zeilen[0].startswith("Buchungstag"):
zeilen.pop(0)
buchungen = []
for zeile in csv.DictReader(zeilen, delimiter=";"):
buchungen.append(Buchung(
datum_aus_text(zeile["Buchungstag"]),
zeile["Empfänger"],
zeile["Verwendungszweck"],
zahl_aus_text(zeile["Betrag"]),
))
return buchungen, kodierung import csv
def lies_regeln(pfad):
"""Liest die Regeln aus einer CSV-Datei mit den Spalten Stichwort und Kategorie.
Gibt ein Dictionary zurück, Stichwort zu Kategorie, in der Reihenfolge der
Datei.
"""
regeln = {}
with open(pfad, encoding="utf-8-sig", newline="") as datei:
for zeile in csv.DictReader(datei, delimiter=";"):
if zeile["Stichwort"]:
regeln[zeile["Stichwort"]] = zeile["Kategorie"]
return regeln
def kategorisiere(buchung, regeln):
"""Gibt die Kategorie der Buchung zurück.
Es gilt die erste Regel, deren Stichwort im Empfänger oder im
Verwendungszweck steht, Groß und klein egal. Passt keine, ist es Sonstiges.
"""
text = (buchung.empfaenger + " " + buchung.zweck).lower()
for stichwort, kategorie in regeln.items():
if stichwort.lower() in text:
return kategorie
return "Sonstiges" from tabulate import tabulate
def euro(betrag):
"""Schreibt einen Betrag deutsch, mit Tausenderpunkt: 1.234,50 €."""
text = f"{betrag:,.2f}"
return text.replace(",", "_").replace(".", ",").replace("_", ".") + " €"
def excel_zahl(betrag):
"""Schreibt einen Betrag so, wie eine deutsche Tabelle ihn liest: 1234,50."""
return f"{betrag:.2f}".replace(".", ",")
def uebersicht(buchungen, monat):
"""Gibt die Übersicht über einen Monat als Text zurück, monat wie "2026-09"."""
im_monat = [buchung for buchung in buchungen if buchung.datum.strftime("%Y-%m") == monat]
if not im_monat:
return f"Übersicht {monat}: keine Buchungen"
ausgaben = {}
anzahl = {}
einnahmen = 0
for buchung in im_monat:
if buchung.betrag < 0:
ausgaben[buchung.kategorie] = ausgaben.get(buchung.kategorie, 0) - buchung.betrag
anzahl[buchung.kategorie] = anzahl.get(buchung.kategorie, 0) + 1
else:
einnahmen += buchung.betrag
zeilen = []
for kategorie, betrag in sorted(ausgaben.items(), key=lambda paar: paar[1], reverse=True):
zeilen.append([kategorie, anzahl[kategorie], euro(betrag)])
tabelle = tabulate(
zeilen,
headers=["Kategorie", "Buchungen", "Ausgaben"],
tablefmt="rounded_outline",
colalign=("left", "right", "right"),
)
gesamt = sum(ausgaben.values())
return "\n".join([
f"Übersicht {monat}: {len(im_monat)} Buchungen",
tabelle,
f"Einnahmen: {euro(einnahmen)}",
f"Ausgaben: {euro(gesamt)}",
f"Bleibt: {euro(einnahmen - gesamt)}",
]) from tabulate import tabulate
from bericht import euro
from einlesen import lies_kontoauszug
from kategorien import kategorisiere, lies_regeln
buchungen, kodierung = lies_kontoauszug("kontoauszug.csv")
regeln = lies_regeln("regeln.csv")
# Die Ausgaben nach Kategorie und Monat, wie in Lektion 19.4.
summen = {}
for buchung in buchungen:
if buchung.betrag < 0:
schluessel = (kategorisiere(buchung, regeln), buchung.datum.strftime("%Y-%m"))
summen[schluessel] = summen.get(schluessel, 0) - buchung.betrag
kategorien = set()
monate = set()
for kategorie, monat in summen:
kategorien.add(kategorie)
monate.add(monat)
monate = sorted(monate)
zeilen = []
for kategorie in sorted(kategorien):
zeile = [kategorie]
for monat in monate:
zeile.append(euro(summen.get((kategorie, monat), 0)))
zeilen.append(zeile)
print(tabulate(zeilen, headers=["Kategorie"] + monate, tablefmt="rounded_outline", colalign=["left"] + ["right"] * len(monate))) Am meisten lernt man an einem Werkzeug für ein Problem, das man selbst hat. Ein paar Ideen, alle mit dem Stoff aus diesem Kurs machbar:
- Ein Monatsvergleich, wie im Beispiel: jede Kategorie über alle Monate nebeneinander, damit man sieht, wo es teurer wird.
- Daueraufträge finden, also Buchungen, die jeden Monat mit demselben Empfänger und demselben Betrag kommen und die man vielleicht gar nicht mehr braucht.
- Der Bericht als Excel-Mappe mit openpyxl aus Abschnitt 13, mit fetten Spaltennamen und Beträgen, mit denen Excel rechnen kann.
- Eine Warnung bei einer Ausgabe, die größer ist als alles, was in den drei Monaten davor in derselben Kategorie vorkam.
- Fotos nach dem Datum im Dateinamen in Monatsordner sortieren, oder die Rechnungen aus dem Download-Ordner jeden Freitag an ihren Platz räumen.
- Das Haushaltsbuch im Browser, aus Abschnitt 18, mit der Datenbank aus Abschnitt 16 darunter.
Ein Werkzeug muss dabei nicht fertig sein, um zu helfen. Die erste Fassung des Kontoauszugs aus Abschnitt 12 hatte keine Tests, keine Regeldatei und keinen Aufruf. Gerechnet hat sie trotzdem schon.
Zum Mitnehmen
Das Werkzeug läuft auf jedem Rechner mit Python, in einer eigenen Umgebung mit tabulate und pytest. Für den eigenen Export ändern sich vor allem die Spaltennamen, damit auch die Zeile, an der lies_kontoauszug die Tabelle erkennt, und dazu die Regeln. Weiter geht es danach in drei Richtungen, zu Daten mit pandas, ins Web mit Flask und mehr oder zu Skripten, die zu festen Zeiten von selbst laufen.
Jetzt du
Basis Konto, kostenlosIm Editor änderst du die Beispiele dieser Lektion und lässt sie gleich laufen. So merkst du am schnellsten, ob es sitzt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 3 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.