[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
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Auf der gleichen Seite steht wie man an den Pfad der EXE kommt: https://pyinstaller.org/en/stable/runti ... sys-argv-0
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1154
Registriert: Freitag 11. Dezember 2020, 15:13

Oh man wie peinlich. Bei der Überschrift dachte ich, wieso auch immer, dass da nichts mehr zu meinem Problem kommt. Dafür habe ich "__file__" gefühlt 100 mal gelesen :roll:
Entschuldigung und vielen Dank. Jetzt hat es funktioniert.

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

Guten Morgen,

von dem Programm bzw. dem Programmablauf das hier beschrieben wurde ist leider nicht mehr viel übrig. Nach dem das alles gut funktioniert hat, kam eine neue Idee auf und damit neue Anforderungen.
Es wurden Zellen festgelegt die ausgelesen werden sollen (es sind mehr als ich ihm Code habe, habe es für die Übersicht hier etwas gekürzt). Dann gibt es zwei Möglichkeiten. Entweder die ausgelesene Werte werden in eine neue Excel-Datei geschrieben oder es wird eine Excel-Datei geöffnet, die einige Formeln beinhaltet. Die ausgelesenen Werte werden dort eingetragen, die Exceldatei berechnet daraus ein Normvolumenstrom, dieser Wert wird dann auch ausgelesen. Dann soll der berechnete Normvolumenstrom mit einem zuvor ausgelesenen Wert verglichen werden, bzw. es soll die Differenz berechnet werden. Im Anschluss kommt dann alles in eine neue Excel-Datei.
Das funktioniert auch soweit alles, aber es würde mich freuen wenn ihr mal drüber schauen könnt. Ich finde es ist teilweise etwas "viel" Code oder man könnte es eleganter lösen.

Code: Alles auswählen

import sys
import tkinter as tk
from datetime import datetime
from functools import partial
from pathlib import Path
from queue import Empty, Queue
from threading import Event, Thread
from tkinter import messagebox, ttk
from tkinter.filedialog import askdirectory

import xlwings as xw
from openpyxl import Workbook, load_workbook
from openpyxl.styles import Alignment

TABLE_NAME = "Messreihe"
WORKBOOK_WITH_READ_DATA = "Ausgelesene Werte.xlsx"
WORKBOOK_WITH_READ_CALCULATED_DATA = "Ausgelesene und berechnete Werte.xlsx"
CALCULATION_WORKBOOK = "Ausl.-progr"
INFO_FILE = "Nicht gelesene Dateien.txt"
ALLOWED_SUFFIXES = [".xlsx", ".xlsm"]
# WORKING_PATH = Path(sys.executable).parent
WORKING_PATH = Path(r"C:\Users\straub\Documents")

# For edit use following syntax:
# custom description: {"protocol": cell-name of protocol, "calculation": cell-name of calculation}
DESCRIPTION_TO_CELL = {
    "Auftragsnummer": {"protocol": "E7", "calculation": "G6"},
    "Code": {"protocol": "X7", "calculation": "G4"},
    "Austritttsdruck 1": {"protocol": "K50", "calculation": None},
    "Wirkleistungsaufnahmen": {"protocol": "K68", "calculation": None,},
    "Summe Normvolumenstrom [Nm³/h]": {"protocol": "L119", "calculation": None},
    "Neu berechneter Normvolumenstrom [Nm³/h]": {"protocol": None, "calculation": None},
    "Differenz Normvolumenstrom [Nm³/h]": {"protocol": None, "calculation": None},
    "Differenz Normvolumenstrom [%]": {"protocol": None, "calculation": None},
}


class App(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        ttk.Label(self, text="Ausgewählter Ordner:").grid(
            row=0, column=0, sticky=tk.W, ipadx=30
        )
        self.user_select = tk.StringVar()
        self.user_select.set("<Kein Oderne ausgewählt>")
        self.selected_folder = ttk.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.Button(
            self, text="Auslesen", command=partial(self.transfer_gui_data, False)
        ).grid(row=5, column=1, sticky=tk.E)
        ttk.Button(
            self,
            text="Auslesen & Berechnen",
            command=partial(self.transfer_gui_data, True),
        ).grid(row=5, column=2, sticky=tk.E)
        ttk.Button(self, text="Beenden", command=self.stop_program).grid(
            row=6, column=2, sticky=tk.E
        )
        self.process_status = tk.StringVar()
        ttk.Label(self, textvariable=self.process_status, font=("Arial", 15)).grid(
            row=2, column=1, columnspan=2, sticky=tk.E
        )

        self.cancel = Event()
        self.process = Queue()
        self.calculation = None
        self.folder_path = None

    def on_progress(self, percentage):
        self.process.put(percentage)

    def open_dialog(self):
        self.folder_path = Path(askdirectory(mustexist=True))
        self.user_select.set(self.folder_path.name)
        self.process_status.set("Bereit zum lesen 🤓")

    @staticmethod
    def show_info_file_message():
        messagebox.showinfo(
            "Info Datei",
            "Nicht alle gefundenen Dateien konnten gelesen werden, bitte Info-Datei beachten.",
        )

    def stop_program(self):
        self.cancel.set()
        self.quit()

    def update_process(self):
        try:
            percent = self.process.get(block=False)
        except Empty:
            pass
        else:
            if percent == 100:
                self.process_status.set("Fertig! 😎")
                return
            try:
                int(percent)
                self.process_status.set(f"Time for Coffee ☕: {percent:.0f} %")
            except ValueError:
                self.process_status.set("Auf Excel warten 😴 ")
        self.after(10, self.update_process)

    def transfer_gui_data(self, calculation_state):
        Thread(
            target=control_program_sequence,
            args=[
                self.folder_path,
                self.cancel,
                self.show_info_file_message,
                calculation_state,
                self.on_progress,
            ],
        ).start()
        self.update_process()


def calculate_actually_volume(workbook, cell_content):
    for index, cell in enumerate(DESCRIPTION_TO_CELL):
        if DESCRIPTION_TO_CELL[cell]["calculation"] is not None:
            workbook.sheets["Berechnungsblatt"].range(
                DESCRIPTION_TO_CELL[cell]["calculation"]
            ).value = cell_content[index]
    return workbook.sheets["Berechnungsblatt"].range("O33").value


def calculate_volume_difference(cell_contents):
    try:
        old_volume = float(cell_contents[-2])
        new_volume = float(cell_contents[-1])
        difference = abs(old_volume - new_volume)
        return difference, 100 * difference / old_volume
    except TypeError:
        return "-" * 2


def collect_files(folder_path):
    return [
        file
        for file in folder_path.iterdir()
        if file.is_file()
        and file.suffix in ALLOWED_SUFFIXES
        and file.name.split("_")[-1] != WORKBOOK_WITH_READ_DATA
    ]


def control_program_sequence(
    folder_path,
    cancel,
    show_info_file_message,
    calculation,
    on_progress=lambda percentage: None,
):
    info_file = False
    timestamp = datetime.now().strftime("%Y%m%d_%H%M")
    workbook, output_file = open_output_file(timestamp, calculation)
    worksheet = workbook.active
    write_column_description(worksheet)
    if calculation:
        on_progress("-")
        calculation_workbook = search_calculation_file()
        with xw.App(visible=False, add_book=False) as excel_app:
            excel_app.display_alerts = False
            excel_calculation = excel_app.books.open(
                calculation_workbook, update_links=False
            )
            control_read_write_data(
                folder_path,
                cancel,
                show_info_file_message,
                worksheet,
                workbook,
                timestamp,
                output_file,
                info_file,
                on_progress,
                excel_calculation,
            )
    else:
        control_read_write_data(
            folder_path,
            cancel,
            show_info_file_message,
            worksheet,
            workbook,
            timestamp,
            output_file,
            info_file,
            on_progress,
        )


def control_read_write_data(
    folder_path,
    cancel,
    show_info_file_message,
    worksheet,
    workbook,
    timestamp,
    output_file,
    info_file,
    on_progress=lambda percentage: None,
    excel_calculation=None,
):
    files = collect_files(folder_path)
    for number, filename in enumerate(files, 1):
        if cancel.is_set():
            break
        on_progress(number / len(files) * 100)
        cell_contents = read_excel_file(filename)
        if cell_contents:
            if excel_calculation is not None:
                cell_contents.append(
                    calculate_actually_volume(excel_calculation, cell_contents)
                )
                cell_contents.extend(iter(calculate_volume_difference(cell_contents)))
            write_into_output_file(worksheet, cell_contents)
        else:
            write_info_file(timestamp, filename)
            info_file = True
    workbook.save(output_file)
    if info_file:
        show_info_file_message()


def search_calculation_file():
    for file in WORKING_PATH.iterdir():
        if CALCULATION_WORKBOOK in file.name:
            return file


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


def read_excel_file(filename):
    try:
        worksheet = load_workbook(filename, data_only=True)[TABLE_NAME]
    except KeyError:
        return []
    return [
        worksheet[DESCRIPTION_TO_CELL[description]["protocol"]].value
        for description in DESCRIPTION_TO_CELL
        if DESCRIPTION_TO_CELL[description]["protocol"] is not None
    ]


def write_column_description(worksheet):
    for column, description in enumerate(DESCRIPTION_TO_CELL, 1):
        cell = worksheet.cell(
            row=1,
            column=column,
        )
        cell.alignment = Alignment(textRotation=90)
        cell.value = description


def write_info_file(timestamp, file):
    with (WORKING_PATH / f"{timestamp}_{INFO_FILE}").open(
        "a", encoding="UTF-8"
    ) as info_file:
        info_file.write(
            f'-> Datei "{file}" konnte nicht nach gegebenen Kriterien ausgelesen werden.\n'
        )


def write_into_output_file(worksheet, cell_contents):
    worksheet.append(cell_contents)


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


if __name__ == "__main__":
    main()
Vielen Dank!

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

WORKING_PATH schein eigentlich `Path.home() / "Documents"` zu sein.
Wenn eine Option gar nicht gebraucht wird, muß sie nicht im Wörterbuch mit None stehen, sondern kann ganz weg bleiben.
`self.selected_folder` wird gar nicht gebraucht.
In self.process sollten nur Daten von einem Typ landen. Diese komischer int-Konvertierungsversuch in update_process ist seltsam. Das würde mit einem `percent is None` viel klarer.
`calculate_actually_volume` berechnet tatsächlich das Volumen, gibt es auch eine Funktion, die nur so tut, als ob?
Du brauchst nur die Values des Wörterbuchs, und statt mit einem Index zu arbeiten, benutze zip:

Code: Alles auswählen

def calculate_total_volume(workbook, cell_content):
    berechnungsblatt = workbook.sheets["Berechnungsblatt"]
    for value, cell_ranges in zip(cell_content, DESCRIPTION_TO_CELL.values()):
        if "calculation" in cell_ranges:
            berechnungsblatt.range(
                cell_ranges["calculation"]
            ).value = value
    return berechnungsblatt.range("O33").value
`calculate_volume_difference` sollte nur einen Typ zurückliefern, nicht mal ein Tuple mit floats und mal ein Tuple mit Strings.

Ist es überhaupt nötig, die Berechnung des Volumens per Excel-Skript zu machen? Wäre das nicht viel einfach in Python zu programmieren?
Benutzeravatar
Dennis89
User
Beiträge: 1154
Registriert: Freitag 11. Dezember 2020, 15:13

Vielen Dank für die schnelle Antwort.
WORKING_PATH schein eigentlich `Path.home() / "Documents"` zu sein.
Das hatte ich vergessen zu schreiben, der ist nur zum testen da, wenn alles passt wird 'WORKING_PATH = Path(sys.executable).parent' genutzt. Aber ja dein Vorschlag hätte ich trotzdem nehmen können/sollen.
Wenn eine Option gar nicht gebraucht wird, muß sie nicht im Wörterbuch mit None stehen, sondern kann ganz weg bleiben.
Kannst du mir da bitte noch mal helfen, was würde daraus dann werden?

Code: Alles auswählen

"Differenz Normvolumenstrom [%]": {"protocol": None, "calculation": None}
Wenn ich 'None' weglasse, dann muss ich auch den Doppelpunkt weglassen und dann wird aus meinem Dictonary ein Set. Wenn ich '{"protocol": None, "calculation": None}' ganz weg lasse, dann habe ich ja auch einen Syntax-Fehler.
`calculate_actually_volume` berechnet tatsächlich das Volumen, gibt es auch eine Funktion, die nur so tut, als ob?
Hoffentlich müsst ihr wenigstens etwas über meine englischen Fähigkeiten grinsen, dann könnte ich sie noch als Stimmungsaufheller tarnen.
calculate_volume_difference` sollte nur einen Typ zurückliefern, nicht mal ein Tuple mit floats und mal ein Tuple mit Strings.
Okay, dann gebe ich im Fehlerfall eine Tuple mit zwei Nullen zurück.

Zur Zeit brauchen wir die Excel noch, darin befinden sich verschiedene Maschineninformationen und auch Anpassungsfaktoren die für die Berechnung notwendig sind. Das ist jetzt nichts, das ein Problem für Python wäre, aber es fehlt an Personal, dass das umsetzt. Da die Excel eh Fehler enthält und nicht mehr wirklich auf dem Stand der Dinge ist, muss sie in Zukunft ersetzt werden. Wann das geändert wird oder ob eher weitere Korrekturfaktoren dazu kommen kann ich nicht sagen.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Benutzeravatar
Kebap
User
Beiträge: 687
Registriert: Dienstag 15. November 2011, 14:20
Wohnort: Dortmund

> Okay, dann gebe ich im Fehlerfall eine Tuple mit zwei Nullen zurück.

Üblicherweise benutzt Python dafür Exceptions.
MorgenGrauen: 1 Welt, 8 Rassen, 13 Gilden, >250 Abenteuer, >5000 Waffen & Rüstungen,
>7000 NPC, >16000 Räume, >200 freiwillige Programmierer, nur Text, viel Spaß, seit 1992.
Benutzeravatar
Dennis89
User
Beiträge: 1154
Registriert: Freitag 11. Dezember 2020, 15:13

Hallo,

ja die benutze ich auch, wenn die Exception eintritt, dann gebe ich jetzt '0, 0' zurück, anstatt den Strings von meinem gezeigten Code.
Die Nullen passen meiner Meinung auch gut im weiteren Programmablauf. Und dass ein Messprotokoll nicht vollständig ausgefüllt ist und es dazu zu der Ausnahme kommt, tritt schon hin und wieder auf.

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

Bei solchen Werten muss man halt aufpassen, dass man die immer von echten Werten unterscheiden kann. Also 0, 0 darf dann niemals als tatsächliches Messwertepaar vorkommen *und* man muss überall prüfen das auch ja nicht mit diesen Werten irgendwo tatsächlich gerechnet wird als wären das gültige Messwerte. Das kann sehr fehleranfällig sein/werden. Für nicht vorhandene Messwerte ist es nicht unüblich `math.nan` als Wert zu verwenden. Das fällt bei Berechnungen wenigstens deutlich auf. Und lässt sich auch in Numpy & Co als Wert verwenden.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
Dennis89
User
Beiträge: 1154
Registriert: Freitag 11. Dezember 2020, 15:13

Okay, den Einwand kann ich nachvollziehen und werde es mir für das nächste Programm merken.
In meinem Fall hier, werden die Nullen erst nach dem auslesen und nach dem rechnen gesetzt.
Wenn beim auslesen Zellen leer sind werden die nicht durch Nullen ersetzt. Wenn meine Berechnung aber nicht funktioniert, dann wird als Ergebnis eine bzw zwei Nullen in die Ausgabedatei geschrieben.

Danke für euren Input.

Grüße
Dennis
"When I got the music, I got a place to go" [Rancid, 1993]
Antworten