diff --git a/brancheneinstufung.py b/brancheneinstufung.py index ffa80af1..d75e3704 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -85,12 +85,12 @@ except ImportError: print("tiktoken nicht gefunden. Token-Zaehlung wird geschaetzt.") # Debugging-Ausgabe -# ============================================================================== -# 2. GLOBALE KONSTANTEN UND KONFIGURATION -# (Logisch 'config.py') -# ============================================================================== + # ============================================================================== + # 2. GLOBALE KONSTANTEN UND KONFIGURATION + # (Logisch 'config.py') + # ============================================================================== -# --- Dateipfade --- + # --- Dateipfade --- CREDENTIALS_FILE = "service_account.json" API_KEY_FILE = "api_key.txt" # OpenAI SERP_API_KEY_FILE = "serpApiKey.txt" @@ -183,9 +183,9 @@ class Config: return None -# --- Globale Spalten-Mapping (WICHTIG: MUSS ZU IHREM SHEET PASSEN!) --- -# --- Globale Spalten-Mapping (WICHTIG: MUSS ZU IHREM SHEET PASSEN!) --- -# Version 1.7.4 - 57 Spalten (A-BE) + # --- Globale Spalten-Mapping (WICHTIG: MUSS ZU IHREM SHEET PASSEN!) --- + # --- Globale Spalten-Mapping (WICHTIG: MUSS ZU IHREM SHEET PASSEN!) --- + # Version 1.7.4 - 57 Spalten (A-BE) COLUMN_MAP = { # ReEval Flag & CRM-Daten (A-M) "ReEval Flag": 0, # A @@ -260,7 +260,7 @@ COLUMN_MAP = { "Tokens": 56, # BE (vorher BD) } -# --- Globale Variablen fuer Branch Mapping (werden von load_target_schema() befuellt) --- + # --- Globale Variablen fuer Branch Mapping (werden von load_target_schema() befuellt) --- BRANCH_MAPPING = {} TARGET_SCHEMA_STRING = "Ziel-Branchenschema nicht verfuegbar." ALLOWED_TARGET_BRANCHES = [] @@ -268,10 +268,10 @@ FOCUS_TARGET_BRANCHES = [] # NEU TARGET_SCHEMA_STRING = "Ziel-Branchenschema nicht verfuegbar." FOCUS_BRANCHES_PROMPT_PART = "" # NEU: Für den Prompt-Teil der Fokusbranchen -# Marker für URLs, die erneut per SERP gesucht werden sollen + # Marker für URLs, die erneut per SERP gesucht werden sollen URL_CHECK_MARKER = "URL_CHECK_NEEDED" # <<< NEU HINZUFÜGEN -# Liste gängiger User-Agents für Rotation + # Liste gängiger User-Agents für Rotation USER_AGENTS = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36', @@ -286,22 +286,22 @@ USER_AGENTS = [ 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:108.0) Gecko/20100101 Firefox/108.0', ] -# ============================================================================== -# Ende Basis-Setup Block -# ============================================================================== + # ============================================================================== + # Ende Basis-Setup Block + # ============================================================================== -# ============================================================================== -# GLOBALE HELPER FUNCTIONS (PART 1: Retry Decorator) -# ============================================================================== + # ============================================================================== + # GLOBALE HELPER FUNCTIONS (PART 1: Retry Decorator) + # ============================================================================== -# Imports für den Retry Decorator (um NameErrors zu vermeiden) + # Imports für den Retry Decorator (um NameErrors zu vermeiden) from openai.error import AuthenticationError, OpenAIError, RateLimitError, APIError, Timeout, InvalidRequestError, ServiceUnavailableError # Beispielhafte spezifische Fehler -# Logger fuer den Retry Decorator selbst (Nutzt den globalen Root Logger) + # Logger fuer den Retry Decorator selbst (Nutzt den globalen Root Logger) decorator_logger = logging.getLogger(__name__ + ".Retry") -# --- Retry Decorator --- -# KORRIGIERTE Version (Behandelt SpreadsheetNotFound und 404/400/401/403 HTTPError explizit) + # --- Retry Decorator --- + # KORRIGIERTE Version (Behandelt SpreadsheetNotFound und 404/400/401/403 HTTPError explizit) def retry_on_failure(func): """ Decorator, der eine Funktion bei bestimmten Fehlern mehrmals wiederholt. @@ -408,15 +408,15 @@ def retry_on_failure(func): return wrapper # Gibt die Wrapper-Funktion zurück -# ============================================================================== -# Ende Retry Decorator Block -# ============================================================================== + # ============================================================================== + # Ende Retry Decorator Block + # ============================================================================== -# ============================================================================== -# GLOBALE HELPER FUNCTIONS (PART 2: Logging & Token Count) -# ============================================================================== + # ============================================================================== + # GLOBALE HELPER FUNCTIONS (PART 2: Logging & Token Count) + # ============================================================================== -# --- Token Count Funktion --- + # --- Token Count Funktion --- def token_count(text, model=None): """Zaehlt Tokens via tiktoken oder schaetzt ueber Leerzeichen.""" logger = logging.getLogger(__name__) # Logger-Instanz holen @@ -436,7 +436,7 @@ def token_count(text, model=None): return len(str(text).split()) -# --- Logging Helpers --- + # --- Logging Helpers --- LOG_FILE = None # Initialisierung def create_log_filename(mode): @@ -461,17 +461,17 @@ def create_log_filename(mode): logger.error(f"FEHLER: Konnte Logdateinamen auch im Fallback-Verzeichnis '{log_dir_path}' nicht erstellen: {e_fallback}") return None -# ============================================================================== -# Ende Grundlegende Helfer Block -# ============================================================================== + # ============================================================================== + # Ende Grundlegende Helfer Block + # ============================================================================== -# ============================================================================== -# GLOBALE HELPER FUNCTIONS (PART 2: Text, String & URL Utilities) -# ============================================================================== + # ============================================================================== + # GLOBALE HELPER FUNCTIONS (PART 2: Text, String & URL Utilities) + # ============================================================================== -# --- Text Normalisierung & Reinigung --- -# Basierend auf Code aus Teil 3. -# Nutzt globale Helfer: re, unicodedata. + # --- Text Normalisierung & Reinigung --- + # Basierend auf Code aus Teil 3. + # Nutzt globale Helfer: re, unicodedata. def simple_normalize_url(url): """Normalisiert URL zu domain.tld oder k.A. (ohne www, ohne Pfad).""" # Verwenden Sie logger, da das Logging jetzt konfiguriert ist @@ -679,18 +679,18 @@ def fuzzy_similarity(str1, str2): # Sicherstellen, dass beide Inputs Strings sind return SequenceMatcher(None, str(str1).lower(), str(str2).lower()).ratio() -# ============================================================================== -# Ende Text, String & URL Utilities Block -# ============================================================================== + # ============================================================================== + # Ende Text, String & URL Utilities Block + # ============================================================================== -# ============================================================================== -# GLOBALE HELPER FUNCTIONS (PART 3: Numeric Extraction Utilities) -# ============================================================================== + # ============================================================================== + # GLOBALE HELPER FUNCTIONS (PART 3: Numeric Extraction Utilities) + # ============================================================================== -# --- Numerische Extraktion --- -# Basierend auf Code aus Teil 4 & Teil 2. -# Extrahiert und normalisiert Zahlenwerte aus Strings. -# Nutzt globale Helfer: clean_text, re. + # --- Numerische Extraktion --- + # Basierend auf Code aus Teil 4 & Teil 2. + # Extrahiert und normalisiert Zahlenwerte aus Strings. + # Nutzt globale Helfer: clean_text, re. def extract_numeric_value(raw_value, is_umsatz=False): """ Extrahiert und normalisiert Zahlenwerte (Umsatz in Mio, Mitarbeiter). @@ -780,10 +780,10 @@ def extract_numeric_value(raw_value, is_umsatz=False): return str(int(mitarbeiter_int)) if mitarbeiter_int > 0 else "k.A." # Nur positive Ergebnisse > 0 -# --- Numerische Extraktion fuer FILTERLOGIK (gibt 0 statt k.A. zurueck) --- -# Basierend auf Code aus Teil 2. -# Extrahiert und normalisiert Zahlenwerte fuer Vergleichslogik. -# Nutzt globale Helfer: clean_text, re. + # --- Numerische Extraktion fuer FILTERLOGIK (gibt 0 statt k.A. zurueck) --- + # Basierend auf Code aus Teil 2. + # Extrahiert und normalisiert Zahlenwerte fuer Vergleichslogik. + # Nutzt globale Helfer: clean_text, re. def get_numeric_filter_value(value_str, is_umsatz=False): """ Extrahiert und normalisiert Zahlenwerte fuer die Filterlogik (Umsatz in Mio, Mitarbeiter int). @@ -875,21 +875,21 @@ def get_numeric_filter_value(value_str, is_umsatz=False): return 0.0 if is_umsatz else 0 -# ============================================================================== -# Ende Numerische Extraktion Utilities Block -# ============================================================================== + # ============================================================================== + # Ende Numerische Extraktion Utilities Block + # ============================================================================== -# ============================================================================== -# GLOBALE HELPER FUNCTIONS (PART 4: Gender & Email Utilities) -# ============================================================================== + # ============================================================================== + # GLOBALE HELPER FUNCTIONS (PART 4: Gender & Email Utilities) + # ============================================================================== -# --- Gender und Email Helpers --- -# Basierend auf Code aus Teil 4. -# Nutzt globale Helfer: gender_guesser, Config.API_KEYS, requests, retry_on_failure, -# simple_normalize_url, normalize_string, re. + # --- Gender und Email Helpers --- + # Basierend auf Code aus Teil 4. + # Nutzt globale Helfer: gender_guesser, Config.API_KEYS, requests, retry_on_failure, + # simple_normalize_url, normalize_string, re. -# Annahme: gender_guesser ist installiert -# Initialisieren Sie den Detector einmal global, um Ressourcen zu sparen + # Annahme: gender_guesser ist installiert + # Initialisieren Sie den Detector einmal global, um Ressourcen zu sparen try: import gender_guesser.detector as gender gender_detector = gender.Detector() @@ -1029,21 +1029,21 @@ def get_email_address(firstname, lastname, website): return "" # Gebe leeren String zurueck -# ============================================================================== -# Ende Gender & Email Utilities Block -# ============================================================================== + # ============================================================================== + # Ende Gender & Email Utilities Block + # ============================================================================== -# ============================================================================== -# GLOBALE HELPER FUNCTIONS (PART 5: Schema Loading Utility) -# ============================================================================== + # ============================================================================== + # GLOBALE HELPER FUNCTIONS (PART 5: Schema Loading Utility) + # ============================================================================== -# --- Schema Loading (Ziel-Branchenschema) --- -# Basierend auf Code aus Teil 4. -# Lädt die Liste der erlaubten Zielbranchen aus einer CSV-Datei. -# Nutzt globale Variablen: BRANCH_MAPPING, TARGET_SCHEMA_STRING, ALLOWED_TARGET_BRANCHES. -# Nutzt globale Helfer: csv, os, logger. + # --- Schema Loading (Ziel-Branchenschema) --- + # Basierend auf Code aus Teil 4. + # Lädt die Liste der erlaubten Zielbranchen aus einer CSV-Datei. + # Nutzt globale Variablen: BRANCH_MAPPING, TARGET_SCHEMA_STRING, ALLOWED_TARGET_BRANCHES. + # Nutzt globale Helfer: csv, os, logger. -# Globale Variablen für Branch Mapping (werden von load_target_schema() befüllt) + # Globale Variablen für Branch Mapping (werden von load_target_schema() befüllt) BRANCH_MAPPING = {} # Wird in dieser Version nicht primaer fuer Mapping genutzt, kann aber beibehalten werden TARGET_SCHEMA_STRING = "Ziel-Branchenschema nicht verfuegbar." # String-Repraesentation des Schemas fuer Prompts ALLOWED_TARGET_BRANCHES = [] # Liste der erlaubten Kurzformen @@ -1128,22 +1128,22 @@ def load_target_schema(csv_filepath=BRANCH_MAPPING_FILE): logger.warning("Keine gueltigen Zielbranchen im Schema gefunden. Branchenbewertung ist nicht moeglich.") -# map_external_branch ist in dieser Version nicht mehr notwendig, -# da die Branchenevaluation ueber ChatGPT (evaluate_branche_chatgpt) -# direkt gegen ALLOWED_TARGET_BRANCHES validiert. + # map_external_branch ist in dieser Version nicht mehr notwendig, + # da die Branchenevaluation ueber ChatGPT (evaluate_branche_chatgpt) + # direkt gegen ALLOWED_TARGET_BRANCHES validiert. -# ============================================================================== -# Ende Schema Loading Utility Block -# ============================================================================== + # ============================================================================== + # Ende Schema Loading Utility Block + # ============================================================================== -# ============================================================================== -# GLOBALE HELPER FUNCTIONS (PART 6: OpenAI API Call Wrapper) -# ============================================================================== + # ============================================================================== + # GLOBALE HELPER FUNCTIONS (PART 6: OpenAI API Call Wrapper) + # ============================================================================== -# --- OpenAI / CHATGPT FUNCTIONS --- -# Zentrale Funktion fuer OpenAI Chat API Aufrufe. -# Nutzt globale Helfer: Config.API_KEYS, openai, retry_on_failure, token_count (optional), logger, ValueError. + # --- OpenAI / CHATGPT FUNCTIONS --- + # Zentrale Funktion fuer OpenAI Chat API Aufrufe. + # Nutzt globale Helfer: Config.API_KEYS, openai, retry_on_failure, token_count (optional), logger, ValueError. @retry_on_failure # Wende den Decorator auf diesen API Call an def call_openai_chat(prompt, temperature=0.3, model=None): """ @@ -1244,17 +1244,17 @@ def call_openai_chat(prompt, temperature=0.3, model=None): raise e # Leiten Sie die Exception weiter -# ============================================================================== -# Ende OpenAI API Call Wrapper Block -# ============================================================================== + # ============================================================================== + # Ende OpenAI API Call Wrapper Block + # ============================================================================== -# ============================================================================== -# GLOBALE HELPER FUNCTIONS (PART 7: OpenAI Summary Helpers) -# ============================================================================== + # ============================================================================== + # GLOBALE HELPER FUNCTIONS (PART 7: OpenAI Summary Helpers) + # ============================================================================== -# --- OpenAI Summary Helpers --- -# Funktionen zur Zusammenfassung von Website-Inhalten mittels OpenAI. -# Nutzt globale Helfer: call_openai_chat, logger, token_count (optional), retry_on_failure. + # --- OpenAI Summary Helpers --- + # Funktionen zur Zusammenfassung von Website-Inhalten mittels OpenAI. + # Nutzt globale Helfer: call_openai_chat, logger, token_count (optional), retry_on_failure. def summarize_website_content(raw_text): """ @@ -1314,10 +1314,10 @@ def summarize_website_content(raw_text): return f"k.A. (Fehler Zusammenfassung: {str(e)[:50]}...)" # Signalisiert Fehler -# --- Batch-Zusammenfassungsfunktion --- -# Fasst mehrere Texte in einem einzigen OpenAI API Call zusammen. -# Basierend auf summarize_batch_openai aus Teil 7/9. -# Nutzt globale Helfer: call_openai_chat, logger, token_count (optional), retry_on_failure. + # --- Batch-Zusammenfassungsfunktion --- + # Fasst mehrere Texte in einem einzigen OpenAI API Call zusammen. + # Basierend auf summarize_batch_openai aus Teil 7/9. + # Nutzt globale Helfer: call_openai_chat, logger, token_count (optional), retry_on_failure. @retry_on_failure # Wende den Decorator auf den gesamten Batch-API Call an def summarize_batch_openai(tasks_data): """ @@ -1457,18 +1457,18 @@ def summarize_batch_openai(tasks_data): return summaries # Rueckgabe des Dictionarys mit Ergebnissen oder Fehlern -# ============================================================================== -# Ende OpenAI Summary Helpers Block -# ============================================================================== + # ============================================================================== + # Ende OpenAI Summary Helpers Block + # ============================================================================== -# ============================================================================== -# GLOBALE HELPER FUNCTIONS (PART 8: OpenAI Branch Helper) -# ============================================================================== + # ============================================================================== + # GLOBALE HELPER FUNCTIONS (PART 8: OpenAI Branch Helper) + # ============================================================================== -# --- OpenAI Branch Helper --- -# Funktion zur Branchenbewertung mittels OpenAI. -# Nutzt globale Helfer: ALLOWED_TARGET_BRANCHES, TARGET_SCHEMA_STRING, -# call_openai_chat, logger, re, retry_on_failure. + # --- OpenAI Branch Helper --- + # Funktion zur Branchenbewertung mittels OpenAI. + # Nutzt globale Helfer: ALLOWED_TARGET_BRANCHES, TARGET_SCHEMA_STRING, + # call_openai_chat, logger, re, retry_on_failure. @retry_on_failure def evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kategorien, website_summary): logger = logging.getLogger(__name__) @@ -1673,23 +1673,23 @@ def evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kateg return result -# ============================================================================== -# Ende OpenAI Branch Helper Block -# ============================================================================== + # ============================================================================== + # Ende OpenAI Branch Helper Block + # ============================================================================== -# ============================================================================== -# GLOBALE HELPER FUNCTIONS (PART 9: SerpAPI Search Helpers) -# ============================================================================== + # ============================================================================== + # GLOBALE HELPER FUNCTIONS (PART 9: SerpAPI Search Helpers) + # ============================================================================== -# --- SERP API / LINKEDIN FUNCTIONS --- -# Funktionen zur Suche ueber SerpAPI (Google Search). -# Nutzt globale Helfer: Config.API_KEYS, requests, retry_on_failure, -# simple_normalize_url, normalize_company_name, unquote, logger, re. + # --- SERP API / LINKEDIN FUNCTIONS --- + # Funktionen zur Suche ueber SerpAPI (Google Search). + # Nutzt globale Helfer: Config.API_KEYS, requests, retry_on_failure, + # simple_normalize_url, normalize_company_name, unquote, logger, re. -# serp_wikipedia_lookup ist bereits in Teil 1/18 enthalten (oder sollte es sein, da es direkt nach retry_on_failure kam). -# Es ist hier im globalen Sektion 3 Block nun korrekt platziert. + # serp_wikipedia_lookup ist bereits in Teil 1/18 enthalten (oder sollte es sein, da es direkt nach retry_on_failure kam). + # Es ist hier im globalen Sektion 3 Block nun korrekt platziert. @retry_on_failure # Wende den Decorator an def serp_wikipedia_lookup(company_name, website=None, min_score=0.4): """ @@ -2157,17 +2157,17 @@ def search_linkedin_contacts(company_name, website, position_query, crm_kurzform return [] # Signalisiert Fehler bei der Suche -# ============================================================================== -# Ende SerpAPI Search Helpers Block -# ============================================================================== + # ============================================================================== + # Ende SerpAPI Search Helpers Block + # ============================================================================== -# ============================================================================== -# GLOBALE HELPER FUNCTIONS (PART 10: Website Raw Scraping Function) -# ============================================================================== + # ============================================================================== + # GLOBALE HELPER FUNCTIONS (PART 10: Website Raw Scraping Function) + # ============================================================================== -# --- Globale Funktion zum Scrapen des Website Rohtextes --- -# Basierend auf get_website_raw aus Teil 7. Global platziert. -# Nutzt globale Helfer: simple_normalize_url, clean_text, re, requests, BeautifulSoup, Config, getattr, logger, retry_on_failure, USER_AGENTS, URL_CHECK_MARKER. + # --- Globale Funktion zum Scrapen des Website Rohtextes --- + # Basierend auf get_website_raw aus Teil 7. Global platziert. + # Nutzt globale Helfer: simple_normalize_url, clean_text, re, requests, BeautifulSoup, Config, getattr, logger, retry_on_failure, USER_AGENTS, URL_CHECK_MARKER. @retry_on_failure # Wende den Decorator auf diese Funktion an def get_website_raw(url, max_length=20000, verify_cert=False): # verify_cert Default ist jetzt False """ @@ -2312,20 +2312,20 @@ def get_website_raw(url, max_length=20000, verify_cert=False): # verify_cert Def logger.debug(traceback.format_exc()) return f"k.A. (Fehler Parsing: {str(e_parse)[:50]}...)" -# ============================================================================== -# Ende Website Raw Scraping Funktion Block -# ============================================================================== + # ============================================================================== + # Ende Website Raw Scraping Funktion Block + # ============================================================================== -# ============================================================================== -# GLOBALE HELPER FUNCTIONS (PART 11: Website Details Scraping Function) -# ============================================================================== + # ============================================================================== + # GLOBALE HELPER FUNCTIONS (PART 11: Website Details Scraping Function) + # ============================================================================== -# --- Experimentelle Website Details Scraping Funktion --- -# Basierend auf scrape_website_details aus Teil 10. Global platziert. -# Nutzt globale Helfer: retry_on_failure, requests, BeautifulSoup, Config, getattr, clean_text, logger. -# Diese Funktion ist als experimentelles Dienstprogramm gedacht. -# Ihre Implementierung hängt stark von der Struktur der Zielwebsites ab. -# Derzeit extrahiert sie nur grundlegende Meta-Informationen. + # --- Experimentelle Website Details Scraping Funktion --- + # Basierend auf scrape_website_details aus Teil 10. Global platziert. + # Nutzt globale Helfer: retry_on_failure, requests, BeautifulSoup, Config, getattr, clean_text, logger. + # Diese Funktion ist als experimentelles Dienstprogramm gedacht. + # Ihre Implementierung hängt stark von der Struktur der Zielwebsites ab. + # Derzeit extrahiert sie nur grundlegende Meta-Informationen. def scrape_website_details(url): """ EXPERIMENTELL: Scrapt eine Website und extrahiert spezifische Details. @@ -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): """ @@ -3796,9 +3982,9 @@ class DataProcessor: # return '' # Fallback Rueckgabe -# ============================================================================== -# Ende DataProcessor Klasse Start & Init & Basis Status-Checker Block -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse Start & Init & Basis Status-Checker Block + # ============================================================================== # --- Interne Hilfsmethoden zur Pruefung, ob ein Schritt ausgefuehrt werden soll --- @@ -4012,9 +4198,9 @@ class DataProcessor: return False -# ============================================================================== -# Ende DataProcessor Klasse Status-Checker Helpers Block -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse Status-Checker Helpers Block + # ============================================================================== # --- Methode: Verarbeitung einer einzelnen Zeile --- # Diese Methode ist das Herzstueck der Zeilen-basierten Verarbeitung. @@ -4886,9 +5072,9 @@ class DataProcessor: self.logger.info(f"Sequentielle Verarbeitung abgeschlossen. {processed_count} Zeilen im Bereich [{start_sheet_row}, {end_index_in_all_data}] bearbeitet.") # <<< GEÄNDERT -# ============================================================================== -# Ende DataProcessor Klasse Prozess: Sequenziell Block -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse Prozess: Sequenziell Block + # ============================================================================== # ========================================================================== # === Prozess Methoden (Re-Evaluation) ===================================== @@ -5045,9 +5231,9 @@ class DataProcessor: self.logger.info(f"Re-Evaluierung abgeschlossen. {processed_count} Zeilen verarbeitet (Gefunden: {found_count}, Limit: {row_limit}).") # <<< GEÄNDERT -# ============================================================================== -# Ende DataProcessor Klasse Prozess: Re-Evaluation Block -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse Prozess: Re-Evaluation Block + # ============================================================================== # ========================================================================== # === Batch Processing Methods ============================================= @@ -5601,9 +5787,9 @@ class DataProcessor: # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. -# ============================================================================== -# Ende DataProcessor Klasse Batch: Wiki Verification Block -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse Batch: Wiki Verification Block + # ============================================================================== # ========================================================================== # === Batch Processing Methods ============================================= @@ -6011,9 +6197,9 @@ class DataProcessor: # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. -# ============================================================================== -# Ende DataProcessor Klasse Batch: Website Scraping Block -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse Batch: Website Scraping Block + # ============================================================================== # ========================================================================== # === Batch Processing Methods ============================================= @@ -6351,9 +6537,9 @@ class DataProcessor: self.logger.info(f"Website-Zusammenfassung (Batch) abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. -# ============================================================================== -# Ende DataProcessor Klasse Batch: Summarization Block -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse Batch: Summarization Block + # ============================================================================== # ========================================================================== # === Batch Processing Methods ============================================= @@ -6611,9 +6797,9 @@ class DataProcessor: self.logger.info(f"Brancheneinschaetzung (Parallel Batch) abgeschlossen. {processed_tasks_count} Zeilen verarbeitet, {skipped_count} Zeilen uebersprungen.") -# ============================================================================== -# Ende DataProcessor Klasse Batch: Branch Evaluation Block -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse Batch: Branch Evaluation Block + # ============================================================================== # ========================================================================== # === Batch Processing Methods ============================================= @@ -7351,9 +7537,9 @@ class DataProcessor: self.logger.info(f"Modus 'contact_search' abgeschlossen. {processed_count} Zeilen verarbeitet (in Batch aufgenommen), {skipped_count} Zeilen uebersprungen.") # <<< GEÄNDERT # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. -# ============================================================================== -# Ende DataProcessor Klasse Batch: SerpAPI Suchen & Contacts Block -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse Batch: SerpAPI Suchen & Contacts Block + # ============================================================================== # ========================================================================== @@ -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) ============================ # ========================================================================== @@ -8381,9 +8671,9 @@ class DataProcessor: self.logger.info("Modelltraining und -evaluation abgeschlossen.") # <<< GEÄNDERT -# ============================================================================== -# Ende DataProcessor Klasse Utility: ML Prep & Training Block -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse Utility: ML Prep & Training Block + # ============================================================================== # ========================================================================== # === Utility Methods (Other Specific Tasks) =============================== @@ -9146,30 +9436,30 @@ class DataProcessor: # Keine Pause nach diesem Modus noetig, da die naechste Aktion im Dispatcher (Block 34) folgt. -# ============================================================================== -# Ende DataProcessor Klasse Utility: Other Specific Tasks Block -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse Utility: Other Specific Tasks Block + # ============================================================================== -# ============================================================================== -# Ende DataProcessor Klasse -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse + # ============================================================================== # --- 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. + # ============================================================================== + # Hauptausfuehrungsblock & Globale Funktionen nach Klassen + # ============================================================================== + # Der naechste Block (Block 34) enthaelt die main Funktion und den Entry Point. -# ============================================================================== -# 6. MAIN FUNCTION (HAUPTEINSTIEGSPUNKT & UI DISPATCHER) -# ============================================================================== + # ============================================================================== + # 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 + # Der globale Root Logger wird in main() konfiguriert + # logger = logging.getLogger(__name__) # Diesen Logger gibt es schon, keine Neudefinition hier def main(): """ @@ -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: @@ -9829,11 +10130,11 @@ def main(): print("\nVerarbeitung abgeschlossen. Es konnte keine Logdatei erstellt werden.") -# ============================================================================== -# 7. ENTRY POINT -# ============================================================================== + # ============================================================================== + # 7. ENTRY POINT + # ============================================================================== -# Fuehrt die main-Funktion aus, wenn das Skript direkt gestartet wird. + # 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 und Funktionen MÜSSEN VOR diesem Block definiert sein. @@ -9842,616 +10143,10 @@ if __name__ == '__main__': main() -# ============================================================================== -# Ende DataProcessor Klasse -# ============================================================================== + # ============================================================================== + # Ende DataProcessor Klasse + # ============================================================================== # --- 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() \ No newline at end of file