This commit is contained in:
2025-06-19 17:58:23 +00:00
parent c1ec9770bc
commit 0a05497db1

View File

@@ -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: <Exakter Kurzname der Branche aus der Liste>")
schema_lines.append("Konfidenz: <Hoch, Mittel oder Niedrig>")
schema_lines.append("Uebereinstimmung: <ok oder X (Vergleich deines Vorschlags mit der extrahierten Kurzform der CRM-Referenz)>")
schema_lines.append("Begruendung: <Sehr kurze Begruendung fuer deinen Branchenvorschlag>")
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!) ---