CSV-Statistik Viewer.

Python PyQt6 Applikation

Zurück

Im August 2026 hat der Hosting-Provider „neocities.org“ zwei Änderungen an seinem Webseiten-Online-Server durchgeführt. Erstens: die Zwei-Faktor-Authentifizierung (2FA). Schützt dein Online-Konto zusätzlich zum Passwort durch einen zweiten unabhängigen Nachweis. Zweitens: Website-Traffic-Statistiken (Site Traffic Stats). Hier hat man die Möglichkeit, eine CSV-Datei herunterzuladen. Diese CSV-Zahlen eignen sich am besten, um Muster zu erkennen, nicht um die genaue Anzahl der Personen (Besucher) zu bestimmen. Weiterhin baut „neocities.org“ daraus ein Diagramm. Das gefällt mir sehr und ich möchte, in diesem Kapitel, eine ähnliche Software, mit Python (PyQt6) erstellen. Wichtig für mich war auch, den „PyQt6 Python CSV-Statistik-Viewer“ so zu gestalten, dass er für andere Zwecke umgebaut werden kann!


CSV-Statistik Viewer


Das obige Bild zeigt eine Hits /Views /Bandwidth-Statistik für den Zeitstrahl vom 23.11.2025 bis 26.08.206 an. Klickt man auf die Spaltenbeschriftung, wie „Datum“, „Hits“, „Views“ und „Bandbreite“, so wird der Tabelleninhalt auf Maximum oder Minimum von oben nach unten sortiert. Bei der von Neocities bereitgestellten CSV-Statistik handelt es sich um die drei wichtigsten Kernmetriken zur Messung des Datenverkehrs (Web-Traffic) meiner Website. Hits bezeichnet jede einzelne Dateiabfrage, Views misst die echten Seitenaufrufe durch menschliche Besucher, und Bandwidth ist die Datenmenge, die dabei übertragen wird.

Details zu Hits (Treffer):
Jeder einzelne Abruf einer Datei auf meinem Webserver zählt als ein "Hit". Beispiel: Lädt ein Besucher eine einzige HTML-Seite, auf der sich Text, 5 Bilder und 1 CSS-Stylesheet befinden, registriert der Server 7 Hits (1 HTML + 5 Bilder + 1 CSS). Aussagekraft: Diese Zahl ist meistens am höchsten, sagt aber wenig über die tatsächliche Anzahl der echten menschlichen Besucher aus.

Details zu Views (Seitenaufrufe / Pageviews):
Ein "View" wird gezählt, wenn eine vollständige HTML-Seite (ein Dokument) im Browser eines Nutzers geladen wird. Wenn derselbe Besucher die oben genannte Seite aufruft, zählt dies als 1 View, völlig egal, wie viele Bilder oder Skripte auf dieser Seite eingebunden sind. Dies ist die wichtigste Metrik, um das tatsächliche Interesse und die Reichweite meines Websiteinhaltes zu messen.

Details zu Bandwidth (Bandbreite):
Steht für das gesamte Datenvolumen, das in einem bestimmten Zeitraum von meiner Website an den Browser des Besuchers übertragen wurde. Jedes Mal, wenn jemand die Seite aufruft, müssen die darauf enthaltenen Dateien (HTML, CSS, Bilder, Musik, JavaScript) von den Neocities-Servern auf das Gerät des Besuchers geladen werden. Ein Beispiel: Wenn die Startseite (HTML + Bilder) insgesamt 2 Megabyte (MB) groß ist und an einem Tag von 50 Personen aufgerufen wird, beträgt der gemessene Bandwidth-Wert für diesen Tag 100 MB. Maßeinheit: In der CSV-Datei wird dieser Wert standardmäßig in Bytes angegeben. Die Zahl muss also umgerechnet werden, um sie leichter lesbar zu machen. Da die reine Byte-Zahl oft sehr lang und unübersichtlich ist, hilft folgende Faustformel zur Interpretation:
Bytes geteilt durch 1.024 = Kilobytes (KB).
Bytes geteilt durch 1.048.576 (oder zweimal durch 1.024) = Megabytes (MB).
Bytes geteilt durch 1.073.741.824 = Gigabytes (GB).
Bei Neocities ist die monatliche Bandbreite je nach Kontotyp limitiert (kostenlose Accounts haben beispielsweise ein anderes Limit als Supporter-Accounts). Wenn der Wert in der CSV im Auge behalten wird, sieht man rechtzeitig, ob die Seite Gefahr läuft, das monatliche Transferlimit zu überschreiten. Wenn an bestimmten Tagen der Bandwidth-Wert sprunghaft ansteigt, die Besucherzahlen aber gleichbleiben, haben entweder bestehende Nutzer sehr viele Unterseiten durchforstet oder Suchmaschinen-Bots haben die Medien intensiv indexiert.


