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!
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.
#!/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.
PyQt6 CSV-Statistik Viewer. Ohne Gewähr © 2026 Hans Busche