Zeichensatzkonvertierung für Windows-Dateinamen

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Klassensprecher
User
Beiträge: 2
Registriert: Donnerstag 10. November 2022, 01:38

Moin, zusammen.

Seit einigen Tagen mache ich meine ersten Schritte in der Python-Programmierung. Ich habe jetzt für mich ein Programmierprojekt definiert, das folgendes leisten soll:

1. Unter Windows ein oder mehrere aus Thunderbird in ein Verzeichnis exportierte Emails (*.eml-Datei(en)) in ein bestimmtes Dateinamen-Schema umzuwandeln
2. Diese Datei(en) anschließend in eine PDF-Datei umzuwandeln.

Derzeit experimentiere ich noch mit Punkt 1.

Das Dateinamen-Schema soll wie folgt ausschauen: YYYY-MM-DD hhmmss [Absender] Subject

Dabei soll 'YYYY-MM-DD hhmmss' Versanddatum und -uhrzeit der Email sein. Also z.B. '2023-11-01 122304 [Hans Müller] Betreff dieser Email.eml.

Der Absender in [] enstammt dem "Sender"-Feld des Mail-Headers. ("Hans.Müller@mailserver.de" <hans.mueller@mailserver.de>)

Je nachdem ob die Sender-Angabe einen Mail-Tag hat oder nicht, wäre dies in diesem Beispiel "Hans Müller" oder "hans mueller".

Fast funktioniert das Ganze schon so wie gewünscht. Woran ich jedoch scheitere, ist die erforderlche Zeichenkonvertierung für den Dateinamen. Dies immer dann, wenn der Tag diakritische Zeichen enthält. Vielleich kann jemand, der bis hierher mitglesen hat, mir einen hilfreichen Hinweis geben.

Dafür schon einmal im Voraus vielen Dank.

Viele Grüße

Code: Alles auswählen

import os
import re
import email
import unicodedata
import shutil
from tkinter import filedialog
import tkinter as tk
from datetime import datetime
from urllib.parse import quote

# Funktion zum Extrahieren des Datums, der Uhrzeit, des Mail-Tags und des Betreffs aus der E-Mail
def extract_email_info(eml_file):
    with open(eml_file, 'r', encoding='cp1252') as file:
        msg = email.message_from_file(file)
        date_str = msg['Date']
        subject = msg['Subject']
        from_field = msg['From']
        
    # with open(eml_file, 'r', encoding='utf-8') as file:    

    # Das Datum und die Uhrzeit aus dem E-Mail-Header extrahieren
    email_date = datetime.strptime(date_str, "%a, %d %b %Y %H:%M:%S %z")
    date_formatted = email_date.strftime("%Y-%m-%d %H%M%S")

    # Den Mail-Tag aus dem 'From:'-Feld extrahieren
    mail_tag = from_field.split('<')[0].strip()
    
    # Diakritische Zeichen normalisieren
    mail_tag = unicodedata.normalize('NFKD', mail_tag).encode('ascii', 'ignore').decode('utf-8')

    # Das Prefix wie 'Re: ' oder 'Aw: ' aus dem Betreff entfernen
    subject = re.sub(r'^re:', "", subject, count = 1, flags = re.I).strip()
    subject = re.sub(r'^aw:', "", subject, count = 1, flags = re.I).strip()
    
   
    # Diakritische Zeichen im Betreff normalisieren
    subject = unicodedata.normalize('NFKD', subject).encode('ascii', 'ignore').decode('utf-8')
    
    # Die Anführungszeichen um den Mail-Tag und entfernen
    
    mail_tag = mail_tag.strip('\"').split('@')[0].replace(".", " ")
    
    # Wenn ein @-Zeichen im Mail-Tag ist, nur den Teil vor dem @-Zeichen verwenden
    # if '@' in mail_tag:
    #    mail_tag = mail_tag.split('@')[0]

    return date_formatted, mail_tag, subject

# Funktion zum Konvertieren der Dateinamen
def rename_eml_files(eml_files):
    for eml_file in eml_files:
        date_formatted, mail_tag, subject = extract_email_info(eml_file)
        _, extension = os.path.splitext(eml_file)
        
       
        subject = subject.replace(':', '').lstrip()  # Doppelpunkte entfernen

        # Das aktuelle Verzeichnis (Verzeichnis der Ausgangsdatei) ermitteln
        current_directory = os.path.dirname(eml_file)
        
        # Den neuen Dateipfad erstellen (mit os.path.join() für Windows-Kompatibilität)
        new_filename = os.path.join(current_directory, f"{date_formatted} [{mail_tag}] {quote(subject.strip())}{extension}")        

        new_filename = new_filename.replace('"', '')  # Anführungszeichen entfernen
        new_filename = new_filename.replace(' .eml', '.eml')  # Leerzeichen vor .eml entfernen
        # new_filename = new_filename.replace('  ', ' ')  # Doppelte Leerzeichen auf ein Leerzeichen reduzieren
        # new_filename = new_filename.replace(':', '')  # Doppelpunkte entfernen

        try:
            os.rename(eml_file, new_filename)
            print(f"Umbenannt: {eml_file} -> {new_filename}")
        except Exception as e:
            print(f"Fehler beim Umbenennen von {eml_file}: {str(e)}")