Python Code
#!/usr/bin/env python3 # -*- coding: utf-8 -*- # ***************************************************************************** # Funktionen: Dark Mode. Tabelle mit 5 Spalten (Datum, Hits, Views, Bandbreite # in MB, Reserve). Sortierbar per Klick auf die Spaltenüberschrift (numerisch/ # Datum, nicht alphabetisch). Eingebettetes Diagramm (Hits & Views im # Zeitverlauf) via matplotlib. Automatisches Laden der zuletzt verwendeten Datei # beim Programmstart. Export/Öffnen der Daten in Excel. Infofeld mit Summen und # Fehlermeldungen. Benötigte Pakete: pip install PyQt6 openpyxl matplotlib # (c) Hans Busche Datum: 30.08.2026 # ***************************************************************************** import sys import csv import os import subprocess from datetime import datetime from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QTableWidget, QTableWidgetItem, QLabel, QFileDialog, QMessageBox, QHeaderView, QSplitter ) from PyQt6.QtGui import QPalette, QColor, QIcon from PyQt6.QtCore import Qt, QSettings try: from openpyxl import Workbook except ImportError: Workbook = None try: from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure import matplotlib.dates as mdates MATPLOTLIB_AVAILABLE = True except ImportError: MATPLOTLIB_AVAILABLE = False # Verzeichnis dieser Datei, damit das Icon unabhängig vom Startort gefunden wird BASE_DIR = os.path.dirname(os.path.abspath(__file__)) ICON_PATH = os.path.join(BASE_DIR, "Schaefer.ico") # ***************************************************************************** # QTableWidgetItem, das bei Sortierung einen echten Vergleichswert. (Zahl oder # Datum) statt des angezeigten Textes nutzt. # ***************************************************************************** class SortableTableWidgetItem(QTableWidgetItem): def __init__(self, text, sort_value): super().__init__(text) self.sort_value = sort_value def __lt__(self, other): if isinstance(other, SortableTableWidgetItem): return self.sort_value < other.sort_value return super().__lt__(other) # ***************************************************************************** # PyQt6 - das Fenster # ***************************************************************************** class CSVViewer(QMainWindow): ORG_NAME = "MeineFirma" APP_NAME = "CSVStatistikViewer" def __init__(self): super().__init__() self.setWindowTitle("CSV Statistik Viewer") self.resize(950, 700) # Icon if os.path.exists(ICON_PATH): self.setWindowIcon(QIcon(ICON_PATH)) # Liste von Tupeln: (date_de, hits, views, bandwidth, date_obj) self.csv_data = [] self.current_csv_path = None self.settings = QSettings(self.ORG_NAME, self.APP_NAME) self.init_ui() self.apply_dark_theme() self.try_auto_load() # ------------------------------------------------------------------ UI def init_ui(self): central = QWidget() self.setCentralWidget(central) layout = QVBoxLayout(central) layout.setSpacing(12) layout.setContentsMargins(15, 15, 15, 15) # --- Buttons --- button_layout = QHBoxLayout() self.btn_load = QPushButton("CSV-Datei einlesen") self.btn_load.setStyleSheet(""" QPushButton { background-color: #2d7dd2; color: white; font-weight: bold; font-size: 14px; padding: 10px 20px; border-radius: 6px; border: none; } QPushButton:hover { background-color: #3b8fe6; } QPushButton:pressed { background-color: #1f5fa0; } """) self.btn_load.clicked.connect(self.load_csv_dialog) self.btn_export = QPushButton("In Excel öffnen") self.btn_export.setStyleSheet(""" QPushButton { background-color: #2e8b57; color: white; font-weight: bold; font-size: 14px; padding: 10px 20px; border-radius: 6px; border: none; } QPushButton:hover { background-color: #3aa76d; } QPushButton:pressed { background-color: #226b41; } """) self.btn_export.clicked.connect(self.export_to_excel) button_layout.addWidget(self.btn_load) button_layout.addWidget(self.btn_export) button_layout.addStretch() layout.addLayout(button_layout) # --- Tabelle (5 Spalten, letzte ist Reserve) --- self.table = QTableWidget() self.table.setColumnCount(5) self.table.setHorizontalHeaderLabels( ["Datum", "Hits", "Views", "Bandbreite (MB)", "Reserve"] ) self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) self.table.setSortingEnabled(True) # Sortierung per Klick auf Kopfzeile # --- Diagramm --- if MATPLOTLIB_AVAILABLE: self.figure = Figure(figsize=(5, 3)) self.canvas = FigureCanvas(self.figure) self.ax = self.figure.add_subplot(111) self.style_chart_axes() else: self.canvas = QLabel( "matplotlib ist nicht installiert.\nBitte installieren mit: pip install matplotlib" ) self.canvas.setAlignment(Qt.AlignmentFlag.AlignCenter) self.canvas.setStyleSheet("color: #e0e0e0;") # Tabelle und Diagramm in einem Splitter, damit beides sichtbar # und die Aufteilung frei verschiebbar ist splitter = QSplitter(Qt.Orientation.Vertical) splitter.addWidget(self.table) splitter.addWidget(self.canvas) splitter.setStretchFactor(0, 1) splitter.setStretchFactor(1, 1) layout.addWidget(splitter) # --- Infofeld am Fußende --- self.info_label = QLabel("Noch keine Daten geladen.") self.info_label.setWordWrap(True) self.info_label.setStyleSheet(""" QLabel { background-color: #2b2b2b; color: #e0e0e0; border: 1px solid #444; border-radius: 6px; padding: 10px; font-size: 13px; } """) layout.addWidget(self.info_label) def apply_dark_theme(self): self.setStyleSheet(""" QMainWindow { background-color: #1e1e1e; } QTableWidget { background-color: #252526; color: #e0e0e0; gridline-color: #444; selection-background-color: #3b8fe6; selection-color: white; border: 1px solid #444; } QHeaderView::section { background-color: #333333; color: #e0e0e0; padding: 6px; border: 1px solid #444; font-weight: bold; } QTableWidget::item { padding: 4px; } QSplitter::handle { background-color: #444; } """) palette = QPalette() palette.setColor(QPalette.ColorRole.Window, QColor(30, 30, 30)) palette.setColor(QPalette.ColorRole.WindowText, QColor(224, 224, 224)) palette.setColor(QPalette.ColorRole.Base, QColor(37, 37, 38)) palette.setColor(QPalette.ColorRole.Text, QColor(224, 224, 224)) self.setPalette(palette) def style_chart_axes(self): """Dunkles Farbschema für das eingebettete Diagramm.""" self.figure.patch.set_facecolor("#1e1e1e") self.ax.set_facecolor("#252526") self.ax.tick_params(colors="#e0e0e0") for spine in self.ax.spines.values(): spine.set_color("#555555") self.ax.xaxis.label.set_color("#e0e0e0") self.ax.yaxis.label.set_color("#e0e0e0") self.ax.title.set_color("#e0e0e0") self.ax.grid(True, color="#3a3a3a", linewidth=0.6) # --------------------------------------------------------------- Logik @staticmethod def parse_date(date_str: str): """Wandelt 'YYYY-MM-DD' in ein datetime-Objekt und den deutschen Anzeigetext 'DD.MM.YYYY' um.""" dt = datetime.strptime(date_str.strip(), "%Y-%m-%d") return dt, dt.strftime("%d.%m.%Y") # 1 MB = 1.000.000 Byte (dezimal, wie bei Netzwerk-/Traffic-Statistiken üblich) BYTES_PER_MB = 1_000_000 @classmethod def bytes_to_mb(cls, bandwidth_bytes: int) -> float: """Rechnet Byte in Megabyte um.""" return bandwidth_bytes / cls.BYTES_PER_MB @staticmethod def format_de_number(value: float, decimals: int = 2) -> str: """Formatiert eine Zahl im deutschen Format (Punkt als Tausender-, Komma als Dezimaltrennzeichen), z. B. 1234.5 -> '1.234,50'.""" formatted = f"{value:,.{decimals}f}" formatted = formatted.replace(",", "X").replace(".", ",").replace("X", ".") return formatted def load_csv_dialog(self): path, _ = QFileDialog.getOpenFileName( self, "CSV-Datei auswählen", "", "CSV-Dateien (*.csv);;Alle Dateien (*)" ) if not path: return self.load_csv_from_path(path) def try_auto_load(self): """Lädt beim Programmstart automatisch die zuletzt verwendete Datei. Falls noch keine bekannt ist, wird versucht, eine Beispieldatei im Programmordner zu finden.""" last_path = self.settings.value("last_csv_path", "", type=str) if last_path and os.path.isfile(last_path): self.load_csv_from_path(last_path, silent_missing=True) return script_dir = os.path.dirname(os.path.abspath(__file__)) default_path = os.path.join(script_dir, "beispiel_statistik.csv") if os.path.isfile(default_path): self.load_csv_from_path(default_path, silent_missing=True) def load_csv_from_path(self, path, silent_missing=False): rows = [] errors = [] try: with open(path, newline="", encoding="utf-8") as f: reader = csv.reader(f) header = next(reader, None) if header is None: raise ValueError("Die CSV-Datei ist leer.") # beliebig viele Zeilen möglich for line_num, row in enumerate(reader, start=2): if not row or all(not cell.strip() for cell in row): continue if len(row) < 4: errors.append(f"Zeile {line_num}: zu wenige Spalten ({row})") continue try: day_raw, hits_raw, views_raw, bandwidth_raw = [c.strip() for c in row[:4]] date_obj, date_de = self.parse_date(day_raw) hits = int(hits_raw) views = int(views_raw) bandwidth = int(bandwidth_raw) rows.append((date_de, hits, views, bandwidth, date_obj)) except ValueError as e: errors.append(f"Zeile {line_num}: ungültiges Format ({e})") except Exception as e: if not silent_missing: QMessageBox.critical(self, "Fehler beim Einlesen", str(e)) self.info_label.setText(f"Fehler beim Einlesen der Datei:\n{e}") return self.csv_data = rows self.current_csv_path = path self.settings.setValue("last_csv_path", path) self.populate_table(rows) self.update_chart(rows) self.update_info(rows, errors) def populate_table(self, rows): self.table.setSortingEnabled(False) # während des Befüllens deaktivieren self.table.setRowCount(len(rows)) for row_idx, (date_de, hits, views, bandwidth, date_obj) in enumerate(rows): date_item = SortableTableWidgetItem(date_de, date_obj) hits_item = SortableTableWidgetItem(str(hits), hits) views_item = SortableTableWidgetItem(str(views), views) bandwidth_mb = self.bytes_to_mb(bandwidth) bandwidth_item = SortableTableWidgetItem( f"{self.format_de_number(bandwidth_mb)} MB", bandwidth_mb ) reserve_item = QTableWidgetItem("") for item in (date_item, hits_item, views_item, bandwidth_item, reserve_item): item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) self.table.setItem(row_idx, 0, date_item) self.table.setItem(row_idx, 1, hits_item) self.table.setItem(row_idx, 2, views_item) self.table.setItem(row_idx, 3, bandwidth_item) self.table.setItem(row_idx, 4, reserve_item) self.table.setSortingEnabled(True) # danach wieder aktivieren def update_chart(self, rows): if not MATPLOTLIB_AVAILABLE: return self.ax.clear() self.style_chart_axes() if not rows: self.canvas.draw() return # Für das Diagramm immer chronologisch sortieren, # unabhängig von der aktuellen Tabellensortierung sorted_rows = sorted(rows, key=lambda r: r[4]) dates = [r[4] for r in sorted_rows] hits = [r[1] for r in sorted_rows] views = [r[2] for r in sorted_rows] n = len(dates) # Darstellung an die Datenmenge anpassen, damit große Datensätze # (z. B. > 50 Zeilen) nicht durch zu viele Punkte/Marker unleserlich werden if n <= 30: show_marker = "o" marker_size = 5 line_width = 1.6 marker_every = 1 elif n <= 100: show_marker = "o" marker_size = 3 line_width = 1.2 # nur an ca. 25 Stellen einen Marker zeichnen marker_every = max(1, n // 25) else: show_marker = None # bei sehr vielen Punkten ganz ohne Marker marker_size = 0 line_width = 1.0 marker_every = 1 self.ax.plot( dates, hits, marker=show_marker, markersize=marker_size, markevery=marker_every, linewidth=line_width, color="#3b8fe6", label="Hits", ) self.ax.plot( dates, views, marker=show_marker, markersize=marker_size, markevery=marker_every, linewidth=line_width, color="#3aa76d", label="Views", ) self.ax.set_title("Hits & Views im Zeitverlauf") self.ax.set_xlabel("Datum") self.ax.set_ylabel("Anzahl") # X-Achse: Anzahl der Datumsbeschriftungen begrenzen, damit sie sich # bei vielen Zeilen nicht überlappen locator = mdates.AutoDateLocator(minticks=5, maxticks=12) self.ax.xaxis.set_major_locator(locator) self.ax.xaxis.set_major_formatter(mdates.DateFormatter("%d.%m.%Y")) self.figure.autofmt_xdate(rotation=45) legend = self.ax.legend(facecolor="#252526", edgecolor="#555555") for text in legend.get_texts(): text.set_color("#e0e0e0") self.figure.tight_layout() self.canvas.draw() def update_info(self, rows, errors=None): errors = errors or [] sum_hits = sum(r[1] for r in rows) sum_views = sum(r[2] for r in rows) sum_bandwidth_mb = self.bytes_to_mb(sum(r[3] for r in rows)) text = ( f"Summe Hits: {sum_hits} | Summe Views: {sum_views} | " f"Summe Bandbreite: {self.format_de_number(sum_bandwidth_mb)} MB | " f"Zeilen: {len(rows)}" ) if self.current_csv_path: text += f"\nAktuelle Datei: {self.current_csv_path}" if errors: text += "\n\nFehler beim Einlesen einzelner Zeilen:\n" + "\n".join(errors) self.info_label.setText(text) def export_to_excel(self): if not self.csv_data: msg = "Es wurden noch keine CSV-Daten geladen." QMessageBox.warning(self, "Keine Daten", msg) self.info_label.setText(f"Fehler: {msg}") return if Workbook is None: msg = "Das Modul 'openpyxl' ist nicht installiert.\nBitte installieren mit: pip install openpyxl" QMessageBox.critical(self, "Fehlendes Modul", msg) self.info_label.setText(f"Fehler: {msg}") return save_path, _ = QFileDialog.getSaveFileName( self, "Excel-Datei speichern", "statistik.xlsx", "Excel-Dateien (*.xlsx)" ) if not save_path: return try: wb = Workbook() ws = wb.active ws.title = "Statistik" ws.append(["Datum", "Hits", "Views", "Bandbreite (MB)", "Reserve"]) for date_de, hits, views, bandwidth, _date_obj in self.csv_data: bandwidth_mb = round(self.bytes_to_mb(bandwidth), 2) ws.append([date_de, hits, views, bandwidth_mb, ""]) # Spaltenbreite grob an Inhalt anpassen for col_cells in ws.columns: max_len = max((len(str(c.value)) if c.value is not None else 0) for c in col_cells) ws.column_dimensions[col_cells[0].column_letter].width = max_len + 4 wb.save(save_path) except Exception as e: QMessageBox.critical(self, "Fehler beim Speichern", str(e)) self.info_label.setText(f"Fehler beim Speichern der Excel-Datei:\n{e}") return self.open_in_excel(save_path) sum_hits = sum(r[1] for r in self.csv_data) sum_views = sum(r[2] for r in self.csv_data) self.info_label.setText( f"Summe Hits: {sum_hits} | Summe Views: {sum_views} | " f"Datei gespeichert: {save_path}" ) def open_in_excel(self, path): """Öffnet die gespeicherte Datei mit Excel (bzw. dem Standardprogramm).""" try: if sys.platform.startswith("win"): os.startfile(path) # öffnet Excel unter Windows direkt elif sys.platform == "darwin": subprocess.Popen(["open", "-a", "Microsoft Excel", path]) else: subprocess.Popen(["xdg-open", path]) except Exception as e: msg = f"Datei wurde gespeichert, konnte aber nicht automatisch in Excel geöffnet werden:\n{e}" QMessageBox.warning(self, "Excel konnte nicht geöffnet werden", msg) self.info_label.setText(f"Fehler: {msg}") def main(): app = QApplication(sys.argv) window = CSVViewer() window.show() sys.exit(app.exec()) if __name__ == "__main__": main() # ***************************************************************************** # Ende # *****************************************************************************


