v1.6.6: Füge SerpAPI-Suche für fehlende Wiki-URLs großer Firmen hinzu

- Füge neuen Betriebsmodus `--mode find_wiki_serp` hinzu.
- Implementiere neue Funktion `serp_wikipedia_lookup`, die SerpAPI nutzt, um gezielt nach Wikipedia-Artikeln für einen Firmennamen zu suchen.
- Implementiere neue Funktion `process_find_wiki_with_serp`:
    - Lädt aktuelle Sheet-Daten.
    - Filtert Zeilen, bei denen Spalte M (Wiki URL) leer/'k.A.' ist UND Spalte K (CRM Mitarbeiter) einen Schwellenwert (Standard: 500) überschreitet.
    - Ruft `serp_wikipedia_lookup` für gefilterte Zeilen auf.
    - Bei erfolgreicher URL-Findung:
        - Schreibt die gefundene URL in Spalte M.
        - Setzt Flag 'x' in Spalte A (ReEval Flag).
        - Löscht Timestamps in Spalten AN (Wikipedia Timestamp) und AO (Timestamp letzte Prüfung).
    - Führt gebündelte Sheet-Updates am Ende durch.
- Integriere den neuen Modus `find_wiki_serp` in die Argumentenverarbeitung und Ausführungslogik der `main`-Funktion.
- Füge notwendige Imports hinzu und stelle sicher, dass die neuen Funktionen Logging verwenden.
- Aktualisiere Versionsnummer in `Config.VERSION` auf v1.6.6.
This commit is contained in:
2025-04-22 05:19:53 +00:00
parent 166c87a451
commit 849840054c

View File

