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