Initial commit: CSV Processor

This commit is contained in:
Sebastian Zell 2026-01-16 11:23:43 +00:00
commit a2a9d7ac5a
11 changed files with 2880 additions and 0 deletions

1
.abacus.donotdelete Normal file
View File

@ -0,0 +1 @@
gAAAAABpah9sgktIo6WojeILJBTHfR6Y65YiGuG0qoAK_wkpSOJpY133w3Qhh_4zqQPY8sHOUAOL9HFXUWkBA7kBFFZAYMDIxETI-Af7l5LIyNiPhdZXzBI=

68
.gitignore vendored Normal file
View File

@ -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/

21
LICENSE Normal file
View File

@ -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.

217
README.md Normal file
View File

@ -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

94
build_debian.sh Executable file
View File

@ -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"

49
build_linux.sh Executable file
View File

@ -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"

48
build_windows.bat Normal file
View File

@ -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

1546
csv_processor.py Normal file

File diff suppressed because it is too large Load Diff

829
csv_processor_gui.py Normal file
View File

@ -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()

BIN
dist/csv-processor-debian.deb vendored Normal file

Binary file not shown.

7
requirements.txt Normal file
View File

@ -0,0 +1,7 @@
# CSV-Processor Abhängigkeiten
# Excel-Support (XLSX/XLS)
openpyxl>=3.0.0
# OpenDocument-Support (ODS)
odfpy>=1.4.0