Abschnitt 19 · Lektion 3
Kategorisieren
Regeln als Dictionary im Code gehen gut, bis der erste neue Laden dazukommt. Und der kommt jeden Monat. Dann öffnet man den Code, sucht die Stelle, setzt ein Komma an die richtige Stelle und hofft, dass man nichts anderes erwischt hat. Für etwas, das so oft wechselt, ist das der falsche Ort.
In der ersten Fassung des Kontoauszugs standen die Regeln für die Kategorien aber genau da, in
main.py. Dasselbe Problem hat ein Postfach, in dem Mails nach Regeln in Ordner sortiert werden, so
wie es jedes Mailprogramm anbietet.
Regeln in einer Datei
Stichwort;Ordner
rechnung;Rechnungen
newsletter;Werbung
TSV;Verein
versandhaus;Einkäufe import csv
def lies_regeln(pfad):
"""Liest Stichwort und Ordner aus einer CSV-Datei, in ihrer Reihenfolge."""
regeln = {}
with open(pfad, encoding="utf-8-sig", newline="") as datei:
for zeile in csv.DictReader(datei, delimiter=";"):
regeln[zeile["Stichwort"]] = zeile["Ordner"]
return regeln
print(lies_regeln("ordner.csv")) Die Regeln stehen in ordner.csv, in jeder Zeile ein Stichwort und der Ordner, in den eine passende
Mail gehört. lies_regeln macht daraus ein Dictionary, wie in Lektion 12.1 mit csv.DictReader.
Ein Dictionary behält seine Reihenfolge, also stehen die Regeln darin genauso wie in der Datei.
Das bringt mehr, als man denkt. Eine neue Regel ist jetzt eine neue Zeile, und die kann jeder schreiben, der eine Tabelle bedienen kann, auch in Excel. Der Code muss dafür nicht angefasst werden, und was nicht angefasst wird, geht auch nicht kaputt.
utf-8-sig steht dabei, weil Excel beim Speichern als „CSV UTF-8“ ein BOM an den Anfang setzt. Dann
hieße die erste Spalte nicht mehr Stichwort.
Zwei Felder, eine Suche
Stichwort;Ordner
rechnung;Rechnungen
newsletter;Werbung
TSV;Verein
versandhaus;Einkäufe import csv
def lies_regeln(pfad):
"""Liest Stichwort und Ordner aus einer CSV-Datei, in ihrer Reihenfolge."""
regeln = {}
with open(pfad, encoding="utf-8-sig", newline="") as datei:
for zeile in csv.DictReader(datei, delimiter=";"):
regeln[zeile["Stichwort"]] = zeile["Ordner"]
return regeln
def ordner_fuer(absender, betreff, regeln):
"""Gibt den Ordner für eine Mail zurück: die erste passende Regel, sonst Posteingang."""
text = (absender + " " + betreff).lower()
for stichwort, ordner in regeln.items():
if stichwort.lower() in text:
return ordner
return "Posteingang" from postfach import lies_regeln, ordner_fuer
MAILS = [
("rechnung@stadtwerke-lindenau.example", "Ihre Abschlagsrechnung Oktober"),
("info@tsv-lindenau.example", "Einladung zur Jahreshauptversammlung"),
("newsletter@versandhaus-elster.example", "Nur heute: 20 Prozent auf alles"),
("service@versandhaus-elster.example", "Ihre Rechnung zu Bestellung 4711"),
("mia.berger@example.org", "Fotos vom Sommerfest"),
]
regeln = lies_regeln("ordner.csv")
for absender, betreff in MAILS:
print(f"{ordner_fuer(absender, betreff, regeln)}: {betreff}") Eine Mail hat einen Absender und einen Betreff, und das Stichwort kann in beiden stehen. Bei den
Stadtwerken steht rechnung im Absender und im Betreff, beim Versandhaus nur im Betreff.
ordner_fuer setzt deshalb beide zu einem Text zusammen, mit einem Leerzeichen dazwischen, und
schreibt ihn klein.
Das Leerzeichen hat einen Grund. Ohne klebten das Ende des einen Felds und der Anfang des anderen zu einem Wort zusammen, und darin könnte ein Stichwort stecken, das in keinem der beiden steht.
Klein geschrieben wird auf beiden Seiten. In ordner.csv steht TSV in Großbuchstaben, weil der
Verein sich so schreibt, und wer die Datei pflegt, denkt nicht an Python. Erst mit
stichwort.lower() wird daraus etwas, das in info@tsv-lindenau.example gefunden wird.
Was zu keiner Regel passt, bleibt im Posteingang. Und wie in der ersten Fassung gilt die erste passende Regel.
Welche Schleife außen steht
Stichwort;Ordner
rechnung;Rechnungen
newsletter;Werbung
TSV;Verein
versandhaus;Einkäufe import csv
def lies_regeln(pfad):
"""Liest Stichwort und Ordner aus einer CSV-Datei, in ihrer Reihenfolge."""
regeln = {}
with open(pfad, encoding="utf-8-sig", newline="") as datei:
for zeile in csv.DictReader(datei, delimiter=";"):
regeln[zeile["Stichwort"]] = zeile["Ordner"]
return regeln
def ordner_fuer(absender, betreff, regeln):
"""Gibt den Ordner für eine Mail zurück: erst im Absender gesucht, dann im Betreff."""
for feld in [absender, betreff]:
for stichwort, ordner in regeln.items():
if stichwort.lower() in feld.lower():
return ordner
return "Posteingang" from postfach import lies_regeln, ordner_fuer
MAILS = [
("rechnung@stadtwerke-lindenau.example", "Ihre Abschlagsrechnung Oktober"),
("info@tsv-lindenau.example", "Einladung zur Jahreshauptversammlung"),
("newsletter@versandhaus-elster.example", "Nur heute: 20 Prozent auf alles"),
("service@versandhaus-elster.example", "Ihre Rechnung zu Bestellung 4711"),
("mia.berger@example.org", "Fotos vom Sommerfest"),
]
regeln = lies_regeln("ordner.csv")
for absender, betreff in MAILS:
print(f"{ordner_fuer(absender, betreff, regeln)}: {betreff}") Diese Fassung sieht genauso vernünftig aus. Hier wird erst der Absender gegen alle Regeln geprüft,
dann der Betreff. Dabei landet die Rechnung vom Versandhaus aber unter Einkäufe. versandhaus steht
nämlich im Absender, und der kommt zuerst dran, obwohl rechnung in der Datei weiter oben steht.
Beides lässt sich vertreten, man muss sich nur entscheiden. Wer die Datei pflegt, ordnet die Regeln nach Wichtigkeit, die wichtigste oben, und erwartet, dass das auch so gilt. Mit dem zusammengesetzten Text stimmt das, dort zählt nur die Reihenfolge in der Datei. Bei zwei Schleifen zählt zusätzlich, in welchem Feld ein Stichwort steht, und das sieht man der Datei nicht an.
Eine Zeile ohne Stichwort
Stichwort;Ordner
rechnung;Rechnungen
;Werbung
TSV;Verein
versandhaus;Einkäufe import csv
def lies_regeln(pfad):
"""Liest Stichwort und Ordner aus einer CSV-Datei, in ihrer Reihenfolge."""
regeln = {}
with open(pfad, encoding="utf-8-sig", newline="") as datei:
for zeile in csv.DictReader(datei, delimiter=";"):
regeln[zeile["Stichwort"]] = zeile["Ordner"]
return regeln
def ordner_fuer(absender, betreff, regeln):
"""Gibt den Ordner für eine Mail zurück: die erste passende Regel, sonst Posteingang."""
text = (absender + " " + betreff).lower()
for stichwort, ordner in regeln.items():
if stichwort.lower() in text:
return ordner
return "Posteingang" from postfach import lies_regeln, ordner_fuer
MAILS = [
("rechnung@stadtwerke-lindenau.example", "Ihre Abschlagsrechnung Oktober"),
("info@tsv-lindenau.example", "Einladung zur Jahreshauptversammlung"),
("newsletter@versandhaus-elster.example", "Nur heute: 20 Prozent auf alles"),
("service@versandhaus-elster.example", "Ihre Rechnung zu Bestellung 4711"),
("mia.berger@example.org", "Fotos vom Sommerfest"),
]
regeln = lies_regeln("ordner.csv")
print(regeln)
for absender, betreff in MAILS:
print(f"{ordner_fuer(absender, betreff, regeln)}: {betreff}")
print("Steckt der leere Text in jedem Text?", "" in "fotos vom sommerfest") Auf diesen Fehler stößt man mit Regeln in einer Datei früher oder später. Jemand löscht in der
Tabelle ein Stichwort, weil die Regel nicht mehr gebraucht wird, und lässt den Ordner daneben
stehen. In der Datei steht dann ;Werbung.
lies_regeln macht daraus eine Regel mit dem leeren Text als Stichwort, und der leere Text steckt
in jedem Text. Das sieht man an der letzten Zeile. Ab dieser Regel landet alles in der Werbung, auch
die Fotos vom Sommerfest. Kein Fehler, keine Warnung, nur ein Postfach, das plötzlich sehr
aufgeräumt aussieht.
Abhilfe gibt es beim Lesen. Eine Zeile, in deren Spalte Stichwort nichts steht, kommt gar nicht
erst ins Dictionary. Ein leerer Text ist falsch (Lektion 3.5), also reicht ein if vor der
Zuweisung.
Was die Aufgabe will
In kategorien.py kommen dieselben zwei Teile für den Kontoauszug. Die Regeln stehen in
regeln.csv, eine Buchung hat Empfänger und Verwendungszweck, und was zu keiner Regel passt, ist
Sonstiges.
Zum Mitnehmen
Was man öfter ändert als den Code, kommt in eine Datei, die auch jemand ohne Python bearbeiten kann. Gesucht wird in allen Feldern auf einmal, auf beiden Seiten klein geschrieben, und es gilt die erste passende Regel in der Datei. Eine Zeile ohne Stichwort würde alles abfangen, was keine Regel davor trifft, also wird sie beim Lesen übersprungen.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
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 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.