# GUI zum Auswählen der Dateien
def select_files():
    root = tk.Tk()
    root.withdraw()  # Das Hauptfenster ausblenden

    # Dateiauswahldialog anzeigen und Pfad in Windows-Format konvertieren
    file_paths = filedialog.askopenfilenames(
        title="E-Mails auswählen",
        filetypes=[("EML-Dateien", "*.eml")]
    )

    if file_paths:
        # Konvertiere Pfad zu Windows-Stil
        file_paths = [os.path.normpath(path) for path in file_paths]
        rename_eml_files(file_paths)
    else:
        print("Keine Dateien ausgewählt.")

if __name__ == "__main__":
    select_files()
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Klassensprecher: Was genau sind denn die Probleme?

Anmerkungen zum Quelltext:

Die Kommentare über den Funktionen sollten besser DocStrings sein. Sofern der Kommentar etwas enthält was der Funktionsname nicht schon aussagt.

Wenn etwas `file` heisst, dann erwartet der Leser, dass das Objekt Methoden wie `read()` oder `write()` und `close()` hat. Keine Zeichenkette.

`normpath()` konvertiert nicht zu ”Windows-Stil” und ist hier auch gar nicht nötig. Was eher Sinn machen würde ist das die Zeichenketten in `pathlib.Path`-Objekte zu verpacken, damit man nicht mit die alten `os.path`-Funktion verwenden muss.

Beim Einlesen einer E-Mail-Datei einfach von CP1252 als Kodierung auszugehen ist, äh, mutig. Es gibt eine `message_from_binary_file()`-Funktion.

Die Funktion sollte den Zeitstempel nicht als Zeichenkette liefern sondern als `datetime`-Objekt.

Der Code geht nicht so sinnvoll mit dem Fall um, dass der Absender nur eine E-Mail-Adresse in spitzen Klammern enthält:

Code: Alles auswählen

In [578]: "<foo@bar>".split('<')[0].strip()
Out[578]: ''
Das ``.encode('ascii', 'ignore').decode('utf-8')`` um nicht-ASCII-Zeichen raus zu filtern finde ich ein bisschen zu kryptisch formuliert. Das würde ich expliziter ausdrücken. Und da es zweimal vorkommt, in eine Funktion auslagern.

Die Verarbeitung von `mail_tag` wird durch Code unterbrochen der den Betreff bearbeitet. Das ist unnötig unübersichtlich.

Beim `re.sub()` ist `count` überflüssig. Das Muster ist am Textanfang veranker, das *kann* nur einmal vorkommen. Das sollte man auch in *einem* regulärem Ausdruck zusammenfassen können.

