Initial commit: CSV Processor
This commit is contained in:
commit
a2a9d7ac5a
|
|
@ -0,0 +1 @@
|
||||||
|
gAAAAABpah9sgktIo6WojeILJBTHfR6Y65YiGuG0qoAK_wkpSOJpY133w3Qhh_4zqQPY8sHOUAOL9HFXUWkBA7kBFFZAYMDIxETI-Af7l5LIyNiPhdZXzBI=
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Application specific
|
||||||
|
csv_processor_config/
|
||||||
|
|
||||||
|
# NICHT ignorieren: dist/ mit .deb-Datei
|
||||||
|
# !dist/
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Sebastian Zell
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
# CSV-Processor v2.0
|
||||||
|
|
||||||
|
Professionelles Werkzeug zur Verarbeitung und Konvertierung von CSV- und Excel-Dateien.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
✅ **Import** von CSV, Excel (XLSX/XLS)
|
||||||
|
✅ **Export** als CSV, Excel (XLSX), OpenDocument (ODS)
|
||||||
|
✅ **Spaltennamen-Mapping** (z.B. "btr" → "Betrag")
|
||||||
|
✅ **Datenbereinigung** (leere Zeilen/Spalten entfernen)
|
||||||
|
✅ **Spaltenauswahl** (nur gewünschte Spalten exportieren)
|
||||||
|
✅ **Sortierung** nach beliebigen Spalten
|
||||||
|
✅ **Voreinstellungen** für wiederkehrende Aufgaben
|
||||||
|
|
||||||
|
## Programmversionen
|
||||||
|
|
||||||
|
### CLI-Version (Terminal)
|
||||||
|
- **Für wen:** Fortgeschrittene Benutzer, Automatisierung
|
||||||
|
- **Vorteile:** Schnell, scriptfähig, volle Kontrolle
|
||||||
|
- **Datei:** `csv_processor.py`
|
||||||
|
|
||||||
|
### GUI-Version (Grafisch)
|
||||||
|
- **Für wen:** Alle Benutzer, besonders Einsteiger
|
||||||
|
- **Vorteile:** Intuitiv, visuelle Vorschau, einfache Bedienung
|
||||||
|
- **Datei:** `csv_processor_gui.py`
|
||||||
|
|
||||||
|
## Systemanforderungen
|
||||||
|
|
||||||
|
- Python 3.7 oder höher
|
||||||
|
- Tkinter (für GUI-Version, normalerweise in Python enthalten)
|
||||||
|
- Optional: openpyxl (für Excel-Support)
|
||||||
|
- Optional: odfpy (für ODS-Support)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Option 1: Debian-Paket (.deb)
|
||||||
|
|
||||||
|
Für Debian/Ubuntu-basierte Systeme steht ein fertiges Paket bereit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dpkg -i dist/csv-processor-debian.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
Nach der Installation:
|
||||||
|
- CLI-Version: `csv-processor-cli`
|
||||||
|
- GUI-Version: `csv-processor-gui` oder über Anwendungsmenü
|
||||||
|
|
||||||
|
### Option 2: Direkt mit Python ausführen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Abhängigkeiten installieren
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
# CLI-Version starten
|
||||||
|
python csv_processor.py
|
||||||
|
|
||||||
|
# GUI-Version starten
|
||||||
|
python csv_processor_gui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Nutzung
|
||||||
|
|
||||||
|
### CLI-Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python csv_processor.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Sie sehen das Hauptmenü:
|
||||||
|
```
|
||||||
|
╔════════════════════════════════════════════════════════════════╗
|
||||||
|
║ CSV-PROCESSOR v2.0 - Dateiverarbeitung ║
|
||||||
|
╚════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
1. Neue Datei verarbeiten
|
||||||
|
2. Voreinstellungen verwalten
|
||||||
|
0. Beenden
|
||||||
|
```
|
||||||
|
|
||||||
|
**Schritt-für-Schritt:**
|
||||||
|
1. Quelldatei auswählen (CSV oder Excel)
|
||||||
|
2. CSV-Einstellungen konfigurieren (Encoding, Delimiter)
|
||||||
|
3. Optional: Mapping-Datei für Spaltennamen laden
|
||||||
|
4. Leere Zeilen/Spalten entfernen
|
||||||
|
5. Spalten auswählen
|
||||||
|
6. Optional: Sortierung, Kopf-/Fußzeile
|
||||||
|
7. Ausgabeformat wählen und speichern
|
||||||
|
8. Optional: Als Voreinstellung speichern
|
||||||
|
|
||||||
|
### GUI-Version
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python csv_processor_gui.py
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Datei laden:** Klick auf [Durchsuchen] und Datei auswählen
|
||||||
|
2. **Einstellungen:** Checkboxen für Kopfzeile, leere Zeilen/Spalten
|
||||||
|
3. **Spalten:** Im Bereich "Spaltenauswahl" gewünschte Spalten markieren
|
||||||
|
4. **Vorschau:** Optional [Vorschau] klicken
|
||||||
|
5. **Speichern:** [Verarbeiten & Speichern] klicken
|
||||||
|
|
||||||
|
### Spaltenauswahl (CLI)
|
||||||
|
|
||||||
|
| Eingabe | Bedeutung |
|
||||||
|
|---------|-------------------------------|
|
||||||
|
| `q` | Fertig |
|
||||||
|
| `alle` | Alle auswählen |
|
||||||
|
| `keine` | Alle abwählen |
|
||||||
|
| `3` | Spalte 3 umschalten |
|
||||||
|
| `1,2,3` | Spalten 1,2,3 umschalten |
|
||||||
|
| `+5` | Spalte 5 ANwählen |
|
||||||
|
| `-5` | Spalte 5 ABwählen |
|
||||||
|
| `1-10` | Spalten 1-10 umschalten |
|
||||||
|
| `+1-10` | Spalten 1-10 ANwählen |
|
||||||
|
| `-1-10` | Spalten 1-10 ABwählen |
|
||||||
|
|
||||||
|
## Mapping-Dateien
|
||||||
|
|
||||||
|
Mapping-Dateien übersetzen technische Spaltennamen in lesbare Namen:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"btr": "Betrag",
|
||||||
|
"dat": "Datum",
|
||||||
|
"kto": "Kontonummer",
|
||||||
|
"empf": "Empfänger"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Speichern Sie diese als `.json`-Datei und laden Sie sie im Programm.
|
||||||
|
|
||||||
|
## Voreinstellungen
|
||||||
|
|
||||||
|
Voreinstellungen speichern Ihre Konfiguration für wiederkehrende Aufgaben:
|
||||||
|
- Kopfzeile ja/nein
|
||||||
|
- Leere Zeilen/Spalten entfernen
|
||||||
|
- Mapping-Datei
|
||||||
|
- Spaltenauswahl
|
||||||
|
- Ausgabeformat
|
||||||
|
- CSV-Export-Einstellungen
|
||||||
|
- Sortierung
|
||||||
|
|
||||||
|
Voreinstellungen werden im Verzeichnis `csv_processor_config/` gespeichert.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
### Linux-Executable erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_linux.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Erstellt eine standalone-Executable in `dist/`.
|
||||||
|
|
||||||
|
### Debian-Paket erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_debian.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Erstellt ein `.deb`-Paket für Debian/Ubuntu.
|
||||||
|
|
||||||
|
### Windows-Executable erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build_windows.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Oder unter Linux mit Wine:
|
||||||
|
```bash
|
||||||
|
./build_windows_wine.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Projektstruktur
|
||||||
|
|
||||||
|
```
|
||||||
|
csv-processor/
|
||||||
|
├── csv_processor.py # CLI-Version
|
||||||
|
├── csv_processor_gui.py # GUI-Version
|
||||||
|
├── requirements.txt # Python-Abhängigkeiten
|
||||||
|
├── README.md # Diese Datei
|
||||||
|
├── LICENSE # MIT-Lizenz
|
||||||
|
├── build_linux.sh # Build-Script Linux
|
||||||
|
├── build_debian.sh # Build-Script Debian-Paket
|
||||||
|
├── build_windows.bat # Build-Script Windows
|
||||||
|
└── dist/ # Fertige Pakete
|
||||||
|
└── csv-processor-debian.deb
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tipps & Tricks
|
||||||
|
|
||||||
|
### Große Dateien
|
||||||
|
- Bei >10.000 Zeilen: 5-30 Sekunden Verarbeitungszeit
|
||||||
|
- Bei >100.000 Zeilen: CLI-Version ist schneller
|
||||||
|
- Leere Spalten werden vor der Spaltenauswahl entfernt
|
||||||
|
|
||||||
|
### CSV-Export für Excel
|
||||||
|
- Delimiter: Semikolon (;) für deutsche Excel-Versionen
|
||||||
|
- Quoting: Minimal
|
||||||
|
|
||||||
|
### CSV-Export für Datenbanken
|
||||||
|
- Delimiter: Tab
|
||||||
|
- Quoting: Minimal
|
||||||
|
- Keine Kopf-/Fußzeile
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
MIT License - siehe [LICENSE](LICENSE)
|
||||||
|
|
||||||
|
## Autor
|
||||||
|
|
||||||
|
Sebastian Zell <sebastian.zell@zell-aufmass.de>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version:** 2.0
|
||||||
|
**Stand:** Januar 2026
|
||||||
|
|
@ -0,0 +1,94 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Build-Script für Debian-Paket (.deb)
|
||||||
|
# Erstellt ein installierbares Debian-Paket
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
PKG_NAME="csv-processor"
|
||||||
|
PKG_VERSION="2.0.0"
|
||||||
|
PKG_ARCH="amd64"
|
||||||
|
MAINTAINER="Sebastian Zell <sebastian.zell@zell-aufmass.de>"
|
||||||
|
DESCRIPTION="Professionelles Werkzeug zur Verarbeitung von CSV- und Excel-Dateien"
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "CSV-Processor - Debian Package Build"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Prüfe ob PyInstaller installiert ist
|
||||||
|
if ! command -v pyinstaller &> /dev/null; then
|
||||||
|
echo "PyInstaller nicht gefunden. Installiere..."
|
||||||
|
pip install pyinstaller
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build-Verzeichnis vorbereiten
|
||||||
|
echo "Bereite Build-Verzeichnis vor..."
|
||||||
|
BUILD_DIR="deb_build"
|
||||||
|
rm -rf "$BUILD_DIR" 2>/dev/null || true
|
||||||
|
mkdir -p "$BUILD_DIR/DEBIAN"
|
||||||
|
mkdir -p "$BUILD_DIR/usr/bin"
|
||||||
|
mkdir -p "$BUILD_DIR/usr/share/applications"
|
||||||
|
mkdir -p "$BUILD_DIR/usr/share/$PKG_NAME"
|
||||||
|
|
||||||
|
# Executables erstellen
|
||||||
|
echo ""
|
||||||
|
echo "Erstelle Executables mit PyInstaller..."
|
||||||
|
|
||||||
|
# CLI-Version
|
||||||
|
pyinstaller --onefile --name csv-processor-cli --clean csv_processor.py
|
||||||
|
cp dist/csv-processor-cli "$BUILD_DIR/usr/bin/"
|
||||||
|
|
||||||
|
# GUI-Version
|
||||||
|
pyinstaller --onefile --name csv-processor-gui --clean csv_processor_gui.py
|
||||||
|
cp dist/csv-processor-gui "$BUILD_DIR/usr/bin/"
|
||||||
|
|
||||||
|
# Rechte setzen
|
||||||
|
chmod 755 "$BUILD_DIR/usr/bin/csv-processor-cli"
|
||||||
|
chmod 755 "$BUILD_DIR/usr/bin/csv-processor-gui"
|
||||||
|
|
||||||
|
# Desktop-Eintrag erstellen
|
||||||
|
cat > "$BUILD_DIR/usr/share/applications/csv-processor.desktop" << EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Name=CSV-Processor
|
||||||
|
Comment=Professionelle CSV/Excel-Verarbeitung
|
||||||
|
Exec=csv-processor-gui
|
||||||
|
Icon=accessories-text-editor
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
|
Categories=Office;Utility;
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# DEBIAN/control erstellen
|
||||||
|
cat > "$BUILD_DIR/DEBIAN/control" << EOF
|
||||||
|
Package: $PKG_NAME
|
||||||
|
Version: $PKG_VERSION
|
||||||
|
Section: utils
|
||||||
|
Priority: optional
|
||||||
|
Architecture: $PKG_ARCH
|
||||||
|
Maintainer: $MAINTAINER
|
||||||
|
Description: $DESCRIPTION
|
||||||
|
CSV-Processor ist ein professionelles Werkzeug zur Verarbeitung
|
||||||
|
und Konvertierung von CSV- und Excel-Dateien. Es bietet
|
||||||
|
Spaltennamen-Mapping, Datenbereinigung, Spaltenauswahl und
|
||||||
|
Voreinstellungen für wiederkehrende Aufgaben.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Debian-Paket bauen
|
||||||
|
echo ""
|
||||||
|
echo "Erstelle Debian-Paket..."
|
||||||
|
dpkg-deb --build "$BUILD_DIR" "dist/${PKG_NAME}_${PKG_VERSION}_${PKG_ARCH}.deb"
|
||||||
|
|
||||||
|
# Aufräumen
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
rm -rf build/
|
||||||
|
rm -f *.spec
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "Build abgeschlossen!"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "Erstelltes Paket:"
|
||||||
|
ls -la dist/*.deb
|
||||||
|
echo ""
|
||||||
|
echo "Installation mit:"
|
||||||
|
echo " sudo dpkg -i dist/${PKG_NAME}_${PKG_VERSION}_${PKG_ARCH}.deb"
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Build-Script für Linux-Executable
|
||||||
|
# Erstellt standalone-Executables mit PyInstaller
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "========================================"
|
||||||
|
echo "CSV-Processor - Linux Build"
|
||||||
|
echo "========================================"
|
||||||
|
|
||||||
|
# Prüfe ob PyInstaller installiert ist
|
||||||
|
if ! command -v pyinstaller &> /dev/null; then
|
||||||
|
echo "PyInstaller nicht gefunden. Installiere..."
|
||||||
|
pip install pyinstaller
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verzeichnisse vorbereiten
|
||||||
|
echo "Bereite Build-Verzeichnis vor..."
|
||||||
|
rm -rf build/ dist/*.spec 2>/dev/null || true
|
||||||
|
mkdir -p dist
|
||||||
|
|
||||||
|
# CLI-Version bauen
|
||||||
|
echo ""
|
||||||
|
echo "Baue CLI-Version..."
|
||||||
|
pyinstaller --onefile \
|
||||||
|
--name csv-processor-cli \
|
||||||
|
--clean \
|
||||||
|
csv_processor.py
|
||||||
|
|
||||||
|
# GUI-Version bauen
|
||||||
|
echo ""
|
||||||
|
echo "Baue GUI-Version..."
|
||||||
|
pyinstaller --onefile \
|
||||||
|
--name csv-processor-gui \
|
||||||
|
--windowed \
|
||||||
|
--clean \
|
||||||
|
csv_processor_gui.py
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "========================================"
|
||||||
|
echo "Build abgeschlossen!"
|
||||||
|
echo "========================================"
|
||||||
|
echo ""
|
||||||
|
echo "Erstellte Dateien:"
|
||||||
|
ls -la dist/csv-processor-*
|
||||||
|
echo ""
|
||||||
|
echo "Ausführen mit:"
|
||||||
|
echo " ./dist/csv-processor-cli"
|
||||||
|
echo " ./dist/csv-processor-gui"
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
@echo off
|
||||||
|
REM Build-Script für Windows-Executable
|
||||||
|
REM Erstellt standalone-Executables mit PyInstaller
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo CSV-Processor - Windows Build
|
||||||
|
echo ========================================
|
||||||
|
|
||||||
|
REM Prüfe ob Python installiert ist
|
||||||
|
python --version >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Python nicht gefunden. Bitte installieren Sie Python 3.7+
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Prüfe/Installiere PyInstaller
|
||||||
|
pip show pyinstaller >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo PyInstaller nicht gefunden. Installiere...
|
||||||
|
pip install pyinstaller
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Verzeichnisse vorbereiten
|
||||||
|
echo.
|
||||||
|
echo Bereite Build-Verzeichnis vor...
|
||||||
|
if exist build rmdir /s /q build
|
||||||
|
if not exist dist mkdir dist
|
||||||
|
|
||||||
|
REM CLI-Version bauen
|
||||||
|
echo.
|
||||||
|
echo Baue CLI-Version...
|
||||||
|
pyinstaller --onefile --name csv-processor-cli --clean csv_processor.py
|
||||||
|
|
||||||
|
REM GUI-Version bauen
|
||||||
|
echo.
|
||||||
|
echo Baue GUI-Version...
|
||||||
|
pyinstaller --onefile --name csv-processor-gui --windowed --clean csv_processor_gui.py
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ========================================
|
||||||
|
echo Build abgeschlossen!
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
echo Erstellte Dateien befinden sich in: dist\
|
||||||
|
dir dist\csv-processor-*.exe
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,829 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
CSV-Processor GUI v2.0: Grafische Oberfläche für CSV/Excel-Verarbeitung
|
||||||
|
"""
|
||||||
|
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk, filedialog, messagebox, scrolledtext, simpledialog
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
# Optional: Excel Support
|
||||||
|
try:
|
||||||
|
import openpyxl
|
||||||
|
from openpyxl.styles import Font, PatternFill
|
||||||
|
EXCEL_SUPPORT = True
|
||||||
|
except ImportError:
|
||||||
|
EXCEL_SUPPORT = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from odf.opendocument import OpenDocumentSpreadsheet
|
||||||
|
from odf.table import Table, TableRow, TableCell
|
||||||
|
from odf.text import P
|
||||||
|
ODT_SUPPORT = True
|
||||||
|
except ImportError:
|
||||||
|
ODT_SUPPORT = False
|
||||||
|
|
||||||
|
|
||||||
|
class CSVProcessorGUI:
|
||||||
|
def __init__(self, root):
|
||||||
|
self.root = root
|
||||||
|
self.root.title("CSV-Processor v2.0")
|
||||||
|
self.root.geometry("900x700")
|
||||||
|
|
||||||
|
self.config_dir = Path("csv_processor_config")
|
||||||
|
self.config_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
self.current_config = {} # Für temporäre Voreinstellungs-Daten
|
||||||
|
self.headers = []
|
||||||
|
self.data = []
|
||||||
|
self.original_names = {}
|
||||||
|
self.selected_columns = set()
|
||||||
|
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
def setup_ui(self):
|
||||||
|
"""Hauptoberfläche erstellen"""
|
||||||
|
# Menüleiste
|
||||||
|
menubar = tk.Menu(self.root)
|
||||||
|
self.root.config(menu=menubar)
|
||||||
|
|
||||||
|
# Datei-Menü
|
||||||
|
file_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
menubar.add_cascade(label="Datei", menu=file_menu)
|
||||||
|
file_menu.add_command(label="Datei öffnen", command=self.load_file)
|
||||||
|
file_menu.add_command(label="Verarbeiten & Speichern", command=self.process_and_save)
|
||||||
|
file_menu.add_separator()
|
||||||
|
file_menu.add_command(label="Beenden", command=self.root.quit)
|
||||||
|
|
||||||
|
# Voreinstellungen-Menü
|
||||||
|
preset_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
menubar.add_cascade(label="Voreinstellungen", menu=preset_menu)
|
||||||
|
preset_menu.add_command(label="Laden", command=self.load_preset)
|
||||||
|
preset_menu.add_command(label="Speichern", command=self.save_preset)
|
||||||
|
preset_menu.add_command(label="Verwalten", command=self.manage_presets)
|
||||||
|
|
||||||
|
# Hilfe-Menü
|
||||||
|
help_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
menubar.add_cascade(label="Hilfe", menu=help_menu)
|
||||||
|
help_menu.add_command(label="Über", command=self.show_about)
|
||||||
|
|
||||||
|
# Hauptcontainer
|
||||||
|
main_frame = ttk.Frame(self.root, padding="10")
|
||||||
|
main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
|
||||||
|
|
||||||
|
self.root.columnconfigure(0, weight=1)
|
||||||
|
self.root.rowconfigure(0, weight=1)
|
||||||
|
main_frame.columnconfigure(0, weight=1)
|
||||||
|
main_frame.rowconfigure(4, weight=1)
|
||||||
|
|
||||||
|
# Datei-Auswahl
|
||||||
|
file_frame = ttk.LabelFrame(main_frame, text="Quelldatei", padding="10")
|
||||||
|
file_frame.grid(row=0, column=0, sticky=(tk.W, tk.E), pady=5)
|
||||||
|
file_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
ttk.Label(file_frame, text="Datei:").grid(row=0, column=0, sticky=tk.W)
|
||||||
|
self.file_entry = ttk.Entry(file_frame, width=50)
|
||||||
|
self.file_entry.grid(row=0, column=1, sticky=(tk.W, tk.E), padx=5)
|
||||||
|
ttk.Button(file_frame, text="Durchsuchen", command=self.browse_file).grid(row=0, column=2)
|
||||||
|
ttk.Button(file_frame, text="Laden", command=self.load_file).grid(row=0, column=3, padx=5)
|
||||||
|
|
||||||
|
# Einstellungen
|
||||||
|
settings_frame = ttk.LabelFrame(main_frame, text="Verarbeitungseinstellungen", padding="10")
|
||||||
|
settings_frame.grid(row=1, column=0, sticky=(tk.W, tk.E), pady=5)
|
||||||
|
settings_frame.columnconfigure(1, weight=1)
|
||||||
|
|
||||||
|
row = 0
|
||||||
|
self.has_header_var = tk.BooleanVar(value=True)
|
||||||
|
ttk.Checkbutton(settings_frame, text="Datei hat Kopfzeile",
|
||||||
|
variable=self.has_header_var).grid(row=row, column=0, sticky=tk.W, columnspan=2)
|
||||||
|
|
||||||
|
row += 1
|
||||||
|
self.remove_empty_rows_var = tk.BooleanVar(value=True)
|
||||||
|
ttk.Checkbutton(settings_frame, text="Leere Zeilen entfernen",
|
||||||
|
variable=self.remove_empty_rows_var).grid(row=row, column=0, sticky=tk.W, columnspan=2)
|
||||||
|
|
||||||
|
row += 1
|
||||||
|
self.remove_empty_cols_var = tk.BooleanVar(value=True)
|
||||||
|
ttk.Checkbutton(settings_frame, text="Leere Spalten entfernen (automatisch vor Spaltenauswahl)",
|
||||||
|
variable=self.remove_empty_cols_var).grid(row=row, column=0, sticky=tk.W, columnspan=3)
|
||||||
|
|
||||||
|
# Info-Label
|
||||||
|
info_label = ttk.Label(settings_frame,
|
||||||
|
text="ℹ Leere Spalten werden vor der Spaltenauswahl entfernt",
|
||||||
|
foreground="blue", font=('TkDefaultFont', 8))
|
||||||
|
info_label.grid(row=row+1, column=0, columnspan=3, sticky=tk.W, pady=(0,5))
|
||||||
|
|
||||||
|
row += 2
|
||||||
|
ttk.Label(settings_frame, text="Mapping-Datei:").grid(row=row, column=0, sticky=tk.W)
|
||||||
|
self.mapping_entry = ttk.Entry(settings_frame)
|
||||||
|
self.mapping_entry.grid(row=row, column=1, sticky=(tk.W, tk.E), padx=5)
|
||||||
|
ttk.Button(settings_frame, text="...", command=self.browse_mapping, width=3).grid(row=row, column=2)
|
||||||
|
|
||||||
|
row += 1
|
||||||
|
ttk.Label(settings_frame, text="Ausgabeformat:").grid(row=row, column=0, sticky=tk.W)
|
||||||
|
self.format_var = tk.StringVar(value="csv")
|
||||||
|
format_frame = ttk.Frame(settings_frame)
|
||||||
|
format_frame.grid(row=row, column=1, sticky=tk.W, padx=5)
|
||||||
|
ttk.Radiobutton(format_frame, text="CSV", variable=self.format_var, value="csv").pack(side=tk.LEFT)
|
||||||
|
if EXCEL_SUPPORT:
|
||||||
|
ttk.Radiobutton(format_frame, text="Excel", variable=self.format_var, value="xlsx").pack(side=tk.LEFT, padx=10)
|
||||||
|
if ODT_SUPPORT:
|
||||||
|
ttk.Radiobutton(format_frame, text="ODS", variable=self.format_var, value="ods").pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
# Spaltenauswahl
|
||||||
|
columns_frame = ttk.LabelFrame(main_frame, text="Spaltenauswahl", padding="10")
|
||||||
|
columns_frame.grid(row=2, column=0, sticky=(tk.W, tk.E), pady=5)
|
||||||
|
|
||||||
|
button_frame = ttk.Frame(columns_frame)
|
||||||
|
button_frame.pack(fill=tk.X)
|
||||||
|
ttk.Button(button_frame, text="Alle auswählen", command=self.select_all_columns).pack(side=tk.LEFT, padx=5)
|
||||||
|
ttk.Button(button_frame, text="Alle abwählen", command=self.deselect_all_columns).pack(side=tk.LEFT)
|
||||||
|
ttk.Button(button_frame, text="Auswahl umkehren", command=self.invert_selection).pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
# Spalten-Liste mit Checkboxen
|
||||||
|
list_frame = ttk.Frame(columns_frame)
|
||||||
|
list_frame.pack(fill=tk.BOTH, expand=True, pady=5)
|
||||||
|
|
||||||
|
scrollbar = ttk.Scrollbar(list_frame)
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
|
self.columns_listbox = tk.Listbox(list_frame, selectmode=tk.MULTIPLE,
|
||||||
|
yscrollcommand=scrollbar.set, height=10)
|
||||||
|
self.columns_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
scrollbar.config(command=self.columns_listbox.yview)
|
||||||
|
|
||||||
|
# Vorschau
|
||||||
|
preview_frame = ttk.LabelFrame(main_frame, text="Datenvorschau", padding="10")
|
||||||
|
preview_frame.grid(row=3, column=0, sticky=(tk.W, tk.E), pady=5)
|
||||||
|
|
||||||
|
self.info_label = ttk.Label(preview_frame, text="Keine Datei geladen")
|
||||||
|
self.info_label.pack()
|
||||||
|
|
||||||
|
# Status und Log
|
||||||
|
log_frame = ttk.LabelFrame(main_frame, text="Status", padding="10")
|
||||||
|
log_frame.grid(row=4, column=0, sticky=(tk.W, tk.E, tk.N, tk.S), pady=5)
|
||||||
|
|
||||||
|
self.log_text = scrolledtext.ScrolledText(log_frame, height=10, state='disabled')
|
||||||
|
self.log_text.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Aktionsbuttons
|
||||||
|
action_frame = ttk.Frame(main_frame)
|
||||||
|
action_frame.grid(row=5, column=0, sticky=(tk.W, tk.E), pady=10)
|
||||||
|
|
||||||
|
ttk.Button(action_frame, text="Verarbeiten & Speichern",
|
||||||
|
command=self.process_and_save).pack(side=tk.RIGHT, padx=5)
|
||||||
|
ttk.Button(action_frame, text="Vorschau", command=self.show_preview).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
self.log("CSV-Processor GUI v2.0 gestartet")
|
||||||
|
self.log(f"Excel-Support: {'Ja' if EXCEL_SUPPORT else 'Nein'}")
|
||||||
|
self.log(f"ODS-Support: {'Ja' if ODT_SUPPORT else 'Nein'}")
|
||||||
|
|
||||||
|
def log(self, message):
|
||||||
|
"""Nachricht im Log anzeigen"""
|
||||||
|
self.log_text.config(state='normal')
|
||||||
|
self.log_text.insert(tk.END, f"{datetime.now().strftime('%H:%M:%S')} - {message}\n")
|
||||||
|
self.log_text.see(tk.END)
|
||||||
|
self.log_text.config(state='disabled')
|
||||||
|
|
||||||
|
def browse_file(self):
|
||||||
|
"""Datei zum Öffnen auswählen"""
|
||||||
|
filetypes = [
|
||||||
|
("Alle unterstützten Dateien", "*.csv *.xlsx *.xls"),
|
||||||
|
("CSV Dateien", "*.csv"),
|
||||||
|
]
|
||||||
|
if EXCEL_SUPPORT:
|
||||||
|
filetypes.append(("Excel Dateien", "*.xlsx *.xls"))
|
||||||
|
filetypes.append(("Alle Dateien", "*.*"))
|
||||||
|
|
||||||
|
filename = filedialog.askopenfilename(
|
||||||
|
title="Quelldatei auswählen",
|
||||||
|
filetypes=filetypes
|
||||||
|
)
|
||||||
|
if filename:
|
||||||
|
self.file_entry.delete(0, tk.END)
|
||||||
|
self.file_entry.insert(0, filename)
|
||||||
|
|
||||||
|
def browse_mapping(self):
|
||||||
|
"""Mapping-Datei auswählen"""
|
||||||
|
filename = filedialog.askopenfilename(
|
||||||
|
title="Mapping-Datei auswählen",
|
||||||
|
filetypes=[("JSON Dateien", "*.json"), ("Alle Dateien", "*.*")]
|
||||||
|
)
|
||||||
|
if filename:
|
||||||
|
self.mapping_entry.delete(0, tk.END)
|
||||||
|
self.mapping_entry.insert(0, filename)
|
||||||
|
|
||||||
|
def load_file(self):
|
||||||
|
"""Datei laden"""
|
||||||
|
filepath = self.file_entry.get()
|
||||||
|
if not filepath:
|
||||||
|
messagebox.showwarning("Warnung", "Bitte wählen Sie zuerst eine Datei aus.")
|
||||||
|
return
|
||||||
|
|
||||||
|
path = Path(filepath)
|
||||||
|
if not path.exists():
|
||||||
|
messagebox.showerror("Fehler", f"Datei nicht gefunden: {filepath}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.log(f"Lade Datei: {path.name}")
|
||||||
|
|
||||||
|
# Datei laden
|
||||||
|
file_ext = path.suffix.lower()
|
||||||
|
if file_ext in ['.xlsx', '.xls']:
|
||||||
|
self.headers, self.data = self.read_excel(path)
|
||||||
|
else:
|
||||||
|
self.headers, self.data = self.read_csv(path)
|
||||||
|
|
||||||
|
# Mapping anwenden
|
||||||
|
mapping_file = self.mapping_entry.get()
|
||||||
|
if mapping_file and Path(mapping_file).exists():
|
||||||
|
mappings = self.load_column_mappings(Path(mapping_file))
|
||||||
|
self.headers, self.data, self.original_names = self.apply_column_mappings(
|
||||||
|
self.headers, self.data, mappings
|
||||||
|
)
|
||||||
|
self.log(f"Mapping angewendet: {len(mappings)} Spalten umbenannt")
|
||||||
|
else:
|
||||||
|
self.original_names = {h: h for h in self.headers}
|
||||||
|
|
||||||
|
# Spalten in Liste anzeigen
|
||||||
|
self.update_columns_list()
|
||||||
|
|
||||||
|
# Wenn Voreinstellung mit Spaltenauswahl geladen wurde, anwenden
|
||||||
|
if hasattr(self, 'current_config') and 'selected_columns' in self.current_config:
|
||||||
|
self.apply_column_selection_from_preset(self.current_config['selected_columns'])
|
||||||
|
# Config zurücksetzen nach Anwendung
|
||||||
|
self.current_config = {}
|
||||||
|
|
||||||
|
# Info aktualisieren
|
||||||
|
self.info_label.config(text=f"Geladen: {len(self.headers)} Spalten, {len(self.data)} Zeilen")
|
||||||
|
self.log(f"Erfolgreich geladen: {len(self.headers)} Spalten, {len(self.data)} Zeilen")
|
||||||
|
|
||||||
|
# Warnung anzeigen wenn "Leere Spalten entfernen" aktiv ist
|
||||||
|
if self.remove_empty_cols_var.get():
|
||||||
|
self.log("ℹ Leere Spalten werden beim Verarbeiten automatisch entfernt")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Fehler", f"Fehler beim Laden: {str(e)}")
|
||||||
|
self.log(f"FEHLER: {str(e)}")
|
||||||
|
|
||||||
|
def read_csv(self, filepath: Path) -> Tuple[List[str], List[Dict]]:
|
||||||
|
"""CSV-Datei lesen"""
|
||||||
|
encodings = ['utf-8', 'latin-1', 'cp1252']
|
||||||
|
|
||||||
|
for encoding in encodings:
|
||||||
|
try:
|
||||||
|
with open(filepath, 'r', encoding=encoding) as f:
|
||||||
|
first_line = f.readline()
|
||||||
|
f.seek(0)
|
||||||
|
|
||||||
|
sniffer = csv.Sniffer()
|
||||||
|
delimiter = sniffer.sniff(first_line).delimiter
|
||||||
|
|
||||||
|
if self.has_header_var.get():
|
||||||
|
reader = csv.DictReader(f, delimiter=delimiter)
|
||||||
|
headers = list(reader.fieldnames)
|
||||||
|
data = list(reader)
|
||||||
|
else:
|
||||||
|
reader = csv.reader(f, delimiter=delimiter)
|
||||||
|
rows = list(reader)
|
||||||
|
if rows:
|
||||||
|
headers = [f"Spalte_{i+1}" for i in range(len(rows[0]))]
|
||||||
|
data = []
|
||||||
|
for row in rows:
|
||||||
|
row_dict = {headers[i]: row[i] if i < len(row) else ''
|
||||||
|
for i in range(len(headers))}
|
||||||
|
data.append(row_dict)
|
||||||
|
else:
|
||||||
|
headers = []
|
||||||
|
data = []
|
||||||
|
|
||||||
|
return headers, data
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
continue
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
raise Exception(f"Konnte Datei {filepath} nicht lesen")
|
||||||
|
|
||||||
|
def read_excel(self, filepath: Path) -> Tuple[List[str], List[Dict]]:
|
||||||
|
"""Excel-Datei lesen"""
|
||||||
|
if not EXCEL_SUPPORT:
|
||||||
|
raise Exception("Excel-Support nicht verfügbar. Installieren Sie: pip install openpyxl")
|
||||||
|
|
||||||
|
wb = openpyxl.load_workbook(filepath, data_only=True)
|
||||||
|
ws = wb.active
|
||||||
|
|
||||||
|
data = []
|
||||||
|
headers = None
|
||||||
|
|
||||||
|
for i, row in enumerate(ws.iter_rows(values_only=True)):
|
||||||
|
if i == 0 and self.has_header_var.get():
|
||||||
|
headers = [str(cell) if cell is not None else f"Spalte_{j+1}"
|
||||||
|
for j, cell in enumerate(row)]
|
||||||
|
else:
|
||||||
|
if headers is None:
|
||||||
|
headers = [f"Spalte_{j+1}" for j in range(len(row))]
|
||||||
|
|
||||||
|
row_dict = {}
|
||||||
|
for j, cell in enumerate(row):
|
||||||
|
if j < len(headers):
|
||||||
|
row_dict[headers[j]] = str(cell) if cell is not None else ''
|
||||||
|
data.append(row_dict)
|
||||||
|
|
||||||
|
return headers, data
|
||||||
|
|
||||||
|
def load_column_mappings(self, mapping_file: Path) -> Dict[str, str]:
|
||||||
|
"""Spaltennamen-Zuordnungen aus JSON laden"""
|
||||||
|
try:
|
||||||
|
with open(mapping_file, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def apply_column_mappings(self, headers: List[str], data: List[Dict],
|
||||||
|
mappings: Dict[str, str]) -> Tuple[List[str], List[Dict], Dict[str, str]]:
|
||||||
|
"""Spaltennamen umbenennen"""
|
||||||
|
new_headers = [mappings.get(h, h) for h in headers]
|
||||||
|
original_names = {new_h: old_h for old_h, new_h in zip(headers, new_headers)}
|
||||||
|
|
||||||
|
new_data = []
|
||||||
|
for row in data:
|
||||||
|
new_row = {}
|
||||||
|
for old_h, new_h in zip(headers, new_headers):
|
||||||
|
new_row[new_h] = row.get(old_h, '')
|
||||||
|
new_data.append(new_row)
|
||||||
|
|
||||||
|
return new_headers, new_data, original_names
|
||||||
|
|
||||||
|
def update_columns_list(self):
|
||||||
|
"""Spaltenliste aktualisieren"""
|
||||||
|
self.columns_listbox.delete(0, tk.END)
|
||||||
|
self.selected_columns = set(self.headers) # Alle initial ausgewählt
|
||||||
|
|
||||||
|
for i, header in enumerate(self.headers):
|
||||||
|
orig = self.original_names.get(header, header)
|
||||||
|
if orig != header:
|
||||||
|
display = f"☑ {orig} → {header}"
|
||||||
|
else:
|
||||||
|
display = f"☑ {header}"
|
||||||
|
|
||||||
|
self.columns_listbox.insert(tk.END, display)
|
||||||
|
self.columns_listbox.selection_set(i)
|
||||||
|
|
||||||
|
def select_all_columns(self):
|
||||||
|
"""Alle Spalten auswählen"""
|
||||||
|
self.columns_listbox.selection_set(0, tk.END)
|
||||||
|
self.selected_columns = set(self.headers)
|
||||||
|
|
||||||
|
def deselect_all_columns(self):
|
||||||
|
"""Alle Spalten abwählen"""
|
||||||
|
self.columns_listbox.selection_clear(0, tk.END)
|
||||||
|
self.selected_columns = set()
|
||||||
|
|
||||||
|
def invert_selection(self):
|
||||||
|
"""Auswahl umkehren"""
|
||||||
|
for i in range(len(self.headers)):
|
||||||
|
if self.columns_listbox.selection_includes(i):
|
||||||
|
self.columns_listbox.selection_clear(i)
|
||||||
|
else:
|
||||||
|
self.columns_listbox.selection_set(i)
|
||||||
|
|
||||||
|
def get_selected_columns(self) -> List[str]:
|
||||||
|
"""Aktuell ausgewählte Spalten ermitteln"""
|
||||||
|
selected_indices = self.columns_listbox.curselection()
|
||||||
|
return [self.headers[i] for i in selected_indices]
|
||||||
|
|
||||||
|
def process_data(self) -> Tuple[List[str], List[Dict]]:
|
||||||
|
"""Daten verarbeiten"""
|
||||||
|
if not self.headers or not self.data:
|
||||||
|
raise Exception("Keine Daten geladen")
|
||||||
|
|
||||||
|
headers = self.headers[:]
|
||||||
|
data = self.data[:]
|
||||||
|
|
||||||
|
# 1. Leere Zeilen entfernen
|
||||||
|
if self.remove_empty_rows_var.get():
|
||||||
|
original_count = len(data)
|
||||||
|
data = [row for row in data if any(str(v).strip() for v in row.values())]
|
||||||
|
removed = original_count - len(data)
|
||||||
|
if removed > 0:
|
||||||
|
self.log(f"Leere Zeilen entfernt: {removed}")
|
||||||
|
|
||||||
|
# 2. Leere Spalten entfernen (VOR der Spaltenauswahl!)
|
||||||
|
if self.remove_empty_cols_var.get():
|
||||||
|
non_empty_headers = []
|
||||||
|
for header in headers:
|
||||||
|
if any(row.get(header, '').strip() for row in data):
|
||||||
|
non_empty_headers.append(header)
|
||||||
|
|
||||||
|
removed = len(headers) - len(non_empty_headers)
|
||||||
|
if removed > 0:
|
||||||
|
self.log(f"Leere Spalten entfernt: {removed}")
|
||||||
|
# Zeige welche Spalten entfernt wurden
|
||||||
|
removed_cols = [h for h in headers if h not in non_empty_headers]
|
||||||
|
for col in removed_cols[:5]:
|
||||||
|
orig = self.original_names.get(col, col)
|
||||||
|
if orig != col:
|
||||||
|
self.log(f" - {orig} → {col} (leer)")
|
||||||
|
else:
|
||||||
|
self.log(f" - {col} (leer)")
|
||||||
|
if len(removed_cols) > 5:
|
||||||
|
self.log(f" ... und {len(removed_cols)-5} weitere")
|
||||||
|
|
||||||
|
headers = non_empty_headers
|
||||||
|
data = [{h: row.get(h, '') for h in headers} for row in data]
|
||||||
|
|
||||||
|
# 3. Spaltenauswahl anwenden (arbeitet mit nicht-leeren Spalten)
|
||||||
|
selected = self.get_selected_columns()
|
||||||
|
|
||||||
|
# Nur Spalten verwenden, die noch existieren (nicht leer) UND ausgewählt sind
|
||||||
|
final_columns = [h for h in headers if h in selected]
|
||||||
|
|
||||||
|
if not final_columns:
|
||||||
|
raise Exception("Keine Spalten ausgewählt oder alle ausgewählten Spalten sind leer")
|
||||||
|
|
||||||
|
# Zeige Info über übersprungene Spalten
|
||||||
|
skipped_empty = [h for h in selected if h not in headers]
|
||||||
|
skipped_deselected = [h for h in headers if h not in selected]
|
||||||
|
|
||||||
|
if skipped_empty:
|
||||||
|
self.log(f"Übersprungene Spalten (leer): {len(skipped_empty)}")
|
||||||
|
if skipped_deselected:
|
||||||
|
self.log(f"Abgewählte Spalten (rot): {len(skipped_deselected)}")
|
||||||
|
|
||||||
|
headers = final_columns
|
||||||
|
data = [{h: row.get(h, '') for h in headers} for row in data]
|
||||||
|
|
||||||
|
self.log(f"Finale Verarbeitung: {len(headers)} Spalten, {len(data)} Zeilen")
|
||||||
|
return headers, data
|
||||||
|
|
||||||
|
def show_preview(self):
|
||||||
|
"""Vorschau der verarbeiteten Daten"""
|
||||||
|
try:
|
||||||
|
headers, data = self.process_data()
|
||||||
|
|
||||||
|
preview_window = tk.Toplevel(self.root)
|
||||||
|
preview_window.title("Datenvorschau")
|
||||||
|
preview_window.geometry("800x400")
|
||||||
|
|
||||||
|
frame = ttk.Frame(preview_window, padding="10")
|
||||||
|
frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
# Treeview für Tabelle
|
||||||
|
tree = ttk.Treeview(frame, columns=headers, show='headings')
|
||||||
|
|
||||||
|
for col in headers:
|
||||||
|
tree.heading(col, text=col)
|
||||||
|
tree.column(col, width=100)
|
||||||
|
|
||||||
|
for row in data[:100]: # Max 100 Zeilen
|
||||||
|
values = [row.get(col, '') for col in headers]
|
||||||
|
tree.insert('', tk.END, values=values)
|
||||||
|
|
||||||
|
scrollbar = ttk.Scrollbar(frame, orient=tk.VERTICAL, command=tree.yview)
|
||||||
|
tree.configure(yscrollcommand=scrollbar.set)
|
||||||
|
|
||||||
|
tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
|
||||||
|
info_label = ttk.Label(preview_window,
|
||||||
|
text=f"Zeige erste {min(len(data), 100)} von {len(data)} Zeilen")
|
||||||
|
info_label.pack(pady=5)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Fehler", f"Fehler bei Vorschau: {str(e)}")
|
||||||
|
|
||||||
|
def process_and_save(self):
|
||||||
|
"""Daten verarbeiten und speichern"""
|
||||||
|
try:
|
||||||
|
headers, data = self.process_data()
|
||||||
|
|
||||||
|
# Zieldatei wählen
|
||||||
|
output_format = self.format_var.get()
|
||||||
|
|
||||||
|
filetypes = []
|
||||||
|
if output_format == 'csv':
|
||||||
|
filetypes = [("CSV Dateien", "*.csv")]
|
||||||
|
elif output_format == 'xlsx':
|
||||||
|
filetypes = [("Excel Dateien", "*.xlsx")]
|
||||||
|
elif output_format == 'ods':
|
||||||
|
filetypes = [("ODS Dateien", "*.ods")]
|
||||||
|
filetypes.append(("Alle Dateien", "*.*"))
|
||||||
|
|
||||||
|
filename = filedialog.asksaveasfilename(
|
||||||
|
title="Datei speichern",
|
||||||
|
defaultextension=f".{output_format}",
|
||||||
|
filetypes=filetypes
|
||||||
|
)
|
||||||
|
|
||||||
|
if not filename:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Speichern
|
||||||
|
target_file = Path(filename)
|
||||||
|
|
||||||
|
if output_format == 'csv':
|
||||||
|
self.write_csv(target_file, headers, data)
|
||||||
|
elif output_format == 'xlsx':
|
||||||
|
self.write_excel(target_file, headers, data)
|
||||||
|
elif output_format == 'ods':
|
||||||
|
self.write_ods(target_file, headers, data)
|
||||||
|
|
||||||
|
self.log(f"Datei gespeichert: {target_file.name}")
|
||||||
|
messagebox.showinfo("Erfolg", f"Datei erfolgreich gespeichert:\n{target_file}")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Fehler", f"Fehler beim Speichern: {str(e)}")
|
||||||
|
self.log(f"FEHLER: {str(e)}")
|
||||||
|
|
||||||
|
def write_csv(self, filepath: Path, headers: List[str], data: List[Dict]):
|
||||||
|
"""CSV-Datei schreiben"""
|
||||||
|
with open(filepath, 'w', encoding='utf-8', newline='') as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=headers, delimiter=';')
|
||||||
|
writer.writeheader()
|
||||||
|
writer.writerows(data)
|
||||||
|
|
||||||
|
def write_excel(self, filepath: Path, headers: List[str], data: List[Dict]):
|
||||||
|
"""Excel-Datei schreiben"""
|
||||||
|
if not EXCEL_SUPPORT:
|
||||||
|
raise Exception("Excel-Support nicht verfügbar")
|
||||||
|
|
||||||
|
wb = openpyxl.Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "Daten"
|
||||||
|
|
||||||
|
# Kopfzeile
|
||||||
|
for col_num, header in enumerate(headers, 1):
|
||||||
|
cell = ws.cell(row=1, column=col_num, value=header)
|
||||||
|
cell.font = Font(bold=True)
|
||||||
|
cell.fill = PatternFill(start_color="CCCCCC", end_color="CCCCCC", fill_type="solid")
|
||||||
|
|
||||||
|
# Daten
|
||||||
|
for row_num, row_data in enumerate(data, 2):
|
||||||
|
for col_num, header in enumerate(headers, 1):
|
||||||
|
ws.cell(row=row_num, column=col_num, value=row_data.get(header, ''))
|
||||||
|
|
||||||
|
wb.save(filepath)
|
||||||
|
|
||||||
|
def write_ods(self, filepath: Path, headers: List[str], data: List[Dict]):
|
||||||
|
"""ODS-Datei schreiben"""
|
||||||
|
if not ODT_SUPPORT:
|
||||||
|
raise Exception("ODS-Support nicht verfügbar")
|
||||||
|
|
||||||
|
doc = OpenDocumentSpreadsheet()
|
||||||
|
table = Table(name="Daten")
|
||||||
|
|
||||||
|
# Kopfzeile
|
||||||
|
row = TableRow()
|
||||||
|
for header in headers:
|
||||||
|
cell = TableCell()
|
||||||
|
cell.addElement(P(text=header))
|
||||||
|
row.addElement(cell)
|
||||||
|
table.addElement(row)
|
||||||
|
|
||||||
|
# Daten
|
||||||
|
for row_data in data:
|
||||||
|
row = TableRow()
|
||||||
|
for header in headers:
|
||||||
|
cell = TableCell()
|
||||||
|
cell.addElement(P(text=str(row_data.get(header, ''))))
|
||||||
|
row.addElement(cell)
|
||||||
|
table.addElement(row)
|
||||||
|
|
||||||
|
doc.spreadsheet.addElement(table)
|
||||||
|
doc.save(filepath)
|
||||||
|
|
||||||
|
def load_preset(self):
|
||||||
|
"""Voreinstellung laden"""
|
||||||
|
presets = [f.stem for f in self.config_dir.glob("*.json")]
|
||||||
|
|
||||||
|
if not presets:
|
||||||
|
messagebox.showinfo("Info", "Keine Voreinstellungen vorhanden.")
|
||||||
|
return
|
||||||
|
|
||||||
|
dialog = PresetDialog(self.root, "Voreinstellung laden", presets)
|
||||||
|
if dialog.result:
|
||||||
|
preset_file = self.config_dir / f"{dialog.result}.json"
|
||||||
|
try:
|
||||||
|
with open(preset_file, 'r', encoding='utf-8') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
# Einstellungen anwenden
|
||||||
|
if 'has_header' in config:
|
||||||
|
self.has_header_var.set(config['has_header'])
|
||||||
|
if 'remove_empty_rows' in config:
|
||||||
|
self.remove_empty_rows_var.set(config['remove_empty_rows'])
|
||||||
|
if 'remove_empty' in config:
|
||||||
|
self.remove_empty_cols_var.set(config['remove_empty'])
|
||||||
|
|
||||||
|
# Mapping-Datei vorbelegen
|
||||||
|
if 'mapping_file' in config and config['mapping_file']:
|
||||||
|
self.mapping_entry.delete(0, tk.END)
|
||||||
|
self.mapping_entry.insert(0, config['mapping_file'])
|
||||||
|
self.log(f"Mapping-Datei vorbelegt: {config['mapping_file']}")
|
||||||
|
|
||||||
|
if 'output_format' in config:
|
||||||
|
self.format_var.set(config['output_format'])
|
||||||
|
|
||||||
|
# Quelldatei vorbelegen (falls gespeichert)
|
||||||
|
if 'source_file' in config and config['source_file']:
|
||||||
|
source_path = Path(config['source_file'])
|
||||||
|
if source_path.exists():
|
||||||
|
self.file_entry.delete(0, tk.END)
|
||||||
|
self.file_entry.insert(0, config['source_file'])
|
||||||
|
self.log(f"Quelldatei vorbelegt: {source_path.name}")
|
||||||
|
|
||||||
|
# Datei automatisch laden
|
||||||
|
self.load_file()
|
||||||
|
|
||||||
|
# Spaltenauswahl anwenden (nach dem Laden!)
|
||||||
|
if 'selected_columns' in config and self.headers:
|
||||||
|
self.apply_column_selection_from_preset(config['selected_columns'])
|
||||||
|
else:
|
||||||
|
self.log(f"⚠ Gespeicherte Quelldatei nicht gefunden: {config['source_file']}")
|
||||||
|
elif 'selected_columns' in config:
|
||||||
|
# Keine Quelldatei, aber Spaltenauswahl gespeichert
|
||||||
|
# Wird angewendet sobald Datei geladen wird
|
||||||
|
self.current_config = config
|
||||||
|
self.log(f"Spaltenauswahl wird angewendet sobald Datei geladen wird")
|
||||||
|
|
||||||
|
self.log(f"Voreinstellung geladen: {dialog.result}")
|
||||||
|
messagebox.showinfo("Erfolg",
|
||||||
|
f"Voreinstellung '{dialog.result}' geladen.\n\n"
|
||||||
|
"Hinweis: Leere Spalten werden beim Verarbeiten\n"
|
||||||
|
"automatisch übersprungen (vor Spaltenauswahl).")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Fehler", f"Fehler beim Laden: {str(e)}")
|
||||||
|
|
||||||
|
def apply_column_selection_from_preset(self, selected_columns: List[str]):
|
||||||
|
"""Spaltenauswahl aus Voreinstellung anwenden"""
|
||||||
|
saved_selection = set(selected_columns)
|
||||||
|
|
||||||
|
# Alle abwählen
|
||||||
|
self.columns_listbox.selection_clear(0, tk.END)
|
||||||
|
|
||||||
|
# Nur gespeicherte Spalten auswählen (die existieren)
|
||||||
|
selected_count = 0
|
||||||
|
for i, header in enumerate(self.headers):
|
||||||
|
if header in saved_selection:
|
||||||
|
self.columns_listbox.selection_set(i)
|
||||||
|
selected_count += 1
|
||||||
|
|
||||||
|
skipped_count = len(saved_selection) - selected_count
|
||||||
|
|
||||||
|
self.log(f"Spaltenauswahl angewendet: {selected_count} Spalten ausgewählt")
|
||||||
|
if skipped_count > 0:
|
||||||
|
self.log(f" ⚠ {skipped_count} Spalten nicht verfügbar (leer oder nicht vorhanden)")
|
||||||
|
|
||||||
|
# Info-Text anzeigen
|
||||||
|
self.info_label.config(
|
||||||
|
text=f"Geladen: {len(self.headers)} Spalten, {len(self.data)} Zeilen | "
|
||||||
|
f"Ausgewählt: {selected_count} Spalten"
|
||||||
|
)
|
||||||
|
|
||||||
|
def save_preset(self):
|
||||||
|
"""Voreinstellung speichern"""
|
||||||
|
name = simpledialog.askstring("Voreinstellung speichern", "Name der Voreinstellung:")
|
||||||
|
if not name:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Fragen ob Quelldatei mitgespeichert werden soll
|
||||||
|
save_source = False
|
||||||
|
if self.file_entry.get():
|
||||||
|
save_source = messagebox.askyesno(
|
||||||
|
"Quelldatei speichern?",
|
||||||
|
"Möchten Sie den Pfad zur Quelldatei in der Voreinstellung speichern?\n\n"
|
||||||
|
"Ja = Datei wird beim Laden der Voreinstellung automatisch geladen\n"
|
||||||
|
"Nein = Nur Einstellungen werden gespeichert"
|
||||||
|
)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'has_header': self.has_header_var.get(),
|
||||||
|
'remove_empty_rows': self.remove_empty_rows_var.get(),
|
||||||
|
'remove_empty': self.remove_empty_cols_var.get(),
|
||||||
|
'mapping_file': self.mapping_entry.get(),
|
||||||
|
'output_format': self.format_var.get(),
|
||||||
|
'selected_columns': self.get_selected_columns()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Quelldatei optional speichern
|
||||||
|
if save_source:
|
||||||
|
config['source_file'] = self.file_entry.get()
|
||||||
|
|
||||||
|
preset_file = self.config_dir / f"{name}.json"
|
||||||
|
try:
|
||||||
|
with open(preset_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config, f, indent=2, ensure_ascii=False)
|
||||||
|
|
||||||
|
info_text = f"Voreinstellung '{name}' gespeichert.\n\n"
|
||||||
|
info_text += f"Enthält:\n"
|
||||||
|
info_text += f"- Einstellungen: Ja\n"
|
||||||
|
info_text += f"- Mapping: {'Ja' if config['mapping_file'] else 'Nein'}\n"
|
||||||
|
info_text += f"- Quelldatei: {'Ja' if save_source else 'Nein'}\n"
|
||||||
|
info_text += f"- Spaltenauswahl: {len(config['selected_columns'])} Spalten"
|
||||||
|
|
||||||
|
self.log(f"Voreinstellung gespeichert: {name}")
|
||||||
|
messagebox.showinfo("Erfolg", info_text)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Fehler", f"Fehler beim Speichern: {str(e)}")
|
||||||
|
|
||||||
|
def manage_presets(self):
|
||||||
|
"""Voreinstellungen verwalten"""
|
||||||
|
PresetManagerWindow(self.root, self.config_dir)
|
||||||
|
|
||||||
|
def show_about(self):
|
||||||
|
"""Über-Dialog"""
|
||||||
|
messagebox.showinfo(
|
||||||
|
"Über CSV-Processor",
|
||||||
|
"CSV-Processor v2.0\n\n"
|
||||||
|
"Professionelle CSV/Excel-Verarbeitung\n\n"
|
||||||
|
f"Excel-Support: {'Ja' if EXCEL_SUPPORT else 'Nein'}\n"
|
||||||
|
f"ODS-Support: {'Ja' if ODT_SUPPORT else 'Nein'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PresetDialog(simpledialog.Dialog):
|
||||||
|
"""Dialog zur Auswahl einer Voreinstellung"""
|
||||||
|
def __init__(self, parent, title, presets):
|
||||||
|
self.presets = presets
|
||||||
|
self.result = None
|
||||||
|
super().__init__(parent, title)
|
||||||
|
|
||||||
|
def body(self, master):
|
||||||
|
tk.Label(master, text="Voreinstellung auswählen:").pack(pady=5)
|
||||||
|
|
||||||
|
self.listbox = tk.Listbox(master)
|
||||||
|
for preset in self.presets:
|
||||||
|
self.listbox.insert(tk.END, preset)
|
||||||
|
self.listbox.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
||||||
|
|
||||||
|
return self.listbox
|
||||||
|
|
||||||
|
def apply(self):
|
||||||
|
selection = self.listbox.curselection()
|
||||||
|
if selection:
|
||||||
|
self.result = self.presets[selection[0]]
|
||||||
|
|
||||||
|
|
||||||
|
class PresetManagerWindow:
|
||||||
|
"""Fenster zur Verwaltung von Voreinstellungen"""
|
||||||
|
def __init__(self, parent, config_dir):
|
||||||
|
self.config_dir = config_dir
|
||||||
|
self.window = tk.Toplevel(parent)
|
||||||
|
self.window.title("Voreinstellungen verwalten")
|
||||||
|
self.window.geometry("400x300")
|
||||||
|
|
||||||
|
frame = ttk.Frame(self.window, padding="10")
|
||||||
|
frame.pack(fill=tk.BOTH, expand=True)
|
||||||
|
|
||||||
|
tk.Label(frame, text="Voreinstellungen:").pack(anchor=tk.W)
|
||||||
|
|
||||||
|
self.listbox = tk.Listbox(frame)
|
||||||
|
self.listbox.pack(fill=tk.BOTH, expand=True, pady=5)
|
||||||
|
|
||||||
|
button_frame = ttk.Frame(frame)
|
||||||
|
button_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
ttk.Button(button_frame, text="Löschen", command=self.delete_preset).pack(side=tk.LEFT, padx=5)
|
||||||
|
ttk.Button(button_frame, text="Schließen", command=self.window.destroy).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
self.refresh_list()
|
||||||
|
|
||||||
|
def refresh_list(self):
|
||||||
|
"""Liste aktualisieren"""
|
||||||
|
self.listbox.delete(0, tk.END)
|
||||||
|
for preset in self.config_dir.glob("*.json"):
|
||||||
|
self.listbox.insert(tk.END, preset.stem)
|
||||||
|
|
||||||
|
def delete_preset(self):
|
||||||
|
"""Voreinstellung löschen"""
|
||||||
|
selection = self.listbox.curselection()
|
||||||
|
if not selection:
|
||||||
|
messagebox.showwarning("Warnung", "Bitte wählen Sie eine Voreinstellung aus.")
|
||||||
|
return
|
||||||
|
|
||||||
|
preset_name = self.listbox.get(selection[0])
|
||||||
|
if messagebox.askyesno("Bestätigung", f"Voreinstellung '{preset_name}' wirklich löschen?"):
|
||||||
|
preset_file = self.config_dir / f"{preset_name}.json"
|
||||||
|
preset_file.unlink()
|
||||||
|
self.refresh_list()
|
||||||
|
messagebox.showinfo("Erfolg", f"Voreinstellung '{preset_name}' gelöscht.")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
root = tk.Tk()
|
||||||
|
app = CSVProcessorGUI(root)
|
||||||
|
root.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,7 @@
|
||||||
|
# CSV-Processor Abhängigkeiten
|
||||||
|
|
||||||
|
# Excel-Support (XLSX/XLS)
|
||||||
|
openpyxl>=3.0.0
|
||||||
|
|
||||||
|
# OpenDocument-Support (ODS)
|
||||||
|
odfpy>=1.4.0
|
||||||
Loading…
Reference in New Issue