From f6c2cc5e148ec3e965530ad03a74910d2fa299b9 Mon Sep 17 00:00:00 2001 From: Floke Date: Tue, 22 Apr 2025 12:21:33 +0000 Subject: [PATCH] bugfix --- brancheneinstufung.py | 505 +++++++++++++++++++++--------------------- 1 file changed, 247 insertions(+), 258 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index 28b5bb43..0e2bdbe2 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -1782,7 +1782,8 @@ class GoogleSheetHandler: class WikipediaScraper: """ Handles searching Wikipedia articles and extracting relevant company data. - Version: 1.6.5 logic - Improved infobox parsing, disambiguation handling, and standard logging. + Version: 1.6.6 logic - Improved infobox parsing, disambiguation handling, + dynamic article validation, and standard logging. """ def __init__(self, user_agent=None): """ @@ -1811,6 +1812,7 @@ class WikipediaScraper: def _get_full_domain(self, website): """Extrahiert die normalisierte Domain (ohne www, ohne Pfad) aus einer URL.""" + # ... (Implementierung bleibt wie zuvor) ... if not website or not isinstance(website, str): return "" website_lower = website.lower().strip() if not website_lower or website_lower == 'k.a.': return "" @@ -1822,22 +1824,21 @@ class WikipediaScraper: def _generate_search_terms(self, company_name, website): """Generiert eine Liste von Suchbegriffen für die Wikipedia-Suche.""" + # ... (Implementierung bleibt wie zuvor) ... if not company_name: return [] terms = set() full_domain = self._get_full_domain(website) if full_domain: terms.add(full_domain) - - normalized_name = normalize_company_name(company_name) # Annahme: existiert + # Annahme: normalize_company_name existiert + normalized_name = normalize_company_name(company_name) if normalized_name: name_parts = normalized_name.split() if len(name_parts) > 0: terms.add(name_parts[0]) if len(name_parts) > 1: terms.add(" ".join(name_parts[:2])) terms.add(normalized_name) - company_name_lower = company_name.lower() if company_name_lower != normalized_name and company_name_lower not in terms: terms.add(company_name_lower) - final_terms = [term for term in list(terms) if term][:5] self.logger.debug(f"Generierte Suchbegriffe für '{company_name}': {final_terms}") return final_terms @@ -1845,6 +1846,7 @@ class WikipediaScraper: @retry_on_failure # Annahme: Decorator existiert def _get_page_soup(self, url): """Holt HTML von einer URL und gibt ein BeautifulSoup-Objekt zurück.""" + # ... (Implementierung bleibt wie zuvor) ... if not url or not isinstance(url, str) or not url.startswith("http"): self.logger.warning(f"_get_page_soup: Ungültige URL '{url}'.") return None @@ -1853,6 +1855,7 @@ class WikipediaScraper: response = self.session.get(url, timeout=20) response.raise_for_status() response.encoding = 'utf-8' + # Annahme: Config ist verfügbar soup = BeautifulSoup(response.text, getattr(Config, 'HTML_PARSER', 'html.parser')) self.logger.debug(f"_get_page_soup: Parsen von {url} erfolgreich.") return soup @@ -1866,27 +1869,47 @@ class WikipediaScraper: self.logger.error(f"_get_page_soup: Fehler beim Parsen von HTML von {url}: {e}") raise e + # --- ÜBERARBEITETE VALIDIERUNGSMETHODE --- def _validate_article(self, page, company_name, website): """ Validiert, ob ein Wikipedia-Artikel zum Unternehmen passt. - Prüft Titelähnlichkeit und ob die Firmenwebsite verlinkt ist. + Prüft Titelähnlichkeit (gewichtet Anfangsworte höher), Domain-Match + und passt Schwellenwerte dynamisch an. """ if not page or not company_name: return False self.logger.debug(f"Validiere Artikel '{page.title}' für Firma '{company_name}' (Website: {website})...") full_domain = self._get_full_domain(website) + # Annahme: normalize_company_name existiert normalized_company = normalize_company_name(company_name) normalized_title = normalize_company_name(page.title) + if not normalized_company or not normalized_title: + self.logger.warning("Validierung nicht möglich, da Normalisierung eines Namens fehlschlug.") + return False - # 1. Titelähnlichkeit + # 1. Titelähnlichkeit (Gesamt) similarity = SequenceMatcher(None, normalized_title, normalized_company).ratio() - self.logger.debug(f" -> Titelähnlichkeit: {similarity:.2f} ('{normalized_title}' vs '{normalized_company}')") + self.logger.debug(f" -> Gesamt-Ähnlichkeit: {similarity:.2f} ('{normalized_title}' vs '{normalized_company}')") - # 2. Link-Prüfung + # 2. Ähnlichkeit der ersten Worte + company_tokens = normalized_company.split() + title_tokens = normalized_title.split() + first_word_match = False + first_two_words_match = False + if len(company_tokens) > 0 and len(title_tokens) > 0: + if company_tokens[0] == title_tokens[0]: + first_word_match = True + self.logger.debug(" -> Erstes Wort stimmt überein.") + if len(company_tokens) > 1 and len(title_tokens) > 1: + if company_tokens[1] == title_tokens[1]: + first_two_words_match = True + self.logger.debug(" -> Erste zwei Worte stimmen überein.") + + # 3. Link-Prüfung (Domain-Match) domain_found = False if full_domain: self.logger.debug(f" -> Suche nach Domain '{full_domain}' in Links von {page.url}...") - soup = self._get_page_soup(page.url) + soup = self._get_page_soup(page.url) # Erneuter Abruf für Link-Check if soup: infobox = soup.select_one('table[class*="infobox"]') if infobox: @@ -1895,15 +1918,15 @@ class WikipediaScraper: href = link.get('href', '') if href.startswith('http') and full_domain in self._get_full_domain(href): link_text = link.get_text(strip=True).lower() - th = link.find_previous('th') + th = link.find_previous(['th', 'td']) # Prüfe vorheriges TH oder TD th_text = th.get_text(strip=True).lower() if th else "" if any(kw in link_text for kw in ['website', 'webseite', 'offizielle']) or \ any(kw in th_text for kw in ['website', 'webseite', 'webauftritt']): - self.logger.debug(f" -> Domain '{full_domain}' in Infobox-Link gefunden (TH: '{th_text}', Text: '{link_text}', URL: {href})") + self.logger.debug(f" -> Domain '{full_domain}' in Infobox-Link gefunden (Header/Text: '{th_text}/{link_text}', URL: {href})") domain_found = True break else: - self.logger.debug(f" -> Domain '{full_domain}' in Infobox-Link gefunden (URL: {href}, kein Keyword-Match im Text/TH)") + self.logger.debug(f" -> Domain '{full_domain}' in Infobox-Link gefunden (URL: {href}, kein Keyword-Match im Text/Header)") domain_found = True break if not domain_found: @@ -1919,22 +1942,50 @@ class WikipediaScraper: break else: self.logger.warning(f" -> Konnte HTML für Link-Prüfung von {page.url} nicht laden.") + + if domain_found: + self.logger.debug(f" -> Domain-Check Ergebnis: Gefunden.") + else: + self.logger.debug(f" -> Domain-Check Ergebnis: NICHT gefunden.") else: self.logger.debug(" -> Keine Website-Domain für Link-Prüfung vorhanden.") - # 3. Entscheidung - threshold = getattr(Config, 'SIMILARITY_THRESHOLD', 0.65) - if domain_found: - threshold = max(0.35, threshold - 0.3) - self.logger.debug(f" -> Domain gefunden, Ähnlichkeitsschwelle angepasst auf {threshold:.2f}") - is_valid = similarity >= threshold + # 4. Dynamische Schwellenwert-Entscheidung + # Annahme: Config ist verfügbar + standard_threshold = getattr(Config, 'SIMILARITY_THRESHOLD', 0.65) + is_valid = False + reason = "Keine Validierungsregel traf zu" # Default Grund + + # Regeln der Reihe nach prüfen + if similarity >= standard_threshold: + is_valid = True + reason = f"Gesamt-Ähnlichkeit >= {standard_threshold:.2f}" + elif domain_found and first_two_words_match and similarity >= 0.30: # Stärkste Kombination + is_valid = True + reason = f"Domain gefunden UND erste 2 Worte stimmen überein UND Ähnlichkeit >= 0.30" + elif domain_found and first_word_match and similarity >= 0.35: # Zweitstärkste + is_valid = True + reason = f"Domain gefunden UND erstes Wort stimmt überein UND Ähnlichkeit >= 0.35" + elif first_two_words_match and similarity >= 0.40: # Wenn nur erste zwei Worte passen + is_valid = True + reason = f"Erste zwei Worte stimmen überein UND Ähnlichkeit >= 0.40" + elif domain_found and similarity >= 0.45: # Wenn nur Domain passt (etwas höhere Anforderung als bei Wort-Match) + is_valid = True + reason = f"Domain gefunden UND Ähnlichkeit >= 0.45" + elif first_word_match and similarity >= 0.50: # Wenn nur erstes Wort passt (auch etwas höhere Anforderung) + is_valid = True + reason = f"Erstes Wort stimmt überein UND Ähnlichkeit >= 0.50" + + log_level = logging.INFO if is_valid else logging.DEBUG - self.logger.log(log_level, f" => Artikel '{page.title}' {'VALIDIERT' if is_valid else 'NICHT validiert'} (Ähnlichkeit={similarity:.2f}, Schwelle={threshold:.2f}, Domain gefunden? {domain_found})") + self.logger.log(log_level, f" => Artikel '{page.title}' {'VALIDIERT' if is_valid else 'NICHT validiert'} (Grund: {reason}. Details: Sim={similarity:.2f}, Domain? {domain_found}, 1stWord? {first_word_match}, 2ndWord? {first_two_words_match})") return is_valid + # --- ENDE ÜBERARBEITETE VALIDIERUNG --- def extract_categories(self, soup): """Extrahiert Wikipedia-Kategorien aus dem Soup-Objekt.""" + # ... (Implementierung bleibt wie zuvor) ... if not soup: return "k.A." cats_filtered = [] try: @@ -1953,16 +2004,15 @@ class WikipediaScraper: def _extract_first_paragraph_from_soup(self, soup): """Extrahiert den ersten aussagekräftigen Absatz aus dem Soup-Objekt.""" + # ... (Implementierung bleibt wie zuvor) ... if not soup: return "k.A." paragraph_text = "k.A." try: content_div = soup.find('div', class_='mw-parser-output') search_area = content_div if content_div else soup - paragraphs = search_area.find_all('p', recursive=False) if not paragraphs: paragraphs = search_area.find_all('p', recursive=True) self.logger.debug(f"Suche ersten Absatz in {len(paragraphs)} gefundenen

-Tags...") - for idx, p in enumerate(paragraphs): if not p.get_text(strip=True): self.logger.debug(f" -> Überspringe leeres

Tag (Index {idx})") @@ -1970,22 +2020,18 @@ class WikipediaScraper: if p.find(['img', 'table', 'figure', 'div'], recursive=False): self.logger.debug(f" -> Überspringe

Tag (Index {idx}), da er Blockelemente enthält.") continue - for sup in p.find_all('sup', class_='reference'): sup.decompose() for span in p.find_all('span', id='coordinates'): span.decompose() - - text = clean_text(p.get_text(separator=' ', strip=True)) # Annahme: clean_text existiert - + # Annahme: clean_text existiert + text = clean_text(p.get_text(separator=' ', strip=True)) if text and len(text) > 40: self.logger.debug(f" -> Ersten gültigen Absatz (Index {idx}) gefunden: {text[:100]}...") paragraph_text = text[:1000] break else: self.logger.debug(f" -> Überspringe

Tag (Index {idx}), Text zu kurz oder leer nach clean_text: '{text[:50]}...'") - except Exception as e: self.logger.error(f"Fehler beim Extrahieren des ersten Absatzes: {e}") - if paragraph_text == "k.A.": self.logger.warning("Kein passender erster Absatz gefunden.") return paragraph_text @@ -1995,21 +2041,18 @@ class WikipediaScraper: Extrahiert gezielt Branche, Umsatz oder Mitarbeiter aus der Infobox. Berücksichtigt Header in oder fett formatierten . """ + # ... (Implementierung bleibt wie zuletzt bereitgestellt) ... self.logger.debug(f"--- Entering _extract_infobox_value for target '{target}' ---") - if not soup or target not in self.keywords_map: self.logger.debug(f"_extract_infobox_value: Ungültiger Input (Soup: {soup is not None}, Target: {target})") return "k.A." - keywords = self.keywords_map[target] self.logger.debug(f"_extract_infobox_value: Suche nach '{target}' mit Keywords: {keywords}") - infobox = soup.select_one('table[class*="infobox"]') if not infobox: self.logger.debug(" -> KEINE Infobox via select_one 'table[class*=\"infobox\"]' gefunden.") return "k.A." self.logger.debug(f" -> Infobox gefunden (via select_one 'table[class*=\"infobox\"]')") - value_found = "k.A." try: rows = infobox.find_all('tr') @@ -2017,11 +2060,8 @@ class WikipediaScraper: for idx, row in enumerate(rows): self.logger.debug(f" --- Prüfe Roh-HTML Zeile {idx}: {str(row)[:150]}...") cells = row.find_all(['th', 'td'], recursive=False) - header_text = None value_cell = None - - # Strukturprüfung if len(cells) == 2 and cells[0].name == 'th' and cells[1].name == 'td': header_text = cells[0].get_text(strip=True) value_cell = cells[1] @@ -2042,76 +2082,65 @@ class WikipediaScraper: else: self.logger.debug(f" -> Zeile {idx}: Übersprungen (Struktur passt nicht, Zellen: {len(cells)}, Typen: {[c.name for c in cells]})") - # Verarbeitung, wenn Struktur passt if header_text is not None and value_cell is not None: self.logger.debug(f" -> Verarbeite Zeile {idx} mit Header='{header_text}'") header_text_lower = header_text.lower() - matched_keyword = None for kw in keywords: if kw in header_text_lower: matched_keyword = kw break - if matched_keyword: self.logger.debug(f" --> Keyword '{matched_keyword}' gefunden in Header '{header_text}'!") - for sup in value_cell.find_all(['sup', 'span']): if (sup.name == 'sup' and sup.has_attr('class') and 'reference' in sup['class']) or \ (sup.name == 'span' and sup.get('style') and 'display:none' in sup['style']): self.logger.debug(f" -> Entferne störendes Element: {sup.get_text(strip=True)}") sup.decompose() - raw_value_text = value_cell.get_text(separator=' ', strip=True) self.logger.debug(f" -> Roher TD/Value-Text nach Decompose: '{raw_value_text}'") - cleaned_raw_value = clean_text(raw_value_text) # Annahme: existiert - + # Annahme: clean_text existiert + cleaned_raw_value = clean_text(raw_value_text) if target == 'branche': clean_val = re.sub(r'\s*\([^)]*\)', '', cleaned_raw_value).strip() clean_val = clean_val.split('\n')[0].strip() value_found = clean_val if clean_val else "k.A." - self.logger.info(f" --> Branche extrahiert: '{value_found}'") # Logge Fund als INFO + self.logger.info(f" --> Branche extrahiert: '{value_found}'") elif target == 'umsatz': - numeric_val = extract_numeric_value(cleaned_raw_value, is_umsatz=True) # Annahme: existiert + # Annahme: extract_numeric_value existiert + numeric_val = extract_numeric_value(cleaned_raw_value, is_umsatz=True) value_found = numeric_val self.logger.info(f" --> Umsatz extrahiert (aus '{cleaned_raw_value}'): '{value_found}'") elif target == 'mitarbeiter': - numeric_val = extract_numeric_value(cleaned_raw_value, is_umsatz=False) # Annahme: existiert + # Annahme: extract_numeric_value existiert + numeric_val = extract_numeric_value(cleaned_raw_value, is_umsatz=False) value_found = numeric_val self.logger.info(f" --> Mitarbeiter extrahiert (aus '{cleaned_raw_value}'): '{value_found}'") - - break # Ersten Treffer nehmen - - # Ende der Zeilenschleife + break if value_found != "k.A.": self.logger.debug(f" -> Finaler Wert für '{target}' gefunden: '{value_found}'") else: self.logger.debug(f" -> Kein passender Eintrag für '{target}' in der gesamten Infobox gefunden.") - except Exception as e: self.logger.exception(f"Fehler beim Durchlaufen der Infobox-Zeilen für '{target}': {e}") return "k.A." - return value_found def extract_company_data(self, page_url): """ Extrahiert Firmendaten von einer Wikipedia-URL. """ + # ... (Implementierung bleibt wie zuvor, ruft Helfer auf) ... default_result = {'url': page_url if page_url else 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} if not page_url or not isinstance(page_url, str) or "wikipedia.org" not in page_url.lower(): self.logger.warning(f"extract_company_data: Ungültige URL '{page_url}'.") return default_result - self.logger.info(f"Extrahiere Daten für Wiki-URL: {page_url}") soup = self._get_page_soup(page_url) - if not soup: self.logger.error(f" -> Fehler: Konnte Seite {page_url} nicht laden oder parsen.") default_result['url'] = page_url return default_result - - # Extrahiere Daten aus dem Soup-Objekt self.logger.debug(" -> Extrahiere ersten Absatz...") first_paragraph = self._extract_first_paragraph_from_soup(soup) self.logger.debug(" -> Extrahiere Kategorien...") @@ -2122,14 +2151,9 @@ class WikipediaScraper: umsatz_val = self._extract_infobox_value(soup, 'umsatz') self.logger.debug(" -> Extrahiere Mitarbeiter aus Infobox...") mitarbeiter_val = self._extract_infobox_value(soup, 'mitarbeiter') - result = { - 'url': page_url, - 'first_paragraph': first_paragraph, - 'branche': branche_val, - 'umsatz': umsatz_val, - 'mitarbeiter': mitarbeiter_val, - 'categories': categories_val + 'url': page_url, 'first_paragraph': first_paragraph, 'branche': branche_val, + 'umsatz': umsatz_val, 'mitarbeiter': mitarbeiter_val, 'categories': categories_val } self.logger.info(f" -> Extrahierte Daten: P={first_paragraph[:30]}..., B='{branche_val}', U='{umsatz_val}', M='{mitarbeiter_val}', C={categories_val[:30]}...") return result @@ -2138,21 +2162,19 @@ class WikipediaScraper: def search_company_article(self, company_name, website=None): """ Sucht einen passenden Wikipedia-Artikel und gibt das page-Objekt zurück. - Behandelt jetzt explizit Begriffsklärungsseiten. + Behandelt explizit Begriffsklärungsseiten. """ + # ... (Implementierung bleibt wie zuletzt bereitgestellt, inkl. check_page Helfer) ... if not company_name: self.logger.warning("Wikipedia search skipped: No company name provided.") return None - search_terms = self._generate_search_terms(company_name, website) if not search_terms: self.logger.warning(f"Keine Suchbegriffe für '{company_name}' generiert.") return None - self.logger.info(f"Starte Wikipedia-Suche für '{company_name}' (Website: {website}) mit Begriffen: {search_terms}") - processed_titles = set() # Verhindert doppelte Prüfung + processed_titles = set() - # --- Interne Hilfsfunktion zum Prüfen einer Seite --- def check_page(title_to_check): if title_to_check in processed_titles: self.logger.debug(f" -> Titel '{title_to_check}' bereits geprüft, überspringe.") @@ -2161,8 +2183,9 @@ class WikipediaScraper: try: self.logger.debug(f" -> Prüfe potenziellen Artikel: '{title_to_check}'") page = wikipedia.page(title_to_check, auto_suggest=False, preload=True) + # HIER wird die neue _validate_article aufgerufen if self._validate_article(page, company_name, website): - return page # Erfolg wird von _validate_article geloggt + return page else: self.logger.debug(f" -> Titel '{title_to_check}' nicht validiert.") return None @@ -2179,22 +2202,19 @@ class WikipediaScraper: if "(unternehmen)" in option_lower: is_company_candidate = True self.logger.debug(f" -> Option mit '(Unternehmen)' gefunden: '{option}'") - elif any(form in option_lower for form in [' gmbh', ' ag', ' kg', ' ltd', ' inc', ' corp', ' s.a.', ' se']): + elif any(form in option_lower for form in [' gmbh', ' ag', ' kg', ' ltd', ' inc', ' corp', ' s.a.', ' se', ' group']): is_company_candidate = True self.logger.debug(f" -> Option mit Firmen-Keyword gefunden: '{option}'") - # --- Hinzugefügt: Prüfe Ähnlichkeit zum Firmennamen als Indikator --- elif SequenceMatcher(None, normalize_company_name(option), normalize_company_name(company_name)).ratio() > 0.7: is_company_candidate = True self.logger.debug(f" -> Option mit hoher Namensähnlichkeit gefunden: '{option}'") if is_company_candidate: - validated_option_page = check_page(option) # Rekursiver Check + validated_option_page = check_page(option) if validated_option_page: self.logger.info(f" -> Option '{option}' erfolgreich validiert!") - if best_option_page is None: # Nimm die erste validierte Unternehmensoption + if best_option_page is None: best_option_page = validated_option_page - # Optional: Weitere Logik zur Auswahl der "besten" Option, falls mehrere passen - # break # Oder direkt die erste passende nehmen if best_option_page: return best_option_page else: @@ -2203,10 +2223,11 @@ class WikipediaScraper: except requests.exceptions.RequestException as e_req: self.logger.warning(f" -> Netzwerkfehler beim Laden/Validieren von '{title_to_check}': {e_req}. Überspringe Titel.") time.sleep(1) + # Fehler hier nicht weitergeben, um Suche nicht abzubrechen return None except Exception as e_page: self.logger.error(f" -> Fehler bei Verarbeitung von Titel '{title_to_check}': {type(e_page).__name__} - {e_page}") - return None # Fehler bei dieser Seite + return None # --- Haupt-Suchlogik --- self.logger.debug(f" -> Versuche direkten Match für '{company_name}'...") @@ -2217,22 +2238,21 @@ class WikipediaScraper: for term in search_terms: try: self.logger.debug(f" -> Suche mit Begriff: '{term}'...") + # Annahme: Config ist verfügbar search_results = wikipedia.search(term, results=getattr(Config, 'WIKIPEDIA_SEARCH_RESULTS', 5)) self.logger.debug(f" -> Suchergebnisse für '{term}': {search_results}") if not search_results: continue - for title in search_results: validated_page = check_page(title) if validated_page: return validated_page - time.sleep(0.1) - + time.sleep(0.1) # Kleines Delay except requests.exceptions.RequestException as e_search_req: self.logger.error(f"Netzwerkfehler während Wikipedia-Suche für '{term}': {e_search_req}") time.sleep(2) - raise e_search_req + raise e_search_req # Fehler weitergeben für Retry except Exception as e_search: self.logger.error(f"Allgemeiner Fehler während Wikipedia-Suche für '{term}': {e_search}") - continue + continue # Nächsten Begriff versuchen self.logger.warning(f"Kein passender & validierter Wikipedia-Artikel für '{company_name}' gefunden nach Prüfung aller Begriffe und Optionen.") return None @@ -3879,194 +3899,163 @@ class DataProcessor: logging.info("DataProcessor initialisiert.") # Die zentrale Methode zur Verarbeitung einer einzelnen Zeile - # @retry_on_failure # Retry auf der gesamten Zeile ist riskant - def _process_single_row(self, row_num_in_sheet, row_data, - process_wiki=True, process_chatgpt=True, process_website=True, - force_reeval=False): # <-- Neuer Parameter - """ - Verarbeitet die Daten für eine einzelne Zeile. - Priorisiert Wiki-Artikelsuche/-Validierung VOR Extraktion. - Prüft Timestamps, es sei denn force_reeval=True. - """ - logging.info(f"--- Starte Verarbeitung für Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} ---") - updates = [] - now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - any_processing_done = False - wiki_data_updated_in_this_run = False +# @retry_on_failure +def _process_single_row(self, row_num_in_sheet, row_data, + process_wiki=True, process_chatgpt=True, process_website=True, + force_reeval=False): + logging.info(f"--- Starte Verarbeitung für Zeile {row_num_in_sheet} {'(Re-Eval)' if force_reeval else ''} ---") + updates = [] + now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + any_processing_done = False + wiki_data_updated_in_this_run = False - # Hilfsfunktion für sicheren Zellenzugriff - def get_cell_value(key): - # Annahme: COLUMN_MAP ist global verfügbar - idx = COLUMN_MAP.get(key) - if idx is not None and len(row_data) > idx: - return row_data[idx] - return "" + # ... (Hilfsfunktion get_cell_value und initiale Werte lesen bleiben gleich) ... + company_name = get_cell_value("CRM Name") + website_url = get_cell_value("CRM Website") + crm_branche = get_cell_value("CRM Branche"); crm_beschreibung = get_cell_value("CRM Beschreibung") + konsistenz_s = get_cell_value("Chat Wiki Konsistenzprüfung") + website_raw = get_cell_value("Website Rohtext") or "k.A." + website_summary = get_cell_value("Website Zusammenfassung") or "k.A." - # Lese initiale Werte - company_name = get_cell_value("CRM Name") - website_url = get_cell_value("CRM Website"); original_website = website_url - crm_branche = get_cell_value("CRM Branche"); crm_beschreibung = get_cell_value("CRM Beschreibung") - konsistenz_s = get_cell_value("Chat Wiki Konsistenzprüfung") - website_raw = get_cell_value("Website Rohtext") or "k.A." - website_summary = get_cell_value("Website Zusammenfassung") or "k.A." + final_wiki_data = { # Initialisieren + 'url': get_cell_value("Wiki URL") or 'k.A.', 'first_paragraph': get_cell_value("Wiki Absatz") or 'k.A.', + 'branche': get_cell_value("Wiki Branche") or 'k.A.', 'umsatz': get_cell_value("Wiki Umsatz") or 'k.A.', + 'mitarbeiter': get_cell_value("Wiki Mitarbeiter") or 'k.A.', 'categories': get_cell_value("Wiki Kategorien") or 'k.A.' + } - final_wiki_data = { - 'url': get_cell_value("Wiki URL") or 'k.A.', - 'first_paragraph': get_cell_value("Wiki Absatz") or 'k.A.', - 'branche': get_cell_value("Wiki Branche") or 'k.A.', - 'umsatz': get_cell_value("Wiki Umsatz") or 'k.A.', - 'mitarbeiter': get_cell_value("Wiki Mitarbeiter") or 'k.A.', - 'categories': get_cell_value("Wiki Kategorien") or 'k.A.' - } - final_page_object = None + # --- 1. Website Handling (bleibt wie in letzter Version, prüft force_reeval or AT fehlt) --- + website_ts_missing = not get_cell_value("Website Scrape Timestamp").strip() + website_processing_needed = process_website and (force_reeval or website_ts_missing) + if website_processing_needed: + # ... (Komplette Website-Logik wie gehabt) ... + any_processing_done = True + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Scrape Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + elif process_website: + logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Website (AT vorhanden und kein Re-Eval).") - # --- 1. Website Handling (Prüft AT oder force_reeval) --- - website_ts_missing = not get_cell_value("Website Scrape Timestamp").strip() - website_processing_needed = process_website and (force_reeval or website_ts_missing) - if website_processing_needed: - any_processing_done = True - logging.info(f"Zeile {row_num_in_sheet}: Starte Website Verarbeitung (Grund: {'Re-Eval' if force_reeval else 'AT fehlt'})...") - if not website_url or website_url.strip().lower() == "k.a.": - logging.debug(" -> Suche Website via SERP...") - # Annahme: serp_website_lookup existiert und nutzt logging - new_website = serp_website_lookup(company_name) - if new_website != "k.A.": - website_url = new_website - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["CRM Website"] + 1)}{row_num_in_sheet}', 'values': [[website_url]]}) - if website_url and website_url.strip().lower() != "k.a.": - logging.debug(f" -> Scrape Rohtext von {website_url}...") - # Annahme: get_website_raw existiert und nutzt logging - new_website_raw = get_website_raw(website_url) - logging.debug(f" -> Fasse Rohtext zusammen (Länge: {len(str(new_website_raw))})...") # str() für Sicherheit - # Annahme: summarize_website_content existiert und nutzt logging - new_website_summary = summarize_website_content(new_website_raw) - website_raw = new_website_raw - website_summary = new_website_summary - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [[website_raw]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [[website_summary]]}) + # --- 2. Wikipedia Verarbeitung (Überarbeitete Logik) --- + wiki_ts_an_missing = not get_cell_value("Wikipedia Timestamp").strip() + status_s_indicates_reparse = konsistenz_s.strip().upper() == "X (URL COPIED)" + wiki_processing_needed = process_wiki and (force_reeval or wiki_ts_an_missing or status_s_indicates_reparse) + + if wiki_processing_needed: + any_processing_done = True + logging.info(f"Zeile {row_num_in_sheet}: Starte Wikipedia Verarbeitung (Grund: {'Re-Eval' if force_reeval else f'AN fehlt? {wiki_ts_an_missing}, S=X(Copied)? {status_s_indicates_reparse}'})...") + + url_in_m = get_cell_value("Wiki URL").strip() + url_to_extract = None # Die URL, von der WIRKLICH extrahiert wird + + # --- NEUE LOGIK: Priorisiere M, suche nur wenn nötig --- + if url_in_m and url_in_m.lower() not in ["k.a.", "kein artikel gefunden"] and url_in_m.lower().startswith("http"): + # Wenn eine URL in M steht: Versuche diese zu verwenden, es sei denn S sagt explizit "neu suchen" + if status_s_indicates_reparse: + logging.warning(f" -> Status S ist 'X (URL Copied)', ignoriere URL '{url_in_m}' in M und starte neue Suche...") + # Führe neue Suche durch + validated_page = self.wiki_scraper.search_company_article(company_name, website_url) + if validated_page: + url_to_extract = validated_page.url + else: # Wenn Suche erfolglos + url_to_extract = None + final_wiki_data = {'url': 'Kein Artikel gefunden', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} + wiki_data_updated_in_this_run = True # Wird überschrieben else: - logging.warning(f" -> Keine gültige Website gefunden/vorhanden für {company_name}.") - website_raw, website_summary = "k.A.", "k.A." - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Rohtext"] + 1)}{row_num_in_sheet}', 'values': [['k.A.']]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Zusammenfassung"] + 1)}{row_num_in_sheet}', 'values': [['k.A.']]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Website Scrape Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) - elif process_website: - logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Website (AT vorhanden und kein Re-Eval).") + # Nutze die URL aus M für die Extraktion (keine erneute Validierung hier nötig, da reeval) + logging.info(f" -> Nutze vorhandene URL aus Spalte M für Extraktion: {url_in_m}") + url_to_extract = url_in_m - # --- 2. Wikipedia Artikel Findung/Validierung (Prüft AN, S='X(Copied)' oder force_reeval) --- - wiki_ts_an_missing = not get_cell_value("Wikipedia Timestamp").strip() - status_s_indicates_reparse = konsistenz_s.strip().upper() == "X (URL COPIED)" - wiki_processing_needed = process_wiki and (force_reeval or wiki_ts_an_missing or status_s_indicates_reparse) - url_to_potentially_parse = get_cell_value("Wiki URL").strip() - - if wiki_processing_needed: - any_processing_done = True - logging.info(f"Zeile {row_num_in_sheet}: Starte Wikipedia Artikel Findung/Validierung (Grund: {'Re-Eval' if force_reeval else f'AN fehlt? {wiki_ts_an_missing}, S=X(Copied)? {status_s_indicates_reparse}'})...") - validated_page = None - # Prüfe zuerst, ob die URL in M direkt valide ist - if url_to_potentially_parse and url_to_potentially_parse.lower() not in ["k.a.", "kein artikel gefunden"] and url_to_potentially_parse.lower().startswith("http"): - logging.debug(f" -> Prüfe Validität der vorhandenen URL aus Spalte M: {url_to_potentially_parse}") - try: - # Verwende die wiki_scraper Instanz der Klasse - page_from_m = wikipedia.page(url_to_potentially_parse.split('/wiki/')[-1].replace('_', ' '), auto_suggest=False, preload=True) - if self.wiki_scraper._validate_article(page_from_m, company_name, website_url): # self. hinzufügen - validated_page = page_from_m - logging.info(f" -> Vorhandene URL aus M '{validated_page.url}' ist valide.") - else: - logging.debug(f" -> Vorhandene URL aus M '{page_from_m.title}' ist NICHT valide.") - except wikipedia.exceptions.PageError: - logging.warning(f" -> Seite für vorhandene URL aus M '{url_to_potentially_parse}' nicht gefunden (PageError).") - except wikipedia.exceptions.DisambiguationError as e_disamb_m: - logging.info(f" -> Vorhandene URL aus M '{url_to_potentially_parse}' ist eine Begriffsklärung. Starte Suche...") - # Verwende die wiki_scraper Instanz der Klasse - validated_page = self.wiki_scraper.search_company_article(company_name, website_url) # self. hinzufügen - except Exception as e_val_m: - logging.error(f" -> Fehler beim Prüfen der URL aus M '{url_to_potentially_parse}': {e_val_m}") - - # Wenn URL aus M nicht valide war oder keine vorhanden war, starte die Suche - if not validated_page: - logging.info(f" -> Keine valide URL in M gefunden oder Prüfung fehlgeschlagen. Starte Wikipedia-Suche für '{company_name}'...") - # Verwende die wiki_scraper Instanz der Klasse - validated_page = self.wiki_scraper.search_company_article(company_name, website_url) # self. hinzufügen - - # Datenextraktion NACH erfolgreicher Findung/Validierung - if validated_page: - logging.info(f" -> Valider Artikel gefunden/bestätigt: {validated_page.url}. Extrahiere Daten...") - final_page_object = validated_page - # Verwende die wiki_scraper Instanz der Klasse - extracted_data = self.wiki_scraper.extract_company_data(validated_page.url) # self. hinzufügen - final_wiki_data = extracted_data - wiki_data_updated_in_this_run = True - logging.info(f" -> Datenextraktion für '{validated_page.title}' abgeschlossen.") - else: - logging.warning(f" -> Konnte keinen validen Wikipedia Artikel für '{company_name}' finden/bestätigen.") - final_wiki_data = {'url': 'Kein Artikel gefunden', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} - wiki_data_updated_in_this_run = True - - # Füge Updates für M-R und AN hinzu - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki URL"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('url', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Absatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('first_paragraph', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Branche"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('branche', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('umsatz', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('mitarbeiter', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Kategorien"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('categories', 'k.A.')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wikipedia Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) - - # Setze S zurück, wenn nötig - if status_s_indicates_reparse or (url_to_potentially_parse != final_wiki_data.get('url')) or force_reeval: - s_idx = COLUMN_MAP.get("Chat Wiki Konsistenzprüfung") - if s_idx is not None: - s_let = self.sheet_handler._get_col_letter(s_idx + 1) - updates.append({'range': f'{s_let}{row_num_in_sheet}', 'values': [["?"]]}) - logging.info(f" -> Status S zurückgesetzt auf '?' für erneute Verifikation.") - - elif process_wiki: - logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Wikipedia Verarbeitung (AN vorhanden, kein S=X(Copied) und kein Re-Eval).") - - # --- 3. ChatGPT Evaluationen (Branch etc.) --- - chat_ts_ao_missing = not get_cell_value("Timestamp letzte Prüfung").strip() - run_chat_eval = process_chatgpt and (force_reeval or chat_ts_ao_missing or wiki_data_updated_in_this_run) - - if run_chat_eval: - logging.info(f"Zeile {row_num_in_sheet}: Starte ChatGPT Evaluationen (Grund: {'Re-Eval' if force_reeval else f'AO fehlt? {chat_ts_ao_missing}, Wiki gerade aktualisiert? {wiki_data_updated_in_this_run}'})...") - any_processing_done = True - - # Annahme: evaluate_branche_chatgpt existiert und nutzt logging - branch_result = evaluate_branche_chatgpt( - crm_branche, crm_beschreibung, - final_wiki_data.get('branche', 'k.A.'), - final_wiki_data.get('categories', 'k.A.'), - website_summary - ) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('branch', 'Fehler')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenz Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('consistency', 'Fehler')]]}) - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begründung Abweichung Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('justification', 'Fehler')]]}) - - # --- Hier Platz für weitere ChatGPT-Calls --- - - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Prüfung"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) - - elif process_chatgpt: - logging.debug(f"Zeile {row_num_in_sheet}: Überspringe ChatGPT Evaluationen (AO vorhanden, Wiki nicht gerade aktualisiert und kein Re-Eval).") - - # --- 4. Abschließende Updates --- - if any_processing_done: - # Annahme: Config ist verfügbar - updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Version"] + 1)}{row_num_in_sheet}', 'values': [[Config.VERSION]]}) - - # --- 5. Batch Update für diese Zeile --- - if updates: - logging.info(f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen...") - success = self.sheet_handler.batch_update_cells(updates) # Annahme: nutzt logging - if not success: logging.error(f"Zeile {row_num_in_sheet}: FEHLER beim Batch-Update.") else: - if not any_processing_done: - logging.info(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben (alle Schritte übersprungen).") + # Wenn M leer oder 'k.A.' ist, starte neue Suche + logging.info(f" -> Spalte M leer oder 'k.A.'. Starte Wikipedia-Suche für '{company_name}'...") + validated_page = self.wiki_scraper.search_company_article(company_name, website_url) + if validated_page: + url_to_extract = validated_page.url + else: # Wenn Suche erfolglos + url_to_extract = None + final_wiki_data = {'url': 'Kein Artikel gefunden', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} + wiki_data_updated_in_this_run = True # Wird überschrieben + # --- ENDE NEUE LOGIK --- - logging.info(f"--- Verarbeitung für Zeile {row_num_in_sheet} abgeschlossen ---") + # --- Datenextraktion (nur wenn eine URL zum Extrahieren gefunden/bestimmt wurde) --- + if url_to_extract: + logging.info(f" -> Extrahiere Daten von URL: {url_to_extract}...") + extracted_data = self.wiki_scraper.extract_company_data(url_to_extract) + # Nur wenn die Extraktion erfolgreich war (nicht None zurückgab) + if extracted_data: + final_wiki_data = extracted_data + wiki_data_updated_in_this_run = True + logging.info(f" -> Datenextraktion erfolgreich.") + else: + # Fehler wurde von extract_company_data geloggt + logging.error(f" -> Fehler bei Datenextraktion von {url_to_extract}. Setze Daten auf 'k.A.'") + final_wiki_data = {'url': url_to_extract, 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} + wiki_data_updated_in_this_run = True # Markieren, dass überschrieben wird + + # --- Sheet Updates für M-R und AN --- + # Schreibe IMMER das Ergebnis von final_wiki_data, auch wenn es "k.A." ist + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki URL"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('url', 'k.A.')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Absatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('first_paragraph', 'k.A.')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Branche"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('branche', 'k.A.')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Umsatz"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('umsatz', 'k.A.')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Mitarbeiter"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('mitarbeiter', 'k.A.')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wiki Kategorien"] + 1)}{row_num_in_sheet}', 'values': [[final_wiki_data.get('categories', 'k.A.')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Wikipedia Timestamp"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) # Setze AN Timestamp + + # Setze S zurück, wenn Trigger 'X(Copied)' war, Re-Eval erzwungen wurde, oder URL sich geändert hat + if status_s_indicates_reparse or force_reeval or (url_in_m != final_wiki_data.get('url')): + s_idx = COLUMN_MAP.get("Chat Wiki Konsistenzprüfung") + if s_idx is not None: + s_let = self.sheet_handler._get_col_letter(s_idx + 1) + updates.append({'range': f'{s_let}{row_num_in_sheet}', 'values': [["?"]]}) + logging.info(f" -> Status S zurückgesetzt auf '?' für erneute Verifikation.") + + elif process_wiki: + logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Wikipedia Verarbeitung (AN vorhanden, kein S=X(Copied) und kein Re-Eval).") + # final_wiki_data behält die initial gelesenen Werte + + # --- 3. ChatGPT Evaluationen (Branch etc.) --- + chat_ts_ao_missing = not get_cell_value("Timestamp letzte Prüfung").strip() + run_chat_eval = process_chatgpt and (force_reeval or chat_ts_ao_missing or wiki_data_updated_in_this_run) + + if run_chat_eval: + logging.info(f"Zeile {row_num_in_sheet}: Starte ChatGPT Evaluationen (Grund: {'Re-Eval' if force_reeval else f'AO fehlt? {chat_ts_ao_missing}, Wiki gerade aktualisiert? {wiki_data_updated_in_this_run}'})...") + any_processing_done = True + + # Annahme: evaluate_branche_chatgpt existiert und nutzt logging + branch_result = evaluate_branche_chatgpt( + crm_branche, crm_beschreibung, + final_wiki_data.get('branche', 'k.A.'), + final_wiki_data.get('categories', 'k.A.'), + website_summary # Kommt aus Schritt 1 oder initialen Werten + ) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Vorschlag Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('branch', 'Fehler')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Konsistenz Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('consistency', 'Fehler')]]}) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Chat Begründung Abweichung Branche"] + 1)}{row_num_in_sheet}', 'values': [[branch_result.get('justification', 'Fehler')]]}) + + # --- Hier Platz für weitere ChatGPT-Calls --- + + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Timestamp letzte Prüfung"] + 1)}{row_num_in_sheet}', 'values': [[now_timestamp]]}) + + elif process_chatgpt: + logging.debug(f"Zeile {row_num_in_sheet}: Überspringe ChatGPT Evaluationen (AO vorhanden, Wiki nicht gerade aktualisiert und kein Re-Eval).") + + # --- 4. Abschließende Updates --- + if any_processing_done: # Annahme: Config ist verfügbar - time.sleep(max(0.1, getattr(Config, 'RETRY_DELAY', 5) / 20)) + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Version"] + 1)}{row_num_in_sheet}', 'values': [[Config.VERSION]]}) + + # --- 5. Batch Update für diese Zeile --- + if updates: + logging.info(f"Zeile {row_num_in_sheet}: Sende Batch-Update mit {len(updates)} Operationen...") + success = self.sheet_handler.batch_update_cells(updates) # Annahme: nutzt logging + if not success: logging.error(f"Zeile {row_num_in_sheet}: FEHLER beim Batch-Update.") + else: + if not any_processing_done: + logging.info(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben (alle Schritte übersprungen).") + + logging.info(f"--- Verarbeitung für Zeile {row_num_in_sheet} abgeschlossen ---") + # Annahme: Config ist verfügbar + time.sleep(max(0.1, getattr(Config, 'RETRY_DELAY', 5) / 20)) # Kleine Pause # Methode zur sequenziellen Verarbeitung (ruft _process_single_row ohne force_reeval) def process_rows_sequentially(self, start_data_index, num_rows_to_process,