Ein Leerzeichen vor ``.eml`` kann es eigentlich nicht geben so oft wie da vorher `strip()` auf dem Betreff aufgerufen wurde und Anführungszeichen auch nicht, da sorgt das `quote() für.

`str()` ist überflüssig wenn man den Wert in eine Zeichenkette formatiert.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import email
import re
import tkinter as tk
import unicodedata
from datetime import datetime
from pathlib import Path
from tkinter import filedialog
from urllib.parse import quote


def filter_non_ascii(text):
    return "".join(
        character
        for character in unicodedata.normalize("NFKD", text)
        if character.isascii()
    )


def extract_email_info(file_path):
    """
    Zeitstempel, Empfänger, und Betreff aus der E-Mail auslesen.
    """
    with file_path.open("rb") as file:
        message = email.message_from_binary_file(file)

    return (
        datetime.strptime(message["Date"], "%a, %d %b %Y %H:%M:%S %z"),
        filter_non_ascii(
            message["From"]
            .split("<")[0]
            .strip('"')
            .split("@")[0]
            .replace(".", " ")
        ),
        re.sub(
            r"((re|aw):\s*)*",
            "",
            filter_non_ascii(message["Subject"]),
            flags=re.IGNORECASE,
        ).strip(),
    )


def rename_eml_files(file_paths):
    for file_path in file_paths:
        timestamp, sender, subject = extract_email_info(file_path)
        new_file_path = file_path.with_name(
            f"{timestamp:%Y-%m-%d %H%M%S}"
            f" [{quote(sender)}]"
            f" {quote(subject)}.{file_path.suffix}"
        )
        try:
            file_path.rename(new_file_path)
            print(f"Umbenannt: {file_path} -> {new_file_path}")
        except Exception as error:
            print(f"Fehler beim Umbenennen von {file_path}: {error}")


def main():
    root = tk.Tk()
    root.withdraw()

    file_paths = filedialog.askopenfilenames(
        title="E-Mails auswählen", filetypes=[("EML-Dateien", "*.eml")]
    )
    if file_paths:
        rename_eml_files(map(Path, file_paths))
    else:
        print("Keine Dateien ausgewählt.")


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Klassensprecher
User
Beiträge: 2
Registriert: Donnerstag 10. November 2022, 01:38

@__blackjack__

Moin!

Vielen Dank für deine ausführliche Befassung mit meiner Frage.

Um auf deine Rückfrage zu antworten (Was genau sind denn die Probleme?):

Sowohl meine Programmversion wie auch die von dir überarbeitete produzieren
fehlerhafte Dateinamen. Z.B. wird aus dem Subject

[Neuer Eintrag] Widerstandskämpfer aus dem Orchestergraben


dieser Dateiname:

2023-11-16 100447 [Blog%20jj%22%20] %3D%3FUTF-8%3FB%3FW05ldWVyIEVpbnRyYWddIFdpZGVyc3RhbmRza8OkbXBmZXIgYXVzIGRlbSBPcmNoZXN0ZXJncmFiZW4%3D%3F%3D..eml


Also nicht ganz das, was ich mir vorstelle. ;-)

Vielleicht hättest du hierfür auch eine Lösung parat?

Danke für deinen Hinweis zur Kommentierung des Programmcodes in Python. Anfänglich war mir nicht
klar geworden, worin der wesentliche Unterschied zwischen '#' und docstring() bestehen soll. Diese
Fundstelle (https://hellocoding.de/blog/coding-lang ... docstrings) hat dann meinen
Blick etwas erweitert.

Viele Grüße
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Klassensprecher: Schau Dir mal den Betreff an. Da musst Du Dir eine kleine Funktion basteln, die daraus erst einmal einen lesbaren macht. Die Funktion `email.header.decode_header()` wird da eine Rolle spielen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13122
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich habe mir das noch mal angeschaut und festgestellt, dass beim Zeitstempel die Zeitzoneninformation einfach ignoriert wird. Das heisst im Dateinamen landet sehr wahrscheinlich immer die Zeit in der Zeitzone des Absenders. Das würde ich mindestens mal in die lokale Zeitzone umrechnen.

Für das parsen von der Adressinformation gibt es in `email.util` schon eine Funktion, die man nutzen könnte. Und auch die Absenderinformation muss man dekodieren, falls da mal ein Müller oder so vorkommt.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import email
import re
import tkinter as tk
import unicodedata
from email.header import decode_header
from email.utils import parseaddr, parsedate_to_datetime
from pathlib import Path
from tkinter import filedialog
from urllib.parse import quote


def filter_non_ascii(text):
    return "".join(
        character
        for character in unicodedata.normalize("NFKD", text)
        if character.isascii()
    )


def get_decoded_header(message, header_name):
    return "".join(
        part.decode(encoding or "ascii")
        for part, encoding in decode_header(message[header_name])
    )


def get_sender_name(message):
    name, address = parseaddr(get_decoded_header(message, "From"))
    return (
        name if name else address.split("@", maxsplit=1)[0].replace(".", " ")
    )


def get_subject(message):
    return re.sub(
        r"((re|aw):\s*)*",
        "",
        filter_non_ascii(get_decoded_header(message, "Subject")),
        flags=re.IGNORECASE,
    ).strip()


def extract_email_info(file_path):
    """
    Zeitstempel, Empfänger, und Betreff aus der E-Mail auslesen.
    """
    with file_path.open("rb") as file:
        message = email.message_from_binary_file(file)

    return (
        parsedate_to_datetime(message["Date"]).astimezone(),
        get_sender_name(message),
        get_subject(message),
    )


def rename_eml_files(file_paths):
    for file_path in file_paths:
        timestamp, sender, subject = extract_email_info(file_path)
        new_file_path = file_path.with_name(
            f"{timestamp:%Y-%m-%d %H%M%S}"
            f" [{quote(sender)}]"
            f" {quote(subject)}.{file_path.suffix}"
        )
        try:
            file_path.rename(new_file_path)
            print(f"Umbenannt: {file_path} -> {new_file_path}")
        except Exception as error:
            print(f"Fehler beim Umbenennen von {file_path}: {error}")


def main():
    root = tk.Tk()
    root.withdraw()

    file_paths = filedialog.askopenfilenames(
        title="E-Mails auswählen", filetypes=[("EML-Dateien", "*.eml")]
    )
    if file_paths:
        rename_eml_files(map(Path, file_paths))
    else:
        print("Keine Dateien ausgewählt.")


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten