[Code Review] GUI zum definierte Excel-Zellen auslesen

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.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Abend zusammen,

eigentlich wollte ich euch für dieses Projekt "nur" um ein Code Review bitten. Jetzt habe ich mich aber mit der Programmstruktur ziemlich verrannt und benötige etwas Hilfestellung.

Es geht bei dem Programm um folgendes. Es gibt eine Menge an Excel-Dateien mit Messwerten. Meine Aufgabe ist es, aus jeder Datei definierte Zellen auszulesen. In Spalte 1 steht die Beschreibung und in Spalte 2 der dazugehörige Wert.
Also das Programm bekommt die Beschreibung der Werte, liest den dazugehörigen Wert aus und schreibt die Beschreibung mit dem Wert in eine neue Datei. Das ganze für jede Datei die in dem festgelegten Ordner und Unterordner gefunden wurde und alle Beschreibungen enthält.
Es gibt neue und alte Dateien mit Messwerten und es ist nicht garantiert das in beiden Dateien die Beschreibung gleich benannt ist. Es kann sein, das in einer Datei "Volumen" steht und in der anderen "Volumn(xyz)" aber beides mal verbiergt sich dahinter der gesuchte Wert.
Wenn in einer Datei nicht alle gesuchten Beschreibungen gefunden werden, dann ignoriere ich die Datei bzw. schreibe es zur Info in eine Textdatei.

Das Programm funktioniert in einem Punkt nicht. Nehmen wir an in einer Zelle steht "Volumen(xy)" und die Beschreibung die der Benutzer zur Suche eingibt ist "Volumen". Dann findet mein Programm zwar die Zelle "Volumen(xy)" schreibt den Wert aber nicht in die neue Excel. Das macht es nicht, weil ich eine Funktion geschrieben habe, die erst mal die Datei nach den gewünschten Beschreibungen durchsucht und dann zwei Listen zurück gibt, Einmal mit den gefundenen Beschreibungen und einmal mit den nicht gefundenen Beschreibungen (wenn alles gut läuft ist die leer). Jetzt gehe ich hin und vergleiche wieder die Zellenbeschreibung mit dem Inhalt der zurückgegebenen Liste und wenn da was übereinstimmt, schreibe ich die Beschreibung mit Wert in ein Dictonary. Hier passiert auch der Fehler, in der Zelle steht "Volumen(xy)" und in der Liste "Volumen".

Wenn ich jetzt wieder mit verschachtelten 'for'-Schleifen und 're' die Beschreibungen durchsuche, dann mache ich zwei mal das Gleiche und das wirkt auf mich alles viel zu kompliziert und viel zu umständlich. Gerade sehe ich den Wald vor Bäumen nicht mehr und würde mich freuen wenn ihr mir sagen könntet, wie ihr vorgehen würdet. Gerne auch nur in Worte und ich schaue wie ich das in Code umsetze.
Für ein Code-Review ist es jetzt noch zu früh, weil ich befürchte dass ich da noch mal einiges ändern muss. (Dennoch bin ich für jede Anmerkung offen, auch wenn ich noch nicht fertig bin).

Naja jetzt hier mal meinen Code, das bringt vermutlich mehr wie mein getexte:

Code: Alles auswählen

#!/usr/bin/env python3
import re
import tkinter as tk
from datetime import datetime
from pathlib import Path
from tkinter import messagebox, ttk
from tkinter.filedialog import askdirectory

from openpyxl import Workbook, load_workbook

WORKBOOK_WITH_READ_DATA = "Ausgelesene Werte.xlsx"
INFO_FILE = "Nicht gelesene Dateien.txt"
DEFAULT_CELL_NAMES = "Volumen, Drehzahl, Gas"