Grundidee:
Das Skript ist eine PyQt6-Desktop-Anwendung mit einer Klasse CSVViewer(QMainWindow), die das gesamte Fenster darstellt. Zusätzlich gibt es eine kleine Hilfsklasse SortableTableWidgetItem. Ganz unten sorgt main() dafür, dass die Anwendung gestartet wird.

Bausteine am Dateianfang:
Imports: PyQt6 für die Oberfläche, csv zum Einlesen, openpyxl für den Excel-Export, matplotlib für das Diagramm. Die Imports von openpyxl und matplotlib stehen in try/except-Blöcken. Sind sie nicht installiert, stürzt das Programm nicht ab, sondern zeigt später eine verständliche Fehlermeldung statt der Funktion.

SortableTableWidgetItem:
Normalerweise sortiert eine QTableWidget-Spalte alphabetisch als Text – dann würde z. B. "70,44 MB" vor "9,10 MB" einsortiert werden. Diese Klasse merkt sich zusätzlich zum Anzeigetext einen echten Vergleichswert (Zahl oder Datum) und überschreibt __lt__ (den "kleiner als"-Operator), damit beim Klick auf eine Spaltenüberschrift numerisch bzw. chronologisch sortiert wird.

CSVViewer.__init__:
Legt Titel/Größe des Fensters fest, initialisiert die Datenspeicher (self.csv_data, self.current_csv_path) und QSettings (speichert Programmeinstellungen dauerhaft, z. B. den zuletzt geöffneten Dateipfad, damit sie den Neustart überdauern). Danach werden drei Dinge nacheinander aufgerufen:
init_ui() – baut die Oberfläche auf
apply_dark_theme() – Dark Mode
try_auto_load() – lädt beim Start automatisch die letzte Datei


