This commit is contained in:
2025-06-19 18:06:22 +00:00
parent 5a631cca42
commit e22366ebd9

View File

@@ -1,27 +1,11 @@
#!/usr/bin/env python3
"""
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
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.
Automatisiertes Unternehmensbewertungs-Skript - v1.7.9
"""
# ==============================================================================
# 1. IMPORTS
# ==============================================================================
# Standardbibliotheken
import os
import time
import re
@@ -36,8 +20,6 @@ import random
from datetime import datetime
from urllib.parse import urlparse, urlencode, unquote
import openai
# Externe Bibliotheken
import gspread
import wikipedia
import requests
@@ -45,8 +27,6 @@ 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
import numpy as np
from imblearn.over_sampling import SMOTE
@@ -58,41 +38,35 @@ from sklearn.metrics import accuracy_score, classification_report, confusion_mat
import concurrent.futures
from imblearn.pipeline import Pipeline as ImbPipeline
# Spezifische externe Tools
try:
import gender_guesser.detector as gender
gender_detector = gender.Detector()
print("gender_guesser.Detector initialisiert.")
except ImportError:
gender = None
gender_detector = None
print("gender_guesser Bibliothek nicht gefunden. Geschlechtserkennung deaktiviert.")
gender = None; gender_detector = None
print("gender_guesser nicht gefunden.")
except Exception as e:
gender = None
gender_detector = None
print(f"Fehler bei Initialisierung von gender_guesser: {e}. Geschlechtserkennung deaktiviert.")
gender = None; gender_detector = None
print(f"Fehler bei Initialisierung von gender_guesser: {e}.")
try:
import tiktoken
print("tiktoken importiert.")
except ImportError:
tiktoken = None
print("tiktoken nicht gefunden. Token-Zaehlung wird geschaetzt.")
print("tiktoken nicht gefunden.")
# ==============================================================================
# 2. GLOBALE KONFIGURATION (KLASSE)
# 2. ZENTRALE KONFIGURATIONSKLASSE
# ==============================================================================
class Config:
"""Zentrale Konfigurationseinstellungen."""
# --- Grundkonfiguration ---
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)'
# --- Dateipfade & IDs ---
SHEET_ID = "1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo"
SERVICE_ACCOUNT_FILE = "service_account.json"
TOKEN_FILE = "token.json"
@@ -102,26 +76,18 @@ class Config:
SCHEMA_FILE = "ziel_Branchenschema.csv"
BRANCH_MAPPING_FILE = "Branchen.csv"
LOG_DIR = "Log"
# --- ML Modell Artefakte ---
MODEL_FILE = "technician_decision_tree_model.pkl"
IMPUTER_FILE = "median_imputer.pkl"
PATTERNS_FILE_JSON = "technician_patterns.json"
# --- OpenAI & API Konfiguration ---
TOKEN_MODEL = "gpt-3.5-turbo"
MAX_RETRIES = 5
RETRY_DELAY = 10
REQUEST_TIMEOUT = 20
SERPAPI_DELAY = 1.5
# --- Batching & Parallelisierung ---
MAX_SCRAPING_WORKERS = 10
MAX_BRANCH_WORKERS = 10
OPENAI_CONCURRENCY_LIMIT = 3
UPDATE_BATCH_ROW_LIMIT = 50
# --- Plausibilitäts-Schwellenwerte ---
PLAUSI_UMSATZ_MIN_WARNUNG = 50000
PLAUSI_UMSATZ_MAX_WARNUNG = 200000000000
PLAUSI_MA_MIN_WARNUNG_ABS = 1
@@ -131,19 +97,13 @@ class Config:
PLAUSI_RATIO_UMSATZ_PRO_MA_MIN = 25000
PLAUSI_RATIO_UMSATZ_PRO_MA_MAX = 1500000
PLAUSI_ABWEICHUNG_CRM_WIKI_PROZENT = 30
# --- API Schluessel Speicherung (wird zur Laufzeit befüllt) ---
API_KEYS = {}
@classmethod
def load_api_keys(cls):
"""Laedt API-Schluessel aus den definierten Dateien."""
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)
cls.API_KEYS['genderize'] = cls._load_key_from_file(cls.GENDERIZE_API_KEY_FILE)
if cls.API_KEYS.get('openai'):
openai.api_key = cls.API_KEYS['openai']
logger.info("OpenAI API Key erfolgreich geladen.")
@@ -152,7 +112,6 @@ class Config:
@staticmethod
def _load_key_from_file(filepath):
"""Hilfsfunktion zum Laden eines Schluessels aus einer Datei."""
logger = logging.getLogger(Config.__name__)
try:
with open(filepath, "r", encoding="utf-8") as f:
@@ -170,136 +129,76 @@ class Config:
# 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):
"""
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
if not isinstance(text, str): return ""
text = text.lower().strip()
return re.sub(r'[^a-z0-9]', '', 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.")
logger.error(f"DATEI NICHT GEFUNDEN: '{absolute_path}'.")
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)}")
df_mapping.columns = [str(col).strip() for col in 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.")
logger.error(f"FEHLER: Spalten {expected_cols} in '{file_path}' nicht gefunden. Gefunden: {list(df_mapping.columns)}")
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']
index=df_mapping['Branch'].apply(normalize_for_mapping)
).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())
except Exception as e:
logger.error(f"FATALER FEHLER beim Laden der Branchen-Mapping-Datei '{file_path}':\n{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)
global TARGET_SCHEMA_STRING, FOCUS_BRANCHES_PROMPT_PART
logger.info(f"Lade Ziel-Schema und Fokus-Branchen aus '{csv_filepath}'...")
ziel_schema, fokus_branchen = [], []
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 = ""
logger.error(f"DATEI NICHT GEFUNDEN: '{os.path.abspath(csv_filepath)}'.")
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 [], []
next(reader) # Header überspringen
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.")
logger.info(f"Ziel-Schema geladen: {len(ALLOWED_TARGET_BRANCHES)} Branchen, 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.")
# ... (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 [], []
def parse_arguments():
# ... (Ihre parse_arguments-Funktion von vorher, die jetzt Config.XYZ verwendet) ...
parser = argparse.ArgumentParser(description=f"Unternehmensbewertung {Config.VERSION}")
# ...
parser.add_argument("--model_out", type=str, default=Config.MODEL_FILE, help="Pfad für trainiertes Modell.")
parser.add_argument("--imputer_out", type=str, default=Config.IMPUTER_FILE, help="Pfad für Imputer.")
parser.add_argument("--patterns_out", type=str, default=Config.PATTERNS_FILE_JSON, help="Pfad für Feature-Patterns.")
# ... alle anderen Argumente
return parser.parse_args()
# --- Globale Spalten-Mapping (WICHTIG: MUSS ZU IHREM SHEET PASSEN!) ---