@@ -1,18 +1,22 @@
#!/usr/bin/env python3
"""
v1.6.5: Refactor logging & integrate improved WikipediaScraper
v1.6.6: Füge SerpAPI-Suche für fehlende Wiki-URLs großer Firmen hinzu
Git-Änderungsbeschreibung:
- Replace custom `debug_print` function with standard Python `logging` module calls throughout the codebase.
- Use appropriate logging levels (DEBUG, INFO, WARNING, ERROR, CRITICAL, EXCEPTION).
- Refactor logging setup in `main` for clarity and proper handler initialization.
- Integrate updated `WikipediaScraper` class (previously developed as v1.6.5 logic):
- Implement more robust infobox parsing (`_extract_infobox_value`) using flexible selectors, keyword checking (`in`), and improved value cleaning (incl. `sup` removal).
- Remove old infobox fallback functions.
- Enhance article validation (`_validate_article`) with better link checking via `_get_page_soup`.
- Improve reliability of article search (`search_company_article`) with direct match attempt and better error handling.
- Apply `@retry_on_failure` decorator to network-dependent scraper methods (`_get_page_soup`, `search_company_article`).
- Ensure `Config.VERSION` reflects the logical state (v1.6.5 for this commit).
- Füge neuen Betriebsmodus `--mode find_wiki_serp` hinzu.
- Implementiere neue Funktion `serp_wikipedia_lookup`, die SerpAPI nutzt, um gezielt nach Wikipedia-Artikeln für einen Firmennamen zu suchen.
- Implementiere neue Funktion `process_find_wiki_with_serp`:
- Lädt aktuelle Sheet-Daten.
- Filtert Zeilen, bei denen Spalte M (Wiki URL) leer/'k.A.' ist UND Spalte K (CRM Mitarbeiter) einen Schwellenwert (Standard: 500) überschreitet.
- Ruft `serp_wikipedia_lookup` für gefilterte Zeilen auf.
- Bei erfolgreicher URL-Findung:
- Schreibt die gefundene URL in Spalte M.
- Setzt Flag 'x' in Spalte A (ReEval Flag).
- Löscht Timestamps in Spalten AN (Wikipedia Timestamp) und AO (Timestamp letzte Prüfung).
- Führt gebündelte Sheet-Updates am Ende durch.
- Integriere den neuen Modus `find_wiki_serp` in die Argumentenverarbeitung und Ausführungslogik der `main`-Funktion.
- Füge notwendige Imports hinzu und stelle sicher, dass die neuen Funktionen Logging verwenden.
- Aktualisiere Versionsnummer in `Config.VERSION` auf v1.6.6.
"""
import os
@@ -69,7 +73,7 @@ PATTERNS_FILE_JSON = "technician_patterns.json" # Optional
# ==================== KONFIGURATION ====================
class Config:
# ... (Alle deine bisherigen Config-Einstellungen) ...
VERSION = "v1.6.5" # Versionsnummer erhöhen
VERSION = "v1.6.6" # Versionsnummer erhöhen
LANG = "de"
SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo"
MAX_RETRIES = 3
@@ -199,6 +203,200 @@ COLUMN_MAP = {
# Beispielhafte Definition (bitte an deine Spalten anpassen!)
# COLUMN_MAP = { ... dein komplettes Mapping ... }
@retry_on_failure # Annahme: Decorator existiert
def serp_wikipedia_lookup(company_name, website=None):
"""
Sucht über SerpAPI (Google) nach dem wahrscheinlichsten Wikipedia-Artikel für ein Unternehmen.
Args:
company_name (str): Der Name des Unternehmens.
website (str, optional): Die Website des Unternehmens zur Kontextverbesserung. Defaults to None.
Returns:
str: Die URL des ersten gefundenen Wikipedia-Artikels oder None.
"""
serp_key = Config.API_KEYS.get('serpapi')
if not serp_key:
logging.error("SerpAPI Key nicht verfügbar für Wikipedia Lookup.")
return None
if not company_name:
logging.warning("serp_wikipedia_lookup: Kein Firmenname angegeben.")
return None
# Query Konstruktion: Name + "Wikipedia" sollte meistens gut funktionieren
query = f'"{company_name}" Wikipedia'
# Optional: Website hinzufügen, wenn vorhanden und sinnvoll? Kann aber auch einschränken.
# if website and website != "k.A.":
# query = f'"{company_name}" "{website}" Wikipedia'
logging.info(f"Starte SerpAPI Wikipedia-Suche für '{company_name}' mit Query: '{query}'")
params = {
"engine": "google",
"q": query,
"api_key": serp_key,
"hl": "de", # Sprache Deutsch bevorzugen
"gl": "de" # Ergebnisse aus Deutschland bevorzugen
}
api_url = "https://serpapi.com/search"
try:
response = requests.get(api_url, params=params, timeout=15) # Etwas längerer Timeout
response.raise_for_status()
data = response.json()
# Durchsuche organische Ergebnisse nach dem ersten Wikipedia-Link
if "organic_results" in data:
for result in data["organic_results"]:
link = result.get("link")
displayed_link = result.get("displayed_link", "").lower()
# Prüfe, ob es ein Wikipedia-Link ist (flexibler Check)
if link and "wikipedia.org" in link.lower():
# Zusätzliche Prüfung: Ist es wahrscheinlich die Hauptseite des Artikels?
# '/wiki/' ist ein starker Indikator.
if "/wiki/" in link:
logging.info(f" -> SerpAPI: Wikipedia-Link gefunden: {link}")
# Keine weitere Normalisierung hier, gib die gefundene URL zurück
return link
else:
logging.debug(f" -> SerpAPI: Überspringe Link ohne '/wiki/': {link}")
# Manchmal ist der Link selbst keine Wiki-URL, aber der angezeigte Link schon
elif displayed_link and "wikipedia.org" in displayed_link and link:
if "/wiki/" in link: # Stelle sicher, dass der *echte* Link auch auf einen Artikel zeigt
logging.info(f" -> SerpAPI: Wikipedia-Link über Displayed Link gefunden: {link}")
return link
logging.warning(f" -> SerpAPI: Kein passender Wikipedia-Link in organischen Ergebnissen für '{company_name}' gefunden.")
return None
except requests.exceptions.RequestException as e:
logging.error(f"Fehler bei der SerpAPI Wikipedia Suche für '{company_name}': {e}")
raise e # Fehler weitergeben für Retry
except Exception as e:
logging.error(f"Allgemeiner Fehler bei der SerpAPI Wikipedia Suche für '{company_name}': {e}")
return None # Bei unerwarteten Fehlern None zurückgeben
# Kann als eigenständige Funktion oder Methode in DataProcessor implementiert werden
def process_find_wiki_with_serp(sheet_handler, row_limit=None, min_employees=500):
"""
Sucht fehlende Wikipedia-URLs (Spalte M = k.A.) für Unternehmen mit > min_employees
über SerpAPI und trägt gefundene URLs in Spalte M ein. Setzt ReEval-Flag (A)
und löscht Timestamps (AN, AO) für gefundene Einträge.
Args:
sheet_handler (GoogleSheetHandler): Initialisierte Instanz.
row_limit (int, optional): Maximale Anzahl zu prüfender Zeilen. Defaults to None.
min_employees (int, optional): Mindestanzahl Mitarbeiter (Spalte K) als Filter. Defaults to 500.
"""
logging.info(f"Starte Modus 'find_wiki_serp': Suche fehlende Wiki-URLs für Firmen > {min_employees} MA...")
if not sheet_handler.load_data(): return
all_data = sheet_handler.get_all_data_with_headers()
if not all_data or len(all_data) <= 5:
logging.warning("Keine oder zu wenige Daten im Sheet für 'find_wiki_serp' gefunden.")
return
header_rows = 5
data_rows = all_data[header_rows:]
# Benötigte Spaltenindizes holen
try:
col_indices = {
"A": COLUMN_MAP["ReEval Flag"],
"K": COLUMN_MAP["CRM Anzahl Mitarbeiter"],
"M": COLUMN_MAP["Wiki URL"],
"B": COLUMN_MAP["CRM Name"],
"AN": COLUMN_MAP["Wikipedia Timestamp"],
"AO": COLUMN_MAP["Timestamp letzte Prüfung"]
}
col_letters = {key: sheet_handler._get_col_letter(idx + 1) for key, idx in col_indices.items()}
except KeyError as e:
logging.critical(f"FEHLER: Benötigter Spaltenschlüssel '{e}' nicht in COLUMN_MAP gefunden! Modus abgebrochen.")
return
except Exception as e:
logging.critical(f"FEHLER beim Holen der Spaltenbuchstaben: {e}")
return
all_sheet_updates = []
processed_rows = 0
found_urls = 0
skipped_employee_count = 0
skipped_m_filled_count = 0
for idx, row in enumerate(data_rows):
row_num_in_sheet = idx + header_rows + 1
if row_limit is not None and processed_rows >= row_limit:
logging.info(f"Zeilenlimit ({row_limit}) erreicht.")
break
# Prüfe, ob Zeile überhaupt verarbeitet werden soll
try:
# 1. Mitarbeiterzahl prüfen
ma_val_str = row[col_indices["K"]] if len(row) > col_indices["K"] else "0"
try:
# Versuche, die Zahl zu extrahieren (vereinfacht, ohne extract_numeric_value)
ma_val_str_cleaned = re.sub(r"[^\d]", "", ma_val_str) # Nur Ziffern
ma_val = int(ma_val_str_cleaned) if ma_val_str_cleaned else 0
except ValueError:
ma_val = 0 # Im Zweifel als 0 werten
if ma_val <= min_employees:
skipped_employee_count += 1
continue # Nächste Zeile
# 2. Prüfen, ob Wiki URL (M) leer oder "k.A." ist
m_value = row[col_indices["M"]] if len(row) > col_indices["M"] else ""
if m_value and m_value.strip().lower() != "k.a.":
skipped_m_filled_count += 1
continue # Nächste Zeile
# Wenn wir hier sind, ist die Zeile ein Kandidat
processed_rows += 1
company_name = row[col_indices["B"]] if len(row) > col_indices["B"] else ""
if not company_name:
logging.warning(f"Zeile {row_num_in_sheet}: Übersprungen, kein Firmenname für Suche vorhanden.")
continue
# --- SerpAPI Suche durchführen ---
logging.info(f"Zeile {row_num_in_sheet}: Suche Wiki-URL für '{company_name}' (MA: {ma_val})...")
wiki_url_found = serp_wikipedia_lookup(company_name) # Annahme: nutzt logging
time.sleep(1.5) # Pause zwischen SerpAPI-Aufrufen
if wiki_url_found:
logging.info(f" -> URL gefunden: {wiki_url_found}. Bereite Update vor.")
found_urls += 1
# Updates für diese Zeile sammeln
row_updates = [
{'range': f'{col_letters["M"]}{row_num_in_sheet}', 'values': [[wiki_url_found]]}, # URL in M schreiben
{'range': f'{col_letters["A"]}{row_num_in_sheet}', 'values': [['x']]}, # ReEval Flag setzen
{'range': f'{col_letters["AN"]}{row_num_in_sheet}', 'values': [['']]}, # AN löschen
{'range': f'{col_letters["AO"]}{row_num_in_sheet}', 'values': [['']]} # AO löschen
]
all_sheet_updates.extend(row_updates)
else:
logging.info(f" -> Keine Wiki-URL für '{company_name}' via SerpAPI gefunden.")
# Optional: Status in eine separate Spalte schreiben? Vorerst nicht.
except Exception as e:
logging.exception(f"Unerwarteter Fehler bei Verarbeitung von Zeile {row_num_in_sheet}: {e}")
# Mache mit der nächsten Zeile weiter
# --- Batch Update am Ende ---
if all_sheet_updates:
logging.info(f"Sende Batch-Update für {found_urls} gefundene Wiki-URLs ({len(all_sheet_updates)} Zellen)...")
success = sheet_handler.batch_update_cells(all_sheet_updates)
if success:
logging.info(f"Sheet-Update für 'find_wiki_serp' erfolgreich.")
else:
logging.info("Keine neuen Wiki-URLs gefunden zum Eintragen.")
logging.info(f"Modus 'find_wiki_serp' abgeschlossen.")
logging.info(f" Geprüfte Kandidaten (MA>{min_employees}, M leer): {processed_rows}")
logging.info(f" Gefundene & eingetragene URLs: {found_urls}")
logging.info(f" Übersprungen (MA <= {min_employees}): {skipped_employee_count}")
logging.info(f" Übersprungen (M bereits gefüllt): {skipped_m_filled_count}")
def prepare_data_for_modeling(sheet_handler):
"""
Lädt Daten aus dem Google Sheet, bereitet sie für das Decision Tree Modell vor:
@@ -4126,37 +4324,40 @@ def main():
parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript v1.6.5") # Version aktualisiert
valid_modes = ["combined", "wiki", "website", "branch", "summarize", "reeval",
"website_lookup", "website_details", "contacts", "full_run",
"alignment", "train_technician_model", "update_wiki"]
"alignment", "train_technician_model", "update_wiki",
"find_wiki_serp"] # <-- NEUER MODUS HINZUGEFÜGT
parser.add_argument("--mode", type=str, help=f"Betriebsmodus ({', '.join(valid_modes)})")
parser.add_argument("--limit", type=int, help="Maximale Anzahl zu verarbeitender Zeilen", default=None)
parser.add_argument("--start_row", type=int, help="Startzeile im Sheet (1-basiert) für sequenzielle Modi", default=None) # Optionaler Startpunkt
parser.add_argument("--start_row", type=int, help="Startzeile im Sheet (1-basiert) für sequenzielle Modi (full_run)", default=None) # Optionaler Startpunkt für full_run
parser.add_argument("--model_out", type=str, default=MODEL_FILE, help=f"Pfad für Modell (.pkl)")
parser.add_argument("--imputer_out", type=str, default=IMPUTER_FILE, help=f"Pfad für Imputer (.pkl)")
parser.add_argument("--patterns_out", type=str, default=PATTERNS_FILE_TXT, help=f"Pfad für Regeln (.txt)")
args = parser.parse_args()
# Lade API Keys direkt am Anfang
Config.load_api_keys() # Nutzt jetzt logging.debug intern
Config.load_api_keys() # Nutzt jetzt logging.debug/info/warning intern
# Betriebsmodus ermitteln
mode = None
if args.mode and args.mode.lower() in valid_modes:
mode = args.mode.lower()
logging.info(f"Betriebsmodus (aus Kommandozeile): {mode}")
# Logge erst NACHDEM FileHandler konfiguriert ist
# logging.info(f"Betriebsmodus (aus Kommandozeile): {mode}") # Wird später geloggt
print(f"Betriebsmodus (aus Kommandozeile): {mode}") # Frühes Feedback für User
else: # Interaktive Abfrage
print("\nBitte wählen Sie den Betriebsmodus:")
# ... (print-Anweisungen für Modi bleiben gleich) ...
print(" combined: Wiki(AX), Website-Scrape(AR), Summarize(AS), Branch(AO) (Batch, Start bei leerem AO)")
print(" combined: Wiki(AX), Website(AR), Summarize(AS), Branch(AO) (Batch, Start bei leerem AO)")
print(" wiki: Nur Wikipedia-Verifizierung (AX) (Batch, Start bei leerem AX)")
print(" website: Nur Website-Scraping Rohtext (AR) (Batch, Start bei leerem AR)")
print(" summarize: Nur Website-Zusammenfassung (AS) (Batch, Start bei leerem AS)")
print(" branch: Nur Branchen-Einschätzung (AO) (Batch, Start bei leerem AO)")
print(" update_wiki: Wiki-URL aus Spalte U nach M übernehmen & ReEval-Flag setzen")
print(" reeval: Verarbeitet Zeilen mit 'x' in A (volle Verarbeitung)")
print(" website_lookup: Sucht fehlende Websites (D)")
print(" website_details:Extrahiert Details für Zeilen mit 'x' (AR) - EXPERIMENTELL") # Ggf. anpassen
print(" find_wiki_serp: Sucht fehlende Wiki-URLs (M=k.A.) für große Firmen (>500 MA) via SerpAPI") # Neuer Modus erklärt
print(" website_lookup: Sucht fehlende Websites (D) via SerpAPI")
# print(" website_details:Extrahiert Details für Zeilen mit 'x' (AR) - EXPERIMENTELL") # Ggf. ausblenden
print(" contacts: Sucht LinkedIn Kontakte (AM)")
print(" full_run: Verarbeitet sequentiell ab erster Zeile ohne AO (alle TS prüfen)")
print(" full_run: Verarbeitet sequenziell ab erster Zeile ohne AO (alle TS prüfen)")
print(" alignment: Schreibt Header A1:AX5 (!)")
print(" train_technician_model: Trainiert Decision Tree zur Technikerschätzung")
@@ -4170,18 +4371,21 @@ def main():
except Exception as e:
print(f"Fehler Modus-Eingabe ({e}) -> Standard: combined")
mode = "combined"
logging.info(f"Betriebsmodus (interaktiv gewählt): {mode}")
# logging.info(f"Betriebsmodus (interaktiv gewählt): {mode}") # Wird später geloggt
# Zeilenlimit ermitteln
row_limit = None
if args.limit is not None:
if args.limit >= 0:
row_limit = args.limit
logging.info(f"Zeilenlimit (aus Kommandozeile): {row_limit}")
# logging.info(f"Zeilenlimit (aus Kommandozeile): {row_limit}") # Wird später geloggt
print(f"Zeilenlimit (aus Kommandozeile): {row_limit}") # Frühes Feedback
else:
logging.warning("Warnung: Negatives Limit ignoriert.")
print("Warnung: Negatives Limit ignoriert.")
# logging.warning("Warnung: Negatives Limit ignoriert.") # Wird später geloggt
row_limit = None
elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run", "reeval"]: # Relevante Modi
# Frage nur bei Modi, wo es sinnvoll ist (inkl. neuer Modus)
elif mode in ["combined", "wiki", "website", "branch", "summarize", "full_run", "reeval", "update_wiki", "find_wiki_serp"]:
try:
limit_input = input(f"Maximale Anzahl Zeilen für Modus '{mode}'? (Enter=alle): ")
if limit_input.strip():
@@ -4189,54 +4393,60 @@ def main():
limit_val = int(limit_input)
if limit_val >= 0:
row_limit = limit_val
logging.info(f"Zeilenlimit (interaktiv): {row_limit}")
print(f"Zeilenlimit (interaktiv): {row_limit}")
else:
logging.warning("Negatives Limit -> Kein Limit")
print("Negatives Limit -> Kein Limit")
row_limit = None
except ValueError:
logging.warning("Ungültige Zahl -> Kein Limit")
print("Ungültige Zahl -> Kein Limit")
row_limit = None
else:
logging.info("Kein Zeilenlimit angegeben.")
print("Kein Zeilenlimit angegeben.")
row_limit = None
except Exception as e:
logging.error(f"Fehler Limit-Eingabe ({e}) -> Kein Limit")
print(f"Fehler Limit-Eingabe ({e}) -> Kein Limit")
row_limit = None
# Logging der Limit-Info erfolgt nach FileHandler-Setup
# --- Logdatei-Konfiguration abschließen ---
LOG_FILE = create_log_filename(mode) # Annahme: Funktion existiert und gibt Pfad zurück
# Annahme: Funktion existiert und gibt Pfad zurück
LOG_FILE = create_log_filename(mode)
try:
file_handler = logging.FileHandler(LOG_FILE, mode='a', encoding='utf-8')
file_handler.setLevel(log_level) # Nimm das globale Level
file_handler.setFormatter(logging.Formatter(log_format))
logging.getLogger('').addHandler(file_handler) # Füge zum Root-Logger hinzu
# Füge FileHandler zum Root-Logger hinzu
logging.getLogger('').addHandler(file_handler)
logging.info(f"Logging wird jetzt auch in Datei geschrieben: {LOG_FILE}")
except Exception as e:
# Logge Fehler nur auf Konsole, da FileHandler fehlgeschlagen ist
print(f"[ERROR] Konnte FileHandler für Logdatei '{LOG_FILE}' nicht erstellen: {e}")
logging.getLogger('').handlers = [h for h in logging.getLogger('').handlers if not isinstance(h, logging.FileHandler)] # Entferne evtl. defekten Handler
logging.error(f"Konnte FileHandler für Logdatei '{LOG_FILE}' nicht erstellen: {e}")
# Programm kann weiterlaufen, loggt aber nur auf Konsole
# --- Ende Logdatei-Konfiguration ---
# --- Logge finale Startinfos (jetzt auch in Datei) ---
# --- JETZT die Startmeldungen loggen (gehen jetzt in Konsole UND Datei) ---
logging.info(f"===== Skript gestartet =====")
logging.info(f"Version: {Config.VERSION}") # Sollte jetzt v1.6.5 sein
logging.info(f"Betriebsmodus: {mode}")
limit_log_text = str(row_limit) if row_limit is not None else 'N/A für diesen Modus'
if mode in ["combined", "wiki", "website", "branch", "summarize", "full_run", "reeval"]:
if mode in ["combined", "wiki", "website", "branch", "summarize", "full_run", "reeval", "update_wiki", "find_wiki_serp"]:
limit_log_text = str(row_limit) if row_limit is not None else 'Unbegrenzt'
if row_limit == 0: limit_log_text = '0 (Keine Verarbeitung geplant)'
logging.info(f"Zeilenlimit: {limit_log_text}")
logging.info(f"Logdatei: {LOG_FILE}")
# --- Ende finale Startinfos ---
# --- Vorbereitung (Sheet Handler etc.) ---
load_target_schema() # Nutzt jetzt logging intern
# --- Vorbereitung (Schema, Sheet Handler etc.) ---
# Annahme: Diese Funktionen verwenden jetzt logging intern
load_target_schema()
try:
sheet_handler = GoogleSheetHandler() # Nutzt jetzt logging intern
sheet_handler = GoogleSheetHandler()
except Exception as e:
logging.critical(f"FATAL: Initialisierung des GoogleSheetHandlers fehlgeschlagen: {e}")
logging.critical(f"Bitte Logdatei prüfen: {LOG_FILE}")
return # Beende Skript, wenn Sheet nicht geladen werden kann
data_processor = DataProcessor(sheet_handler) # Nutzt jetzt logging intern
# Annahme: DataProcessor verwendet jetzt logging intern
data_processor = DataProcessor(sheet_handler)
# --- Modusausführung ---
start_time = time.time()
@@ -4247,51 +4457,65 @@ def main():
if row_limit == 0:
logging.info("Limit 0 angegeben -> Überspringe Dispatcher für Batch-Modus.")
else:
run_dispatcher(mode, sheet_handler, row_limit) # Nutzt jetzt logging intern
# Annahme: run_dispatcher verwendet logging intern
run_dispatcher(mode, sheet_handler, row_limit)
# Einzelne Zeilen Modi (kein Batch-Dispatcher)
elif mode == "reeval":
data_processor.process_reevaluation_rows(row_limit=row_limit) # Nutzt jetzt logging intern
# Annahme: process_reevaluation_rows verwendet logging intern
data_processor.process_reevaluation_rows(row_limit=row_limit)
elif mode == "website_lookup":
data_processor.process_serp_website_lookup_for_empty() # Nutzt jetzt logging intern
# Annahme: process_serp_website_lookup_for_empty verwendet logging intern
data_processor.process_serp_website_lookup_for_empty()
elif mode == "website_details":
logging.warning("Modus 'website_details' ist experimentell.")
data_processor.process_website_details_for_marked_rows() # Nutzt jetzt logging intern
# Annahme: process_website_details_for_marked_rows verwendet logging intern
data_processor.process_website_details_for_marked_rows()
elif mode == "contacts":
process_contact_research(sheet_handler) # Nutzt jetzt logging intern
# Annahme: process_contact_research verwendet logging intern
process_contact_research(sheet_handler)
elif mode == "full_run":
if row_limit == 0:
logging.info("Limit 0 angegeben -> Überspringe full_run.")
else:
# Prüfe, ob eine explizite Startzeile übergeben wurde
start_data_index = -1 # Initialisieren
if args.start_row and args.start_row > 5:
start_data_index = args.start_row - 5 - 1 # Konvertiere zu 0-basiertem Datenindex
header_rows = 5 # Standard-Annahme
start_data_index = args.start_row - header_rows - 1 # Konvertiere zu 0-basiertem Datenindex
logging.info(f"Nutze expliziten Start-Datenindex {start_data_index} (Sheet Zeile {args.start_row}) für 'full_run'.")
else:
logging.info("Ermittle Startindex für 'full_run' (erste Zeile ohne AO)...")
# Annahme: get_start_row_index verwendet logging intern
start_data_index = sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Prüfung")
if start_data_index != -1 and start_data_index < len(sheet_handler.get_data()):
num_available = len(sheet_handler.get_data()) - start_data_index
num_to_process = min(row_limit, num_available) if row_limit is not None and row_limit >= 0 else num_available
if num_to_process > 0:
logging.info(f"'full_run': Verarbeite {num_to_process} Zeilen ab Daten-Index {start_data_index}.")
# Übergebe Flags an process_rows_sequentially
data_processor.process_rows_sequentially(
start_data_index,
num_to_process,
process_wiki=True,
process_chatgpt=True,
process_website=True
) # Nutzt jetzt logging intern
else:
logging.info("Keine Zeilen für 'full_run' zu verarbeiten (Limit/Startindex).")
# Prüfe, ob get_start_row_index einen gültigen Index zurückgab
if start_data_index != -1:
current_data = sheet_handler.get_data() # Hole aktuelle Daten
if start_data_index < len(current_data):
num_available = len(current_data) - start_data_index
num_to_process = min(row_limit, num_available) if row_limit is not None and row_limit >= 0 else num_available
if num_to_process > 0:
logging.info(f"'full_run': Verarbeite {num_to_process} Zeilen ab Daten-Index {start_data_index}.")
# Annahme: process_rows_sequentially verwendet logging intern
data_processor.process_rows_sequentially(
start_data_index,
num_to_process,
process_wiki=True,
process_chatgpt=True,
process_website=True
)
else:
logging.info("Keine Zeilen für 'full_run' zu verarbeiten (Limit/Startindex).")
else:
logging.warning(f"Startindex {start_data_index} liegt hinter der letzten Datenzeile ({len(current_data)}). Keine Verarbeitung.")
else:
logging.warning(f"Startindex {start_data_index} für 'full_run' ungültig oder keine Zeilen mehr.")
# Fehlermeldung wird von get_start_row_index erwartet
logging.warning(f"Startindex für 'full_run' ungültig (Fehler bei Ermittlung oder Spalte nicht gefunden).")
elif mode == "alignment":
print("\nACHTUNG: Dieser Modus überschreibt die Header-Zeilen A1:AX5!")
@@ -4302,30 +4526,41 @@ def main():
confirm = 'n'
if confirm == 'j':
logging.info("Starte Alignment Demo...")
alignment_demo(sheet_handler.sheet) # Annahme: nutzt logging intern
# Annahme: alignment_demo verwendet logging intern
alignment_demo(sheet_handler.sheet)
else:
logging.info("Alignment Demo abgebrochen.")
# --- Wiki Update Modus ---
elif mode == "update_wiki":
logging.info("Starte Modus 'update_wiki'...")
process_wiki_updates_from_chatgpt(sheet_handler, data_processor, row_limit=row_limit) # Limit optional übergeben
# Annahme: process_wiki_updates_from_chatgpt verwendet logging intern
process_wiki_updates_from_chatgpt(sheet_handler, data_processor, row_limit=row_limit)
# --- NEUER MODUS ---
elif mode == "find_wiki_serp":
logging.info(f"Starte Modus '{mode}'...")
min_employees_for_serp = 500 # Standardwert, ggf. über Argument steuerbar machen
# Annahme: process_find_wiki_with_serp verwendet logging intern
process_find_wiki_with_serp(sheet_handler, row_limit=row_limit, min_employees=min_employees_for_serp)
# --- ENDE NEUER MODUS ---
# Block für Modelltraining
elif mode == "train_technician_model":
logging.info(f"Starte Modus: {mode}")
# (Dieser Block verwendet jetzt logging intern, keine Anpassungen hier nötig,
# außer sicherzustellen, dass prepare_data_for_modeling logging nutzt)
# Annahme: prepare_data_for_modeling verwendet logging intern
prepared_df = data_processor.prepare_data_for_modeling()
if prepared_df is not None and not prepared_df.empty:
logging.info("Aufteilen der Daten für das Modelltraining...")
try:
# Definition von X und y (wie im Original, Annahme: prepare_data... hat Spalten 'name' etc.)
X = prepared_df.drop(columns=['Techniker_Bucket', 'name', 'Anzahl_Servicetechniker_Numeric'])
# Definition von X und y
X = prepared_df.drop(columns=['Techniker_Bucket', 'CRM Name', 'Anzahl_Servicetechniker_Numeric']) # CRM Name statt 'name'
y = prepared_df['Techniker_Bucket']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42, stratify=y)
split_successful = True
logging.info(f"Train/Test Split: {len(X_train)} Train, {len(X_test)} Test samples.")
except KeyError as e:
logging.error(f"FEHLER beim Train/Test Split: Spalte nicht gefunden - {e}. Stellen Sie sicher, dass prepare_data_for_modeling die Spalten korrekt zurückgibt.")
split_successful = False
except Exception as e:
logging.error(f"FEHLER beim Train/Test Split: {e}")
split_successful = False
@@ -4335,7 +4570,6 @@ def main():
numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter']
try:
imputer = SimpleImputer(strategy='median')
# Stelle sicher, dass Spalten existieren bevor Transformation
if all(nf in X_train.columns for nf in numeric_features):
X_train[numeric_features] = imputer.fit_transform(X_train[numeric_features])
X_test[numeric_features] = imputer.transform(X_test[numeric_features])
@@ -4352,16 +4586,12 @@ def main():
if imputation_successful:
logging.info("Starte Decision Tree Training mit GridSearchCV...")
# Parameter Grid (wie im Original)
param_grid = {
'criterion': ['gini', 'entropy'],
'max_depth': [6, 8, 10, 12, 15],
'min_samples_split': [20, 40, 60],
'min_samples_leaf': [10, 20, 30],
'ccp_alpha': [0.0, 0.001, 0.005] # Cost-Complexity Pruning
'criterion': ['gini', 'entropy'], 'max_depth': [6, 8, 10, 12, 15],
'min_samples_split': [20, 40, 60], 'min_samples_leaf': [10, 20, 30],
'ccp_alpha': [0.0, 0.001, 0.005]
}
dtree = DecisionTreeClassifier(random_state=42, class_weight='balanced')
# GridSearchCV (wie im Original)
grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5,
scoring='f1_weighted', n_jobs=-1, verbose=1)
try:
@@ -4371,14 +4601,12 @@ def main():
logging.info(f"Beste Parameter: {grid_search.best_params_}")
logging.info(f"Bester F1-Score (gewichtet, CV): {grid_search.best_score_:.4f}")
# Speichere das beste Modell
model_filename = args.model_out
with open(model_filename, 'wb') as f_mod: pickle.dump(best_estimator, f_mod)
logging.info(f"Bestes Modell gespeichert: '{model_filename}'.")
training_successful = True
except Exception as e_train:
logging.error(f"FEHLER während des Trainings: {e_train}")
logging.exception("Traceback Training:") # Loggt Traceback
logging.exception(f"FEHLER während des Trainings: {e_train}")
training_successful = False
if training_successful:
@@ -4386,23 +4614,24 @@ def main():
try:
y_pred = best_estimator.predict(X_test)
test_accuracy = accuracy_score(y_test, y_pred)
# Sicherstellen, dass Klassen als Liste von Strings übergeben werden
class_labels = [str(cls) for cls in best_estimator.classes_]
report = classification_report(y_test, y_pred, zero_division=0,
labels=best_estimator.classes_, target_names=best_estimator.classes_)
labels=best_estimator.classes_, target_names=class_labels)
conf_matrix = confusion_matrix(y_test, y_pred, labels=best_estimator.classes_)
conf_matrix_df = pd.DataFrame(conf_matrix, index=best_estimator.classes_, columns=best_estimator.classes_)
conf_matrix_df = pd.DataFrame(conf_matrix, index=class_labels, columns=class_labels)
logging.info(f"\n--- Evaluation Test-Set ---")
logging.info(f"Genauigkeit: {test_accuracy:.4f}")
logging.info(f"Classification Report:\n{report}")
logging.info(f"Confusion Matrix:\n{conf_matrix_df}")
print(f"\nModell Genauigkeit (Test): {test_accuracy:.4f}") # Auch auf Konsole
print(f"\nModell Genauigkeit (Test): {test_accuracy:.4f}")
logging.info("Extrahiere Baumregeln...")
try:
feature_names = list(X_train.columns)
class_names = list(best_estimator.classes_) # Sicherstellen, dass es eine Liste ist
rules_text = export_text(best_estimator, feature_names=feature_names,
show_weights=True, spacing=3) # class_names optional
show_weights=True, spacing=3)
patterns_filename = args.patterns_out
with open(patterns_filename, 'w', encoding='utf-8') as f_rules:
f_rules.write(rules_text)
@@ -4410,7 +4639,7 @@ def main():
except Exception as e_export:
logging.error(f"Fehler beim Exportieren der Regeln: {e_export}")
except Exception as e_eval:
logging.error(f"Fehler bei der Evaluation des Test-Sets: {e_eval}")
logging.exception(f"Fehler bei der Evaluation des Test-Sets: {e_eval}")
else:
logging.warning("Datenvorbereitung für Modelltraining fehlgeschlagen oder ergab keine Daten.")
@@ -4421,9 +4650,8 @@ def main():
logging.warning("Skript durch Benutzer unterbrochen (KeyboardInterrupt).")
print("\n! Skript wurde manuell beendet.")
except Exception as e:
# Fange alle unerwarteten Fehler im Hauptblock ab
logging.critical(f"FATAL: Unerwarteter Fehler im Haupt-Ausführungsblock des Modus '{mode}': {e}")
logging.exception("Traceback des kritischen Fehlers:") # Loggt den Traceback
logging.exception("Traceback des kritischen Fehlers:")
# --- Abschluss ---
end_time = time.time()
@@ -4432,7 +4660,7 @@ def main():
logging.info(f"Gesamtdauer: {duration:.2f} Sekunden.")
logging.info(f"===== Skript beendet =====")
# Schließe Logging Handler explizit (optional, aber sauber)
# Schließe Logging Handler explizit
logging.shutdown()
print(f"\nVerarbeitung abgeschlossen. Logfile: {LOG_FILE}")
@@ -4440,4 +4668,43 @@ def main():
# Führt die main-Funktion aus, wenn das Skript direkt gestartet wird
if __name__ == '__main__':
# --- WICHTIG: Fehlende Imports hier hinzufügen ---
import functools # Für retry decorator nötig
import pandas as pd
import numpy as np
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 json
import pickle
import concurrent.futures
import threading
# --- Ende fehlende Imports ---
# --- Annahme: Decorator Definition ist hier oder importiert ---
# Beispielhafte Decorator Definition (falls nicht in separater Datei)
def retry_on_failure(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# ... (Implementierung des Decorators wie zuvor) ...
# Minimalistische Version zur Kompilierung:
try:
return func(*args, **kwargs)
except Exception as e:
logging.warning(f"Retry wird übersprungen (Dummy-Decorator): Fehler in {func.__name__}: {e}")
raise e # Fehler weitergeben
return wrapper
# --- Ende Decorator Annahme ---
# --- Annahme: Restliche Funktionen/Klassen sind definiert ---
# z.B. Config, COLUMN_MAP, create_log_filename, load_target_schema,
# GoogleSheetHandler, WikipediaScraper, DataProcessor,
# Helper-Funktionen (simple_normalize_url etc.),
# Batch-Funktionen (run_dispatcher etc.),
# API-Funktionen (call_openai_chat etc.),
# Neue Funktionen (serp_wikipedia_lookup, process_find_wiki_with_serp)
# ...
# --- Ende Funktions/Klassen Annahmen ---
main()