init_ui() – der Fensteraufbau. Von oben nach unten:
Buttons: "CSV-Datei einlesen" und "In Excel öffnen", jeweils mit eigenem Stylesheet (Farbe, Hover-/Pressed-Effekt) und per .clicked.connect(...) mit ihrer jeweiligen Methode verknüpft. Tabelle: QTableWidget mit 5 Spalten, setSortingEnabled(True) aktiviert die Kopfzeilen-Sortierung. Diagramm: Ein matplotlib-Figure/Canvas-Objekt, eingebettet als normales Qt-Widget. Tabelle und Diagramm liegen in einem QSplitter (vertikal), damit du die Höhe beider Bereiche per Maus verschieben kannst. Infofeld: ein QLabel ganz unten für Summen und Fehlermeldungen.

apply_dark_theme():
Setzt globale Stylesheets sowie eine QPalette mit dunklen Farbwerten für Fenster, Tabelle und Kopfzeilen. style_chart_axes() macht dasselbe für das matplotlib-Diagramm (Hintergrund, Achsenfarben, Gitterlinien).

Der Kernablauf beim Laden einer CSV:
load_csv_dialog() / try_auto_load() – öffnen entweder einen Dateidialog oder ermitteln automatisch den letzten Pfad (aus QSettings) bzw. eine Beispieldatei im Programmordner. Beide rufen am Ende load_csv_from_path(path) auf. load_csv_from_path() liest die Datei zeilenweise mit dem csv-Modul (beliebig viele Zeilen möglich). Pro Zeile: Datum wird über parse_date() von YYYY-MM-DD in ein datetime-Objekt und den deutschen Anzeigetext DD.MM.YYYY umgewandelt. Hits, Views, Bandbreite werden zu Ganzzahlen konvertiert. Fehlerhafte Zeilen (zu wenige Spalten, ungültige Zahlen) landen nicht im Ergebnis, sondern in einer errors-Liste, die später im Infofeld angezeigt wird – das Programm bricht dabei nicht ab. Am Ende wird der Pfad in QSettings gespeichert, damit er beim nächsten Start automatisch geladen wird. Anschließend werden drei Anzeige-Methoden mit den eingelesenen Daten gefüttert: populate_table(), update_chart(), update_info().

