Reparatur Sitz
This commit is contained in:
@@ -3336,6 +3336,192 @@ class WikipediaScraper:
|
||||
|
||||
return value_found
|
||||
|
||||
def _parse_sitz_string_detailed(self, raw_sitz_string_input):
|
||||
"""
|
||||
Versucht, aus einem rohen Sitz-String Stadt und Land detailliert zu extrahieren.
|
||||
Nutzt erweiterte Länderlisten und Heuristiken.
|
||||
|
||||
Args:
|
||||
raw_sitz_string_input (str): Der zu parsende String.
|
||||
|
||||
Returns:
|
||||
dict: {'sitz_stadt': '...', 'sitz_land': '...'}
|
||||
"""
|
||||
sitz_stadt_val = "k.A."
|
||||
sitz_land_val = "k.A."
|
||||
|
||||
if not raw_sitz_string_input or not isinstance(raw_sitz_string_input, str):
|
||||
return {'sitz_stadt': sitz_stadt_val, 'sitz_land': sitz_land_val}
|
||||
|
||||
temp_sitz = raw_sitz_string_input.strip()
|
||||
if not temp_sitz or temp_sitz.lower() == "k.a.":
|
||||
return {'sitz_stadt': sitz_stadt_val, 'sitz_land': sitz_land_val}
|
||||
|
||||
# --- Definitionen (könnten für Performance auch Klassenattribute sein) ---
|
||||
known_countries_detailed = {
|
||||
"deutschland": "Deutschland", "germany": "Deutschland", "de": "Deutschland", "brd": "Deutschland", "d-": "Deutschland",
|
||||
"österreich": "Österreich", "austria": "Österreich", "at": "Österreich", "a-": "Österreich",
|
||||
"schweiz": "Schweiz", "switzerland": "Schweiz", "ch": "Schweiz", "suisse": "Schweiz", "svizzera": "Schweiz", "ch-": "Schweiz",
|
||||
"usa": "USA", "u.s.": "USA", "u.s.a.": "USA", "united states": "USA", "vereinigte staaten": "USA",
|
||||
"vereinigtes königreich": "Vereinigtes Königreich", "united kingdom": "Vereinigtes Königreich", "uk": "Vereinigtes Königreich", "gb": "Vereinigtes Königreich", "england": "Vereinigtes Königreich",
|
||||
"frankreich": "Frankreich", "france": "Frankreich", "fr": "Frankreich", "f-": "Frankreich",
|
||||
"niederlande": "Niederlande", "netherlands": "Niederlande", "nl": "Niederlande", "holland": "Niederlande",
|
||||
"belgien": "Belgien", "belgium": "Belgien", "be": "Belgien",
|
||||
"luxemburg": "Luxemburg", "luxembourg": "Luxemburg", "lu": "Luxemburg",
|
||||
"italien": "Italien", "italy": "Italien", "it": "Italien", "i-": "Italien",
|
||||
"spanien": "Spanien", "spain": "Spanien", "es": "Spanien", "españa": "Spanien",
|
||||
"polen": "Polen", "poland": "Polen", "pl": "Polen",
|
||||
"japan": "Japan", "jp": "Japan",
|
||||
"kanada": "Kanada", "canada": "Kanada", # "ca" ist hier wegen US-Staaten problematisch als alleiniger Key
|
||||
"taiwan": "Taiwan",
|
||||
"dänemark": "Dänemark", "denmark": "Dänemark", "dk": "Dänemark",
|
||||
"schweden": "Schweden", "sweden": "Schweden", "se": "Schweden",
|
||||
"norwegen": "Norwegen", "norway": "Norwegen", "no": "Norwegen",
|
||||
"finnland": "Finnland", "finland": "Finnland", "fi": "Finnland",
|
||||
"irland": "Irland", "ireland": "Irland", "ie": "Irland",
|
||||
"litauen": "Litauen", "lithuania": "Litauen", "lt": "Litauen",
|
||||
# ... (Weitere nach Bedarf aus Ihrer CSV und Beobachtungen ergänzen)
|
||||
}
|
||||
|
||||
region_to_country = {
|
||||
"nrw": "Deutschland", "nordrhein-westfalen": "Deutschland", "hessen": "Deutschland",
|
||||
"bayern": "Deutschland", "bavaria": "Deutschland", "baden-württemberg": "Deutschland", "bw": "Deutschland",
|
||||
"zg": "Schweiz", "zug": "Schweiz", "zh": "Schweiz", "zürich": "Schweiz", "be": "Schweiz", "bern": "Schweiz",
|
||||
"ag": "Schweiz", "aargau": "Schweiz", "sg": "Schweiz", "st. gallen": "Schweiz",
|
||||
"va": "USA", "virginia": "USA", "ca": "USA", "california": "USA",
|
||||
"ny": "USA", "new york": "USA", "il": "USA", "illinois": "USA",
|
||||
"tx": "USA", "texas": "USA", "fl": "USA", "florida": "USA",
|
||||
"pa": "USA", "pennsylvania": "USA", "oh": "USA", "ohio": "USA",
|
||||
"ma": "USA", "massachusetts": "USA", "nj": "USA", "new jersey": "USA",
|
||||
"on": "Kanada", "ontario": "Kanada", # Beispiel für Kanada
|
||||
# ... (Weitere nach Bedarf)
|
||||
}
|
||||
# --- Ende Definitionen ---
|
||||
|
||||
extracted_country = ""
|
||||
original_temp_sitz = temp_sitz # Für späteren Abgleich
|
||||
|
||||
# 1. Land in Klammern am Ende: Stadt (Land) oder Stadt (Region)
|
||||
klammer_match = re.search(r'\(([^)]+)\)$', temp_sitz)
|
||||
if klammer_match:
|
||||
potential_suffix_in_klammer = klammer_match.group(1).strip().lower()
|
||||
if potential_suffix_in_klammer in known_countries_detailed:
|
||||
extracted_country = known_countries_detailed[potential_suffix_in_klammer]
|
||||
temp_sitz = temp_sitz[:klammer_match.start()].strip(" ,")
|
||||
elif potential_suffix_in_klammer in region_to_country:
|
||||
extracted_country = region_to_country[potential_suffix_in_klammer]
|
||||
temp_sitz = temp_sitz[:klammer_match.start()].strip(" ,")
|
||||
|
||||
# 2. Ländercode-Präfix (z.B. D-PLZ, CH-PLZ)
|
||||
if not extracted_country:
|
||||
prefix_match = re.match(r'^([A-Za-z]{1,3})\s*-\s*(\d{4,}[\w\s-]*)$', temp_sitz, re.IGNORECASE)
|
||||
if prefix_match:
|
||||
code, rest_nach_plz = prefix_match.group(1).lower(), prefix_match.group(2)
|
||||
if code in known_countries_detailed:
|
||||
extracted_country = known_countries_detailed[code]
|
||||
temp_sitz = rest_nach_plz.strip() # Der Rest nach dem Präfix und PLZ ist die Stadt
|
||||
# Fallback für US-Staaten Codes (z.B. VA, U.S.)
|
||||
elif code in region_to_country and region_to_country[code] == "USA":
|
||||
extracted_country = "USA"
|
||||
temp_sitz = rest_nach_plz.strip()
|
||||
|
||||
|
||||
# 3. Komma-getrennte Liste: Land oder Region am Ende
|
||||
if not extracted_country and ',' in temp_sitz:
|
||||
parts = [p.strip() for p in temp_sitz.split(',')]
|
||||
if len(parts) > 1:
|
||||
# Prüfe die letzten Teile auf bekannte Länder oder Regionen
|
||||
# Prüfe von längeren Suffixen zu kürzeren
|
||||
for num_suffix_parts in range(min(3, len(parts)-1 ), 0, -1): # max 3 Teile als Suffix, min 1
|
||||
potential_suffix = ", ".join(parts[-(num_suffix_parts):]).lower()
|
||||
if potential_suffix in known_countries_detailed:
|
||||
extracted_country = known_countries_detailed[potential_suffix]
|
||||
temp_sitz = ", ".join(parts[:-(num_suffix_parts)]).strip(" ,")
|
||||
break
|
||||
elif potential_suffix in region_to_country:
|
||||
extracted_country = region_to_country[potential_suffix]
|
||||
temp_sitz = ", ".join(parts[:-(num_suffix_parts)]).strip(" ,")
|
||||
break
|
||||
if not extracted_country: # Fallback, falls oben nichts passte, nur den letzten Teil prüfen
|
||||
last_part_lower = parts[-1].lower()
|
||||
if last_part_lower in known_countries_detailed:
|
||||
extracted_country = known_countries_detailed[last_part_lower]
|
||||
temp_sitz = ", ".join(parts[:-1]).strip(" ,")
|
||||
elif last_part_lower in region_to_country:
|
||||
extracted_country = region_to_country[last_part_lower]
|
||||
temp_sitz = ", ".join(parts[:-1]).strip(" ,")
|
||||
|
||||
# 4. Land steht direkt am Ende des (Rest-)Strings (ohne Komma davor)
|
||||
if not extracted_country:
|
||||
# Sortiere Länder nach Länge absteigend, um spezifischere Übereinstimmungen zuerst zu finden
|
||||
sorted_countries = sorted(known_countries_detailed.keys(), key=len, reverse=True)
|
||||
for country_key in sorted_countries:
|
||||
# Suche nach " Stadt Land" oder nur "Land"
|
||||
if temp_sitz.lower().endswith(f" {country_key}"):
|
||||
extracted_country = known_countries_detailed[country_key]
|
||||
temp_sitz = temp_sitz[:-len(f" {country_key}")].strip(" ,")
|
||||
break
|
||||
elif temp_sitz.lower() == country_key: # Der ganze String ist das Land
|
||||
extracted_country = known_countries_detailed[country_key]
|
||||
temp_sitz = ""
|
||||
break
|
||||
|
||||
# 5. Gesamter (verbleibender) String ist ein bekanntes Land
|
||||
if not extracted_country and temp_sitz.lower() in known_countries_detailed:
|
||||
extracted_country = known_countries_detailed[temp_sitz.lower()]
|
||||
temp_sitz = ""
|
||||
|
||||
sitz_land_val = extracted_country if extracted_country else "k.A."
|
||||
|
||||
# Stadt ist der Rest, PLZ entfernen
|
||||
sitz_stadt_val = re.sub(r'^\d{4,8}\s*', '', temp_sitz).strip(" ,")
|
||||
if not sitz_stadt_val: # Wenn nach allem die Stadt leer ist
|
||||
if original_temp_sitz.lower() != "k.a." and sitz_land_val == "k.A.":
|
||||
# Wenn Original was hatte und kein Land gefunden wurde, nimm Original als Stadt
|
||||
sitz_stadt_val = re.sub(r'^\d{4,8}\s*', '', original_temp_sitz).strip(" ,")
|
||||
else:
|
||||
sitz_stadt_val = "k.A."
|
||||
|
||||
# Finale Bereinigung der Stadt, falls das Land fälschlicherweise noch drin ist
|
||||
if sitz_land_val != "k.A." and sitz_land_val in sitz_stadt_val:
|
||||
sitz_stadt_val = sitz_stadt_val.replace(sitz_land_val, "").strip(" ,")
|
||||
if not sitz_stadt_val : sitz_stadt_val = "k.A."
|
||||
|
||||
|
||||
return {'sitz_stadt': sitz_stadt_val, 'sitz_land': sitz_land_val}
|
||||
|
||||
# Die Methode extract_company_data muss jetzt _parse_sitz_string_detailed verwenden:
|
||||
@retry_on_failure
|
||||
def extract_company_data(self, page_url):
|
||||
# ... (Anfang der Methode bleibt gleich: default_result, URL-Prüfung, soup holen) ...
|
||||
# ... (Extraktion von first_paragraph, categories_val, branche_val, umsatz_val, mitarbeiter_val bleibt gleich) ...
|
||||
|
||||
self.logger.debug(" -> Extrahiere Sitz aus Infobox...")
|
||||
raw_sitz_string = self._extract_infobox_value(soup, 'sitz') # Holt den gesamten Sitz-String
|
||||
|
||||
# NEU: Aufruf der detaillierten Parsing-Methode
|
||||
parsed_sitz = self._parse_sitz_string_detailed(raw_sitz_string)
|
||||
sitz_stadt_val = parsed_sitz['sitz_stadt']
|
||||
sitz_land_val = parsed_sitz['sitz_land']
|
||||
|
||||
result = {
|
||||
'url': page_url,
|
||||
'sitz_stadt': sitz_stadt_val,
|
||||
'sitz_land': sitz_land_val,
|
||||
'first_paragraph': first_paragraph,
|
||||
'branche': branche_val,
|
||||
'umsatz': umsatz_val,
|
||||
'mitarbeiter': mitarbeiter_val,
|
||||
'categories': categories_val
|
||||
}
|
||||
# ... (Rest der Methode mit Logging bleibt gleich) ...
|
||||
self.logger.info(
|
||||
f" -> Extrahierte Daten: Sitz Stadt='{sitz_stadt_val}', Sitz Land='{sitz_land_val}', P='{first_paragraph[:30]}...', "
|
||||
f"B='{branche_val}', U='{umsatz_val}', M='{mitarbeiter_val}', "
|
||||
f"C='{categories_val[:50]}...'"
|
||||
)
|
||||
return result
|
||||
|
||||
@retry_on_failure
|
||||
def search_company_article(self, company_name, website=None):
|
||||
"""
|
||||
@@ -7525,6 +7711,110 @@ class DataProcessor:
|
||||
|
||||
self.logger.info(f"Modus 'check_urls' abgeschlossen. {processed_count} Zeilen mit Marker/Fehler verarbeitet, {found_new_url_count} neue URLs gefunden, {skipped_count} Zeilen uebersprungen.")
|
||||
|
||||
def process_repair_sitz_data(self, start_sheet_row=None, end_sheet_row=None, limit=None):
|
||||
"""
|
||||
Liest bestehende Sitz-Stadt/Land-Angaben, wendet die verbesserte Parsing-Logik
|
||||
an und aktualisiert das Sheet, falls sich Änderungen ergeben.
|
||||
"""
|
||||
self.logger.info(f"Starte Modus 'Sitz-Daten Reparatur'. Bereich: {start_sheet_row if start_sheet_row is not None else 'Komplett ab Datenstart'}, End: {end_sheet_row if end_sheet_row else 'Sheet-Ende'}, Limit: {limit if limit is not None else 'Unbegrenzt'}...")
|
||||
|
||||
if not self.sheet_handler.load_data():
|
||||
self.logger.error("Konnte Sheet-Daten nicht laden für Sitz-Reparatur. Abbruch.")
|
||||
return
|
||||
|
||||
all_data = self.sheet_handler.get_all_data_with_headers()
|
||||
header_offset = self.sheet_handler._header_rows
|
||||
|
||||
stadt_col_idx = COLUMN_MAP.get("Wiki Sitz Stadt")
|
||||
land_col_idx = COLUMN_MAP.get("Wiki Sitz Land")
|
||||
# Optional: Eine Spalte für den originalen Roh-Sitz-String, falls vorhanden
|
||||
# roh_sitz_col_idx = COLUMN_MAP.get("IHRE_ROH_SITZ_SPALTE")
|
||||
|
||||
if stadt_col_idx is None or land_col_idx is None:
|
||||
self.logger.error("Spaltenindizes für 'Wiki Sitz Stadt' oder 'Wiki Sitz Land' nicht in COLUMN_MAP. Abbruch.")
|
||||
return
|
||||
|
||||
updates_fuer_sheet = []
|
||||
processed_rows_count = 0
|
||||
updated_rows_count = 0
|
||||
|
||||
effective_start_row = start_sheet_row if start_sheet_row is not None else header_offset + 1
|
||||
effective_end_row = end_sheet_row if end_sheet_row is not None else len(all_data)
|
||||
|
||||
self.logger.info(f"Prüfe Zeilen {effective_start_row} bis {effective_end_row} für Sitz-Reparatur.")
|
||||
|
||||
for row_num_sheet in range(effective_start_row, effective_end_row + 1):
|
||||
if limit is not None and processed_rows_count >= limit:
|
||||
self.logger.info(f"Limit von {limit} erreichten Zeilen für Sitz-Reparatur erreicht.")
|
||||
break
|
||||
|
||||
row_list_idx = row_num_sheet - 1
|
||||
if row_list_idx >= len(all_data): break # Ende der Daten erreicht
|
||||
|
||||
row_data = all_data[row_list_idx]
|
||||
|
||||
aktuelle_stadt = self._get_cell_value_safe(row_data, "Wiki Sitz Stadt")
|
||||
aktuelle_land = self._get_cell_value_safe(row_data, "Wiki Sitz Land")
|
||||
|
||||
# Erzeuge den Input-String für die Parsing-Funktion
|
||||
# Besser: Wenn Sie den *ursprünglichen* String aus der Wikipedia Infobox
|
||||
# in einer separaten Spalte gespeichert hätten, würden Sie diesen hier verwenden.
|
||||
# Als Fallback kombinieren wir aktuelle Stadt und Land.
|
||||
input_sitz_string = aktuelle_stadt
|
||||
if aktuelle_land and aktuelle_land.lower() not in ["", "k.a."]:
|
||||
if input_sitz_string and input_sitz_string.lower() not in ["", "k.a."]:
|
||||
input_sitz_string += f", {aktuelle_land}" # Kombiniere mit Komma
|
||||
else:
|
||||
input_sitz_string = aktuelle_land # Wenn Stadt leer/kA, nimm nur Land
|
||||
|
||||
if not input_sitz_string or not input_sitz_string.strip() or input_sitz_string.lower() == 'k.a.':
|
||||
# self.logger.debug(f"Zeile {row_num_sheet}: Keine validen aktuellen Sitzdaten ('{aktuelle_stadt}', '{aktuelle_land}') zum Reparieren.")
|
||||
continue
|
||||
|
||||
processed_rows_count += 1
|
||||
|
||||
try:
|
||||
# Verwende die neue Parsing-Methode des WikipediaScrapers
|
||||
# Stellen Sie sicher, dass self.wiki_scraper eine Instanz von WikipediaScraper ist
|
||||
parsed_sitz_info = self.wiki_scraper._parse_sitz_string_detailed(input_sitz_string)
|
||||
neue_stadt = parsed_sitz_info.get('sitz_stadt', 'k.A.')
|
||||
neues_land = parsed_sitz_info.get('sitz_land', 'k.A.')
|
||||
|
||||
# Nur updaten, wenn sich etwas geändert hat
|
||||
if (neue_stadt != aktuelle_stadt and not (neue_stadt == "k.A." and aktuelle_stadt == "")) or \
|
||||
(neues_land != aktuelle_land and not (neues_land == "k.A." and aktuelle_land == "")):
|
||||
self.logger.info(f"Zeile {row_num_sheet}: SITZ-UPDATE. Input: '{input_sitz_string[:60]}...' Alt: '{aktuelle_stadt} / {aktuelle_land}' -> Neu: '{neue_stadt} / {neues_land}'")
|
||||
updates_fuer_sheet.append({
|
||||
'range': f'{self.sheet_handler._get_col_letter(stadt_col_idx + 1)}{row_num_sheet}',
|
||||
'values': [[neue_stadt]]
|
||||
})
|
||||
updates_fuer_sheet.append({
|
||||
'range': f'{self.sheet_handler._get_col_letter(land_col_idx + 1)}{row_num_sheet}',
|
||||
'values': [[neues_land]]
|
||||
})
|
||||
updated_rows_count += 1
|
||||
# else:
|
||||
# self.logger.debug(f"Zeile {row_num_sheet}: Keine Änderung bei Sitzdaten für Input '{input_sitz_string[:60]}...'. Alt: '{aktuelle_stadt} / {aktuelle_land}', Neu: '{neue_stadt} / {neues_land}'")
|
||||
|
||||
|
||||
except Exception as e_parse:
|
||||
self.logger.error(f"Fehler beim Parsen des Sitzes für Zeile {row_num_sheet} mit Input '{input_sitz_string}': {e_parse}")
|
||||
|
||||
# Batch-Update Logik
|
||||
if len(updates_fuer_sheet) >= getattr(Config, 'UPDATE_BATCH_ROW_LIMIT', 50) * 2: # Mal 2, da zwei Spalten pro Zeile
|
||||
self.logger.info(f"Sende Batch-Update für {len(updates_fuer_sheet)//2} Sitzreparaturen...")
|
||||
self.sheet_handler.batch_update_cells(updates_fuer_sheet)
|
||||
updates_fuer_sheet = []
|
||||
# time.sleep(1) # Optionale Pause
|
||||
|
||||
# Letzten Batch senden
|
||||
if updates_fuer_sheet:
|
||||
self.logger.info(f"Sende finalen Batch-Update für {len(updates_fuer_sheet)//2} Sitzreparaturen...")
|
||||
self.sheet_handler.batch_update_cells(updates_fuer_sheet)
|
||||
|
||||
self.logger.info(f"Sitz-Daten Reparatur abgeschlossen. {processed_rows_count} Zeilen geprüft, {updated_rows_count} Zeilen aktualisiert.")
|
||||
|
||||
|
||||
# ==========================================================================
|
||||
# === Utility Methods (ML Data Prep & Training) ============================
|
||||
# ==========================================================================
|
||||
@@ -9238,6 +9528,7 @@ def main():
|
||||
"website_details", # EXPERIMENTELL - Nutzt process_website_details (Block 32)
|
||||
"train_technician_model", # Nutzt train_technician_model (Block 31)
|
||||
"alignment", # Nutzt globale alignment_demo (Block 14)
|
||||
"reparatur_sitz" # NEUER MODUS HIER
|
||||
],
|
||||
"Kombinierte Laeufe (Vordefiniert)": [
|
||||
"combined_all", # Definiert eine Sequenz von Batch-Modi
|
||||
@@ -9787,6 +10078,16 @@ def main():
|
||||
else:
|
||||
logger.error("Sheet-Handler oder Sheet-Objekt nicht verfuegbar fuer Alignment-Demo.")
|
||||
|
||||
elif selected_mode == "reparatur_sitz": # NEUER BLOCK
|
||||
# Hier können Sie Start, Ende und Limit aus args verwenden, falls Sie dafür CLI-Optionen hinzufügen möchten
|
||||
# oder feste Werte / interaktive Abfragen für diesen Modus implementieren.
|
||||
# Für den Anfang ein kompletter Durchlauf (ab Datenstart):
|
||||
data_processor.process_repair_sitz_data(
|
||||
start_sheet_row=None, # Beginnt nach den Headern
|
||||
end_sheet_row=None, # Bis zum Ende des Sheets
|
||||
limit=final_limit_to_use # Verwendet das global ermittelte Limit
|
||||
)
|
||||
|
||||
|
||||
# ---- Modus nicht gefunden (sollte durch Validierung oben abgefangen werden) ----
|
||||
else:
|
||||
@@ -9849,609 +10150,3 @@ if __name__ == '__main__':
|
||||
# --- Ende der DataProcessor Klasse ---
|
||||
# Ein pass statement, um die Klassendefinition abzuschliessen, falls keine weiteren Methoden folgen.
|
||||
pass # <-- DIESES pass STATEMENT GEHOERT ZUM ENDE DER KLASSENDEFINITION
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Hauptausfuehrungsblock & Globale Funktionen nach Klassen
|
||||
# ==============================================================================
|
||||
# Der naechste Block (Block 34) enthaelt die main Funktion und den Entry Point.
|
||||
|
||||
# ==============================================================================
|
||||
# 6. MAIN FUNCTION (HAUPTEINSTIEGSPUNKT & UI DISPATCHER)
|
||||
# ==============================================================================
|
||||
|
||||
# Der globale Root Logger wird in main() konfiguriert
|
||||
# logger = logging.getLogger(__name__) # Diesen Logger gibt es schon, keine Neudefinition hier
|
||||
|
||||
def main():
|
||||
"""
|
||||
Haupteinstiegspunkt des Skripts.
|
||||
Verarbeitet Kommandozeilen-Argumente, richtet Logging ein,
|
||||
initialisiert Komponenten und dispatchet zu den passenden Modi.
|
||||
"""
|
||||
# WICHTIG: Globale Variable LOG_FILE wird benoetigt (Initialisierung Block 1)
|
||||
global LOG_FILE
|
||||
logger = logging.getLogger(__name__) # <<< DIESE ZEILE HINZUFÜGEN
|
||||
|
||||
# --- Initial Logging Setup (Konfiguration von Level und Format) ---
|
||||
# Diese Konfiguration wird wirksam, sobald die Handler hinzugefuegt werden.
|
||||
# Standard-Logging Level festlegen (aus Config Block 1)
|
||||
log_level = logging.DEBUG if getattr(Config, 'DEBUG', False) else logging.INFO
|
||||
log_format = '%(asctime)s - %(levelname)-8s - %(name)-25s - %(message)s' # Angepasstes Format mit breiterem Namen
|
||||
|
||||
# Root-Logger konfigurieren (mit Console Handler, File Handler wird spaeter hinzugefuegt)
|
||||
# handlers=[] verhindert default Console Handler, wir fuegen ihn manuell hinzu fuer mehr Kontrolle
|
||||
logging.basicConfig(level=log_level, format=log_format, handlers=[])
|
||||
|
||||
# Console Handler explizit hinzufuegen
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(log_level) # Nimm das globale Level
|
||||
console_handler.setFormatter(logging.Formatter(log_format))
|
||||
# Pruefen, ob nicht schon ein Console Handler vorhanden ist (z.B. bei wiederholten Aufrufen in Tests)
|
||||
if not any(isinstance(h, logging.StreamHandler) for h in logging.getLogger('').handlers):
|
||||
logging.getLogger('').addHandler(console_handler)
|
||||
|
||||
|
||||
# Testnachricht (geht nur an Konsole, da File Handler noch fehlt)
|
||||
logger.debug("DEBUG Logging initial konfiguriert (nur Konsole).")
|
||||
logger.info("INFO Logging initial konfiguriert (nur Konsole).")
|
||||
|
||||
|
||||
# --- Initialisierung (Argument Parser) ---
|
||||
current_script_version = getattr(Config, 'VERSION', 'unknown') # Aus Config Block 1
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=f"Firmen-Datenanreicherungs-Skript {current_script_version}. Automatisiert Anreicherung und Validierung aus Google Sheets.",
|
||||
formatter_class=argparse.RawTextHelpFormatter # Behaelt Formatierung im Help-Text
|
||||
)
|
||||
|
||||
# Liste der gueltigen Modi - MUSS mit den elif-Zweigen unten uebereinstimmen!
|
||||
# Kategorisiert fuer die Menue-Ausgabe
|
||||
mode_categories = {
|
||||
"Batch-Verarbeitung (Schritt-Optimiert)": [
|
||||
"wiki_verify", # Uebereinstimmend mit process_verification_batch (Block 26)
|
||||
"website_scraping", # Uebereinstimmend mit process_website_scraping_batch (Block 27)
|
||||
"summarize_website", # Uebereinstimmend mit process_summarization_batch (Block 28)
|
||||
"branch_eval", # Uebereinstimmend mit process_branch_batch (Block 29)
|
||||
],
|
||||
"Sequenzielle Verarbeitung (Zeilenweise)": [
|
||||
"full_run", # Nutzt process_rows_sequentially (Block 24)
|
||||
],
|
||||
"Re-Evaluate Markierte Zeilen (Spalte A='x')": [
|
||||
"reeval", # Nutzt process_reevaluation_rows (Block 25)
|
||||
],
|
||||
"Einzelne Dienstprogramme / Suchen": [
|
||||
"find_wiki_serp", # Nutzt process_find_wiki_serp (Block 30)
|
||||
"website_lookup", # Nutzt process_serp_website_lookup (Block 30)
|
||||
"contacts", # Nutzt process_contact_search (Block 30)
|
||||
"update_wiki_suggestions", # Nutzt process_wiki_updates_from_chatgpt (Block 32)
|
||||
"wiki_reextract_missing_an", # Nutzt process_wiki_reextract_missing_an (Block 32)
|
||||
"website_details", # EXPERIMENTELL - Nutzt process_website_details (Block 32)
|
||||
"train_technician_model", # Nutzt train_technician_model (Block 31)
|
||||
"alignment", # Nutzt globale alignment_demo (Block 14)
|
||||
],
|
||||
"Kombinierte Laeufe (Vordefiniert)": [
|
||||
"combined_all", # Definiert eine Sequenz von Batch-Modi
|
||||
]
|
||||
}
|
||||
# Erstellen Sie eine flache Liste aller validen Modi fuer die Validierung
|
||||
valid_modes = [mode for modes in mode_categories.values() for mode in modes]
|
||||
|
||||
|
||||
# Dynamisch generieren des Help-Textes fuer den Modus
|
||||
mode_help_text = "Betriebsmodus. Waehlen Sie einen der folgenden:\n"
|
||||
for category, modes in mode_categories.items():
|
||||
mode_help_text += f"\n{category}:\n"
|
||||
for mode in modes:
|
||||
mode_help_text += f" - {mode}\n"
|
||||
|
||||
parser.add_argument("--mode", type=str, help=mode_help_text)
|
||||
# Hilfsargument fuer die CLI-basierte Modusauswahl (wenn --mode gesetzt ist)
|
||||
parser.add_argument("-m", "--cli-mode", dest="mode", action="store_const", const=valid_modes[0] if valid_modes else None, help=argparse.SUPPRESS) # Unterdruecke in --help
|
||||
|
||||
parser.add_argument("--limit", type=int, help="Maximale Anzahl zu verarbeitender Zeilen in den meisten Modi (prueft Zeilen VOR Ueberspringung/Filterung).", default=None)
|
||||
# start_sheet_row wird primaer fuer full_run verwendet, kann aber auch fuer Bereiche in Batch nuetzlich sein
|
||||
parser.add_argument("--start_sheet_row", type=int, help="Startzeile im Sheet (1-basiert) fuer 'full_run' und einige Batch-Modi. Standard: Automatische Ermittlung basierend auf Timestamp.", default=None)
|
||||
# end_sheet_row fuer Bereiche
|
||||
parser.add_argument("--end_sheet_row", type=int, help="Endzeile im Sheet (1-basiert) fuer 'full_run' und einige Batch-Modi. Standard: Ende des Sheets.", default=None)
|
||||
|
||||
|
||||
# Argument fuer den Re-Eval und Full-Run Modus zur Auswahl der Schritte
|
||||
# Moegliche Werte fuer die Schritte: 'wiki', 'chat', 'web', 'ml_predict', etc. (entsprechend den step_type Schluesseln in _process_single_row Block 19)
|
||||
# Default ist 'all' fuer alle Schritte, oder eine spezifische Liste
|
||||
# Dies sind die Schluessel, die _process_single_row (Block 19) in steps_to_run Set erwartet.
|
||||
valid_single_row_steps = ['wiki', 'chat', 'web', 'ml_predict'] # Fuegen Sie hier weitere Schritt-Schluessel hinzu, die _process_single_row versteht
|
||||
single_row_steps_help = f"Komma-getrennte Liste der Schritte im 'reeval' und 'full_run' Modus (z.B. 'wiki,chat').\nMögliche Schritte: {', '.join(valid_single_row_steps)}.\nStandard: {'all' if valid_single_row_steps else 'keine'}" # Standard: alle verfuegbaren Schritte
|
||||
|
||||
# Standardwert fuer --steps: Alle gueltigen Single-Row Schritte, wenn es welche gibt
|
||||
default_steps_arg = ','.join(valid_single_row_steps) if valid_single_row_steps else ''
|
||||
parser.add_argument("--steps", type=str, help=single_row_steps_help, default=default_steps_arg)
|
||||
|
||||
|
||||
# Argumente fuer find_wiki_serp (falls ueber CLI gesteuert)
|
||||
parser.add_argument("--min_umsatz", type=float, help="Mindestumsatz in MIO € (CRM Spalte J) fuer find_wiki_serp Filter.", default=200.0) # Float fuer Konsistenz
|
||||
parser.add_argument("--min_employees", type=int, help="Mindestmitarbeiterzahl (CRM Spalte K) fuer find_wiki_serp Filter.", default=500)
|
||||
|
||||
|
||||
# Argumente fuer train_technician_model (Pfade fuer Output-Dateien)
|
||||
parser.add_argument("--model_out", type=str, default=MODEL_FILE, help=f"Pfad fuer das trainierte Modell (.pkl). Standard: {MODEL_FILE}") # Block 1 Konstante
|
||||
parser.add_argument("--imputer_out", type=str, default=IMPUTER_FILE, help=f"Pfad fuer den trainierten Imputer (.pkl). Standard: {IMPUTER_FILE}") # Block 1 Konstante
|
||||
parser.add_argument("--patterns_out", type=str, default=PATTERNS_FILE_JSON, help=f"Pfad fuer die Feature-Spaltenliste (.json). Standard: {PATTERNS_FILE_JSON}") # Block 1 Konstante
|
||||
|
||||
# TODO: Fuegen Sie hier weitere CLI-Argumente hinzu, falls andere Modi Parameter benoetigen
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
# --- Konfiguration laden ---
|
||||
Config.load_api_keys() # Nutzt jetzt logging intern (print am Anfang Block 1)
|
||||
|
||||
|
||||
# --- Logdatei-Konfiguration abschliessen ---
|
||||
# Bestimmen Sie den Log-Modus Namen basierend auf CLI oder Interaktion
|
||||
# Wir nutzen den CLI Modus Namen, wenn --mode gesetzt ist, sonst "interactive".
|
||||
log_mode_name = args.mode if args.mode else "interactive"
|
||||
LOG_FILE = create_log_filename(log_mode_name) # Nutzt globale Funktion (Block 3)
|
||||
|
||||
# Wenn die Logdatei erfolgreich erstellt wurde
|
||||
if LOG_FILE:
|
||||
try:
|
||||
# Erstellen Sie den FileHandler fuer die Logdatei
|
||||
# mode='a' zum Anhaengen, encoding='utf-8' fuer Unicode
|
||||
file_handler = logging.FileHandler(LOG_FILE, mode='a', encoding='utf-8')
|
||||
file_handler.setLevel(log_level) # Nimm das globale Level
|
||||
# Verwenden Sie denselben Formatter wie fuer den Console Handler
|
||||
file_handler.setFormatter(logging.Formatter(log_format))
|
||||
# Fuege FileHandler zum Root-Logger hinzu
|
||||
# Pruefen, ob nicht schon ein File Handler mit demselben Pfad vorhanden ist (z.B. bei wiederholten Aufrufen in Tests)
|
||||
if not any(isinstance(h, logging.FileHandler) and h.baseFilename == os.path.abspath(LOG_FILE) for h in logging.getLogger('').handlers):
|
||||
logging.getLogger('').addHandler(file_handler)
|
||||
logger.info(f"Logging wird jetzt auch in Datei geschrieben: {LOG_FILE}")
|
||||
except Exception as e:
|
||||
# Logge Fehler nur auf Konsole, da FileHandler fehlgeschlagen ist
|
||||
# logger.exception loggt auch an die Konsole, wenn kein FileHandler da ist
|
||||
logger.error(f"Konnte FileHandler fuer Logdatei '{LOG_FILE}' nicht erstellen: {e}")
|
||||
# Optional: Entfernen Sie evtl. den fehlerhaften Handler aus der Liste
|
||||
logging.getLogger('').handlers = [h for h in logging.getLogger('').handlers if not isinstance(h, logging.FileHandler) or h.baseFilename == os.path.abspath(LOG_FILE)] # Entferne nur den fehlerhaften Handler
|
||||
|
||||
|
||||
# --- JETZT die Startmeldungen loggen (gehen jetzt in Konsole UND Datei) ---
|
||||
logger.info(f"===== Skript gestartet =====")
|
||||
logger.info(f"Version: {current_script_version}")
|
||||
# Logge den tatsaechtlichen Pfad der Logdatei oder die Fehlermeldung
|
||||
logger.info(f"Logdatei: {LOG_FILE if LOG_FILE else 'FEHLER - Keine Logdatei erstellt'}")
|
||||
# Logge relevante CLI Argumente zur Dokumentation des Laufs
|
||||
logger.info(f"CLI Argumente: {args}")
|
||||
|
||||
|
||||
# --- Vorbereitung (Schema, Handler etc.) ---
|
||||
# Laden Sie das Ziel-Branchenschema (Block 6)
|
||||
# load_target_schema ist mit retry_on_failure dekoriert (Block 2).
|
||||
load_target_schema()
|
||||
|
||||
|
||||
# Initialisiere GoogleSheetHandler (Block 14)
|
||||
sheet_handler = None # Initialisiere Variable
|
||||
try:
|
||||
# Der GoogleSheetHandler Init (_init_ Methode) baut die Verbindung auf und laedt Daten.
|
||||
# Fehler werden dort gefangen und als ConnectionError erneut geworfen.
|
||||
sheet_handler = GoogleSheetHandler() #<- Zeile 13596
|
||||
logger.info("GoogleSheetHandler erfolgreich initialisiert.")
|
||||
except ConnectionError as e:
|
||||
# Wenn die Initialisierung des SheetHandlers fehlschlaegt (Verbindungs-/Ladefehler)
|
||||
logger.critical(f"FATAL: Initialisierung des GoogleSheetHandlers fehlgeschlagen: {e}")
|
||||
logger.critical(f"Bitte ueberpruefen Sie Ihre Google Sheets URL, Credentials und Berechtigungen.")
|
||||
logger.critical(f"Bitte Logdatei pruefen fuer Details: {LOG_FILE}")
|
||||
return # Beende Skript, wenn Sheet nicht geladen werden kann
|
||||
except Exception as e:
|
||||
# Fangen Sie andere unerwartete Fehler bei der Initialisierung ab
|
||||
logger.critical(f"FATAL: Unerwarteter Fehler bei Initialisierung von GoogleSheetHandler: {e}")
|
||||
logger.debug(traceback.format_exc())
|
||||
logger.critical(f"Bitte Logdatei pruefen fuer Details: {LOG_FILE}")
|
||||
return # Beende Skript
|
||||
|
||||
|
||||
# Initialisiere WikipediaScraper (Block 14)
|
||||
wiki_scraper = None # Initialisiere Variable
|
||||
try:
|
||||
# Der WikipediaScraper Init (_init_ Methode) konfiguriert die Bibliothek und Requests.
|
||||
# Fehler werden dort gefangen und erneut geworfen.
|
||||
wiki_scraper = WikipediaScraper()
|
||||
logger.info("WikipediaScraper erfolgreich initialisiert.")
|
||||
except Exception as e:
|
||||
# Wenn die Initialisierung des WikipediaScrapers fehlschlaegt
|
||||
logger.critical(f"FATAL: Initialisierung des WikipediaScrapers fehlgeschlagen: {e}")
|
||||
logger.debug(traceback.format_exc())
|
||||
logger.critical(f"Bitte Logdatei pruefen fuer Details: {LOG_FILE}")
|
||||
# Das Skript kann ohne Wiki Scraper viele Modi nicht sinnvoll laufen
|
||||
return # Beende Skript
|
||||
|
||||
|
||||
# TODO: Initialisieren Sie hier weitere Worker-Instanzen, falls Sie separate Klassen haben (z.B. OpenAIHandler, SerpAPIHandler)
|
||||
# openai_handler = OpenAIHandler()
|
||||
# serpapi_handler = SerpAPIHandler()
|
||||
|
||||
|
||||
# Initialisiere DataProcessor Instanz (Block 15) mit Handlern
|
||||
# Uebergeben Sie alle benoetigten Handler an den DataProcessor.
|
||||
# Die __init__ Methode des DataProcessor (Block 15) prueft die Typen und wirft Value Error, wenn falsch.
|
||||
try:
|
||||
data_processor = DataProcessor(sheet_handler=sheet_handler, wiki_scraper=wiki_scraper)
|
||||
logger.info("DataProcessor erfolgreich initialisiert.")
|
||||
except Exception as e:
|
||||
# Fangen Sie Fehler bei der DataProcessor Initialisierung ab.
|
||||
logger.critical(f"FATAL: Initialisierung des DataProcessors fehlgeschlagen: {e}")
|
||||
logger.debug(traceback.format_exc())
|
||||
logger.critical(f"Bitte Logdatei pruefen fuer Details: {LOG_FILE}")
|
||||
return # Beende Skript
|
||||
|
||||
|
||||
# --- Modusauswahl und Ausfuehrung ---
|
||||
start_process_time = time.time() # Zeitmessung fuer die Verarbeitung starten
|
||||
logger.info(f"Starte Verarbeitung um {datetime.now().strftime('%H:%M:%S')}...")
|
||||
|
||||
|
||||
selected_mode = None # Variable fuer den tatsaechlich auszufuehrenden Modus
|
||||
|
||||
# --- Ermitteln des zu fuehrenden Modus (CLI hat Prioritaet vor interaktiver Auswahl) ---
|
||||
# Wenn das --mode Argument ueber die Kommandozeile gesetzt wurde
|
||||
if args.mode:
|
||||
selected_mode = args.mode.lower() # Konvertiere zu Kleinbuchstaben
|
||||
# Pruefen Sie, ob der gewaehlte Modus in der Liste der validen Modi enthalten ist
|
||||
if selected_mode not in valid_modes:
|
||||
# Logge einen Fehler und beende das Skript, wenn der Modus ungueltig ist.
|
||||
logger.error(f"Ungueltiger Modus '{args.mode}' ueber Kommandozeile angegeben. Gueltige Modi: {', '.join(valid_modes)}")
|
||||
print(f"Fehler: Ungueltiger Modus '{args.mode}'. Bitte ueberpruefen Sie die Liste der gueltigen Modi (siehe --help).")
|
||||
return # Beende das Skript
|
||||
logger.info(f"Betriebsmodus (CLI gewaehlt): {selected_mode}")
|
||||
|
||||
# Wenn das --mode Argument NICHT ueber die Kommandozeile gesetzt wurde
|
||||
else:
|
||||
# --- Interaktive Modusauswahl ueber die Konsole ---
|
||||
print("\nBitte waehlen Sie den Betriebsmodus:")
|
||||
# Zeigen Sie die Liste der validen Modi kategorisiert an, mit Nummern.
|
||||
mode_options_map = {} # Dictionary zum Abbilden von Zahl/Name auf Modusname
|
||||
option_counter = 1 # Zaehler fuer die numerischen Optionen
|
||||
# Iteriere durch die Kategorien und Modi
|
||||
for category, modes in mode_categories.items():
|
||||
print(f"\n{category}:")
|
||||
for mode in modes:
|
||||
print(f" {option_counter}: {mode}")
|
||||
mode_options_map[str(option_counter)] = mode # Bilde die numerische Option auf den Modusnamen ab
|
||||
mode_options_map[mode] = mode # Bilde den Modusnamen (kleingeschrieben) auf sich selbst ab (fuer direkte Eingabe)
|
||||
option_counter += 1 # Erhoehe den Zaehler
|
||||
|
||||
|
||||
# Fuegen Sie eine Option zum Abbrechen hinzu
|
||||
print(f"\n 0: Abbrechen")
|
||||
mode_options_map['0'] = 'exit' # Bilde 0 auf den speziellen 'exit' Modus ab
|
||||
|
||||
|
||||
# Schleife, bis ein gueltiger Modus gewaehlt wurde oder der Benutzer abbricht
|
||||
while selected_mode is None:
|
||||
try:
|
||||
# Lesen Sie die Eingabe vom Benutzer
|
||||
mode_input = input(f"Geben Sie den Modusnamen oder die Zahl ein: ").strip().lower()
|
||||
|
||||
# Pruefen Sie, ob die Eingabe einer Option in der Map entspricht
|
||||
if mode_input in mode_options_map:
|
||||
selected_mode = mode_options_map[mode_input] # Setzen Sie den gewaehlten Modusnamen
|
||||
|
||||
# Wenn der 'exit' Modus gewaehlt wurde
|
||||
if selected_mode == 'exit':
|
||||
logger.info("Modus 'exit' gewaehlt. Skript wird beendet.")
|
||||
print("Abgebrochen durch Benutzer.")
|
||||
return # Beende das Skript
|
||||
|
||||
# Logge den gewaehlten Modus
|
||||
logger.info(f"Betriebsmodus (interaktiv gewaehlt): {selected_mode}")
|
||||
|
||||
else:
|
||||
# Wenn die Eingabe keinem gueltigen Modus entspricht
|
||||
print("Ungueltige Eingabe. Bitte waehlen Sie eine gueltige Option aus der Liste.")
|
||||
|
||||
# Wenn selected_mode immer noch None ist, laeuft die Schleife weiter
|
||||
|
||||
|
||||
except EOFError: # Benutzer hat Ctrl+D gedrueckt (End-of-File)
|
||||
# Fangen Sie das EOFError ab und beenden Sie das Skript sauber.
|
||||
logger.warning("Interaktive Modus-Eingabe abgebrochen (EOFError). Skript wird beendet.")
|
||||
print("\nEingabe abgebrochen.")
|
||||
return # Beende das Skript
|
||||
except Exception as e:
|
||||
# Fangen Sie andere unerwartete Fehler bei der Eingabe ab
|
||||
logger.error(f"Fehler bei interaktiver Modus-Eingabe: {e}")
|
||||
logger.debug(traceback.format_exc())
|
||||
print(f"Ein Fehler ist bei der Modus-Eingabe aufgetreten ({e}). Bitte pruefen Sie die Logdatei.")
|
||||
return # Beende das Skript bei unerwartetem Fehler
|
||||
|
||||
|
||||
# --- Ausfuehrung des gewaehlten Modus ---
|
||||
try:
|
||||
# Holen Sie die CLI-Argumente fuer Start/End/Limit/Steps
|
||||
limit_arg = args.limit
|
||||
start_row_arg = args.start_sheet_row
|
||||
end_row_arg = args.end_sheet_row
|
||||
|
||||
# Sonderbehandlung fuer --steps Argument (relevant fuer reeval und full_run)
|
||||
steps_to_run_set = set() # Initialisiere ein leeres Set
|
||||
# Pruefen Sie, ob das --steps Argument gesetzt ist und nicht "all" (case-insensitive)
|
||||
if args.steps and isinstance(args.steps, str) and args.steps.strip().lower() != 'all':
|
||||
# Teilen Sie den String in Schritte auf und bereinigen Sie Leerzeichen
|
||||
steps_list = [step.strip().lower() for step in args.steps.split(',') if step.strip()]
|
||||
# Filtern Sie nur erlaubte Schritte (die von _process_single_row verstanden werden Block 19)
|
||||
steps_to_run_set = set(step for step in steps_list if step in valid_single_row_steps) # valid_single_row_steps wurde oben definiert
|
||||
|
||||
# Logge eine Warnung, wenn ungueltige Schritte angegeben wurden
|
||||
if len(steps_to_run_set) != len(steps_list):
|
||||
invalid_steps = [step for step in steps_list if step not in valid_single_row_steps]
|
||||
logger.warning(f"Ignoriere ungueltige Schritte im --steps Argument: {invalid_steps}. Fuehre nur {steps_to_run_set} aus.")
|
||||
|
||||
# Wenn nach der Filterung keine gueltigen Schritte uebrig sind
|
||||
if not steps_to_run_set:
|
||||
logger.error("Keine gueltigen Schritte im --steps Argument gefunden. Re-Eval/Full-Run kann nicht gestartet werden.")
|
||||
print("Fehler: Keine gueltigen Schritte fuer den Modus ausgewaehlt. Bitte ueberpruefen Sie das --steps Argument.")
|
||||
return # Skript beenden, wenn keine Schritte ausgewaehlt sind
|
||||
|
||||
# Wenn das --steps Argument 'all' ist oder nicht gesetzt
|
||||
else:
|
||||
# Fuhren Sie standardmaessig alle gueltigen Single-Row Schritte aus.
|
||||
steps_to_run_set = set(valid_single_row_steps) # valid_single_row_steps wurde oben definiert
|
||||
# Logge, welche Schritte ausgewaehlt wurden, wenn es der Standard ist
|
||||
if default_steps_arg: # Wenn es ueberhaupt gueltige Schritte gibt
|
||||
logger.debug(f"--steps Argument 'all' oder nicht gesetzt. Standard Schritte: {steps_to_run_set}.")
|
||||
|
||||
|
||||
# Dispatching basierend auf dem gewaehlten Modus (selected_mode)
|
||||
logger.info(f"Starte Ausfuehrung des Modus: {selected_mode}")
|
||||
|
||||
# ---- KORRIGIERTER if/elif/else BLOCK STARTET HIER ----
|
||||
if selected_mode == "combined_all":
|
||||
# Fuehrt die wichtigsten Batch-Modi nacheinander aus
|
||||
logger.info("--- Start Kombinierter Modus: wiki_verify ---")
|
||||
# Rufe die Methode der DataProcessor Instanz auf (Block 26)
|
||||
data_processor.process_verification_batch(start_sheet_row=start_row_arg, end_sheet_row=end_row_arg, limit=limit_arg)
|
||||
logger.info("--- Start Kombinierter Modus: website_scraping ---")
|
||||
# Rufe die Methode der DataProcessor Instanz auf (Block 27)
|
||||
data_processor.process_website_scraping_batch(start_sheet_row=start_row_arg, end_sheet_row=end_row_arg, limit=limit_arg)
|
||||
logger.info("--- Start Kombinierter Modus: summarize_website ---")
|
||||
# Rufe die Methode der DataProcessor Instanz auf (Block 28)
|
||||
data_processor.process_summarization_batch(start_sheet_row=start_row_arg, end_sheet_row=end_row_arg, limit=limit_arg)
|
||||
logger.info("--- Start Kombinierter Modus: branch_eval ---")
|
||||
# Rufe die Methode der DataProcessor Instanz auf (Block 29)
|
||||
data_processor.process_branch_batch(start_sheet_row=start_row_arg, end_sheet_row=end_row_arg, limit=limit_arg)
|
||||
# TODO: Fuegen Sie hier weitere Batch-Modi hinzu, falls sie im kombinierten Lauf enthalten sein sollen
|
||||
logger.info("--- Kombinierter Modus abgeschlossen ---")
|
||||
|
||||
|
||||
# ---- Batch-VERARBEITUNG (Schritt-Optimiert) ----
|
||||
elif selected_mode == "wiki_verify": # Uebereinstimmend mit process_verification_batch (Block 26)
|
||||
# Rufe die Methode der DataProcessor Instanz auf
|
||||
data_processor.process_verification_batch(start_sheet_row=start_row_arg, end_sheet_row=end_row_arg, limit=limit_arg)
|
||||
|
||||
elif selected_mode == "website_scraping": # Uebereinstimmend mit process_website_scraping_batch (Block 27)
|
||||
# Rufe die Methode der DataProcessor Instanz auf
|
||||
data_processor.process_website_scraping_batch(start_sheet_row=start_row_arg, end_sheet_row=end_row_arg, limit=limit_arg)
|
||||
|
||||
elif selected_mode == "summarize_website": # Uebereinstimmend mit process_summarization_batch (Block 28)
|
||||
# Rufe die Methode der DataProcessor Instanz auf
|
||||
data_processor.process_summarization_batch(start_sheet_row=start_row_arg, end_sheet_row=end_row_arg, limit=limit_arg)
|
||||
|
||||
elif selected_mode == "branch_eval": # Uebereinstimmend mit process_branch_batch (Block 29)
|
||||
# Rufe die Methode der DataProcessor Instanz auf
|
||||
data_processor.process_branch_batch(start_sheet_row=start_row_arg, end_sheet_row=end_row_arg, limit=limit_arg)
|
||||
|
||||
|
||||
# ---- Sequentielle VERARBEITUNG (Zeilenweise) ----
|
||||
elif selected_mode == "full_run": # Nutzt process_rows_sequentially (Block 24)
|
||||
# Full_run verarbeitet sequentiell einen Bereich.
|
||||
# Startzeile wird vom CLI Argument oder automatisch ermittelt (erste leere AO).
|
||||
# Endzeile vom CLI Argument oder bis Ende Sheet.
|
||||
# Limit begrenzt die Anzahl der *verarbeiteten* Zeilen im Bereich.
|
||||
|
||||
calculated_start_sheet_row = start_row_arg # Beginne mit CLI Argument start_sheet_row
|
||||
# Wenn start_sheet_row nicht ueber CLI gesetzt wurde
|
||||
if calculated_start_sheet_row is None:
|
||||
# Automatische Ermittlung der Startzeile (erste Zeile ohne AO)
|
||||
logger.info("Automatische Ermittlung der Startzeile fuer sequenzielle Verarbeitung (erste Zeile ohne AO)...")
|
||||
# get_start_row_index (Block 14) gibt 0-basierten Index in Daten (ohne Header) zurueck.
|
||||
# Prueft auf leeren AO (Block 1 Column Map).
|
||||
start_data_index_no_header = sheet_handler.get_start_row_index(check_column_key="Timestamp letzte Pruefung", min_sheet_row=7)
|
||||
|
||||
# Wenn get_start_row_index -1 zurueckgibt (Fehler)
|
||||
if start_data_index_no_header == -1:
|
||||
logger.error("FEHLER bei automatischer Ermittlung der Startzeile. Kann Full-Run nicht starten.")
|
||||
return # Beende das Skript
|
||||
|
||||
# Berechne die 1-basierte Sheet-Startzeile aus dem 0-basierten Daten-Index
|
||||
calculated_start_sheet_row = start_data_index_no_header + sheet_handler._header_rows + 1 # Block 14 SheetHandler Attribut
|
||||
|
||||
|
||||
# Berechnen Sie die tatsaechliche Anzahl der zu verarbeitenden Zeilen im Bereich.
|
||||
# (basierend auf Endzeile und Limit)
|
||||
total_sheet_rows = len(sheet_handler.get_all_data_with_headers()) # Block 14 SheetHandler
|
||||
calculated_end_sheet_row = end_row_arg if end_row_arg is not None else total_sheet_rows
|
||||
# Stellen Sie sicher, dass die Endzeile nicht vor der Startzeile liegt
|
||||
calculated_end_sheet_row = max(calculated_start_sheet_row - 1, calculated_end_sheet_row)
|
||||
|
||||
|
||||
# Die Anzahl der Zeilen im betrachteten Bereich
|
||||
rows_in_range = max(0, calculated_end_sheet_row - calculated_start_sheet_row + 1)
|
||||
|
||||
# num_to_process ist das Limit, angewendet auf die Zeilen im Bereich.
|
||||
num_to_process_calc = rows_in_range # Standard: alle Zeilen im Bereich
|
||||
# Wenn ein Limit ueber CLI gesetzt wurde und es gueltig ist
|
||||
if limit_arg is not None and isinstance(limit_arg, int) and limit_arg >= 0:
|
||||
num_to_process_calc = min(rows_in_range, limit_arg)
|
||||
|
||||
|
||||
# Wenn es Zeilen zu verarbeiten gibt
|
||||
if num_to_process_calc > 0:
|
||||
logger.info(f"'full_run': Verarbeite {num_to_process_calc} Zeilen im Sheet-Bereich [{calculated_start_sheet_row}, {calculated_end_sheet_row}].")
|
||||
# Rufe die sequentielle Verarbeitungsmethode auf (Block 24)
|
||||
# _process_single_row (Block 19) wird intern aufgerufen.
|
||||
data_processor.process_rows_sequentially(
|
||||
start_sheet_row = calculated_start_sheet_row,
|
||||
num_to_process = num_to_process_calc,
|
||||
# Uebergeben Sie die aus dem --steps Argument ermittelten Flags (steps_to_run_set)
|
||||
process_wiki_steps='wiki' in steps_to_run_set,
|
||||
process_chatgpt_steps='chat' in steps_to_run_set,
|
||||
process_website_steps='web' in steps_to_run_set,
|
||||
process_ml_steps='ml_predict' in steps_to_run_set
|
||||
# TODO: Weitere Schritt-Flags hier uebergeben
|
||||
# force_reeval_in_single_row=False # Normalerweise kein Re-Eval im Full-Run
|
||||
# clear_x_flag=False # Normalerweise kein X loeschen im Full-Run
|
||||
)
|
||||
else:
|
||||
# Wenn keine Zeilen zu verarbeiten sind
|
||||
logger.info(f"Keine Zeilen fuer 'full_run' zu verarbeiten im Bereich [{calculated_start_sheet_row}, {calculated_end_sheet_row}] mit Limit {limit_arg}.")
|
||||
|
||||
|
||||
# ---- Re-EVALUATE Markierte Zeilen ----
|
||||
elif selected_mode == "reeval": # Nutzt process_reevaluation_rows (Block 25)
|
||||
# reeval Modus nutzt immer force_reeval=True in _process_single_row.
|
||||
# Das 'x'-Flag wird von _process_single_row (Block 21) geloescht, wenn clear_flag=True uebergeben wird.
|
||||
# Das Limit wird direkt an process_reevaluation_rows uebergeben und dort gehandhabt.
|
||||
if limit_arg is not None and isinstance(limit_arg, int) and limit_arg <= 0:
|
||||
# Wenn ein Limit von 0 oder weniger angegeben wurde
|
||||
logger.info(f"Limit {limit_arg} angegeben im Re-Eval Modus. Ueberspringe Verarbeitung.")
|
||||
else:
|
||||
# Rufe die Methode der DataProcessor Instanz auf (Block 25)
|
||||
data_processor.process_reevaluation_rows(
|
||||
row_limit=limit_arg, # Uebergibt das Limit (kann None sein)
|
||||
clear_flag=True, # Standardmaessig das 'x'-Flag loeschen
|
||||
# Uebergeben Sie die aus dem --steps Argument ermittelten Schritte (steps_to_run_set)
|
||||
process_wiki_steps='wiki' in steps_to_run_set,
|
||||
process_chatgpt_steps='chat' in steps_to_run_set,
|
||||
process_website_steps='web' in steps_to_run_set,
|
||||
process_ml_steps='ml_predict' in steps_to_run_set
|
||||
# TODO: Weitere Schritt-Flags hier uebergeben
|
||||
)
|
||||
|
||||
|
||||
# ---- Einzelne DIENSTPROGRAMME / SUCHEN ----
|
||||
elif selected_mode == "find_wiki_serp": # Nutzt process_find_wiki_serp (Block 30)
|
||||
# find_wiki_serp sucht leere AY mit Groessenfilter. Nutzt limit, min_employees, min_umsatz.
|
||||
# Start/Endzeile koennen manuell gesetzt werden oder werden automatisch ermittelt (erste leere AY).
|
||||
data_processor.process_find_wiki_serp(
|
||||
start_sheet_row=start_row_arg, # Kann manuell gesetzt werden
|
||||
end_sheet_row=end_row_arg, # Kann manuell gesetzt werden
|
||||
limit=limit_arg, # Kann manuell gesetzt werden
|
||||
min_employees=args.min_employees, # Aus CLI Argument
|
||||
min_umsatz=args.min_umsatz # Aus CLI Argument
|
||||
)
|
||||
|
||||
elif selected_mode == "website_lookup": # Nutzt process_serp_website_lookup (Block 30)
|
||||
# website_lookup sucht leere D. Nutzt limit. Start/Endzeile koennen manuell gesetzt werden.
|
||||
data_processor.process_serp_website_lookup(
|
||||
start_sheet_row=start_row_arg, # Kann manuell gesetzt werden
|
||||
end_sheet_row=end_row_arg, # Kann manuell gesetzt werden
|
||||
limit=limit_arg # Kann manuell gesetzt werden
|
||||
)
|
||||
|
||||
elif selected_mode == "contacts": # Nutzt process_contact_search (Block 30)
|
||||
# contacts sucht leere AM. Nutzt limit. Start/Endzeile koennen manuell gesetzt werden.
|
||||
data_processor.process_contact_search(
|
||||
start_sheet_row=start_row_arg, # Kann manuell gesetzt werden
|
||||
end_sheet_row=end_row_arg, # Kann manuell gesetzt werden
|
||||
limit=limit_arg # Kann manuell gesetzt werden
|
||||
)
|
||||
|
||||
elif selected_mode == "update_wiki_suggestions": # Nutzt process_wiki_updates_from_chatgpt (Block 32)
|
||||
# update_wiki_suggestions prueft Status S. Nutzt limit. Start/Endzeile koennen manuell gesetzt werden.
|
||||
data_processor.process_wiki_updates_from_chatgpt(
|
||||
start_sheet_row=start_row_arg, # Kann manuell gesetzt werden
|
||||
end_sheet_row=end_row_arg, # Kann manuell gesetzt werden
|
||||
limit=limit_arg # Kann manuell gesetzt werden
|
||||
)
|
||||
|
||||
elif selected_mode == "wiki_reextract_missing_an": # Nutzt process_wiki_reextract_missing_an (Block 32)
|
||||
# wiki_reextract_missing_an sucht M gefuellt & AN leer. Nutzt limit. Start/Endzeile koennen manuell gesetzt werden.
|
||||
# Ruft intern _process_single_row mit steps={'wiki'} und force_reeval=True auf.
|
||||
data_processor.process_wiki_reextract_missing_an(
|
||||
start_sheet_row=start_row_arg, # Kann manuell gesetzt werden
|
||||
end_sheet_row=end_row_arg, # Kann manuell gesetzt werden
|
||||
limit=limit_arg # Kann manuell gesetzt werden
|
||||
)
|
||||
|
||||
|
||||
elif selected_mode == "website_details": # EXPERIMENTELL - Nutzt process_website_details (Block 32)
|
||||
# website_details sucht 'x' in A. Nutzt limit. Start/Endzeile koennen manuell gesetzt werden.
|
||||
data_processor.process_website_details(
|
||||
start_sheet_row=start_row_arg, # Kann manuell gesetzt werden
|
||||
end_sheet_row=end_row_arg, # Kann manuell gesetzt werden
|
||||
limit=limit_arg # Kann manuell gesetzt werden
|
||||
)
|
||||
|
||||
|
||||
elif selected_mode == "train_technician_model": # Nutzt train_technician_model (Block 31)
|
||||
# training braucht keine Zeilenlimits im Sinne eines Bereichs oder der Anzahl zu verarbeitender Zeilen im Sheet.
|
||||
# Es nutzt prepare_data_for_modeling (Block 31), die alle relevanten Zeilen filtert.
|
||||
# Die output-Pfade werden aus CLI Argumenten genommen (args).
|
||||
data_processor.train_technician_model(
|
||||
model_out=args.model_out, # Aus CLI Argument
|
||||
imputer_out=args.imputer_out, # Aus CLI Argument
|
||||
patterns_out=args.patterns_out # Aus CLI Argument (JSON Datei)
|
||||
)
|
||||
|
||||
elif selected_mode == "alignment": # Nutzt globale alignment_demo (Block 14)
|
||||
# alignment_demo ist eine globale Funktion, die das sheet Objekt braucht.
|
||||
# Sie braucht keine Zeilenlimits oder Start/Ende.
|
||||
if sheet_handler and sheet_handler.sheet:
|
||||
alignment_demo(sheet_handler.sheet)
|
||||
else:
|
||||
logger.error("Sheet-Handler oder Sheet-Objekt nicht verfuegbar fuer Alignment-Demo.")
|
||||
|
||||
|
||||
# ---- Modus nicht gefunden (sollte durch Validierung oben abgefangen werden) ----
|
||||
else:
|
||||
# Dieser Zweig sollte aufgrund der Validierung am Anfang nie erreicht werden.
|
||||
logger.error(f"Unerwarteter Modus '{selected_mode}' erreichte das Ausfuehrungsende des Dispatchers.")
|
||||
print(f"Interner Fehler: Unbekannter Modus '{selected_mode}'.")
|
||||
|
||||
|
||||
# --- Ausnahmebehandlung fuer den gesamten Ausfuehrungsblock ---
|
||||
except KeyboardInterrupt:
|
||||
# Wenn der Benutzer das Skript manuell unterbricht (Ctrl+C)
|
||||
logger.warning("Skript durch Benutzer unterbrochen (KeyboardInterrupt).")
|
||||
print("\n! Skript wurde manuell beendet.")
|
||||
except Exception as e:
|
||||
# Dieser Block faengt alle unerwarteten Exceptions ab, die in den aufgerufenen
|
||||
# Funktionen/Methoden passieren und nicht intern gefangen und behandelt werden.
|
||||
logger.critical(f"FATAL: Unerwarteter Fehler waehrend der Ausfuehrung von Modus '{selected_mode}': {e}")
|
||||
# exception() loggt den Fehlertyp, die Nachricht und den vollständigen Traceback.
|
||||
logger.exception("Traceback des kritischen Fehlers:")
|
||||
# Gebe eine Fehlermeldung an die Konsole aus, die auf das Log verweist.
|
||||
print(f"\n! Ein kritischer Fehler ist aufgetreten: {type(e).__name__} - {e}")
|
||||
print(f"Bitte pruefen Sie die Logdatei fuer Details: {LOG_FILE}")
|
||||
|
||||
|
||||
# --- Abschluss der Skriptausfuehrung ---
|
||||
end_process_time = time.time() # Ende der Zeitmessung
|
||||
duration = end_process_time - start_process_time # Berechne die Gesamtdauer
|
||||
logger.info(f"Verarbeitung abgeschlossen um {datetime.now().strftime('%H:%M:%S')}.")
|
||||
logger.info(f"Gesamtdauer: {duration:.2f} Sekunden.")
|
||||
logger.info(f"===== Skript beendet =====")
|
||||
|
||||
# Schliesse Logging Handler explizit
|
||||
# Dies stellt sicher, dass alle gepufferten Logmeldungen in die Datei geschrieben werden.
|
||||
logging.shutdown()
|
||||
|
||||
# Logfile Pfad fuer den Nutzer auf der Konsole ausgeben
|
||||
if LOG_FILE:
|
||||
print(f"\nVerarbeitung abgeschlossen. Logfile: {LOG_FILE}")
|
||||
else:
|
||||
print("\nVerarbeitung abgeschlossen. Es konnte keine Logdatei erstellt werden.")
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# 7. ENTRY POINT
|
||||
# ==============================================================================
|
||||
|
||||
# Fuehrt die main-Funktion aus, wenn das Skript direkt gestartet wird.
|
||||
if __name__ == '__main__':
|
||||
# Die main() Funktion enthaltet nun die gesamte Logik und Initialisierung.
|
||||
# Alle globalen imports (Block 1) und globalen Funktionen (Block 2-13) MÜSSEN VOR diesem Block definiert sein.
|
||||
# Alle Klassen (Block 14-33) MÜSSEN VOR diesem Block definiert sein.
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user