diff --git a/brancheneinstufung.py b/brancheneinstufung.py index f67f1bf4..f043e058 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Automatisiertes Unternehmensbewertungs-Skript - Refactoring v1.7.8 +Automatisiertes Unternehmensbewertungs-Skript - Refactoring v1.7.9 Basierend auf v1.6.x - Umstrukturierung in modulare Klassen und flexibles UI. Dieses Skript dient der automatisierten Anreicherung, Validierung und Standardisierung @@ -8,7 +8,7 @@ von Unternehmensdaten, primär aus einem Google Sheet, ergänzt durch Web Scrapi Wikipedia, OpenAI (ChatGPT) und SerpAPI (Google Search, LinkedIn). Autor: Christian Godelmann -Version: v1.7.8 +Version: v1.7.9 Hinweis zur Struktur: Dieser Code wird in logischen Bloecken uebermittelt. Fuegen Sie die Bloecke @@ -30,26 +30,21 @@ import json import pickle import threading import traceback -import logging # logging Modul importieren +import logging import argparse -import random # Fuer Jitter im Retry Decorator +import random from datetime import datetime from urllib.parse import urlparse, urlencode, unquote -import openai # Sicherstellen, dass openai global importiert wird +import openai + # Externe Bibliotheken import gspread -# Stellen Sie sicher, dass gspread >= 5.0.0 installiert ist, da APIError anders behandelt wird -# (Unser Code sollte mit den neueren Versionen kompatibel sein) import wikipedia -# Stellen Sie sicher, dass wikipedia-api nicht gleichzeitig installiert ist (Konflikt). -import requests # Fuer HTTP-Anfragen -# Stellen Sie sicher, dass requests >= 2.0.0 installiert ist -from bs4 import BeautifulSoup # Fuer HTML-Parsing -# Stellen Sie sicher, dass lxml oder html5lib installiert ist (empfohlen statt html.parser) -# z.B. pip install lxml -from oauth2client.service_account import ServiceAccountCredentials # gspread dependency -from difflib import SequenceMatcher # Fuer String-Aehnlichkeit -import unicodedata # Fuer Text-Normalisierung +import requests +from bs4 import BeautifulSoup +from oauth2client.service_account import ServiceAccountCredentials +from difflib import SequenceMatcher +import unicodedata # Bibliotheken fuer Datenanalyse und ML import pandas as pd @@ -60,14 +55,12 @@ from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.impute import SimpleImputer from sklearn.tree import DecisionTreeClassifier, export_text from sklearn.metrics import accuracy_score, classification_report, confusion_matrix -import concurrent.futures # Fuer parallele Verarbeitung -from sklearn.model_selection import GridSearchCV -from imblearn.pipeline import Pipeline as ImbPipeline # Alias, um Kollision mit sklearn.pipeline zu vermeiden +import concurrent.futures +from imblearn.pipeline import Pipeline as ImbPipeline # Spezifische externe Tools try: - import gender_guesser.detector as gender # Fuer Geschlechtserkennung - # Initialisieren Sie den Detector einmal global (wird im Helper benutzt) + import gender_guesser.detector as gender gender_detector = gender.Detector() print("gender_guesser.Detector initialisiert.") except ImportError: @@ -79,83 +72,23 @@ except Exception as e: gender_detector = None print(f"Fehler bei Initialisierung von gender_guesser: {e}. Geschlechtserkennung deaktiviert.") - -# Optional: tiktoken fuer Token-Zaehlung (Modus 8) try: import tiktoken - print("tiktoken importiert.") # Debugging-Ausgabe (geht nur an Konsole vor Logger Setup) + print("tiktoken importiert.") except ImportError: tiktoken = None - print("tiktoken nicht gefunden. Token-Zaehlung wird geschaetzt.") # Debugging-Ausgabe + print("tiktoken nicht gefunden. Token-Zaehlung wird geschaetzt.") - - - -def load_branch_mapping(file_path=Config.BRANCH_MAPPING_FILE): - """ - Lädt das Mapping von Detail-Branche zu Branchen-Gruppe aus einer CSV-Datei. - Ist extrem robust gegen Kodierungs-, Spaltennamen- und Pfad-Fehler. - """ - logger = logging.getLogger(__name__) - absolute_path = os.path.abspath(file_path) - logger.info(f"Lade Branchen-Mapping von: '{absolute_path}'") - - if not os.path.exists(file_path): - logger.error(f"DATEI NICHT GEFUNDEN: '{absolute_path}'. Branchen-Mapping wird leer sein.") - return {} - - try: - # encoding='utf-8-sig' behandelt das BOM-Problem. - df_mapping = pd.read_csv(file_path, sep=';', encoding='utf-8-sig') - logger.info(f"Datei '{file_path}' gelesen. {len(df_mapping)} Zeilen gefunden.") - - # Bereinige ALLE Spaltennamen von Leerzeichen und unsichtbaren Zeichen - original_columns = list(df_mapping.columns) - df_mapping.columns = [str(col).strip() for col in original_columns] - - # Prüfe, ob die bereinigten Spaltennamen sich von den Originalen unterscheiden - if original_columns != list(df_mapping.columns): - logger.warning(f"Spaltennamen wurden bereinigt. Original: {original_columns} -> Neu: {list(df_mapping.columns)}") - - # Harte Überprüfung der erwarteten Spaltennamen - expected_cols = ['Branch Group', 'Branch'] - if not all(col in df_mapping.columns for col in expected_cols): - logger.error(f"FEHLER: Die erwarteten Spalten {expected_cols} wurden in '{file_path}' nicht gefunden. " - f"Gefundene Spalten nach Bereinigung: {list(df_mapping.columns)}. Branchen-Mapping wird leer sein.") - return {} - - # Normalisierte Keys erstellen - df_mapping['normalized_keys'] = df_mapping['Branch'].apply(normalize_for_mapping) - - # Dictionary erstellen - branch_map_dict = pd.Series( - df_mapping['Branch Group'].str.strip().values, - index=df_mapping['normalized_keys'] - ).to_dict() - - logger.info(f"Branchen-Mapping aus '{file_path}' erfolgreich geladen ({len(branch_map_dict)} Einträge).") - return branch_map_dict - - except Exception: - logger.error(f"FATALER, UNERWARTETER FEHLER beim Laden der Branchen-Mapping-Datei '{file_path}'. Branchen-Mapping wird leer sein.") - logger.error(traceback.format_exc()) - return {} - -# In Config-Klasse oder global aufrufen: - - - -# --- Globale Konfiguration Klasse --- # ============================================================================== -# 2. GLOBALE KONSTANTEN UND KONFIGURATION (ZENTRALISIERT) +# 2. GLOBALE KONFIGURATION (KLASSE) # ============================================================================== class Config: """Zentrale Konfigurationseinstellungen.""" # --- Grundkonfiguration --- - VERSION = "v1.7.9" # Version auf 1.7.9 erhöht - LANG = "de" - DEBUG = True + VERSION = "v1.7.9" + LANG = "de" + DEBUG = True HTML_PARSER = "html.parser" USER_AGENT = 'Mozilla/5.0 (compatible; UnternehmenSkript/1.0; +https://www.example.com/bot)' @@ -205,7 +138,7 @@ class Config: @classmethod def load_api_keys(cls): """Laedt API-Schluessel aus den definierten Dateien.""" - logger = logging.getLogger(cls.__name__) # Logger innerhalb der Methode holen + logger = logging.getLogger(cls.__name__) logger.info("Lade API-Schluessel...") cls.API_KEYS['openai'] = cls._load_key_from_file(cls.API_KEY_FILE) cls.API_KEYS['serpapi'] = cls._load_key_from_file(cls.SERP_API_KEY_FILE) @@ -233,6 +166,143 @@ class Config: logger.error(f"FEHLER beim Lesen der Schluesseldatei '{filepath}': {e}") return None +# ============================================================================== +# 3. GLOBALE HILFSFUNKTIONEN +# ============================================================================== + +def normalize_for_mapping(text): + """ + Normalisiert einen String aggressiv für Mapping-Zwecke. + Entfernt alles, was kein Buchstabe oder keine Zahl ist, und macht alles klein. + """ + if not isinstance(text, str): + return "" + text = text.lower() + text = text.strip() + text = re.sub(r'[^a-z0-9]', '', text) + return text + +def load_branch_mapping(file_path=Config.BRANCH_MAPPING_FILE): + """ + Lädt das Mapping von Detail-Branche zu Branchen-Gruppe aus einer CSV-Datei. + Ist extrem robust gegen Kodierungs-, Spaltennamen- und Pfad-Fehler. + """ + logger = logging.getLogger(__name__) + absolute_path = os.path.abspath(file_path) + logger.info(f"Lade Branchen-Mapping von: '{absolute_path}'") + + if not os.path.exists(file_path): + logger.error(f"DATEI NICHT GEFUNDEN: '{absolute_path}'. Branchen-Mapping wird leer sein.") + return {} + + try: + df_mapping = pd.read_csv(file_path, sep=';', encoding='utf-8-sig') + logger.info(f"Datei '{file_path}' gelesen. {len(df_mapping)} Zeilen gefunden.") + + original_columns = list(df_mapping.columns) + df_mapping.columns = [str(col).strip() for col in original_columns] + + if original_columns != list(df_mapping.columns): + logger.warning(f"Spaltennamen wurden bereinigt. Original: {original_columns} -> Neu: {list(df_mapping.columns)}") + + expected_cols = ['Branch Group', 'Branch'] + if not all(col in df_mapping.columns for col in expected_cols): + logger.error(f"FEHLER: Die erwarteten Spalten {expected_cols} wurden in '{file_path}' nicht gefunden. " + f"Gefundene Spalten nach Bereinigung: {list(df_mapping.columns)}. Branchen-Mapping wird leer sein.") + return {} + + df_mapping['normalized_keys'] = df_mapping['Branch'].apply(normalize_for_mapping) + + if df_mapping['normalized_keys'].duplicated().any(): + duplicates = df_mapping[df_mapping['normalized_keys'].duplicated()]['normalized_keys'] + logger.warning(f"WARNUNG: Duplikate in normalisierten Branchen-Keys gefunden! Dies kann zu inkonsistentem Mapping führen. Duplikate: {list(duplicates)}") + + branch_map_dict = pd.Series( + df_mapping['Branch Group'].str.strip().values, + index=df_mapping['normalized_keys'] + ).to_dict() + + logger.info(f"Branchen-Mapping aus '{file_path}' erfolgreich geladen ({len(branch_map_dict)} Einträge).") + return branch_map_dict + + except Exception: + logger.error(f"FATALER, UNERWARTETER FEHLER beim Laden der Branchen-Mapping-Datei '{file_path}'. Branchen-Mapping wird leer sein.") + logger.error(traceback.format_exc()) + return {} + +def load_target_schema(csv_filepath=Config.SCHEMA_FILE): + """ + Lädt das Ziel-Branchenschema und die als Fokus markierten Branchen aus einer CSV-Datei. + Gibt ein Tupel mit zwei Listen zurück: (alle_branchen, fokus_branchen). + Ist robust gegen Dateifehler. + """ + logger = logging.getLogger(__name__) + global TARGET_SCHEMA_STRING, FOCUS_BRANCHES_PROMPT_PART # Setzt die globalen Variablen für Prompts + + ziel_schema = [] + fokus_branchen = [] + + absolute_path = os.path.abspath(csv_filepath) + if not os.path.exists(csv_filepath): + logger.error(f"DATEI NICHT GEFUNDEN: '{absolute_path}'. Ziel-Schema und Fokus-Branchen werden leer sein.") + TARGET_SCHEMA_STRING = "Ziel-Branchenschema nicht verfuegbar (Datei nicht gefunden)." + FOCUS_BRANCHES_PROMPT_PART = "" + return [], [] + + try: + with open(csv_filepath, "r", encoding="utf-8-sig") as f: + reader = csv.reader(f, delimiter=';') + try: + header_row = next(reader) + logger.debug(f"Ueberspringe Header-Zeile im Schema: {header_row}") + except StopIteration: + logger.warning(f"Schema-Datei '{csv_filepath}' ist leer oder hat keinen Header.") + return [], [] + + for row in reader: + if row and len(row) >= 1 and row[0].strip(): + target_branch = row[0].strip() + ziel_schema.append(target_branch) + if len(row) >= 2 and row[1].strip().upper() in ["X", "FOKUS", "JA", "TRUE", "1"]: + fokus_branchen.append(target_branch) + logger.debug(f" -> Fokusbranche gefunden: '{target_branch}'") + except Exception as e: + logger.error(f"FEHLER beim Laden der Schema-Datei '{csv_filepath}': {e}") + logger.error(traceback.format_exc()) + return [], [] + + ALLOWED_TARGET_BRANCHES = sorted(list(set(ziel_schema)), key=str.lower) + FOCUS_TARGET_BRANCHES = sorted(list(set(fokus_branchen)), key=str.lower) + + logger.info(f"Ziel-Schema geladen: {len(ALLOWED_TARGET_BRANCHES)} eindeutige Zielbranchen, davon {len(FOCUS_TARGET_BRANCHES)} Fokusbranchen.") + + if ALLOWED_TARGET_BRANCHES: + schema_lines = ["Ziel-Branchenschema: Folgende Branchenbereiche sind gueltig (Kurzformen):"] + schema_lines.extend(f"- {branch}" for branch in ALLOWED_TARGET_BRANCHES) + schema_lines.append("\nBitte ordne das Unternehmen ausschliesslich in einen dieser Bereiche ein. Gib NUR den exakten Kurznamen der Branche zurueck (keine Praefixe oder zusaetzliche Erklaerungen ausser im 'Begruendung'-Feld).") + schema_lines.append("Antworte ausschliesslich im folgenden Format (keine Einleitung, kein Schlusssatz):") + schema_lines.append("Branche: ") + schema_lines.append("Konfidenz: ") + schema_lines.append("Uebereinstimmung: ") + schema_lines.append("Begruendung: ") + TARGET_SCHEMA_STRING = "\n".join(schema_lines) + + if FOCUS_TARGET_BRANCHES: + focus_prompt_lines = ["\nZusätzlicher Hinweis: Wenn die Wahl zwischen mehreren passenden Branchen besteht, priorisiere bitte, wenn möglich, eine der folgenden Fokusbranchen:"] + focus_prompt_lines.extend(f"- {branch}" for branch in FOCUS_TARGET_BRANCHES) + FOCUS_BRANCHES_PROMPT_PART = "\n".join(focus_prompt_lines) + else: + FOCUS_BRANCHES_PROMPT_PART = "" + logger.info("Keine Fokusbranchen im Schema definiert.") + else: + TARGET_SCHEMA_STRING = "Ziel-Branchenschema nicht verfuegbar (Keine gueltigen Branchen in Datei gefunden)." + FOCUS_BRANCHES_PROMPT_PART = "" + logger.warning("Keine gueltigen Zielbranchen im Schema gefunden. Branchenbewertung ist nicht moeglich.") + + return ALLOWED_TARGET_BRANCHES, FOCUS_TARGET_BRANCHES + +# --- Globale Spalten-Mapping (WICHTIG: MUSS ZU IHREM SHEET PASSEN!) --- + # --- Globale Spalten-Mapping (WICHTIG: MUSS ZU IHREM SHEET PASSEN!) --- # --- Globale Spalten-Mapping (WICHTIG: MUSS ZU IHREM SHEET PASSEN!) ---