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:
@@ -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()
|
||||
Reference in New Issue
Block a user