From 97c81b466b8d4f34a0c186bc1d6c3d8684962cd3 Mon Sep 17 00:00:00 2001 From: mistangl Date: Wed, 30 Jul 2025 16:12:52 +0200 Subject: [PATCH] =?UTF-8?q?logging=20in=20plant2dxf.py,=20svg2dxf.py=20und?= =?UTF-8?q?=20dxf2lib.py=20implementiert=20SVG2DXF:=20Inkscape-Option=20hi?= =?UTF-8?q?nzugef=C3=BCgt,=20die=20mit=20einem=20SVG=20Parser=20arbeitet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/dxf2lib.py | 78 +------------- lib/plant2dxf.py | 107 +++++++++++++------ lib/svg2dxf.py | 263 ++++++++++++++++++++++++++++++++++++++++++----- lib/utils.py | 56 ++++++++++ 4 files changed, 376 insertions(+), 128 deletions(-) create mode 100644 lib/utils.py diff --git a/lib/dxf2lib.py b/lib/dxf2lib.py index 7ebefdc..f90549c 100644 --- a/lib/dxf2lib.py +++ b/lib/dxf2lib.py @@ -4,31 +4,10 @@ from ezdxf.math import Vec2 import argparse import sys import shutil -import datetime import configparser -import logging +from utils import check_environment_var, setup_logger -def check_environment_var(env_str: str, fallback: str) -> str: - """ - Prüft eine Umgebungsvariable und gibt den Wert oder einen Fallback zurück. - - Args: - env_str (str): Name der zu prüfenden Umgebungsvariable - fallback (str): Fallback-Wert, falls die Variable nicht gesetzt ist - - Returns: - str: Wert der Umgebungsvariable oder Fallback-Wert - - Note: - Gibt eine Warnung aus, falls die Umgebungsvariable nicht gesetzt ist - """ - out_path = os.environ.get(env_str) - if out_path: - return out_path - print(f"[WARN] Umgebungsvariable {env_str} ist nicht gesetzt oder leer. Verwende '{fallback}'.") - return fallback - def get_bbox(entities): """ Berechnet die Bounding Box für eine Liste von DXF-Entities. @@ -75,51 +54,6 @@ def get_bbox(entities): return Vec2((min_x + max_x) / 2, (min_y + max_y) / 2) -def setup_logger(log_dir): - """ - Erstellt und konfiguriert einen Logger für die DXF2LIB Verarbeitung. - - Args: - log_dir (str): Verzeichnis für die Log-Dateien - - Returns: - logging.Logger: Konfigurierter Logger - - Note: - Erstellt eine Log-Datei mit Zeitstempel und formatiert die Ausgaben - """ - # Logger erstellen - logger = logging.getLogger('dxf2lib') - logger.setLevel(logging.INFO) - - # Bestehende Handler entfernen (falls Logger bereits existiert) - for handler in logger.handlers[:]: - logger.removeHandler(handler) - - # Zeitstempel für Log-Datei - timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") - log_file = os.path.join(log_dir, f"dxf2lib_{timestamp}.log") - - # File Handler erstellen - file_handler = logging.FileHandler(log_file, encoding='utf-8') - file_handler.setLevel(logging.INFO) - - # Console Handler erstellen - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - - # Formatter erstellen - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') - file_handler.setFormatter(formatter) - console_handler.setFormatter(formatter) - - # Handler zum Logger hinzufügen - logger.addHandler(file_handler) - logger.addHandler(console_handler) - - return logger, log_file - - def create_block_library(input_dir, output_file, config, logger=None): """ Erstellt eine DXF-Block-Bibliothek aus einzelnen DXF-Dateien. @@ -299,12 +233,8 @@ if __name__ == "__main__": sys.exit(1) # Verzeichnisse über Umgebungsvariablen oder Fallback - if args.input: - INPUT_DIR = args.input - print(f"Verwende Input-Verzeichnis: {INPUT_DIR} \n") - else: - INPUT_DIR = os.path.join(check_environment_var("PROJECT_WORK", "."), "converted_dxfs") - print(f"Kein Input-Verzeichnis angegeben, verwende Standard: {INPUT_DIR} \n") + INPUT_DIR = check_environment_var("PROJECT_WORK", ".") + print(f"Verwende Input-Verzeichnis: {INPUT_DIR} \n") OUTPUT_FILE = os.path.join(check_environment_var("PROJECT_DATA", "."),"block_libaries", f"{args.name}.dxf") @@ -315,7 +245,7 @@ if __name__ == "__main__": print(f"Log-Verzeichnis erstellt: {log_dir}") # Logger Setup - logger, log_file = setup_logger(log_dir) + logger, log_file = setup_logger(log_dir, name='dxf2lib') logger.info("=== DXF2LIB Verarbeitung gestartet ===") logger.info(f"Input-Verzeichnis: {INPUT_DIR}") logger.info(f"Output-Datei: {OUTPUT_FILE}") diff --git a/lib/plant2dxf.py b/lib/plant2dxf.py index 94958ec..e856a0e 100644 --- a/lib/plant2dxf.py +++ b/lib/plant2dxf.py @@ -13,12 +13,21 @@ import configparser import ezdxf from pathlib import Path import math +from utils import check_environment_var, setup_logger # --------------------------------------------------------- CFG-Leser für shapes.cfg -def get_shape_cfg(teileart, cfg_path): +def get_shape_cfg(teileart, cfg_path, logger=None): parser = configparser.ConfigParser() - with open(cfg_path, encoding='utf-8') as f: - parser.read_file(f) + try: + with open(cfg_path, encoding='utf-8') as f: + parser.read_file(f) + except Exception as e: + msg = f"Fehler beim Lesen der Config-Datei {cfg_path}: {e}" + if logger: + logger.error(msg) + else: + print(msg) + return [] section = teileart if section not in parser: return [] @@ -53,14 +62,6 @@ ATTR_TAG = "TeileId" # Attributtag im Block RADIUS = 400 # Radius der Kreiselkreise (mm) # --------------------------------------------------------- Hilfsfunktionen -def check_environment_var(env_str: str) -> Path: - """Liefert Path aus Umgebungsvariable oder beendet mit Fehlermeldung.""" - out_path = os.environ.get(env_str) - if out_path: - return Path(out_path) - print(f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer.") - sys.exit(1) - def extract_coords(planquadrat: str) -> tuple[float, float]: """Extrahiert X/Y Koordinaten aus PlanquadratString.""" m = re.search(r"X:(\d+[\.,]?\d*)\s+Y:(\d+[\.,]?\d*)", planquadrat) @@ -87,8 +88,9 @@ def import_block(block_name: str, from_doc, to_doc) -> None: for ent in src: tgt.add_entity(ent.copy()) -def berechne_hoehe(csv_path): - y_werte = [] +def berechne_hoehe(csv_path, logger=None): + y_werte = [] + try: with csv_path.open(newline="", encoding="utf-8") as fh: reader = csv.DictReader(fh, delimiter=';') for row in reader: @@ -96,11 +98,23 @@ def berechne_hoehe(csv_path): try: _, y = extract_coords(planquadrat) y_werte.append(y) - except Exception: - continue - if not y_werte: - raise ValueError("Keine Y-Koordinaten in der CSV gefunden!") - return max(y_werte) + except Exception as e: + if logger: + logger.warning(f"Fehler beim Extrahieren der Koordinate aus '{planquadrat}': {e}") + except Exception as e: + if logger: + logger.error(f"Fehler beim Lesen der CSV-Datei {csv_path}: {e}") + else: + print(f"Fehler beim Lesen der CSV-Datei {csv_path}: {e}") + return 0 + if not y_werte: + msg = "Keine Y-Koordinaten in der CSV gefunden!" + if logger: + logger.error(msg) + else: + print(msg) + raise ValueError(msg) + return max(y_werte) def transform_coords(x: float, y: float, height: float) -> tuple[float, float]: """Transformiert Bildschirmkoordinaten (0,0 oben links) ins DXF-KoSy (0,0 unten links).""" @@ -276,7 +290,7 @@ def normalize_func_name(name): # --------------------------------------------------------- Hauptfunktion def main(csv_path: Path, lib_path: Path, cfg_path: Path, - output_path: Path, verbose=False): + output_path: Path, verbose=False, logger=None): # Bibliothek nur laden, wenn Datei existiert lib_doc = None @@ -284,12 +298,21 @@ def main(csv_path: Path, lib_path: Path, cfg_path: Path, try: lib_doc = ezdxf.readfile(lib_path) if verbose: - print(f"[INFO] Bibliothek geladen: {lib_path}") + logger.info(f"[INFO] Bibliothek geladen: {lib_path}") if logger else print(f"[INFO] Bibliothek geladen: {lib_path}") except Exception as e: - sys.exit(f"Fehler beim Lesen der Bibliothek '{lib_path}': {e}") + msg = f"Fehler beim Lesen der Bibliothek '{lib_path}': {e}" + if logger: + logger.error(msg) + else: + print(msg) + sys.exit(1) else: - print(f"[INFO] Keine Bibliothek gefunden unter {lib_path}.") - exit(1) + msg = f"[INFO] Keine Bibliothek gefunden unter {lib_path}." + if logger: + logger.warning(msg) + else: + print(msg) + sys.exit(1) # Neue Ziel­zeichnung (DXF R2018) doc = ezdxf.new(dxfversion="R2018") @@ -297,7 +320,15 @@ def main(csv_path: Path, lib_path: Path, cfg_path: Path, # CSV einlesen # Höhe bestimmen für Koordinaten-Transformation - height = berechne_hoehe(csv_path) + try: + height = berechne_hoehe(csv_path, logger=logger) + except Exception as e: + msg = f"Fehler bei der Höhenberechnung: {e}" + if logger: + logger.error(msg) + else: + print(msg) + sys.exit(1) # Verarbeitung der Blöcke with csv_path.open(newline="", encoding="utf-8") as fh: @@ -312,22 +343,33 @@ def main(csv_path: Path, lib_path: Path, cfg_path: Path, x_screen, y_screen = extract_coords(planquadrat) x, y = transform_coords(x_screen, y_screen, height) except Exception as e: - print(f"[WARN] {teileid}: {e}") + msg = f"[WARN] {teileid}: {e}" + if logger: + logger.warning(msg) + else: + print(msg) continue # Funktions-Dispatch: handle_ (mit _ statt Leerzeichen und Punkten, alles klein) func_name = f'handle_{normalize_func_name(teileart)}' handler = globals().get(func_name) - symbols = get_shape_cfg(teileart, cfg_path) + symbols = get_shape_cfg(teileart, cfg_path, logger=logger) if handler: handler(msp, teileid, merkmale, x, y, doc, lib_doc, verbose, symbols) else: - print(f"[WARN] Keine Routine für TeileArt '{teileart}'. Überspringe '{teileid}'.") - continue + msg = f"[WARN] Keine Routine für TeileArt '{teileart}'. Überspringe '{teileid}'." + if logger: + logger.warning(msg) + else: + print(msg) + continue # DXF speichern doc.saveas(output_path) - print(f"[DONE] DXF gespeichert unter: {output_path}") + if logger: + logger.info(f"[DONE] DXF gespeichert unter: {output_path}") + else: + print(f"[DONE] DXF gespeichert unter: {output_path}") if __name__ == "__main__": parser = argparse.ArgumentParser( @@ -340,10 +382,14 @@ if __name__ == "__main__": args = parser.parse_args() # Verzeichnisse aus Umgebungs­variablen + log_dir = check_environment_var("PROJECT_LOG") data_dir = check_environment_var("PROJECT_DATA") work_dir = check_environment_var("PROJECT_WORK") config_dir = check_environment_var("PROJECT_CFG") + logger = setup_logger(log_dir, name='plant2dxf') + logger.info("=== plant2dxf Verarbeitung gestartet ===") + # CSV‑Pfad: nur Dateiname → im WORK‑Ordner suchen if os.sep not in args.file and "/" not in args.file: csv_path = work_dir / args.file @@ -354,4 +400,5 @@ if __name__ == "__main__": lib_path = Path(args.lib) if args.lib else data_dir / "blocks.dxf" output_path = Path(args.output) if args.output else (work_dir / f"{csv_path.stem}.dxf") - main(csv_path, lib_path, cfg_path, output_path, verbose=args.verbose) + main(csv_path, lib_path, cfg_path, output_path, verbose=args.verbose, logger=logger) + logger.info("=== plant2dxf Verarbeitung abgeschlossen ===") diff --git a/lib/svg2dxf.py b/lib/svg2dxf.py index d0a95a3..7df943b 100644 --- a/lib/svg2dxf.py +++ b/lib/svg2dxf.py @@ -3,14 +3,209 @@ import subprocess import sys import argparse import time -# Hilfsfunktion wie in plant2dxf.py +import ezdxf +from svg.path import parse_path, Line, Arc, CubicBezier, QuadraticBezier, Close +import xml.etree.ElementTree as ET +import re -def check_environment_var(env_str: str, fallback: str) -> str: - out_path = os.environ.get(env_str) - if out_path: - return out_path - print(f"[WARN] Umgebungsvariable {env_str} ist nicht gesetzt oder leer. Verwende '{fallback}'.") - return fallback +from utils import check_environment_var, setup_logger + +class SVGtoDXFConverter: + def __init__(self, logger=None): + # Standard DXF-Farben (AutoCAD Color Index) + self.color_map = { + 'red': 1, 'yellow': 2, 'green': 3, 'cyan': 4, 'blue': 5, + 'magenta': 6, 'white': 7, 'gray': 8, 'grey': 8, 'black': 7 + } + self.logger = logger + + def svg_to_dxf(self, svg_file: str, dxf_file: str): + """Konvertiert eine SVG-Datei zu DXF (AutoCAD 2018)""" + try: + # DXF-Dokument für AutoCAD 2018 erstellen + doc = ezdxf.new('R2018') + msp = doc.modelspace() + + # SVG parsen + tree = ET.parse(svg_file) + root = tree.getroot() + + # SVG-Namespace definieren + namespaces = { + 'svg': 'http://www.w3.org/2000/svg', + '': 'http://www.w3.org/2000/svg' # Default namespace + } + + # Alle path-Elemente finden und konvertieren + for path_elem in root.iter(): + if path_elem.tag.endswith('path') or path_elem.tag == 'path': + self.convert_path_element(path_elem, msp) + + # DXF speichern + doc.saveas(dxf_file) + if self.logger: + self.logger.info(f"DXF-Datei erfolgreich erstellt: {dxf_file}") + else: + print(f"DXF-Datei erfolgreich erstellt: {dxf_file}") + except Exception as e: + if self.logger: + self.logger.error(f"Fehler beim Parsen der SVG-Datei {svg_file}: {e}") + else: + print(f"Fehler beim Parsen der SVG-Datei {svg_file}: {e}") + + def convert_path_element(self, path_elem, msp): + """Konvertiert ein SVG path-Element zu DXF-Entities""" + d = path_elem.get('d') + if not d: + return + + # Farbe aus stroke-Attribut extrahieren + color_index = self.get_color_from_stroke(path_elem.get('stroke', 'black')) + + try: + path = parse_path(d) + self.convert_path_to_dxf(path, msp, color_index) + except Exception as e: + if self.logger: + self.logger.warning(f"Fehler beim Parsen des Pfads: {e}") + else: + print(f"Warnung: Fehler beim Parsen des Pfads: {e}") + + def get_color_from_stroke(self, stroke_value: str) -> int: + """Konvertiert SVG stroke-Farbe zu DXF Color Index""" + if not stroke_value or stroke_value == 'none': + return 7 # Standard: weiß/schwarz + + stroke_value = stroke_value.lower().strip() + + # Direkte Farbnamen + if stroke_value in self.color_map: + return self.color_map[stroke_value] + + # Hex-Farben zu RGB und dann zu nächster DXF-Farbe + if stroke_value.startswith('#'): + return self.hex_to_dxf_color(stroke_value) + + # RGB-Farben + rgb_match = re.match(r'rgb\((\d+),\s*(\d+),\s*(\d+)\)', stroke_value) + if rgb_match: + r, g, b = map(int, rgb_match.groups()) + return self.rgb_to_dxf_color(r, g, b) + + return 7 # Fallback: weiß/schwarz + + def hex_to_dxf_color(self, hex_color: str) -> int: + """Konvertiert Hex-Farbe zu DXF Color Index""" + hex_color = hex_color.lstrip('#') + if len(hex_color) == 6: + r = int(hex_color[0:2], 16) + g = int(hex_color[2:4], 16) + b = int(hex_color[4:6], 16) + return self.rgb_to_dxf_color(r, g, b) + return 7 + + def rgb_to_dxf_color(self, r: int, g: int, b: int) -> int: + """Konvertiert RGB-Werte zu nächstem DXF Color Index""" + # Vereinfachte Zuordnung zu Hauptfarben + if r > 200 and g < 100 and b < 100: + return 1 # Rot + elif r > 200 and g > 200 and b < 100: + return 2 # Gelb + elif r < 100 and g > 200 and b < 100: + return 3 # Grün + elif r < 100 and g > 200 and b > 200: + return 4 # Cyan + elif r < 100 and g < 100 and b > 200: + return 5 # Blau + elif r > 200 and g < 100 and b > 200: + return 6 # Magenta + elif r > 200 and g > 200 and b > 200: + return 7 # Weiß + else: + return 8 # Grau + + def convert_path_to_dxf(self, path, msp, color_index: int): + """Konvertiert SVG-Pfad zu DXF-Entities""" + current_point = None + + for segment in path: + if isinstance(segment, Line): + start = (segment.start.real, -segment.start.imag) # Y-Achse umkehren + end = (segment.end.real, -segment.end.imag) + line = msp.add_line(start, end) + line.dxf.color = color_index + current_point = end + + elif isinstance(segment, Arc): + # Arc zu Polyline approximieren + points = self.arc_to_points(segment) + if len(points) > 1: + for i in range(len(points) - 1): + start = (points[i].real, -points[i].imag) + end = (points[i+1].real, -points[i+1].imag) + line = msp.add_line(start, end) + line.dxf.color = color_index + current_point = (points[-1].real, -points[-1].imag) + + elif isinstance(segment, (CubicBezier, QuadraticBezier)): + # Bezier-Kurven zu Polyline approximieren + points = self.bezier_to_points(segment) + if len(points) > 1: + for i in range(len(points) - 1): + start = (points[i].real, -points[i].imag) + end = (points[i+1].real, -points[i+1].imag) + line = msp.add_line(start, end) + line.dxf.color = color_index + current_point = (points[-1].real, -points[-1].imag) + + elif isinstance(segment, Close): + # Pfad schließen - zurück zum Startpunkt + if current_point and len(path) > 0: + start_point = (path[0].start.real, -path[0].start.imag) + if current_point != start_point: + line = msp.add_line(current_point, start_point) + line.dxf.color = color_index + + def arc_to_points(self, arc, num_points: int = 20): + """Approximiert einen Bogen durch Punkte""" + points = [] + for i in range(num_points + 1): + t = i / num_points + point = arc.point(t) + points.append(point) + return points + + def bezier_to_points(self, bezier, num_points: int = 20): + """Approximiert eine Bezier-Kurve durch Punkte""" + points = [] + for i in range(num_points + 1): + t = i / num_points + point = bezier.point(t) + points.append(point) + return points + +# Batch-Konvertierung für mehrere Dateien +def batch_convert(svg_files: list, output_dir: str = '.', logger=None): + """Konvertiert mehrere SVG-Dateien""" + converter = SVGtoDXFConverter(logger=logger) + + for svg_file in svg_files: + try: + # Ausgabedateiname generieren + base_name = svg_file.rsplit('.', 1)[0] + dxf_file = f"{output_dir}/{base_name}.dxf" + + converter.svg_to_dxf(svg_file, dxf_file) + if logger: + logger.info(f"✓ {svg_file} -> {dxf_file}") + else: + print(f"✓ {svg_file} -> {dxf_file}") + + except Exception as e: + if logger: + logger.error(f"✗ Fehler bei {svg_file}: {e}") + else: + print(f"✗ Fehler bei {svg_file}: {e}") if __name__ == "__main__": @@ -18,7 +213,8 @@ if __name__ == "__main__": parser = argparse.ArgumentParser(description="SVG/XML zu DXF Konverter") parser.add_argument('-i', '--input', type=str, help='Input-Verzeichnis mit SVG/XML-Dateien') parser.add_argument('-o', '--output', type=str, help='Output-Verzeichnis für DXF-Dateien') - + parser.add_argument('-k', '--inkscape', action='store_true', help='Batch-Konvertierung über Inkscape') + if len(sys.argv) == 2 and sys.argv[1] in ("-h", "--help"): parser.print_help() sys.exit(0) @@ -40,24 +236,43 @@ if __name__ == "__main__": OUTPUT_DIR = os.path.join(check_environment_var("PROJECT_WORK", "."), "converted_dxfs") print(f"Kein Output-Verzeichnis angegeben, verwende Standard: {OUTPUT_DIR} \n") - INKSCAPE = "inkscape" # ggf. vollständiger Pfad angeben - os.makedirs(OUTPUT_DIR, exist_ok=True) files = [f for f in os.listdir(INPUT_DIR) if f.lower().endswith((".svg", ".xml"))] - for filename in files: - input_path = os.path.join(INPUT_DIR, filename) - output_path = os.path.join(OUTPUT_DIR, os.path.splitext(filename)[0] + ".dxf") + log_dir = check_environment_var("PROJECT_LOG") + logger = setup_logger(log_dir, name='svg2dxf') + logger.info("=== SVG2DXF Verarbeitung gestartet ===") + logger.info(f"Input-Verzeichnis: {INPUT_DIR}") + logger.info(f"Output-Verzeichnis: {OUTPUT_DIR}") + + + if args.inkscape: + INKSCAPE = "inkscape" # ggf. vollständiger Pfad angeben + for filename in files: + input_path = os.path.join(INPUT_DIR, filename) + output_path = os.path.join(OUTPUT_DIR, os.path.splitext(filename)[0] + ".dxf") + if logger: + logger.info(f"Konvertiere: {input_path} → {output_path}") + else: + print(f"Konvertiere: {input_path} → {output_path}") + try: + subprocess.run([ + INKSCAPE, + input_path, + "--export-type=dxf", + f"--export-filename={output_path}" + ], check=True) + except subprocess.CalledProcessError as e: + if logger: + logger.error(f"[FEHLER] Inkscape-Absturz bei {filename}: {e}") + else: + print(f"[FEHLER] Inkscape-Absturz bei {filename}: {e}") + time.sleep(0.5) # kleine Pause zur Entlastung von RAM/GTK + + else: + batch_convert(files, OUTPUT_DIR, logger=logger) + + logger.info("=== SVG2DXF Verarbeitung abgeschlossen ===") + - print(f"Konvertiere: {input_path} → {output_path}") - try: - subprocess.run([ - INKSCAPE, - input_path, - "--export-type=dxf", - f"--export-filename={output_path}" - ], check=True) - except subprocess.CalledProcessError as e: - print(f"[FEHLER] Inkscape-Absturz bei {filename}: {e}") - time.sleep(0.5) # kleine Pause zur Entlastung von RAM/GTK diff --git a/lib/utils.py b/lib/utils.py new file mode 100644 index 0000000..daf0326 --- /dev/null +++ b/lib/utils.py @@ -0,0 +1,56 @@ +import os +import sys +import logging +from pathlib import Path + + +def check_environment_var(env_str: str) -> Path: + """Liefert Path aus Umgebungsvariable oder beendet mit Fehlermeldung.""" + out_path = os.environ.get(env_str) + if out_path: + return Path(out_path) + msg = f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer." + print(msg) + sys.exit(1) + + +def setup_logger(log_dir: Path, name: str = 'app') -> logging.Logger: + """ + Erstellt und konfiguriert einen Logger, der sowohl in eine Datei als auch auf die Konsole schreibt. + Die Logdatei erhält einen Zeitstempel im Namen und ist UTF-8 kodiert. + + Args: + log_dir (Path): Verzeichnis für die Log-Dateien + name (str): Name des Loggers (z.B. 'plant2dxf', 'dxf2lib') + Returns: + logging.Logger: Konfigurierter Logger + """ + from datetime import datetime + logger = logging.getLogger(name) + logger.setLevel(logging.INFO) + + # Vorherige Handler entfernen, um doppelte Logs zu vermeiden + for handler in logger.handlers[:]: + logger.removeHandler(handler) + + # Log-Verzeichnis anlegen (falls nicht vorhanden) + log_dir.mkdir(parents=True, exist_ok=True) + + # Logdatei mit Zeitstempel im Namen erzeugen + log_file = log_dir / f"{name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log" + file_handler = logging.FileHandler(log_file, encoding='utf-8') + file_handler.setLevel(logging.INFO) + + # Handler für die Konsole + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + + # Einheitliches Log-Format definieren + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + file_handler.setFormatter(formatter) + console_handler.setFormatter(formatter) + + # Handler dem Logger hinzufügen + logger.addHandler(file_handler) + logger.addHandler(console_handler) + return logger \ No newline at end of file