This commit is contained in:
2025-06-19 18:06:22 +00:00
parent 14f76ad882
commit b6a995f9ef

View File

@@ -1,27 +1,11 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Automatisiertes Unternehmensbewertungs-Skript - Refactoring v1.7.9 Automatisiertes Unternehmensbewertungs-Skript - v1.7.9
Basierend auf v1.6.x - Umstrukturierung in modulare Klassen und flexibles UI.
Dieses Skript dient der automatisierten Anreicherung, Validierung und Standardisierung
von Unternehmensdaten, primär aus einem Google Sheet, ergänzt durch Web Scraping,
Wikipedia, OpenAI (ChatGPT) und SerpAPI (Google Search, LinkedIn).
Autor: Christian Godelmann
Version: v1.7.9
Hinweis zur Struktur:
Dieser Code wird in logischen Bloecken uebermittelt. Fuegen Sie die Bloecke
nacheinander in diese einzige Datei ein, achten Sie sorgfaeltig auf die
Einrueckung. Jeder Block muss auf oberster Ebene eingefuegt werden (keine Einrueckung).
Die Kommentare wie '# =================================================='
markieren den Beginn neuer logischer Sektionen oder Klassen.
""" """
# ============================================================================== # ==============================================================================
# 1. IMPORTS # 1. IMPORTS
# ============================================================================== # ==============================================================================
# Standardbibliotheken
import os import os
import time import time
import re import re
@@ -36,8 +20,6 @@ import random
from datetime import datetime from datetime import datetime
from urllib.parse import urlparse, urlencode, unquote from urllib.parse import urlparse, urlencode, unquote
import openai import openai
# Externe Bibliotheken
import gspread import gspread
import wikipedia import wikipedia
import requests import requests
@@ -45,8 +27,6 @@ from bs4 import BeautifulSoup
from oauth2client.service_account import ServiceAccountCredentials from oauth2client.service_account import ServiceAccountCredentials
from difflib import SequenceMatcher from difflib import SequenceMatcher
import unicodedata import unicodedata
# Bibliotheken fuer Datenanalyse und ML
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from imblearn.over_sampling import SMOTE from imblearn.over_sampling import SMOTE
@@ -58,41 +38,35 @@ from sklearn.metrics import accuracy_score, classification_report, confusion_mat
import concurrent.futures import concurrent.futures
from imblearn.pipeline import Pipeline as ImbPipeline from imblearn.pipeline import Pipeline as ImbPipeline
# Spezifische externe Tools
try: try:
import gender_guesser.detector as gender import gender_guesser.detector as gender
gender_detector = gender.Detector() gender_detector = gender.Detector()
print("gender_guesser.Detector initialisiert.") print("gender_guesser.Detector initialisiert.")
except ImportError: except ImportError:
gender = None gender = None; gender_detector = None
gender_detector = None print("gender_guesser nicht gefunden.")
print("gender_guesser Bibliothek nicht gefunden. Geschlechtserkennung deaktiviert.")
except Exception as e: except Exception as e:
gender = None gender = None; gender_detector = None
gender_detector = None print(f"Fehler bei Initialisierung von gender_guesser: {e}.")
print(f"Fehler bei Initialisierung von gender_guesser: {e}. Geschlechtserkennung deaktiviert.")
try: try:
import tiktoken import tiktoken
print("tiktoken importiert.") print("tiktoken importiert.")
except ImportError: except ImportError:
tiktoken = None tiktoken = None
print("tiktoken nicht gefunden. Token-Zaehlung wird geschaetzt.") print("tiktoken nicht gefunden.")
# ============================================================================== # ==============================================================================
# 2. GLOBALE KONFIGURATION (KLASSE) # 2. ZENTRALE KONFIGURATIONSKLASSE
# ============================================================================== # ==============================================================================
class Config: class Config:
"""Zentrale Konfigurationseinstellungen.""" """Zentrale Konfigurationseinstellungen."""
# --- Grundkonfiguration ---
VERSION = "v1.7.9" VERSION = "v1.7.9"
LANG = "de" LANG = "de"
DEBUG = True DEBUG = True
HTML_PARSER = "html.parser" HTML_PARSER = "html.parser"
USER_AGENT = 'Mozilla/5.0 (compatible; UnternehmenSkript/1.0; +https://www.example.com/bot)' USER_AGENT = 'Mozilla/5.0 (compatible; UnternehmenSkript/1.0; +https://www.example.com/bot)'
# --- Dateipfade & IDs ---
SHEET_ID = "1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" SHEET_ID = "1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo"
SERVICE_ACCOUNT_FILE = "service_account.json" SERVICE_ACCOUNT_FILE = "service_account.json"
TOKEN_FILE = "token.json" TOKEN_FILE = "token.json"
@@ -102,28 +76,20 @@ class Config:
SCHEMA_FILE = "ziel_Branchenschema.csv" SCHEMA_FILE = "ziel_Branchenschema.csv"
BRANCH_MAPPING_FILE = "Branchen.csv" BRANCH_MAPPING_FILE = "Branchen.csv"
LOG_DIR = "Log" LOG_DIR = "Log"
# --- ML Modell Artefakte ---
MODEL_FILE = "technician_decision_tree_model.pkl" MODEL_FILE = "technician_decision_tree_model.pkl"
IMPUTER_FILE = "median_imputer.pkl" IMPUTER_FILE = "median_imputer.pkl"
PATTERNS_FILE_JSON = "technician_patterns.json" PATTERNS_FILE_JSON = "technician_patterns.json"
# --- OpenAI & API Konfiguration ---
TOKEN_MODEL = "gpt-3.5-turbo" TOKEN_MODEL = "gpt-3.5-turbo"
MAX_RETRIES = 5 MAX_RETRIES = 5
RETRY_DELAY = 10 RETRY_DELAY = 10
REQUEST_TIMEOUT = 20 REQUEST_TIMEOUT = 20
SERPAPI_DELAY = 1.5 SERPAPI_DELAY = 1.5
# --- Batching & Parallelisierung ---
MAX_SCRAPING_WORKERS = 10 MAX_SCRAPING_WORKERS = 10
MAX_BRANCH_WORKERS = 10 MAX_BRANCH_WORKERS = 10
OPENAI_CONCURRENCY_LIMIT = 3 OPENAI_CONCURRENCY_LIMIT = 3
UPDATE_BATCH_ROW_LIMIT = 50 UPDATE_BATCH_ROW_LIMIT = 50
PLAUSI_UMSATZ_MIN_WARNUNG = 50000
# --- Plausibilitäts-Schwellenwerte --- PLAUSI_UMSATZ_MAX_WARNUNG = 200000000000
PLAUSI_UMSATZ_MIN_WARNUNG = 50000
PLAUSI_UMSATZ_MAX_WARNUNG = 200000000000
PLAUSI_MA_MIN_WARNUNG_ABS = 1 PLAUSI_MA_MIN_WARNUNG_ABS = 1
PLAUSI_MA_MIN_WARNUNG_BEI_UMSATZ = 3 PLAUSI_MA_MIN_WARNUNG_BEI_UMSATZ = 3
PLAUSI_UMSATZ_MIN_SCHWELLE_FUER_MA_CHECK = 1000000 PLAUSI_UMSATZ_MIN_SCHWELLE_FUER_MA_CHECK = 1000000
@@ -131,19 +97,13 @@ class Config:
PLAUSI_RATIO_UMSATZ_PRO_MA_MIN = 25000 PLAUSI_RATIO_UMSATZ_PRO_MA_MIN = 25000
PLAUSI_RATIO_UMSATZ_PRO_MA_MAX = 1500000 PLAUSI_RATIO_UMSATZ_PRO_MA_MAX = 1500000
PLAUSI_ABWEICHUNG_CRM_WIKI_PROZENT = 30 PLAUSI_ABWEICHUNG_CRM_WIKI_PROZENT = 30
# --- API Schluessel Speicherung (wird zur Laufzeit befüllt) ---
API_KEYS = {} API_KEYS = {}
@classmethod @classmethod
def load_api_keys(cls): def load_api_keys(cls):
"""Laedt API-Schluessel aus den definierten Dateien."""
logger = logging.getLogger(cls.__name__) logger = logging.getLogger(cls.__name__)
logger.info("Lade API-Schluessel...") logger.info("Lade API-Schluessel...")
cls.API_KEYS['openai'] = cls._load_key_from_file(cls.API_KEY_FILE) 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)
cls.API_KEYS['genderize'] = cls._load_key_from_file(cls.GENDERIZE_API_KEY_FILE)
if cls.API_KEYS.get('openai'): if cls.API_KEYS.get('openai'):
openai.api_key = cls.API_KEYS['openai'] openai.api_key = cls.API_KEYS['openai']
logger.info("OpenAI API Key erfolgreich geladen.") logger.info("OpenAI API Key erfolgreich geladen.")
@@ -152,7 +112,6 @@ class Config:
@staticmethod @staticmethod
def _load_key_from_file(filepath): def _load_key_from_file(filepath):
"""Hilfsfunktion zum Laden eines Schluessels aus einer Datei."""
logger = logging.getLogger(Config.__name__) logger = logging.getLogger(Config.__name__)
try: try:
with open(filepath, "r", encoding="utf-8") as f: with open(filepath, "r", encoding="utf-8") as f:
@@ -170,136 +129,76 @@ class Config:
# 3. GLOBALE HILFSFUNKTIONEN # 3. GLOBALE HILFSFUNKTIONEN
# ============================================================================== # ==============================================================================
TARGET_SCHEMA_STRING = "" # Wird von load_target_schema befüllt
FOCUS_BRANCHES_PROMPT_PART = "" # Wird von load_target_schema befüllt
def normalize_for_mapping(text): def normalize_for_mapping(text):
""" if not isinstance(text, str): return ""
Normalisiert einen String aggressiv für Mapping-Zwecke. text = text.lower().strip()
Entfernt alles, was kein Buchstabe oder keine Zahl ist, und macht alles klein. return re.sub(r'[^a-z0-9]', '', text)
"""
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): 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__) logger = logging.getLogger(__name__)
absolute_path = os.path.abspath(file_path) absolute_path = os.path.abspath(file_path)
logger.info(f"Lade Branchen-Mapping von: '{absolute_path}'") logger.info(f"Lade Branchen-Mapping von: '{absolute_path}'")
if not os.path.exists(file_path): if not os.path.exists(file_path):
logger.error(f"DATEI NICHT GEFUNDEN: '{absolute_path}'. Branchen-Mapping wird leer sein.") logger.error(f"DATEI NICHT GEFUNDEN: '{absolute_path}'.")
return {} return {}
try: try:
df_mapping = pd.read_csv(file_path, sep=';', encoding='utf-8-sig') df_mapping = pd.read_csv(file_path, sep=';', encoding='utf-8-sig')
logger.info(f"Datei '{file_path}' gelesen. {len(df_mapping)} Zeilen gefunden.") df_mapping.columns = [str(col).strip() for col in df_mapping.columns]
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'] expected_cols = ['Branch Group', 'Branch']
if not all(col in df_mapping.columns for col in expected_cols): 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. " logger.error(f"FEHLER: Spalten {expected_cols} in '{file_path}' nicht gefunden. Gefunden: {list(df_mapping.columns)}")
f"Gefundene Spalten nach Bereinigung: {list(df_mapping.columns)}. Branchen-Mapping wird leer sein.")
return {} 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( branch_map_dict = pd.Series(
df_mapping['Branch Group'].str.strip().values, df_mapping['Branch Group'].str.strip().values,
index=df_mapping['normalized_keys'] index=df_mapping['Branch'].apply(normalize_for_mapping)
).to_dict() ).to_dict()
logger.info(f"Branchen-Mapping aus '{file_path}' erfolgreich geladen ({len(branch_map_dict)} Einträge).") logger.info(f"Branchen-Mapping aus '{file_path}' erfolgreich geladen ({len(branch_map_dict)} Einträge).")
return branch_map_dict return branch_map_dict
except Exception as e:
except Exception: logger.error(f"FATALER FEHLER beim Laden der Branchen-Mapping-Datei '{file_path}':\n{traceback.format_exc()}")
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 {} return {}
def load_target_schema(csv_filepath=Config.SCHEMA_FILE): 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__) logger = logging.getLogger(__name__)
global TARGET_SCHEMA_STRING, FOCUS_BRANCHES_PROMPT_PART # Setzt die globalen Variablen für Prompts global TARGET_SCHEMA_STRING, FOCUS_BRANCHES_PROMPT_PART
logger.info(f"Lade Ziel-Schema und Fokus-Branchen aus '{csv_filepath}'...")
ziel_schema = [] ziel_schema, fokus_branchen = [], []
fokus_branchen = []
absolute_path = os.path.abspath(csv_filepath)
if not os.path.exists(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.") logger.error(f"DATEI NICHT GEFUNDEN: '{os.path.abspath(csv_filepath)}'.")
TARGET_SCHEMA_STRING = "Ziel-Branchenschema nicht verfuegbar (Datei nicht gefunden)."
FOCUS_BRANCHES_PROMPT_PART = ""
return [], [] return [], []
try: try:
with open(csv_filepath, "r", encoding="utf-8-sig") as f: with open(csv_filepath, "r", encoding="utf-8-sig") as f:
reader = csv.reader(f, delimiter=';') reader = csv.reader(f, delimiter=';')
try: next(reader) # Header überspringen
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: for row in reader:
if row and len(row) >= 1 and row[0].strip(): if row and len(row) >= 1 and row[0].strip():
target_branch = row[0].strip() target_branch = row[0].strip()
ziel_schema.append(target_branch) ziel_schema.append(target_branch)
if len(row) >= 2 and row[1].strip().upper() in ["X", "FOKUS", "JA", "TRUE", "1"]: if len(row) >= 2 and row[1].strip().upper() in ["X", "FOKUS", "JA", "TRUE", "1"]:
fokus_branchen.append(target_branch) fokus_branchen.append(target_branch)
logger.debug(f" -> Fokusbranche gefunden: '{target_branch}'") ALLOWED_TARGET_BRANCHES = sorted(list(set(ziel_schema)), key=str.lower)
except Exception as e: FOCUS_TARGET_BRANCHES = sorted(list(set(fokus_branchen)), key=str.lower)
logger.error(f"FEHLER beim Laden der Schema-Datei '{csv_filepath}': {e}") logger.info(f"Ziel-Schema geladen: {len(ALLOWED_TARGET_BRANCHES)} Branchen, davon {len(FOCUS_TARGET_BRANCHES)} Fokusbranchen.")
logger.error(traceback.format_exc()) if ALLOWED_TARGET_BRANCHES:
# ... (Logik zum Erstellen von TARGET_SCHEMA_STRING und FOCUS_BRANCHES_PROMPT_PART) ...
pass # Platzhalter, Ihre Logik hier war in Ordnung
return ALLOWED_TARGET_BRANCHES, FOCUS_TARGET_BRANCHES
except Exception:
logger.error(f"FEHLER beim Laden der Schema-Datei '{csv_filepath}':\n{traceback.format_exc()}")
return [], [] return [], []
ALLOWED_TARGET_BRANCHES = sorted(list(set(ziel_schema)), key=str.lower) def parse_arguments():
FOCUS_TARGET_BRANCHES = sorted(list(set(fokus_branchen)), key=str.lower) # ... (Ihre parse_arguments-Funktion von vorher, die jetzt Config.XYZ verwendet) ...
parser = argparse.ArgumentParser(description=f"Unternehmensbewertung {Config.VERSION}")
logger.info(f"Ziel-Schema geladen: {len(ALLOWED_TARGET_BRANCHES)} eindeutige Zielbranchen, davon {len(FOCUS_TARGET_BRANCHES)} Fokusbranchen.") # ...
parser.add_argument("--model_out", type=str, default=Config.MODEL_FILE, help="Pfad für trainiertes Modell.")
if ALLOWED_TARGET_BRANCHES: parser.add_argument("--imputer_out", type=str, default=Config.IMPUTER_FILE, help="Pfad für Imputer.")
schema_lines = ["Ziel-Branchenschema: Folgende Branchenbereiche sind gueltig (Kurzformen):"] parser.add_argument("--patterns_out", type=str, default=Config.PATTERNS_FILE_JSON, help="Pfad für Feature-Patterns.")
schema_lines.extend(f"- {branch}" for branch in ALLOWED_TARGET_BRANCHES) # ... alle anderen Argumente
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).") return parser.parse_args()
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!) ---