class App(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        master.columnconfigure(0, weight=100)
        self.user_select = tk.StringVar()
        self.user_select.set("<Kein Oderne ausgewählt>")
        ttk.Label(self, text="Ausgewählter Ordner:").grid(row=0, column=0, sticky=tk.W)
        self.selected_folder = tk.Label(self, textvariable=self.user_select)
        self.selected_folder.grid(row=1, column=0, sticky=tk.W)
        ttk.Button(self, text="Ordner auswählen", command=self.open_dialog).grid(
            row=2, column=0, sticky=tk.W
        )
        ttk.Label(self, text="Zellennamen die ausgelesen werden sollen").grid(
            row=0, column=4, sticky=tk.W
        )
        ttk.Label(self, text="getrennt mit Komma eingeben:").grid(
            row=1, column=4, sticky=tk.W
        )
        self.cells_to_read = tk.StringVar()
        self.cells_to_read.set(DEFAULT_CELL_NAMES)
        ttk.Entry(self, textvariable=self.cells_to_read).grid(
            row=2, column=4, sticky=tk.W
        )
        ttk.Button(self, text="Auslesen", command=self.control_read_write_data).grid(
            row=5, column=5
        )
        ttk.Button(self, text="Beenden", command=self.quit).grid(
            row=5, column=4, sticky=tk.E
        )
        self.coffee = ttk.Label(self, text="Zeit für Kaffee...")
        self.process_bar = ttk.Progressbar(self)

        self.timestamp = None
        self.folder_path = None
        self.info_file = False

    @property
    def keywords(self):
        return [keyword.strip() for keyword in self.cells_to_read.get().split(",")]

    def open_dialog(self):
        self.folder_path = Path(askdirectory(mustexist=True))
        self.user_select.set(self.folder_path.name)

    def control_read_write_data(self):
        self.timestamp = datetime.now().strftime("%Y%m%d_%H%M")
        if not self.folder_path:
            messagebox.showerror(title="Oh nein!", message="Kein Ordner ausgewählt")
        else:
            self.coffee.grid(row=4, column=2, columnspan=2, sticky=tk.W)
            self.process_bar.grid(row=4, column=4, sticky=tk.W)
            self.process_bar.start(10)
            for file in self.folder_path.glob("**/*.xlsx"):
                if not [
                    name
                    for name in file.name.split("_")
                    if name == WORKBOOK_WITH_READ_DATA
                ]:
                    keyword_to_cell_content = self.read_file(file)
                    if keyword_to_cell_content:
                        self.write_read_data_to_file(keyword_to_cell_content)
            self.process_bar.stop()
            self.process_bar.grid_forget()
            self.coffee.grid_forget()
            if self.info_file:
                messagebox.showinfo(
                    "Info Datei",
                    "Nicht alle gefundenen Dateien konnten gelesen werden, bitte Info-Datei beachten.",
                )
                self.info_file = False

    def check_file_for_keywords(self, rows):
        founded_keywords = []
        not_founded_keywords = []
        for row, _ in rows:
            for keyword in self.keywords:
                _keyword = re.search(rf"\b{keyword}\b", row.value, re.IGNORECASE)
                if _keyword:
                    founded_keywords.append(_keyword.group().lower())
        for keyword in self.keywords:
            if keyword.casefold() not in founded_keywords:
                not_founded_keywords.append(keyword)
        print(f'founded_keywords: {founded_keywords}')
        return founded_keywords, not_founded_keywords

    def write_info_file(self, file, not_founded_keywords):
        with (self.folder_path / f"{self.timestamp}_{INFO_FILE}").open(
            "a", encoding="UTF-8"
        ) as info_file:
            info_file.write(
                f'-> In der Datei "{file}" wurden folgende Suchwörter nicht gefunden: {not_founded_keywords}'
            )
        self.info_file = True

    def read_file(self, file):
        keyword_to_cell_content = {}
        worksheet = load_workbook(file)["Tabelle1"]
        founded_keywords, not_founded_keywords = self.check_file_for_keywords(
            worksheet.iter_rows(min_col=1, max_col=2)
        )
        if not len(not_founded_keywords):
            for description, cell_value in worksheet.iter_rows(min_col=1, max_col=2):
                print(f'description.value: {description.value}')
                if str(description.value).lower() in founded_keywords:
                    keyword_to_cell_content[description.value] = cell_value.value
        else:
            self.write_info_file(file, not_founded_keywords)
        print(f'keyword_to_cell_content: {keyword_to_cell_content}')
        return keyword_to_cell_content

    def write_read_data_to_file(self, keyword_to_file):
        file_path = self.folder_path / f"{self.timestamp}_{WORKBOOK_WITH_READ_DATA}"
        if file_path.exists():
            workbook = load_workbook(file_path)
        else:
            workbook = Workbook()
        worksheet = workbook.active
        for key in keyword_to_file:
            worksheet.append([key, keyword_to_file[key]])
        workbook.save(file_path)


def main():
    root = tk.Tk()
    root.title("Auswertung")
    app = App(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()

Die 'print'-Ausgaben dazu (damit versteht man vielleicht meinen Versuch die Fehlerursache zu beschreiben):

Code: Alles auswählen

founded_keywords: ['volumen', 'drehzahl', 'gas']
description.value: l
description.value: Volumen (xy)
description.value: j
description.value: drehzahl
description.value: Gas
description.value: Apfel
keyword_to_cell_content: {'drehzahl': 33, 'Gas': 'H2'}
Das ist nur von mir eine erstellte Beispieldatei, bei "Apfel" war ich hungrig. :D

Vielen Dank vorab.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Vergiss im ersten Schritt die GUI. Solange Du noch keine funktionierende Datenverarbeitung hast, brauchst Du keine GUI. Und auch wenn Du dann eine GUI aufsetzt, sollte die nicht so sehr mit der Datenverarbeitung verwoben sein.
`read_file` liest nicht nur, sondern schreibt auch ein Info-File; sehr verwirrend. Dann gehst Du ja zweimal alle Zeilen durch, einmal nur um die Keywords zu ermitteln und einmal um dann die Werte dazu herauszusuchen.
Firmen gründet man, Keywords eher nicht. Keywords werden nur gefunden.
Die nicht-gegründeten Keywords ergeben sich einfach dadurch, dass man die gegründeten Schlüsselwörter von allen per Mengenoperationen abzieht.

Die Ausgabedateien öffnet man nur einmal und nicht einmal pro Inputdatei.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

dank für die Antwort und sorry für den peinlichen Übersetzungsfehler.

Wäre dass für das aussuchen der Keywörter und dem ermitteln der nicht gefundenen Wörter der richtige Ansatz?

Code: Alles auswählen

#!/usr/bin/env python3
import re
from pathlib import Path

from openpyxl import load_workbook

FOLDER_PATH = Path('/home/dennis/Dokumente/Test')
WORKBOOK_WITH_READ_DATA = "Ausgelesene Werte.xlsx"
INFO_FILE = "Nicht gelesene Dateien.txt"
DEFAULT_CELL_NAMES = ['Volumen', 'Drehzahl', 'Gas']


def main():
    for file in FOLDER_PATH.glob("**/*.xlsx"):
        keyword_to_row_content = {}
        if not [name for name in file.name.split("_") if name == WORKBOOK_WITH_READ_DATA]:
            worksheet = load_workbook(file)["Tabelle1"]
            for description, content in worksheet.iter_rows(min_col=1, max_col=2):
                for keyword in DEFAULT_CELL_NAMES:
                    if re.search(rf"\b{keyword}\b", description.value, re.IGNORECASE):
                        keyword_to_row_content[keyword] = content.value
            not_found_keywords = [set(DEFAULT_CELL_NAMES) - set(keyword_to_row_content)]
            print(not_found_keywords)
            print(keyword_to_row_content)


if __name__ == '__main__':
    main()
Ich müsste jetzt halt nach jeder ausgelesenen Datei, die Keywörter plus Inhalt der Ausgabedatei hinzufügen, die ich ja als erstes einmal öffne. Ist das richtig/effizient? Ich denke schon, dann hat man kein großes Dictonary und die Suche nach den nicht gefundenen Wörter wäre übersichtlich.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Eine Liste zu erzeugen, nur um zu prüfen, ob die leer ist, ist etwas umständlich:

Code: Alles auswählen

        if not [name for name in file.name.split("_") if name == WORKBOOK_WITH_READ_DATA]:
Hier benutzt man any:

Code: Alles auswählen

        if not any(name == WORKBOOK_WITH_READ_DATA for name in file.name.split("_")):
Da aber WORKBOOK_WITH_READ_DATA mit ".xlsx" endet, kann die Bedingung nur ganz am Ende stehen:

Code: Alles auswählen

        if file.name.split("_")[-1] != WORKBOOK_WITH_READ_DATA:
Warum ist `not_found_keywords` eine Liste mit exakt einem Element?

Ansonsten könnte Dein Code noch einige Funktionen vertragen:

Code: Alles auswählen

#!/usr/bin/env python3
import re
from pathlib import Path
from openpyxl import load_workbook

FOLDER_PATH = Path('/home/dennis/Dokumente/Test')
WORKBOOK_WITH_READ_DATA = "Ausgelesene Werte.xlsx"
INFO_FILE = "Nicht gelesene Dateien.txt"
DEFAULT_CELL_NAMES = ['Volumen', 'Drehzahl', 'Gas']


def find_keyword(description):
    match = re.search(rf"\b({'|'.join(DEFAULT_CELL_NAMES)})\b", description, re.IGNORECASE)
    if match:
        return match.group(1)
    return None

def read_excel_file(filename):
    keyword_to_row_content = {}
    worksheet = load_workbook(filename)["Tabelle1"]
    for description, content in worksheet.iter_rows(min_col=1, max_col=2):
        keyword = find_keyword(description.value)
        if keyword is not None:
            keyword_to_row_content[keyword] = content.value
    return keyword_to_row_content

def main():
    for filename in FOLDER_PATH.glob("**/*.xlsx"):
        if filename.name.split("_")[-1] != WORKBOOK_WITH_READ_DATA:
            keyword_to_row_content = read_excel_file(filename)
            not_found_keywords = set(DEFAULT_CELL_NAMES) - set(keyword_to_row_content)
            print(not_found_keywords)
            print(keyword_to_row_content)


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

Ich würde das `split()` aus der Bedingung noch weg lassen, denn ``if filename.name.endswith("_" + WORKBOOK_WITH_READ_DATA)`` beschreibt den Test IMHO noch ein bisschen verständlicher für den Leser.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Vielen Dank für die Antworten und Verbesserungen.
Warum ist `not_found_keywords` eine Liste mit exakt einem Element?
Meinst du weil not_found_keywords == [set()] ist? Ehrlich gesagt, ist mir nichts besseres eingefallen.

Ich würde gerne vermeiden, das es zu Problemen kommt, wenn der Benutzer Groß- und Kleinschreibung ignoriert. Gesucht wird in deinem Code zwar die richtige Beschreibung, aber 'not_found_keywords' ist dann trotzdem befüllt. Mir fällt als Lösung nur sowas ein:

Code: Alles auswählen

not_found_keywords = set(name.lower() for name in DEFAULT_CELL_NAMES) - set(name.lower() for name in keyword_to_row_content)
Gibt es da bessere Ansätze?

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Für ein Edit ist es leider zu spät.
Hier noch mein ergänzter Code mit schreiben in die Ausgabe Datei und dem erstellen der Info-Datei:

Code: Alles auswählen

#!/usr/bin/env python3
import re
from datetime import datetime
from pathlib import Path

from openpyxl import Workbook, load_workbook

FOLDER_PATH = Path("/home/dennis/Dokumente/Test")
WORKBOOK_WITH_READ_DATA = "Ausgelesene Werte.xlsx"
INFO_FILE = "Nicht gelesene Dateien.txt"
DEFAULT_CELL_NAMES = ["volumen", "Drehzahl", "Gas"]


def find_keyword(description):
    match = re.search(
        rf"\b({'|'.join(DEFAULT_CELL_NAMES)})\b", description, re.IGNORECASE
    )
    if match:
        return match.group(1)
    return None


def read_excel_file(filename):
    keyword_to_row_content = {}
    worksheet = load_workbook(filename)["Tabelle1"]
    for description, content in worksheet.iter_rows(min_col=1, max_col=2):
        keyword = find_keyword(description.value)
        if keyword is not None:
            keyword_to_row_content[keyword] = content.value
    return keyword_to_row_content


def write_info_file(timestamp, file, not_found_keywords):
    with (FOLDER_PATH / f"{timestamp}_{INFO_FILE}").open(
        "a", encoding="UTF-8"
    ) as info_file:
        info_file.write(
            f'-> In der Datei "{file}" wurden folgende Suchwörter nicht gefunden: {not_found_keywords}'
        )


def write_into_output_file(worksheet, keyword_to_row_content):
    for keyword in keyword_to_row_content:
        worksheet.append([keyword, keyword_to_row_content[keyword]])


def main():
    timestamp = datetime.now().strftime("%Y%m%d_%H%M")
    output_file = FOLDER_PATH / f"{timestamp}_{WORKBOOK_WITH_READ_DATA}"
    if output_file.exists():
        workbook = load_workbook(output_file)
    else:
        workbook = Workbook()
    worksheet = workbook.active
    for filename in FOLDER_PATH.glob("**/*.xlsx"):
        if filename.name.split("_")[-1] != WORKBOOK_WITH_READ_DATA:
            keyword_to_row_content = read_excel_file(filename)
            not_found_keywords = set(name.lower() for name in DEFAULT_CELL_NAMES) - set(
                name.lower() for name in keyword_to_row_content
            )
            if not not_found_keywords:
                write_into_output_file(worksheet, keyword_to_row_content)
            else:
                write_info_file(timestamp, filename, not_found_keywords)
    workbook.save(output_file)


if __name__ == "__main__":
    main()
Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Wörterbücher haben die Methode items():

Code: Alles auswählen

def write_into_output_file(worksheet, keyword_to_row_content):
    for row in keyword_to_row_content.items():
        worksheet.append(row)
Da die Schlüssel in keyword_to_row_content aus DEFAULT_CELL_NAMES stammen, ist immer garantiert, dass sie die gleiche Schreibweise haben, es ist also nicht nötig, alles in Kleinbuchstaben zu verwandeln.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen und Danke für den Hinweis.

Entweder verstehe ich deinen letzten Satz nicht oder ich habe mich falsch ausgedrückt. 'keyword_to_row_content' besteht doch aus dem 'match' und das gibt mir den Zelleninhalt zurück. Wenn ich nach 'drehzahl' suche, und in der 'Excel steht 'Drehzahl', dann steht in meinem Dictonary auch 'Drehzahl'. (So habe ich meinen Test von gestern Abend noch in Erinnerung und so würde ich den Code auch interpretieren)
Da wäre ich jetzt wieder an meinem Problem, dass ich, wie zu Beginn, Verrenkungen mit Listen machen muss, oder eben so wie oben mit 'lower' oder so, wie du/ihr es vorschlagt.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Du hast natürlich Recht.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Das "natürlich" könnte man auch durch "zufällig" oder "auch mal" ersetzen :D

Der Code würde jetzt die Anforderungen erfüllen, dann gehts jetzt wieder an die GUI. Da meintest du, dass die zu eng mit der Logik verbunden war. Soll es eine GUI-Klasse und eine Logik-Klasse (benötige ich ja eigentlich nicht?) geben oder war es zu viel des guten, dass die GUI eine Eigenschaft mit Keywörter hatte?

Wie soll ich die Struktur aufbauen? Wie geht ihr an so ein Problem ran, wenn ihr mal kurz eure ganze Erfahung vergessen würdet?

Vielen Dank und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
sparrow
User
Beiträge: 4195
Registriert: Freitag 17. April 2009, 10:28

@Dennis89: Ich mache das immer ganz simpel: Wenn ich das GUI gegen ein CLI austauschen kann, ohne an den Funktionen etwas zu ändern, die nicht als Schnittstelle zum Benutzer funktionieren, dann passt das.
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo und Danke für die Antwort.

Ich dachte das hätte ich bei meinem Code aus Beitrag eigentlich auch. Die Funktionen, wenn sie gut wären, könnte man doch nehmen, das 'self' entfernen und mit den richtigen Argumenten in einem anderen Code nutzen. Oder ist dass das angesprochene Problem. Das ich überall mit 'self' auf die Klasse zugreife?

Soll ich die Funktionen, die lesen, schreiben etc. außerhalb der GUI-Klasse lassen? Fühlt sich irgendwie auch falsch an.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13112
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: Es ist damit gemeint die Logik *ohne sie zu verändern* sowohl in einer CLI als auch in einer GUI verwenden zu können. Die Logik sollte ja unabhängig davon sein wie sie verwendet wird und nichts von der Benutzerschnittstelle wissen. Also ja das muss alles ausserhalb von Klassen bleiben die irgendwas mit GUI zu tun haben.

Was dem Code noch fehlt IMHO ist eine Funktion die den gesamten Job umfasst, nicht die `main()` ist und a) parametrisiert ist, also die Schnittstelle bietet von der die GUI die Informationen rein füttert, die der Benutzer über die GUI eingibt, und b) eine Möglichkeit bietet Informationen über den Fortschritt nach draussen zu liefern. Weil das ja potentiell etwas länger läuft und man das deswegen eher in einem Thread laufen lassen würde, damit die GUI nicht blockiert. Ein üblicher Weg ist eine Rückruffunktion entgegen zu nehmen, die mit Fortschrittsinfos aufgerufen wird. Also beispielsweise immer mit dem aktuell verarbeiteten Tabellendateinamen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo und danke für die Antwort.

