bugfix
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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.
|
Basierend auf v1.6.x - Umstrukturierung in modulare Klassen und flexibles UI.
|
||||||
|
|
||||||
Dieses Skript dient der automatisierten Anreicherung, Validierung und Standardisierung
|
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).
|
Wikipedia, OpenAI (ChatGPT) und SerpAPI (Google Search, LinkedIn).
|
||||||
|
|
||||||
Autor: Christian Godelmann
|
Autor: Christian Godelmann
|
||||||
Version: v1.7.8
|
Version: v1.7.9
|
||||||
|
|
||||||
Hinweis zur Struktur:
|
Hinweis zur Struktur:
|
||||||
Dieser Code wird in logischen Bloecken uebermittelt. Fuegen Sie die Bloecke
|
Dieser Code wird in logischen Bloecken uebermittelt. Fuegen Sie die Bloecke
|
||||||
@@ -30,26 +30,21 @@ import json
|
|||||||
import pickle
|
import pickle
|
||||||
import threading
|
import threading
|
||||||
import traceback
|
import traceback
|
||||||
import logging # logging Modul importieren
|
import logging
|
||||||
import argparse
|
import argparse
|
||||||
import random # Fuer Jitter im Retry Decorator
|
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 # Sicherstellen, dass openai global importiert wird
|
import openai
|
||||||
|
|
||||||
# Externe Bibliotheken
|
# Externe Bibliotheken
|
||||||
import gspread
|
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
|
import wikipedia
|
||||||
# Stellen Sie sicher, dass wikipedia-api nicht gleichzeitig installiert ist (Konflikt).
|
import requests
|
||||||
import requests # Fuer HTTP-Anfragen
|
from bs4 import BeautifulSoup
|
||||||
# Stellen Sie sicher, dass requests >= 2.0.0 installiert ist
|
from oauth2client.service_account import ServiceAccountCredentials
|
||||||
from bs4 import BeautifulSoup # Fuer HTML-Parsing
|
from difflib import SequenceMatcher
|
||||||
# Stellen Sie sicher, dass lxml oder html5lib installiert ist (empfohlen statt html.parser)
|
import unicodedata
|
||||||
# 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
|
|
||||||
|
|
||||||
# Bibliotheken fuer Datenanalyse und ML
|
# Bibliotheken fuer Datenanalyse und ML
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -60,14 +55,12 @@ from sklearn.model_selection import train_test_split, GridSearchCV
|
|||||||
from sklearn.impute import SimpleImputer
|
from sklearn.impute import SimpleImputer
|
||||||
from sklearn.tree import DecisionTreeClassifier, export_text
|
from sklearn.tree import DecisionTreeClassifier, export_text
|
||||||
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
|
from sklearn.metrics import accuracy_score, classification_report, confusion_matrix
|
||||||
import concurrent.futures # Fuer parallele Verarbeitung
|
import concurrent.futures
|
||||||
from sklearn.model_selection import GridSearchCV
|
from imblearn.pipeline import Pipeline as ImbPipeline
|
||||||
from imblearn.pipeline import Pipeline as ImbPipeline # Alias, um Kollision mit sklearn.pipeline zu vermeiden
|
|
||||||
|
|
||||||
# Spezifische externe Tools
|
# Spezifische externe Tools
|
||||||
try:
|
try:
|
||||||
import gender_guesser.detector as gender # Fuer Geschlechtserkennung
|
import gender_guesser.detector as gender
|
||||||
# Initialisieren Sie den Detector einmal global (wird im Helper benutzt)
|
|
||||||
gender_detector = gender.Detector()
|
gender_detector = gender.Detector()
|
||||||
print("gender_guesser.Detector initialisiert.")
|
print("gender_guesser.Detector initialisiert.")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@@ -79,83 +72,23 @@ except Exception as e:
|
|||||||
gender_detector = None
|
gender_detector = None
|
||||||
print(f"Fehler bei Initialisierung von gender_guesser: {e}. Geschlechtserkennung deaktiviert.")
|
print(f"Fehler bei Initialisierung von gender_guesser: {e}. Geschlechtserkennung deaktiviert.")
|
||||||
|
|
||||||
|
|
||||||
# Optional: tiktoken fuer Token-Zaehlung (Modus 8)
|
|
||||||
try:
|
try:
|
||||||
import tiktoken
|
import tiktoken
|
||||||
print("tiktoken importiert.") # Debugging-Ausgabe (geht nur an Konsole vor Logger Setup)
|
print("tiktoken importiert.")
|
||||||
except ImportError:
|
except ImportError:
|
||||||
tiktoken = None
|
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:
|
class Config:
|
||||||
"""Zentrale Konfigurationseinstellungen."""
|
"""Zentrale Konfigurationseinstellungen."""
|
||||||
# --- Grundkonfiguration ---
|
# --- Grundkonfiguration ---
|
||||||
VERSION = "v1.7.9" # Version auf 1.7.9 erhöht
|
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)'
|
||||||
|
|
||||||
@@ -205,7 +138,7 @@ class Config:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def load_api_keys(cls):
|
def load_api_keys(cls):
|
||||||
"""Laedt API-Schluessel aus den definierten Dateien."""
|
"""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...")
|
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['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}")
|
logger.error(f"FEHLER beim Lesen der Schluesseldatei '{filepath}': {e}")
|
||||||
return None
|
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!) ---
|
||||||
# --- 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