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 #!/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: Git-Änderungsbeschreibung:
- Replace custom `debug_print` function with standard Python `logging` module calls throughout the codebase. - Füge neuen Betriebsmodus `--mode find_wiki_serp` hinzu.
- Use appropriate logging levels (DEBUG, INFO, WARNING, ERROR, CRITICAL, EXCEPTION). - Implementiere neue Funktion `serp_wikipedia_lookup`, die SerpAPI nutzt, um gezielt nach Wikipedia-Artikeln für einen Firmennamen zu suchen.
- Refactor logging setup in `main` for clarity and proper handler initialization. - Implementiere neue Funktion `process_find_wiki_with_serp`:
- Integrate updated `WikipediaScraper` class (previously developed as v1.6.5 logic): - Lädt aktuelle Sheet-Daten.
- Implement more robust infobox parsing (`_extract_infobox_value`) using flexible selectors, keyword checking (`in`), and improved value cleaning (incl. `sup` removal). - Filtert Zeilen, bei denen Spalte M (Wiki URL) leer/'k.A.' ist UND Spalte K (CRM Mitarbeiter) einen Schwellenwert (Standard: 500) überschreitet.
- Remove old infobox fallback functions. - Ruft `serp_wikipedia_lookup` für gefilterte Zeilen auf.
- Enhance article validation (`_validate_article`) with better link checking via `_get_page_soup`. - Bei erfolgreicher URL-Findung:
- Improve reliability of article search (`search_company_article`) with direct match attempt and better error handling. - Schreibt die gefundene URL in Spalte M.
- Apply `@retry_on_failure` decorator to network-dependent scraper methods (`_get_page_soup`, `search_company_article`). - Setzt Flag 'x' in Spalte A (ReEval Flag).
- Ensure `Config.VERSION` reflects the logical state (v1.6.5 for this commit). - 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 import os
@@ -69,7 +73,7 @@ PATTERNS_FILE_JSON = "technician_patterns.json" # Optional
# ==================== KONFIGURATION ==================== # ==================== KONFIGURATION ====================
class Config: class Config:
# ... (Alle deine bisherigen Config-Einstellungen) ... # ... (Alle deine bisherigen Config-Einstellungen) ...
VERSION = "v1.6.5" # Versionsnummer erhöhen VERSION = "v1.6.6" # Versionsnummer erhöhen
LANG = "de" LANG = "de"
SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo"
MAX_RETRIES = 3 MAX_RETRIES = 3
@@ -199,6 +203,200 @@ COLUMN_MAP = {
# Beispielhafte Definition (bitte an deine Spalten anpassen!) # Beispielhafte Definition (bitte an deine Spalten anpassen!)
# COLUMN_MAP = { ... dein komplettes Mapping ... } # 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): def prepare_data_for_modeling(sheet_handler):
""" """
Lädt Daten aus dem Google Sheet, bereitet sie für das Decision Tree Modell vor: 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 parser = argparse.ArgumentParser(description="Firmen-Datenanreicherungs-Skript v1.6.5") # Version aktualisiert
valid_modes = ["combined", "wiki", "website", "branch", "summarize", "reeval", valid_modes = ["combined", "wiki", "website", "branch", "summarize", "reeval",
"website_lookup", "website_details", "contacts", "full_run", "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("--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("--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("--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("--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)") parser.add_argument("--patterns_out", type=str, default=PATTERNS_FILE_TXT, help=f"Pfad für Regeln (.txt)")
args = parser.parse_args() args = parser.parse_args()
# Lade API Keys direkt am Anfang # 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 # Betriebsmodus ermitteln
mode = None mode = None
if args.mode and args.mode.lower() in valid_modes: if args.mode and args.mode.lower() in valid_modes:
mode = args.mode.lower() 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 else: # Interaktive Abfrage
print("\nBitte wählen Sie den Betriebsmodus:") print("\nBitte wählen Sie den Betriebsmodus:")
# ... (print-Anweisungen für Modi bleiben gleich) ... print(" combined: Wiki(AX), Website(AR), Summarize(AS), Branch(AO) (Batch, Start bei leerem AO)")
print(" combined: Wiki(AX), Website-Scrape(AR), Summarize(AS), Branch(AO) (Batch, Start bei leerem AO)")
print(" wiki: Nur Wikipedia-Verifizierung (AX) (Batch, Start bei leerem AX)") print(" wiki: Nur Wikipedia-Verifizierung (AX) (Batch, Start bei leerem AX)")
print(" website: Nur Website-Scraping Rohtext (AR) (Batch, Start bei leerem AR)") print(" website: Nur Website-Scraping Rohtext (AR) (Batch, Start bei leerem AR)")
print(" summarize: Nur Website-Zusammenfassung (AS) (Batch, Start bei leerem AS)") print(" summarize: Nur Website-Zusammenfassung (AS) (Batch, Start bei leerem AS)")
print(" branch: Nur Branchen-Einschätzung (AO) (Batch, Start bei leerem AO)") 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(" update_wiki: Wiki-URL aus Spalte U nach M übernehmen & ReEval-Flag setzen")
print(" reeval: Verarbeitet Zeilen mit 'x' in A (volle Verarbeitung)") print(" reeval: Verarbeitet Zeilen mit 'x' in A (volle Verarbeitung)")
print(" website_lookup: Sucht fehlende Websites (D)") 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_details:Extrahiert Details für Zeilen mit 'x' (AR) - EXPERIMENTELL") # Ggf. anpassen 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(" 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(" alignment: Schreibt Header A1:AX5 (!)")
print(" train_technician_model: Trainiert Decision Tree zur Technikerschätzung") print(" train_technician_model: Trainiert Decision Tree zur Technikerschätzung")
@@ -4170,18 +4371,21 @@ def main():
except Exception as e: except Exception as e:
print(f"Fehler Modus-Eingabe ({e}) -> Standard: combined") print(f"Fehler Modus-Eingabe ({e}) -> Standard: combined")
mode = "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 # Zeilenlimit ermitteln
row_limit = None row_limit = None
if args.limit is not None: if args.limit is not None:
if args.limit >= 0: if args.limit >= 0:
row_limit = args.limit 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: 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 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: try:
limit_input = input(f"Maximale Anzahl Zeilen für Modus '{mode}'? (Enter=alle): ") limit_input = input(f"Maximale Anzahl Zeilen für Modus '{mode}'? (Enter=alle): ")
if limit_input.strip(): if limit_input.strip():
@@ -4189,54 +4393,60 @@ def main():
limit_val = int(limit_input) limit_val = int(limit_input)
if limit_val >= 0: if limit_val >= 0:
row_limit = limit_val row_limit = limit_val
logging.info(f"Zeilenlimit (interaktiv): {row_limit}") print(f"Zeilenlimit (interaktiv): {row_limit}")
else: else:
logging.warning("Negatives Limit -> Kein Limit") print("Negatives Limit -> Kein Limit")
row_limit = None row_limit = None
except ValueError: except ValueError:
logging.warning("Ungültige Zahl -> Kein Limit") print("Ungültige Zahl -> Kein Limit")
row_limit = None row_limit = None
else: else:
logging.info("Kein Zeilenlimit angegeben.") print("Kein Zeilenlimit angegeben.")
row_limit = None row_limit = None
except Exception as e: except Exception as e:
logging.error(f"Fehler Limit-Eingabe ({e}) -> Kein Limit") print(f"Fehler Limit-Eingabe ({e}) -> Kein Limit")
row_limit = None row_limit = None
# Logging der Limit-Info erfolgt nach FileHandler-Setup
# --- Logdatei-Konfiguration abschließen --- # --- 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: try:
file_handler = logging.FileHandler(LOG_FILE, mode='a', encoding='utf-8') file_handler = logging.FileHandler(LOG_FILE, mode='a', encoding='utf-8')
file_handler.setLevel(log_level) # Nimm das globale Level file_handler.setLevel(log_level) # Nimm das globale Level
file_handler.setFormatter(logging.Formatter(log_format)) 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}") logging.info(f"Logging wird jetzt auch in Datei geschrieben: {LOG_FILE}")
except Exception as e: 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}") 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"===== Skript gestartet =====")
logging.info(f"Version: {Config.VERSION}") # Sollte jetzt v1.6.5 sein logging.info(f"Version: {Config.VERSION}") # Sollte jetzt v1.6.5 sein
logging.info(f"Betriebsmodus: {mode}") logging.info(f"Betriebsmodus: {mode}")
limit_log_text = str(row_limit) if row_limit is not None else 'N/A für diesen Modus' 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' 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)' if row_limit == 0: limit_log_text = '0 (Keine Verarbeitung geplant)'
logging.info(f"Zeilenlimit: {limit_log_text}") logging.info(f"Zeilenlimit: {limit_log_text}")
logging.info(f"Logdatei: {LOG_FILE}") logging.info(f"Logdatei: {LOG_FILE}")
# --- Ende finale Startinfos --- # --- Ende finale Startinfos ---
# --- Vorbereitung (Sheet Handler etc.) --- # --- Vorbereitung (Schema, Sheet Handler etc.) ---
load_target_schema() # Nutzt jetzt logging intern # Annahme: Diese Funktionen verwenden jetzt logging intern
load_target_schema()
try: try:
sheet_handler = GoogleSheetHandler() # Nutzt jetzt logging intern sheet_handler = GoogleSheetHandler()
except Exception as e: except Exception as e:
logging.critical(f"FATAL: Initialisierung des GoogleSheetHandlers fehlgeschlagen: {e}") logging.critical(f"FATAL: Initialisierung des GoogleSheetHandlers fehlgeschlagen: {e}")
logging.critical(f"Bitte Logdatei prüfen: {LOG_FILE}") logging.critical(f"Bitte Logdatei prüfen: {LOG_FILE}")
return # Beende Skript, wenn Sheet nicht geladen werden kann 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 --- # --- Modusausführung ---
start_time = time.time() start_time = time.time()
@@ -4247,51 +4457,65 @@ def main():
if row_limit == 0: if row_limit == 0:
logging.info("Limit 0 angegeben -> Überspringe Dispatcher für Batch-Modus.") logging.info("Limit 0 angegeben -> Überspringe Dispatcher für Batch-Modus.")
else: 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) # Einzelne Zeilen Modi (kein Batch-Dispatcher)
elif mode == "reeval": 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": 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": elif mode == "website_details":
logging.warning("Modus 'website_details' ist experimentell.") 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": 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": elif mode == "full_run":
if row_limit == 0: if row_limit == 0:
logging.info("Limit 0 angegeben -> Überspringe full_run.") logging.info("Limit 0 angegeben -> Überspringe full_run.")
else: else:
# Prüfe, ob eine explizite Startzeile übergeben wurde # Prüfe, ob eine explizite Startzeile übergeben wurde
start_data_index = -1 # Initialisieren
if args.start_row and args.start_row > 5: 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'.") logging.info(f"Nutze expliziten Start-Datenindex {start_data_index} (Sheet Zeile {args.start_row}) für 'full_run'.")
else: else:
logging.info("Ermittle Startindex für 'full_run' (erste Zeile ohne AO)...") 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") 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()): # Prüfe, ob get_start_row_index einen gültigen Index zurückgab
num_available = len(sheet_handler.get_data()) - start_data_index 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 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: if num_to_process > 0:
logging.info(f"'full_run': Verarbeite {num_to_process} Zeilen ab Daten-Index {start_data_index}.") logging.info(f"'full_run': Verarbeite {num_to_process} Zeilen ab Daten-Index {start_data_index}.")
# Übergebe Flags an process_rows_sequentially # Annahme: process_rows_sequentially verwendet logging intern
data_processor.process_rows_sequentially( data_processor.process_rows_sequentially(
start_data_index, start_data_index,
num_to_process, num_to_process,
process_wiki=True, process_wiki=True,
process_chatgpt=True, process_chatgpt=True,
process_website=True process_website=True
) # Nutzt jetzt logging intern )
else: else:
logging.info("Keine Zeilen für 'full_run' zu verarbeiten (Limit/Startindex).") logging.info("Keine Zeilen für 'full_run' zu verarbeiten (Limit/Startindex).")
else: else:
logging.warning(f"Startindex {start_data_index} für 'full_run' ungültig oder keine Zeilen mehr.") logging.warning(f"Startindex {start_data_index} liegt hinter der letzten Datenzeile ({len(current_data)}). Keine Verarbeitung.")
else:
# 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": elif mode == "alignment":
print("\nACHTUNG: Dieser Modus überschreibt die Header-Zeilen A1:AX5!") print("\nACHTUNG: Dieser Modus überschreibt die Header-Zeilen A1:AX5!")
@@ -4302,30 +4526,41 @@ def main():
confirm = 'n' confirm = 'n'
if confirm == 'j': if confirm == 'j':
logging.info("Starte Alignment Demo...") 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: else:
logging.info("Alignment Demo abgebrochen.") logging.info("Alignment Demo abgebrochen.")
# --- Wiki Update Modus ---
elif mode == "update_wiki": elif mode == "update_wiki":
logging.info("Starte Modus '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 # Block für Modelltraining
elif mode == "train_technician_model": elif mode == "train_technician_model":
logging.info(f"Starte Modus: {mode}") logging.info(f"Starte Modus: {mode}")
# (Dieser Block verwendet jetzt logging intern, keine Anpassungen hier nötig, # Annahme: prepare_data_for_modeling verwendet logging intern
# außer sicherzustellen, dass prepare_data_for_modeling logging nutzt)
prepared_df = data_processor.prepare_data_for_modeling() prepared_df = data_processor.prepare_data_for_modeling()
if prepared_df is not None and not prepared_df.empty: if prepared_df is not None and not prepared_df.empty:
logging.info("Aufteilen der Daten für das Modelltraining...") logging.info("Aufteilen der Daten für das Modelltraining...")
try: try:
# Definition von X und y (wie im Original, Annahme: prepare_data... hat Spalten 'name' etc.) # Definition von X und y
X = prepared_df.drop(columns=['Techniker_Bucket', 'name', 'Anzahl_Servicetechniker_Numeric']) X = prepared_df.drop(columns=['Techniker_Bucket', 'CRM Name', 'Anzahl_Servicetechniker_Numeric']) # CRM Name statt 'name'
y = prepared_df['Techniker_Bucket'] 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) 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 split_successful = True
logging.info(f"Train/Test Split: {len(X_train)} Train, {len(X_test)} Test samples.") 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: except Exception as e:
logging.error(f"FEHLER beim Train/Test Split: {e}") logging.error(f"FEHLER beim Train/Test Split: {e}")
split_successful = False split_successful = False
@@ -4335,7 +4570,6 @@ def main():
numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter'] numeric_features = ['Finaler_Umsatz', 'Finaler_Mitarbeiter']
try: try:
imputer = SimpleImputer(strategy='median') imputer = SimpleImputer(strategy='median')
# Stelle sicher, dass Spalten existieren bevor Transformation
if all(nf in X_train.columns for nf in numeric_features): if all(nf in X_train.columns for nf in numeric_features):
X_train[numeric_features] = imputer.fit_transform(X_train[numeric_features]) X_train[numeric_features] = imputer.fit_transform(X_train[numeric_features])
X_test[numeric_features] = imputer.transform(X_test[numeric_features]) X_test[numeric_features] = imputer.transform(X_test[numeric_features])
@@ -4352,16 +4586,12 @@ def main():
if imputation_successful: if imputation_successful:
logging.info("Starte Decision Tree Training mit GridSearchCV...") logging.info("Starte Decision Tree Training mit GridSearchCV...")
# Parameter Grid (wie im Original)
param_grid = { param_grid = {
'criterion': ['gini', 'entropy'], 'criterion': ['gini', 'entropy'], 'max_depth': [6, 8, 10, 12, 15],
'max_depth': [6, 8, 10, 12, 15], 'min_samples_split': [20, 40, 60], 'min_samples_leaf': [10, 20, 30],
'min_samples_split': [20, 40, 60], 'ccp_alpha': [0.0, 0.001, 0.005]
'min_samples_leaf': [10, 20, 30],
'ccp_alpha': [0.0, 0.001, 0.005] # Cost-Complexity Pruning
} }
dtree = DecisionTreeClassifier(random_state=42, class_weight='balanced') dtree = DecisionTreeClassifier(random_state=42, class_weight='balanced')
# GridSearchCV (wie im Original)
grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5, grid_search = GridSearchCV(estimator=dtree, param_grid=param_grid, cv=5,
scoring='f1_weighted', n_jobs=-1, verbose=1) scoring='f1_weighted', n_jobs=-1, verbose=1)
try: try:
@@ -4371,14 +4601,12 @@ def main():
logging.info(f"Beste Parameter: {grid_search.best_params_}") logging.info(f"Beste Parameter: {grid_search.best_params_}")
logging.info(f"Bester F1-Score (gewichtet, CV): {grid_search.best_score_:.4f}") logging.info(f"Bester F1-Score (gewichtet, CV): {grid_search.best_score_:.4f}")
# Speichere das beste Modell
model_filename = args.model_out model_filename = args.model_out
with open(model_filename, 'wb') as f_mod: pickle.dump(best_estimator, f_mod) with open(model_filename, 'wb') as f_mod: pickle.dump(best_estimator, f_mod)
logging.info(f"Bestes Modell gespeichert: '{model_filename}'.") logging.info(f"Bestes Modell gespeichert: '{model_filename}'.")
training_successful = True training_successful = True
except Exception as e_train: except Exception as e_train:
logging.error(f"FEHLER während des Trainings: {e_train}") logging.exception(f"FEHLER während des Trainings: {e_train}")
logging.exception("Traceback Training:") # Loggt Traceback
training_successful = False training_successful = False
if training_successful: if training_successful:
@@ -4386,23 +4614,24 @@ def main():
try: try:
y_pred = best_estimator.predict(X_test) y_pred = best_estimator.predict(X_test)
test_accuracy = accuracy_score(y_test, y_pred) 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, 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 = 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"\n--- Evaluation Test-Set ---")
logging.info(f"Genauigkeit: {test_accuracy:.4f}") logging.info(f"Genauigkeit: {test_accuracy:.4f}")
logging.info(f"Classification Report:\n{report}") logging.info(f"Classification Report:\n{report}")
logging.info(f"Confusion Matrix:\n{conf_matrix_df}") 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...") logging.info("Extrahiere Baumregeln...")
try: try:
feature_names = list(X_train.columns) 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, 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 patterns_filename = args.patterns_out
with open(patterns_filename, 'w', encoding='utf-8') as f_rules: with open(patterns_filename, 'w', encoding='utf-8') as f_rules:
f_rules.write(rules_text) f_rules.write(rules_text)
@@ -4410,7 +4639,7 @@ def main():
except Exception as e_export: except Exception as e_export:
logging.error(f"Fehler beim Exportieren der Regeln: {e_export}") logging.error(f"Fehler beim Exportieren der Regeln: {e_export}")
except Exception as e_eval: 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: else:
logging.warning("Datenvorbereitung für Modelltraining fehlgeschlagen oder ergab keine Daten.") logging.warning("Datenvorbereitung für Modelltraining fehlgeschlagen oder ergab keine Daten.")
@@ -4421,9 +4650,8 @@ def main():
logging.warning("Skript durch Benutzer unterbrochen (KeyboardInterrupt).") logging.warning("Skript durch Benutzer unterbrochen (KeyboardInterrupt).")
print("\n! Skript wurde manuell beendet.") print("\n! Skript wurde manuell beendet.")
except Exception as e: 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.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 --- # --- Abschluss ---
end_time = time.time() end_time = time.time()
@@ -4432,7 +4660,7 @@ def main():
logging.info(f"Gesamtdauer: {duration:.2f} Sekunden.") logging.info(f"Gesamtdauer: {duration:.2f} Sekunden.")
logging.info(f"===== Skript beendet =====") logging.info(f"===== Skript beendet =====")
# Schließe Logging Handler explizit (optional, aber sauber) # Schließe Logging Handler explizit
logging.shutdown() logging.shutdown()
print(f"\nVerarbeitung abgeschlossen. Logfile: {LOG_FILE}") 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 # Führt die main-Funktion aus, wenn das Skript direkt gestartet wird
if __name__ == '__main__': 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() main()