fbpx

So EINFACH! Automatischer E-Mail Report für dein Skript!

Inhalt
    Add a header to begin generating the table of contents

    Du hast keine Zeit oder keine Lust auf das Ergebnis deines Skripts zu warten?
    Vielleicht muss das Ergebnis auch per E-Mail an verschiedene Stellen gesendet werden?
    Egal warum, automatisiere die Arbeit und lass Python einen automatischen E-Mail Report für dein Skript versenden!

    Unter meinem Video zum Vergleichen von Excel Dateien kam die folgende Frage:

    YouTube Kommentar 1

    Natürlich kann man!
    Und hier zeige ich dir, wie.

    Hintergrund

    Vor kurzem habe ich zwei Artikel veröffentlicht, auf denen ich hier aufbauen möchte.

    Ich werde hier nicht mehr auf die Details eingehen, sondern nur noch zeigen, wie du die beiden Funktionen zusammenführen kannst, um dir einen automatischen E-Mail Report zu senden.

    Code übernehmen und anpassen (Vorbereitung)

    Als Erstes nehmen wir uns einfach den Code für den Excel Vergleich.
    Der hat sich nicht geändert. Ich verwende auch dieselben Dateien.
    Natürlich bekommst du alles hier zusammen in einem Download!

    import openpyxl
    from openpyxl.styles import PatternFill
    
    old_file = openpyxl.load_workbook("old_file.xlsx", keep_vba=True)
    new_file = openpyxl.load_workbook("new_file.xlsx", keep_vba=True)
    marker_color = PatternFill(fgColor='00008080', fill_type='solid')
    
    for sheetname_old_file, sheetname_new_file in zip(old_file.sheetnames, new_file.sheetnames):
        sheet_old_file = old_file[sheetname_old_file]
        sheet_new_file = new_file[sheetname_new_file]
        # last filled row
        max_row = max(sheet_old_file.max_row, sheet_new_file.max_row)
        # last filled column
        max_column = max(sheet_old_file.max_column, sheet_new_file.max_column)
        print(sheet_old_file, sheet_new_file)
        for col_idx in range(1, max_column + 1):
            for row_idx in range(1, max_row + 1):
                old_cell_value = sheet_old_file.cell(column=col_idx, row=row_idx).value
                new_cell_value = sheet_new_file.cell(column=col_idx, row=row_idx).value
                if old_cell_value != new_cell_value:
                    sheet_new_file.cell(column=col_idx, row=row_idx).fill = marker_color
                    print('\nUnterschied gefunden:')
                    print(f'Spalte: {col_idx}, Zeile: {row_idx}')
                    print(f'Alter Wert: {old_cell_value}')
                    print(f'Neuer Wert: {new_cell_value}')
    
            print(f'Spalte: {col_idx} fertig.')
    
    new_file.save('result.xlsx')
    new_file.close()

    Unser Skript wird am Ende zwei verschiedene Aufgaben erfüllen:

    • Excel Dateien vergleichen
    • Automatischen E-Mail Report versenden

    Deshalb möchte ich die Aufgaben auch im Code sauber getrennt haben.
    Dafür mache ich aus der Logik hier erstmal eine Funktion:

    import openpyxl
    from openpyxl.styles import PatternFill
    
    old_file = openpyxl.load_workbook("old_file.xlsx", keep_vba=True)
    new_file = openpyxl.load_workbook("new_file.xlsx", keep_vba=True)
    marker_color = PatternFill(fgColor='00008080', fill_type='solid')
    
    def compare_excel_files():
        for sheetname_old_file, sheetname_new_file in zip(old_file.sheetnames, new_file.sheetnames):
            sheet_old_file = old_file[sheetname_old_file]
            sheet_new_file = new_file[sheetname_new_file]
            # last filled row
            max_row = max(sheet_old_file.max_row, sheet_new_file.max_row)
            # last filled column
            max_column = max(sheet_old_file.max_column, sheet_new_file.max_column)
            print(sheet_old_file, sheet_new_file)
            for col_idx in range(1, max_column + 1):
                for row_idx in range(1, max_row + 1):
                    old_cell_value = sheet_old_file.cell(column=col_idx, row=row_idx).value
                    new_cell_value = sheet_new_file.cell(column=col_idx, row=row_idx).value
                    if old_cell_value != new_cell_value:
                        sheet_new_file.cell(column=col_idx, row=row_idx).fill = marker_color
                        print('\nUnterschied gefunden:')
                        print(f'Spalte: {col_idx}, Zeile: {row_idx}')
                        print(f'Alter Wert: {old_cell_value}')
                        print(f'Neuer Wert: {new_cell_value}')
    
                print(f'Spalte: {col_idx} fertig.')
    
        new_file.save('result.xlsx')
        new_file.close()

    Als Nächstes nehme ich mir auch den Code für den E-Mail Versand in eine weitere Funktion:

    import smtplib
    from email.mime.multipart import MIMEMultipart
    from email.mime.text import MIMEText
    
    SENDER = 'info@ingo-janssen.de'
    PASSWORD = input('E-Mail Account Passwort eingeben: ')
    SMTP_SERVER = 'smtp.ionos.de'
    SMTP_PORT = 465
    
    RECIPIENT = ['info@ingo-janssen.de']
    SUBJECT = 'Ein netter Gruß.'
    MESSAGE_TEXT = '''Guten Morgen!
    Ich wünsche dir einen wunderschönen Tag!'''
    MESSAGE_HTML = '''
    <html>
        <body>
            <p>Guten Morgen!</p>
            <p>Ich wünsche dir einen wunderschönen Tag!</p>
            <p>Schau doch mal auf meiner <a href="https://ingo-janssen.de">Webseite</a> vorbei</p>
        </body>
    </html>
    '''
    
    def send_email_notification():
        message = MIMEMultipart()
        message['Subject'] = SUBJECT
        message['From'] = SENDER
        message['To'] = ','.join(RECIPIENT)
    
        message_content_plain = MIMEText(MESSAGE_TEXT, 'plain')
        message_content_html = MIMEText(MESSAGE_HTML, 'html')
    
        message.attach(message_content_plain)
        message.attach(message_content_html)
    
        with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as mail_server:
            mail_server.login(SENDER, PASSWORD)
            mail_server.sendmail(SENDER, RECIPIENT, message.as_string())

    So weit, so gut.

    Excel Vergleich erweitern

    Was wollen wir denn jetzt erreichen?
    Am Ende soll das Ergebnis aus dem Excel Vergleich in einer E-Mail landen.
    Ich stelle mir das in etwa so vor:

    ZelleAlter WertNeuer Wert
    A1abcabd

    Das heißt, wir müssen den Excel Vergleich so erweitern, dass wir die Daten überhaupt erstmal sammeln!
    Aktuell geben wir die Unterschiede einfach nur aus.

    Daten sammeln klingt nach Variable?
    Genau das ist es.
    Wir kontrollieren jede Zelle ein einziges Mal.
    Da bietet es sich an, ein Dictionary zu verwenden, um die Unterschiede zu sammeln.

    Wir können den Namen der Zelle als Schlüssel verwenden und die Änderungen als Werte in einer Liste sammeln.

    Das Dictionary sieht dann so aus:

    changes = {
        'A1': ['abc', 'abd'],
    }

    Durch die Position in der Liste wissen wir, ob es sich um den alten oder neuen Wert handelt.

    Also los gehts!
    Erstmal ein Dictionary anlegen, dass wir dann später benutzen können.
    Da es alle Änderungen sammeln soll, kommt es einfach an den Anfang unserer Excel Vergleich Funktion, noch bevor die for-Schleife anfängt.

    def compare_excel_files():
        changes = {}
        ...

    Weiter unten in der Funktion haben wir bereits das Ergebnis, bzw. die Unterschiede und geben sie mittels print() aus.
    Hier können wir uns einfach dran hängen und unser Dictionary befüllen.

    Ein Problem haben wir dabei nur: Der Name der Zelle
    Wir haben einen Index, aber keinen Namen.

    Zum Glück liefert uns OpenPyxl hier eine Lösung: get_column_letter().
    Damit wird der Spaltenindex in einen Excel typischen Namen wie “A”, “B”, “C”, etc. übersetzt.
    Den Zeilenindex brauchen wir hingegen nicht anzufassen. Der ist bereits eine Nummer und damit genau so, wie wir ihn brauchen.

    from openpyxl.utils import get_column_letter
    
    cell_name = f'{get_column_letter(col_idx)}{row_idx}'

    Damit ist unser cell_name übersetzt in Werte wie “B83”, “H20”, etc.
    Und damit können wir als Nächstes das Dictionary für die Änderungen befüllen:

    changes[cell_name] = [old_cell_value, new_cell_value]

    Den Namen der Zelle nehmen wir als Schlüssel und für den Wert erstellen wir eine Liste mit dem alten und dem neuen Wert.
    Der letzte Schritt ist nur noch der Aufruf der E-Mail Funktion.
    Dabei übergeben wir das Dictionary mit den Änderungen, damit die E-Mail die auch anzeigen kann:

    send_email_notification(changes)

    Automatischen E-Mail Report erstellen

    Der Excel Vergleich übergibt die gefundenen Änderungen an den automatischen E-Mail Report.
    Die Übergabe muss natürlich auch entgegengenommen werden. Darum kümmern wir uns zuerst:

    def send_email_notification(changes):

    Als Nächstes brauchen wir ein Template für unsere Nachricht.
    Als Variable haben wir schon das alte Template aus dem letzten Artikel oben aufgenommen:

    MESSAGE_HTML = '''
    <html>
        <body>
            <p>Guten Morgen!</p>
            <p>Ich wünsche dir einen wunderschönen Tag!</p>
            <p>Schau doch mal auf meiner <a href="https://ingo-janssen.de">Webseite</a> vorbei</p>
        </body>
    </html>
    '''

    Wir wollen eine kurze Nachricht ausgegeben, worum es sich handelt und anschließend einfach die Änderungen tabellarisch darstellen. Der Text und die Spaltenbezeichnungen können wir fest hinterlegen. Die ändern sich nicht. Und für den Inhalt fügen wir einen Platzhalter ein.

    Ich habe dafür INHALT gewählt. Es ist aber egal, was du nimmst. Es muss nur eine Zeichensequenz sein, die sonst nicht in der E-Mail vorkommt. Wir nutzen die Sequenz später, um unsere gefundenen Änderungen einzusetzen, den Platzhalter also zu ersetzen. Und wir wollen ja an der richtigen Stelle einfügen.

    MESSAGE_TEMPLATE = '''
    <html>
        <body>
            <p>Hier die Änderungen in den Dateien</p>
            <table>
                <tr>
                    <td>Zelle</td>
                    <td>Alter Wert</td>
                    <td>Neuer Wert</td>
                </tr>
                INHALT
            </table>
        </body>
    </html>
    '''

    Dann geht’s jetzt an den Inhalt.
    Aus den Änderungen, die uns von der Excel Vergleich Funktion übergeben wurden, können wir jetzt den HTML Inhalt zusammenbauen. Da in unserem Template bereits die ganze HTML Tabelle enthalten ist, müssen nur noch die einzelnen Zeilen der Tabelle eingefügt werden. Für jede gefundene Änderung erzeugen wir also eine neue und verteilen die Inhalte mit einem auf die einzelnen Spalten.

    Das lässt sich ganz einfach in einer kleinen List Comprehension erledigen:

    table_content = [f'<tr><td>{cell}</td><td>{values[0]}</td><td>{values[1]}</td>' for cell, values in changes.items()]

    Damit haben wir eine Liste, bei der jeder Eintrag eine Zeile in der Tabelle darstellt. Am Ende muss alles wieder in einen String. Wir müssen die Elemente der Liste also wieder zusammenfügen. Ein Trennzeichen wird dabei nicht benötigt. HTML interessiert sich nicht dafür, ob wir Leerzeilen oder sonst etwas drin haben, solang die Struktur stimmt.

    Um die Liste mit Strings zu einem langen String zusammenzusetzen, hilft join() weiter. Das ist eine String Funktion, die eine Liste entgegennimmt und einfach jedes Element der Liste mit dem String zusammenfügt. Es lassen sich so zum Beispiel super Komma separierte Listen schaffen. Oder in unserem Fall einfach eine Liste ohne Trennzeichen zu einem langen String zusammenführen.

    ''.join(table_content)

    In dem Template ist noch der Platzhalter (INHALT) den wir ersetzen wollen. Dafür bekommen wir bei Python an Strings die Funktion replace().
    Die Funktion erwartet den Wert, den wir ersetzen wollen, gefolgt von dem Wert, mit dem wir ersetzen wollen.
    Das Zusammenfügen der Liste und Ersetzen des Inhalts lässt sich bequem in einem Schritt erledigen:

    message_content = MESSAGE_TEMPLATE.replace('INHALT', ''.join(table_content))

    Plain Text möchten wir diesmal nicht hinzufügen, die Zeilen können wir also löschen. Damit sieht die gesamte Funktion für den automatischen E-Mail Report dann so aus:

    def send_email_notification(changes):
        message = MIMEMultipart()
        message['Subject'] = SUBJECT
        message['From'] = SENDER
        message['To'] = ','.join(RECIPIENT)
    
        table_content = [f'<tr><td>{cell}</td><td>{values[0]}</td><td>{values[1]}</td>' for cell, values in changes.items()]
        message_content = MESSAGE_TEMPLATE.replace('INHALT', ''.join(table_content))
    
        message_content_html = MIMEText(message_content, 'html')
        try:
            message.attach(message_content_html)
        except (IOError, ValueError) as e:
            print(e)
        with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as mail_server:
            mail_server.login(SENDER, PASSWORD)
            mail_server.sendmail(SENDER, RECIPIENT, message.as_string())

    Abschließend müssen wir nur noch den Excel Vergleich aufrufen. Damit bekommen wir nicht nur das Ergebnis in einer neuen Excel Datei, sondern auch eine E-Mail wie diese hier:

    E-Mail Screenshot

    Zusammenfassung

    Für den Excel Vergleich oder auch andere Tools lässt sich einfach ein automatischer E-Mail Report erstellen. Dafür sammelst du alle gefundenen Unterschiede in einem Dictionary, erzeugst die notwendigen HTML Tags außen herum und fügst sie mit einem replace() in dein Template ein.

    Kurse

    Code Challenges und YouTube Videos führen nicht immer zum gewünschten Erfolg.
    Manchmal ist es besser, gezielt und strukturiert durch ein Thema zu arbeiten.

    Schau dich einfach mal bei meinen Kursen um.
    Ohne lästiges Blah Blah steigen wir direkt in das Thema ein und behandeln alles, was du brauchst.

    Picture of Ingo Janßen

    Ingo Janßen

    Lerne nicht einfach programmieren. Löse Probleme und automatisiere Aufgaben!

    Das könnte dich auch interessieren

    Nach oben scrollen
    Newsletter Popup Form

    Keine Inhalte mehr verpassen?

    Melde dich direkt für den "Code-Kompass" an und erhalte nützliche Tipps und Informationen direkt in deinen Posteingang.