Ich behaupte mal das ich grundsätzlich verstanden habe, was theoretisch zu tun ist.
Erst mal lasse ich das mit dem Thread weg. Den würde ich dann einbauen, wenn ich die Schnittstellenfunktion so habe, wie sie sein muss.

Ich hab jetzt eine Funktion 'control_program_sequence' Die gibt den Pfad und die Suchwörter, die der User in dem GUI eingegeben hat, an die Funktionen, die "arbeiten" weiter und nimmt Rückgaben von ihnen entgegen.

War der erste Schritt so gedacht?

Falls ja, dann muss ich jetzt durch drücken des Buttons 'Auslesen' einen Thread starten, der dann 'control_program_sequence' laufen lässt? Die Fortschrittinfos müsste ich dann über 'after' erhalten?

Code: Alles auswählen

#!/usr/bin/env python3
import re
import tkinter as tk
from datetime import datetime
from pathlib import Path
from tkinter import messagebox, ttk
from tkinter.filedialog import askdirectory

from openpyxl import Workbook, load_workbook

WORKBOOK_WITH_READ_DATA = "Ausgelesene Werte.xlsx"
INFO_FILE = "Nicht gelesene Dateien.txt"
DEFAULT_CELL_NAMES = "Volumen, Drehzahl, Gas"