populate_table():
Füllt die Tabelle zeilenweise. Wichtig: setSortingEnabled(False) vor und True nach dem Befüllen – sonst würde Qt während des Einfügens ständig neu sortieren und Zeilen könnten durcheinandergeraten. Die Bandbreite wird hier über bytes_to_mb() in MB umgerechnet und mit format_de_number() als deutsche Zahl (Komma statt Punkt) formatiert; als Sortierwert dient trotzdem die echte Fließkommazahl.

update_chart():
Sortiert die Daten immer chronologisch (unabhängig von der Tabellensortierung) und zeichnet zwei Linien (Hits, Views) über die Zeit. Je nach Zeilenanzahl passt es Linienstärke/Marker automatisch an (volle Marker bei ≤30 Zeilen, ausgedünnte Marker bei ≤100, gar keine Marker darüber), damit das Diagramm bei vielen Datenpunkten nicht "zuklebt". AutoDateLocator begrenzt zusätzlich die Anzahl der Datumsbeschriftungen auf der X-Achse.

update_info():
Berechnet Summe Hits, Summe Views und Summe Bandbreite (in MB) und schreibt sie zusammen mit der Zeilenanzahl, dem aktuellen Dateipfad und eventuellen Fehlermeldungen ins Infofeld.

export_to_excel() und open_in_excel():
Erstellt mit openpyxl eine neue Arbeitsmappe, schreibt Kopfzeile und Daten (Bandbreite ebenfalls in MB), passt grob die Spaltenbreite an und speichert die Datei über einen Speichern-Dialog. open_in_excel() öffnet die gespeicherte Datei danach automatisch – je nach Betriebssystem unterschiedlich (os.startfile unter Windows, open -a "Microsoft Excel" unter macOS, xdg-open unter Linux).

Ein kleiner Hinweis am Rande: Am Ende von export_to_excel() wird das Infofeld noch einmal aktualisiert, aber ohne die Bandbreiten-Summe (die steht nur nach dem CSV-Import drin, nicht nach dem Excel-Export). Das ist rein kosmetisch.


Zurück