import argparse import json import os import re import sys from pathlib import Path from configparser import RawConfigParser from fnmatch import fnmatch import ezdxf from openpyxl import Workbook """ Dieses Programm: - liest eine DXF Datei und sammelt alle TEXT und MTEXT Objekte ODER liest Texte aus einer TXT Datei (ein Text pro Zeile) - filtert die Texte basierend auf Mustern in translator.cfg - übersetzt optional Texte mit Trigrammen, Bigrammen und Einzelwörtern aus translation/.cfg - schreibt die Ergebnisse in eine Excel- oder JSON-Datei - speichert die Ausgabedatei im PROJECT_WORK Verzeichnis """ def check_environment_var(env_str: str) -> Path: """Prüft ob eine Umgebungsvariable gesetzt ist und gibt den Pfad zurück.""" out_path = os.environ.get(env_str) if out_path: return Path(out_path) else: print(f"Umgebungsvariable {env_str} ist nicht gesetzt oder leer.") sys.exit(1) def load_translation_config(translation_lang: str, translation_dir: Path) -> tuple[dict[str, str], dict[str, str], dict[str, str]]: """ Lädt eine Übersetzungs-Config-Datei (z.B. CSK.cfg, EN.cfg, FR.cfg). Args: translation_lang: Sprach-Code (z.B. "CSK", "EN", "FR") translation_dir: Verzeichnis mit Übersetzungs-Configs (aus PROJECT_TRANSLATION) Returns: Tuple mit (Trigramme-Dict, Bigramme-Dict, Single-Dict) """ config_file = translation_dir / f"{translation_lang}.cfg" if not config_file.exists(): print(f"FEHLER: Übersetzungs-Konfiguration nicht gefunden: {config_file}") sys.exit(1) # RawConfigParser mit case-sensitive Keys config = RawConfigParser() # Überschreibe optionxform um Groß-/Kleinschreibung zu erhalten config.optionxform = str try: config.read(config_file, encoding='utf-8') except Exception as e: print(f"Fehler beim Lesen der Übersetzungs-Konfiguration: {e}") sys.exit(1) trigramme = {} bigramme = {} single = {} # Lade Trigramme if config.has_section('trigramme'): for key in config['trigramme']: original = key translation = config['trigramme'][key] if translation and not translation.startswith('#'): trigramme[original] = translation # Lade Bigramme if config.has_section('bigramme'): for key in config['bigramme']: original = key translation = config['bigramme'][key] if translation and not translation.startswith('#'): bigramme[original] = translation # Lade Single if config.has_section('single'): for key in config['single']: original = key translation = config['single'][key] if translation and not translation.startswith('#'): single[original] = translation print(f"\nÜbersetzungs-Config geladen ({translation_lang}):") print(f" Trigramme: {len(trigramme)}") print(f" Bigramme: {len(bigramme)}") print(f" Einzelwörter: {len(single)}") return trigramme, bigramme, single def load_ignore_patterns(cfg_path: Path) -> tuple[list[str], list[str]]: """ Liest die ignore_pattern aus der translator.cfg Datei. Unterstützt sowohl Wildcard-Muster (*,?) als auch reguläre Ausdrücke. Args: cfg_path: Pfad zum Konfigurationsverzeichnis (PROJECT_CFG) Returns: Tuple mit (Wildcard-Mustern, Regex-Mustern) """ config_file = cfg_path / "translator.cfg" if not config_file.exists(): print(f"FEHLER: Konfigurationsdatei nicht gefunden: {config_file}") print(f"Bitte erstellen Sie die Datei 'translator.cfg' im Verzeichnis: {cfg_path}") sys.exit(1) # RawConfigParser verhindert dass Backslashes in Regex-Mustern escaped werden config = RawConfigParser() # Überschreibe optionxform um Groß-/Kleinschreibung zu erhalten config.optionxform = str try: config.read(config_file, encoding='utf-8') except Exception as e: print(f"Fehler beim Lesen der Konfigurationsdatei: {e}") sys.exit(1) wildcard_patterns = [] regex_patterns = [] # Lade Wildcard-Muster if config.has_section('ignore_pattern_wildcard'): for key in config['ignore_pattern_wildcard']: pattern = config['ignore_pattern_wildcard'][key].strip() if pattern and not pattern.startswith('#'): wildcard_patterns.append(pattern) # Lade Regex-Muster if config.has_section('ignore_pattern_regex'): for key in config['ignore_pattern_regex']: pattern = config['ignore_pattern_regex'][key].strip() if pattern and not pattern.startswith('#'): regex_patterns.append(pattern) print(f"Geladene Wildcard-Muster: {len(wildcard_patterns)}") for pattern in wildcard_patterns: print(f" - {pattern}") print(f"Geladene Regex-Muster: {len(regex_patterns)}") for pattern in regex_patterns: print(f" - {pattern}") return wildcard_patterns, regex_patterns def translate_text(text: str, trigramme: dict[str, str], bigramme: dict[str, str], single: dict[str, str]) -> str: """ Übersetzt einen Text durch Ersetzen von Trigrammen, Bigrammen und Einzelwörtern. Ersetzungen erfolgen in dieser Reihenfolge: Trigramme → Bigramme → Einzelwörter. Bei Einzelwörtern werden auch Satzzeichen berücksichtigt: - "Weight:" wird zu "Hmotnost:" (Doppelpunkt bleibt erhalten) - "(Sub-distributor)" wird zu "(Rozvaděč)" (Klammern bleiben erhalten) Bindestriche im Wort (wie "Sub-distributor") bleiben Teil des Kern-Worts. Args: text: Zu übersetzender Text trigramme: Dictionary mit Drei-Wort-Phrasen bigramme: Dictionary mit Zwei-Wort-Phrasen single: Dictionary mit Einzelwörtern Returns: Übersetzter Text (leer wenn keine Übersetzung gefunden wurde) """ translated_text = text has_translation = False # 1. Trigramme ersetzen (längste Phrasen zuerst) for original, translation in trigramme.items(): if original in translated_text: translated_text = translated_text.replace(original, translation) has_translation = True # 2. Bigramme ersetzen for original, translation in bigramme.items(): if original in translated_text: translated_text = translated_text.replace(original, translation) has_translation = True # 3. Einzelwörter ersetzen words = translated_text.split() translated_words = [] for word in words: if word in single: # Direkter Match translated_words.append(single[word]) has_translation = True else: # Prüfe ob Wort mit Satzzeichen am Ende existiert (z.B. "Weight:" -> "Weight") # Trenne Satzzeichen am Anfang und Ende vom Kern-Wort # Kern-Wort kann Buchstaben, Zahlen, Unterstriche und Bindestriche enthalten match = re.match(r'^([^\w-]*)([\w-]+)([^\w-]*)$', word) if match: prefix, core_word, suffix = match.groups() if core_word in single: # Übersetze Kern-Wort und behalte Satzzeichen translated_words.append(prefix + single[core_word] + suffix) has_translation = True else: translated_words.append(word) else: translated_words.append(word) translated_text = ' '.join(translated_words) # Gib nur übersetzten Text zurück wenn tatsächlich etwas übersetzt wurde return translated_text if has_translation else "" def should_ignore_text(text: str, wildcard_patterns: list[str], regex_patterns: list[str], debug: bool = False) -> bool: """ Prüft ob ein Text ignoriert werden soll basierend auf den Mustern. Args: text: Zu prüfender Text wildcard_patterns: Liste der Wildcard-Muster (*,?) regex_patterns: Liste der regulären Ausdrücke debug: Wenn True, gibt Debug-Informationen aus Returns: True wenn Text ignoriert werden soll, False sonst """ # Prüfe Wildcard-Muster for pattern in wildcard_patterns: if fnmatch(text, pattern): if debug: print(f" DEBUG: '{text}' matched wildcard '{pattern}'") return True # Prüfe Regex-Muster for pattern in regex_patterns: try: if re.search(pattern, text): if debug: print(f" DEBUG: '{text}' matched regex '{pattern}'") return True except re.error: # Falls ungültiger Regex, ignoriere dieses Muster print(f"WARNUNG: Ungültiger regulärer Ausdruck: {pattern}") continue return False def extract_text_from_dxf(filename: Path) -> list[str]: """ Extrahiert alle TEXT und MTEXT Objekte aus einer DXF Datei. Verwendet ein Set, um Duplikate zu vermeiden. Filtert leere Texte heraus. Args: filename: Pfad zur DXF Datei Returns: Alphabetisch sortierte Liste aller gefundenen Texte (ohne Duplikate, ohne leere Texte) """ texts_set = set() # Set für eindeutige Texte total_count = 0 empty_count = 0 try: doc = ezdxf.readfile(filename) msp = doc.modelspace() # Sammle alle TEXT Objekte for entity in msp.query('TEXT'): if entity.dxf.text: total_count += 1 text = entity.dxf.text.strip() # Entferne führende/nachfolgende Leerzeichen # Ignoriere leere Texte if not text: empty_count += 1 continue texts_set.add(text) # Sammle alle MTEXT Objekte for entity in msp.query('MTEXT'): if entity.text: total_count += 1 text = entity.text.strip() # Entferne führende/nachfolgende Leerzeichen # Ignoriere leere Texte if not text: empty_count += 1 continue texts_set.add(text) # Konvertiere Set zu sortierter Liste texts = sorted(texts_set) print(f"Anzahl gefundener TEXT/MTEXT Objekte: {total_count}") print(f"Leere Texte ignoriert: {empty_count}") print(f"Duplikate entfernt: {total_count - empty_count - len(texts)}") print(f"Eindeutige Texte extrahiert: {len(texts)}") except Exception as e: print(f"Fehler beim Lesen der DXF Datei: {e}") sys.exit(1) return texts def filter_texts(texts: list[str], wildcard_patterns: list[str], regex_patterns: list[str], debug: bool = False) -> tuple[list[str], list[str]]: """ Filtert eine Liste von Texten basierend auf Wildcard- und Regex-Mustern. Diese Funktion ist unabhängig von der Quelle der Texte (DXF oder TXT). Args: texts: Liste der zu filternden Texte wildcard_patterns: Liste der Wildcard-Muster zum Filtern regex_patterns: Liste der Regex-Muster zum Filtern debug: Wenn True, gibt Debug-Informationen aus Returns: Tuple mit (akzeptierte Texte, ignorierte Texte) - beide alphabetisch sortiert """ accepted_texts = [] ignored_texts = [] for text in texts: if should_ignore_text(text, wildcard_patterns, regex_patterns, debug): ignored_texts.append(text) else: accepted_texts.append(text) # Sortiere beide Listen alphabetisch accepted_texts.sort() ignored_texts.sort() print(f"\nFilterung abgeschlossen:") print(f" Akzeptierte Texte: {len(accepted_texts)}") print(f" Ignorierte Texte: {len(ignored_texts)}") return accepted_texts, ignored_texts def read_texts_from_file(filename: Path) -> list[str]: """ Liest Texte aus einer UTF-8 Textdatei (ein Text pro Zeile). Entfernt Duplikate und leere Zeilen. Args: filename: Pfad zur Textdatei Returns: Alphabetisch sortierte Liste aller Texte (ohne Duplikate, ohne leere Zeilen) """ texts_set = set() try: with open(filename, 'r', encoding='utf-8') as f: for line in f: text = line.strip() if text: # Ignoriere leere Zeilen # Konvertiere escaped Zeilenumbrüche zurück text = text.replace('\\n', '\n').replace('\\r', '\r') texts_set.add(text) texts = sorted(texts_set) print(f"Texte aus Datei gelesen: {len(texts)}") return texts except Exception as e: print(f"Fehler beim Lesen der Textdatei: {e}") sys.exit(1) def write_texts_to_excel(texts: list[str], output_path: Path, translation_dicts: tuple[dict[str, str], dict[str, str], dict[str, str]] = None): """ Schreibt die Texte in die erste Spalte einer Excel-Datei. Optional: Schreibt Übersetzungen in die zweite Spalte. Args: texts: Liste der zu schreibenden Texte output_path: Pfad zur Ausgabe-Excel-Datei translation_dicts: Optional: Tuple mit (Trigramme, Bigramme, Single) für Übersetzungen """ wb = Workbook() ws = wb.active ws.title = "Texte" # Kopfzeile ws['A1'] = "Text" if translation_dicts: ws['B1'] = "Übersetzung" # Texte schreiben for idx, text in enumerate(texts, start=2): ws[f'A{idx}'] = text # Optional: Übersetzung in zweite Spalte if translation_dicts: trigramme, bigramme, single = translation_dicts translated = translate_text(text, trigramme, bigramme, single) if translated: # Nur wenn Übersetzung gefunden wurde ws[f'B{idx}'] = translated # Sonst bleibt die Zelle leer # Spaltenbreiten anpassen ws.column_dimensions['A'].width = 50 if translation_dicts: ws.column_dimensions['B'].width = 50 try: wb.save(output_path) print(f"Excel-Datei erfolgreich erstellt: {output_path}") if translation_dicts: print(f" Mit Übersetzungen in Spalte B") except Exception as e: print(f"Fehler beim Schreiben der Excel-Datei: {e}") sys.exit(1) def write_texts_to_json(texts: list[str], ignored_texts: list[str], output_path: Path, translation_dicts: tuple[dict[str, str], dict[str, str], dict[str, str]] = None): """ Schreibt die Texte in eine JSON-Datei mit drei Blöcken. Args: texts: Liste der akzeptierten Texte ignored_texts: Liste der ignorierten Texte output_path: Pfad zur Ausgabe-JSON-Datei translation_dicts: Optional: Tuple mit (Trigramme, Bigramme, Single) für Übersetzungen """ output_data = {} # Block 1: Übersetzte Texte (als Liste von Dicts mit source/target) # Block 2: Nicht übersetzte Texte (nur akzeptierte Texte ohne Übersetzung) if translation_dicts: trigramme, bigramme, single = translation_dicts translations = [] untranslated_texts = [] for text in texts: translated = translate_text(text, trigramme, bigramme, single) if translated: # Übersetzung vorhanden translations.append({ "source": text, "target": translated }) else: # Keine Übersetzung vorhanden untranslated_texts.append(text) output_data["translations"] = translations output_data["texts"] = untranslated_texts else: # Ohne Übersetzungs-Config: alle akzeptierten Texte sind nicht übersetzt output_data["translations"] = [] output_data["texts"] = texts # Block 3: Ignorierte Texte (als Liste) output_data["ignored"] = ignored_texts try: with open(output_path, 'w', encoding='utf-8') as f: json.dump(output_data, f, ensure_ascii=False, indent=2) print(f"JSON-Datei erfolgreich erstellt: {output_path}") if translation_dicts: print(f" Übersetzungen: {len(output_data['translations'])}") print(f" Nicht übersetzte Texte: {len(output_data['texts'])}") else: print(f" Akzeptierte Texte: {len(output_data['texts'])}") print(f" Ignorierte Texte: {len(ignored_texts)}") except Exception as e: print(f"Fehler beim Schreiben der JSON-Datei: {e}") sys.exit(1) if __name__ == '__main__': parser = argparse.ArgumentParser( description='Extrahiert TEXT und MTEXT Objekte aus einer DXF Datei oder filtert Texte aus einer TXT Datei. Optional mit Übersetzungsfunktion.', prog='translate' ) # Mutually exclusive group: entweder DXF-Extraktion oder TXT-Filterung input_group = parser.add_mutually_exclusive_group(required=True) input_group.add_argument( '-e', '--extract', action='store_true', help='Extrahiert Texte aus DXF/DWG Datei (benötigt --filename)' ) input_group.add_argument( '--fromtxt', action='store', metavar='FILE.txt', help='Filtert Texte aus einer TXT Datei (ein Text pro Zeile)' ) parser.add_argument( '-f', '--filename', action='store', help='DXF/DWG Datei (.dxf, .dwg) die verarbeitet werden soll (nur mit --extract)', metavar='myfile.dxf' ) parser.add_argument( '-o', '--outname', action='store', help='Name der Ausgabedatei (ohne Pfad, mit oder ohne .xlsx/.txt Endung)', metavar='output.xlsx' ) parser.add_argument( '-d', '--debug', action='store_true', help='Aktiviert Debug-Ausgabe für Pattern-Matching' ) parser.add_argument( '-t', '--export-type', action='store', default='excel', choices=['excel', 'json'], help='Exportformat: excel (Standard) oder json (JSON-Datei)' ) parser.add_argument( '--translate', action='store', metavar='LANG', help='Übersetzt Texte mit Config aus translation/.cfg (z.B. CSK, EN, FR)' ) args = parser.parse_args() # Validierung: --extract benötigt --filename if args.extract and not args.filename: parser.error("--extract benötigt --filename") # Validierung: --filename nur mit --extract if args.filename and not args.extract: parser.error("--filename kann nur mit --extract verwendet werden") # Prüfe Umgebungsvariablen work_dir = check_environment_var('PROJECT_WORK') cfg_dir = check_environment_var('PROJECT_CFG') # Lade Ignore-Muster aus Konfigurationsdatei wildcard_patterns, regex_patterns = load_ignore_patterns(cfg_dir) # Optional: Lade Übersetzungs-Config translation_dicts = None if args.translate: translation_dir = check_environment_var('PROJECT_TRANSLATION') translation_dicts = load_translation_config(args.translate, translation_dir) if args.debug: print("DEBUG-Modus aktiviert") # Bestimme Input-Modus und Datei if args.extract: # DXF/DWG-Modus input_filename = args.filename file_extension = input_filename.lower() if not file_extension.endswith(('.dxf', '.dwg')): print("Mit --extract nur .dxf oder .dwg Dateien erlaubt") sys.exit(1) # Versuche zuerst Datei im Work-Ordner zu finden filename = work_dir / input_filename if not filename.exists(): # Falls nicht gefunden, prüfe ob es ein vollständiger Pfad ist filename = Path(input_filename) if not filename.exists(): print(f"Datei nicht gefunden: {input_filename}") print(f" Geprüft in Work-Ordner: {work_dir / input_filename}") print(f" Geprüft als vollständiger Pfad: {filename}") sys.exit(1) print(f"Lese DXF Datei: {filename}") all_texts = extract_text_from_dxf(filename) else: # args.fromtxt # TXT-Modus input_filename = args.fromtxt if not input_filename.lower().endswith('.txt'): print("Mit --fromtxt nur .txt Dateien erlaubt") sys.exit(1) # Versuche zuerst Datei im Work-Ordner zu finden filename = work_dir / input_filename if not filename.exists(): # Falls nicht gefunden, prüfe ob es ein vollständiger Pfad ist filename = Path(input_filename) if not filename.exists(): print(f"Datei nicht gefunden: {input_filename}") print(f" Geprüft in Work-Ordner: {work_dir / input_filename}") print(f" Geprüft als vollständiger Pfad: {filename}") sys.exit(1) print(f"Lese Textdatei: {filename}") all_texts = read_texts_from_file(filename) # Schritt 2: Texte filtern texts, ignored_texts = filter_texts(all_texts, wildcard_patterns, regex_patterns, args.debug) # Erstelle Output-Dateinamen basierend auf Export-Typ if args.outname: # Verwende benutzerdefinierten Namen output_filename = args.outname # Füge Endung hinzu falls nicht vorhanden if args.export_type == 'excel': if not output_filename.endswith('.xlsx'): output_filename += '.xlsx' else: # json if not output_filename.endswith('.json'): output_filename += '.json' else: # Standardname basierend auf Eingabedatei if args.export_type == 'excel': output_filename = filename.stem + "_texts.xlsx" else: # json output_filename = filename.stem + "_texts.json" output_path = work_dir / output_filename # Schreibe Texte in gewähltem Format if args.export_type == 'excel': write_texts_to_excel(texts, output_path, translation_dicts) else: # json write_texts_to_json(texts, ignored_texts, output_path, translation_dicts) print("Verarbeitung erfolgreich abgeschlossen.")