class App(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        master.columnconfigure(0, weight=100)
        self.user_select = tk.StringVar()
        self.user_select.set("<Kein Oderne ausgewählt>")
        ttk.Label(self, text="Ausgewählter Ordner:").grid(row=0, column=0, sticky=tk.W)
        self.selected_folder = tk.Label(self, textvariable=self.user_select)
        self.selected_folder.grid(row=1, column=0, sticky=tk.W)
        ttk.Button(self, text="Ordner auswählen", command=self.open_dialog).grid(
            row=2, column=0, sticky=tk.W
        )
        ttk.Label(self, text="Zellennamen die ausgelesen werden sollen").grid(
            row=0, column=4, sticky=tk.W
        )
        ttk.Label(self, text="getrennt mit Komma eingeben:").grid(
            row=1, column=4, sticky=tk.W
        )
        self.cells_to_read = tk.StringVar()
        self.cells_to_read.set(DEFAULT_CELL_NAMES)
        ttk.Entry(self, textvariable=self.cells_to_read).grid(
            row=2, column=4, sticky=tk.W
        )
        ttk.Button(self, text="Auslesen", command=self.control_program_sequence).grid(
            row=5, column=5
        )
        ttk.Button(self, text="Beenden", command=self.quit).grid(
            row=5, column=4, sticky=tk.E
        )
        self.coffee = ttk.Label(self, text="Zeit für Kaffee...")
        self.process_bar = ttk.Progressbar(self)

        self.folder_path = None
        self.info_file = False

    def open_dialog(self):
        self.folder_path = Path(askdirectory(mustexist=True))
        self.user_select.set(self.folder_path.name)

    def control_program_sequence(self):
        timestamp = datetime.now().strftime("%Y%m%d_%H%M")
        keywords = keywords_to_list(self.cells_to_read)
        workbook, output_file = open_output_file(timestamp, self.folder_path)
        worksheet = workbook.active
        for filename in self.folder_path.glob("**/*.xlsx"):
            if filename.name.split("_")[-1] != WORKBOOK_WITH_READ_DATA:
                keyword_to_row_content = read_excel_file(filename, keywords)
                not_found_keywords = set(name.lower() for name in keywords) - set(
                    name.lower() for name in keyword_to_row_content
                )
                if not not_found_keywords:
                    write_into_output_file(worksheet, keyword_to_row_content)
                else:
                    write_info_file(
                        timestamp, self.folder_path, filename, not_found_keywords
                    )
                    self.info_file = True
        workbook.save(output_file)
        if self.info_file:
            messagebox.showinfo(
                "Info Datei",
                "Nicht alle gefundenen Dateien konnten gelesen werden, bitte Info-Datei beachten.",
            )
            self.info_file = False


def find_keyword(description, keywords):
    match = re.search(rf"\b({'|'.join(keywords)})\b", description, re.IGNORECASE)
    if match:
        return match.group(1)
    return None


def keywords_to_list(cells_to_read):
    return [keyword.strip() for keyword in cells_to_read.get().split(",")]


def open_output_file(timestamp, folder_path):
    output_file = folder_path / f"{timestamp}_{WORKBOOK_WITH_READ_DATA}"
    if output_file.exists():
        workbook = load_workbook(output_file)
    else:
        workbook = Workbook()
    return workbook, output_file


def read_excel_file(filename, keywords):
    keyword_to_row_content = {}
    worksheet = load_workbook(filename)["Tabelle1"]
    for description, content in worksheet.iter_rows(min_col=1, max_col=2):
        keyword = find_keyword(description.value, keywords)
        if keyword is not None:
            keyword_to_row_content[keyword] = content.value
    return keyword_to_row_content


def write_info_file(timestamp, folder_path, file, not_found_keywords):
    with (folder_path / f"{timestamp}_{INFO_FILE}").open(
        "a", encoding="UTF-8"
    ) as info_file:
        info_file.write(
            f'-> In der Datei "{file}" wurden folgende Suchwörter nicht gefunden: {not_found_keywords}'
        )


def write_into_output_file(worksheet, keyword_to_row_content):
    for row in keyword_to_row_content.items():
        worksheet.append(row)


def main():
    root = tk.Tk()
    root.title("Auswertung")
    app = App(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
__blackjack__
User
Beiträge: 13112
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: Ich würde sagen `control_program_sequence()` enthält Sachen die da nicht rein gehören. Mal angenommen Du würdest zusätzlich eine CLI anbieten wollen, wie viel von dem Code der nichts mit der GUI zu tun hat, aber in `control_program_sequence()` steckt, müsstest Du dann noch mal fast genau so in einer anderen Funktion schreiben?
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

Guten Morgen,

danke für die Antwort.
Ja fast die ganze Funktion müsste ich noch einmal neu schreiben. Ich tat mir bei der weiteren Aufteilung schwer, passende Funktionsnamen zu finden und das war eigentlich immer das Zeichen für mich, dass ich da was falsch mache.

Jetzt habe ich die ganze Funktion von der Klasse getrennt und die bekommt nur die zwei Sachen, die der User eingibt. Den Pfad und die Suchwörter.
Da das ganze ja noch weiter geht, mit den Fortschrittinfos und dem Thread, habe ich jetzt eine Funktion 'transfer_gui_data', die bis jetzt eigentlich nur eine Funktion auserhalb der Klasse aufruft. Das hätte ich auch direkt an den Button binden können, aber ich denke die Funktion, wenn dass der richtige Weg ist, wird benötigt und erweitert?

Und ich habe das Suchen der Excel-Dateien auch noch in eine extra Funktion ausgelagert.

Code: Alles auswählen

#!/usr/bin/env python3
import re
import tkinter as tk
from datetime import datetime
from pathlib import Path
from tkinter import messagebox, ttk
from tkinter.filedialog import askdirectory

from openpyxl import Workbook, load_workbook

WORKBOOK_WITH_READ_DATA = "Ausgelesene Werte.xlsx"
INFO_FILE = "Nicht gelesene Dateien.txt"
DEFAULT_CELL_NAMES = "Volumen, Drehzahl, Gas"


class App(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        master.columnconfigure(0, weight=100)
        self.user_select = tk.StringVar()
        self.user_select.set("<Kein Oderne ausgewählt>")
        ttk.Label(self, text="Ausgewählter Ordner:").grid(row=0, column=0, sticky=tk.W)
        self.selected_folder = tk.Label(self, textvariable=self.user_select)
        self.selected_folder.grid(row=1, column=0, sticky=tk.W)
        ttk.Button(self, text="Ordner auswählen", command=self.open_dialog).grid(
            row=2, column=0, sticky=tk.W
        )
        ttk.Label(self, text="Zellennamen die ausgelesen werden sollen").grid(
            row=0, column=4, sticky=tk.W
        )
        ttk.Label(self, text="getrennt mit Komma eingeben:").grid(
            row=1, column=4, sticky=tk.W
        )
        self.cells_to_read = tk.StringVar()
        self.cells_to_read.set(DEFAULT_CELL_NAMES)
        ttk.Entry(self, textvariable=self.cells_to_read).grid(
            row=2, column=4, sticky=tk.W
        )
        ttk.Button(self, text="Auslesen", command=self.transfer_gui_data).grid(
            row=5, column=5
        )
        ttk.Button(self, text="Beenden", command=self.quit).grid(
            row=5, column=4, sticky=tk.E
        )
        self.coffee = ttk.Label(self, text="Zeit für Kaffee...")
        self.process_bar = ttk.Progressbar(self)

        self.folder_path = None

    def open_dialog(self):
        self.folder_path = Path(askdirectory(mustexist=True))
        self.user_select.set(self.folder_path.name)

    def transfer_gui_data(self):
        control_program_sequence(self.cells_to_read, self.folder_path)


def control_program_sequence(cells_to_read, folder_path):
    info_file = False
    timestamp = datetime.now().strftime("%Y%m%d_%H%M")
    keywords = keywords_to_list(cells_to_read)
    workbook, output_file = open_output_file(timestamp, folder_path)
    worksheet = workbook.active
    filename = looking_for_xlsx_file(folder_path)
    keyword_to_row_content = read_excel_file(filename, keywords)
    not_found_keywords = set(name.lower() for name in keywords) - set(
        name.lower() for name in keyword_to_row_content
    )
    if not not_found_keywords:
        write_into_output_file(worksheet, keyword_to_row_content)
    else:
        write_info_file(timestamp, folder_path, filename, not_found_keywords)
        info_file = True
    workbook.save(output_file)
    if info_file:
        messagebox.showinfo(
            "Info Datei",
            "Nicht alle gefundenen Dateien konnten gelesen werden, bitte Info-Datei beachten.",
        )


def looking_for_xlsx_file(folder_path):
    for filename in folder_path.glob("**/*.xlsx"):
        if filename.name.split("_")[-1] != WORKBOOK_WITH_READ_DATA:
            return filename


def find_keyword(description, keywords):
    match = re.search(rf"\b({'|'.join(keywords)})\b", description, re.IGNORECASE)
    if match:
        return match.group(1)
    return None


def keywords_to_list(cells_to_read):
    return [keyword.strip() for keyword in cells_to_read.get().split(",")]


def open_output_file(timestamp, folder_path):
    output_file = folder_path / f"{timestamp}_{WORKBOOK_WITH_READ_DATA}"
    if output_file.exists():
        workbook = load_workbook(output_file)
    else:
        workbook = Workbook()
    return workbook, output_file


def read_excel_file(filename, keywords):
    keyword_to_row_content = {}
    worksheet = load_workbook(filename)["Tabelle1"]
    for description, content in worksheet.iter_rows(min_col=1, max_col=2):
        keyword = find_keyword(description.value, keywords)
        if keyword is not None:
            keyword_to_row_content[keyword] = content.value
    return keyword_to_row_content


def write_info_file(timestamp, folder_path, file, not_found_keywords):
    with (folder_path / f"{timestamp}_{INFO_FILE}").open(
        "a", encoding="UTF-8"
    ) as info_file:
        info_file.write(
            f'-> In der Datei "{file}" wurden folgende Suchwörter nicht gefunden: {not_found_keywords}'
        )


def write_into_output_file(worksheet, keyword_to_row_content):
    for row in keyword_to_row_content.items():
        worksheet.append(row)


def main():
    root = tk.Tk()
    root.title("Auswertung")
    app = App(root)
    app.pack()
    app.mainloop()


if __name__ == "__main__":
    main()
Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Dennis89
User
Beiträge: 1156
Registriert: Freitag 11. Dezember 2020, 15:13

__blackjack__ hat geschrieben: Dienstag 8. November 2022, 07:51 [...]eine Möglichkeit bietet Informationen über den Fortschritt nach draussen zu liefern. Weil das ja potentiell etwas länger läuft und man das deswegen eher in einem Thread laufen lassen würde, damit die GUI nicht blockiert. Ein üblicher Weg ist eine Rückruffunktion entgegen zu nehmen, die mit Fortschrittsinfos aufgerufen wird. Also beispielsweise immer mit dem aktuell verarbeiteten Tabellendateinamen.
In der Hoffnung, dass mein vorheriger Code zum aktuellen Stand so ist, wie er sein soll und die Logik nun vollständig von dem GUI getrennt ist, habe ich mir über das Zitat Gedanken gemacht.

Ich weis nur nicht ob meine Vorstellung dazu richtig ist. Ich würde mit 'after' immer wieder abfragen, wie viele Dateien noch auszulesen sind. Damit das geht, muss ich erst alle Excel-Dateien in dem Verzeichnis zählen. Wenn ich dann immer den aktuellen Stand der noch übrigen Dateien abfragen will, dann muss ich die Logik in eine Klasse packen. War das so gemeint oder bin ich wieder am Ziel vorbei?

Und zusätzlich läuft die Logik dann noch in einem eigenen Thread.

Danke und Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Die Logik sollte nichts von der GUI wissen müssen. In `control_program_sequence` ist `cells_to_read` aber ein StringVar-Object, statt eines einfachen Strings.
Benutzeravatar
__blackjack__
User
Beiträge: 13112
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Dennis89: Ich hatte eine einfache Rückruffunktion im Sinn. Für die GUI könnte man dann einfach eine Funktion übergeben, welche die Infos die der Funktion übergeben werden in eine Queue steckt und die die dann mit `after()` regelmässig abfragen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten