bugfix
This commit is contained in:
@@ -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!) ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user