diff --git a/brancheneinstufung.py b/brancheneinstufung.py index b6920166..1522d5ab 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -1493,35 +1493,29 @@ class GoogleSheetHandler: class WikipediaScraper: """ Handles searching Wikipedia articles and extracting relevant company data. - Version: 1.6.5 logic - Improved infobox parsing reliability and detailed logging. + Version: 1.6.5 logic - Improved infobox parsing, disambiguation handling, and standard logging. """ def __init__(self, user_agent=None): """ Initialisiert den Scraper mit einer Requests-Session und Logger. """ - # --- Logging explizit für diese Klasse holen und Level setzen --- - # Holt Logger basierend auf dem Modulnamen (__name__ wird hier zu 'WikipediaScraper' oder dem Dateinamen) self.logger = logging.getLogger(__name__) - # Das Level wird global in main() gesetzt, hier zur Sicherheit nochmal debug loggen self.logger.debug(f"Logger für WikipediaScraper ('{__name__}') initialisiert.") - # --- Ende Logging-Anpassung --- - # Verwende User-Agent aus Config oder einen Standardwert self.user_agent = user_agent or getattr(Config, 'USER_AGENT', 'Mozilla/5.0 (compatible; Datenanreicherungsskript/1.0)') self.session = requests.Session() self.session.headers.update({'User-Agent': self.user_agent}) self.logger.debug(f"Requests Session mit User-Agent '{self.user_agent}' initialisiert.") - # Erweiterte Keyword Map für robustere Infobox-Extraktion self.keywords_map = { 'branche': ['branche', 'wirtschaftszweig', 'industry', 'tätigkeit', 'sektor', 'produkte', 'leistungen'], 'umsatz': ['umsatz', 'erlös', 'revenue', 'jahresumsatz', 'konzernumsatz', 'ergebnis'], 'mitarbeiter': ['mitarbeiter', 'mitarbeiterzahl', 'beschäftigte', 'employees', 'number of employees', 'personal', 'belegschaft'] } try: - wiki_lang = getattr(Config, 'LANG', 'de') # Sprache aus Config holen + wiki_lang = getattr(Config, 'LANG', 'de') wikipedia.set_lang(wiki_lang) - wikipedia.set_rate_limiting(True) # Respektiere Wikipedia API Limits + wikipedia.set_rate_limiting(True) self.logger.info(f"Wikipedia library language set to '{wiki_lang}'. Rate limiting enabled.") except Exception as e: self.logger.warning(f"Fehler beim Setzen der Wikipedia-Sprache oder Rate Limiting: {e}") @@ -1531,13 +1525,9 @@ class WikipediaScraper: if not website or not isinstance(website, str): return "" website_lower = website.lower().strip() if not website_lower or website_lower == 'k.a.': return "" - # Schema entfernen website_lower = re.sub(r'^https?:\/\/', '', website_lower) - # User:Pass entfernen if '@' in website_lower: website_lower = website_lower.split('@', 1)[1] - # www. entfernen if website_lower.startswith('www.'): website_lower = website_lower[4:] - # Pfad und Port entfernen domain = website_lower.split('/')[0].split(':')[0] return domain @@ -1546,27 +1536,24 @@ class WikipediaScraper: if not company_name: return [] terms = set() full_domain = self._get_full_domain(website) - if full_domain: terms.add(full_domain) # Domain als Suchbegriff + if full_domain: terms.add(full_domain) - # Normalisierten Namen hinzufügen (Annahme: Funktion existiert) - normalized_name = normalize_company_name(company_name) + normalized_name = normalize_company_name(company_name) # Annahme: existiert if normalized_name: name_parts = normalized_name.split() - if len(name_parts) > 0: terms.add(name_parts[0]) # Erstes Wort - if len(name_parts) > 1: terms.add(" ".join(name_parts[:2])) # Erste zwei Worte - terms.add(normalized_name) # Ganzer normalisierter Name + 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) - # Originalnamen hinzufügen, falls signifikant anders company_name_lower = company_name.lower() if company_name_lower != normalized_name and company_name_lower not in terms: terms.add(company_name_lower) - # Nimm nur sinnvolle (nicht leere) und begrenzte Anzahl Begriffe - final_terms = [term for term in list(terms) if term][:5] # Max 5 Begriffe + 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 - @retry_on_failure # Annahme: Decorator existiert und behandelt Exceptions + @retry_on_failure # Annahme: Decorator existiert def _get_page_soup(self, url): """Holt HTML von einer URL und gibt ein BeautifulSoup-Objekt zurück.""" if not url or not isinstance(url, str) or not url.startswith("http"): @@ -1574,23 +1561,20 @@ class WikipediaScraper: return None try: self.logger.debug(f"_get_page_soup: Rufe URL ab: {url}") - response = self.session.get(url, timeout=20) # Timeout etwas höher - response.raise_for_status() # Fehler für 4xx/5xx auslösen - # Encoding explizit auf UTF-8 setzen (Standard für Wikipedia) + response = self.session.get(url, timeout=20) + response.raise_for_status() response.encoding = 'utf-8' - soup = BeautifulSoup(response.text, getattr(Config, 'HTML_PARSER', 'html.parser')) # Nutze Parser aus Config + soup = BeautifulSoup(response.text, getattr(Config, 'HTML_PARSER', 'html.parser')) self.logger.debug(f"_get_page_soup: Parsen von {url} erfolgreich.") return soup except requests.exceptions.Timeout: self.logger.error(f"_get_page_soup: Timeout beim Abrufen von {url}") - raise # Fehler weitergeben für Retry + raise except requests.exceptions.RequestException as e: self.logger.error(f"_get_page_soup: Netzwerkfehler beim Abrufen von HTML von {url}: {e}") - raise e # Fehler weitergeben für Retry + raise e except Exception as e: self.logger.error(f"_get_page_soup: Fehler beim Parsen von HTML von {url}: {e}") - # Parsing-Fehler nicht unbedingt wiederholen, None zurückgeben? Oder doch für Retry? - # Fürs Erste weitergeben, falls temporäres Problem raise e def _validate_article(self, page, company_name, website): @@ -1609,21 +1593,19 @@ class WikipediaScraper: similarity = SequenceMatcher(None, normalized_title, normalized_company).ratio() self.logger.debug(f" -> Titelähnlichkeit: {similarity:.2f} ('{normalized_title}' vs '{normalized_company}')") - # 2. Link-Prüfung (nur wenn Domain vorhanden) + # 2. Link-Prüfung 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) # Hole Soup für Link-Prüfung + soup = self._get_page_soup(page.url) if soup: - # Suche zuerst in der Infobox infobox = soup.select_one('table[class*="infobox"]') if infobox: website_links = infobox.find_all('a', href=True) for link in website_links: href = link.get('href', '') - if href.startswith('http') and full_domain in self._get_full_domain(href): # Vergleiche Domains + if href.startswith('http') and full_domain in self._get_full_domain(href): link_text = link.get_text(strip=True).lower() - # Prüfe, ob Keyword im Link-Text ODER in der Tabellenüberschrift der Zeile th = link.find_previous('th') th_text = th.get_text(strip=True).lower() if th else "" if any(kw in link_text for kw in ['website', 'webseite', 'offizielle']) or \ @@ -1631,39 +1613,34 @@ class WikipediaScraper: self.logger.debug(f" -> Domain '{full_domain}' in Infobox-Link gefunden (TH: '{th_text}', Text: '{link_text}', URL: {href})") domain_found = True break - else: # Akzeptiere auch ohne Keyword, wenn URL eindeutig scheint + else: self.logger.debug(f" -> Domain '{full_domain}' in Infobox-Link gefunden (URL: {href}, kein Keyword-Match im Text/TH)") domain_found = True break - # Wenn nicht in Infobox, suche in allen externen Links if not domain_found: self.logger.debug(" -> Domain nicht in Infobox-Links gefunden, suche in allen externen Links...") - all_links = soup.find_all('a', href=True, class_=re.compile(r'.*\bexternal\b.*')) # Suche explizit externe Links - if not all_links: # Fallback: alle Links prüfen - all_links = soup.find_all('a', href=True) - + all_links = soup.find_all('a', href=True, class_=re.compile(r'.*\bexternal\b.*')) + if not all_links: all_links = soup.find_all('a', href=True) for link in all_links: href = link.get('href', '') if href.startswith('http') and full_domain in self._get_full_domain(href): - # Ignoriere Links zu anderen Wikimedia-Projekten etc. if not any(site in href for site in ['wikipedia.org', 'wikimedia.org', 'wikidata.org', 'archive.org', 'webcitation.org']): self.logger.debug(f" -> Domain '{full_domain}' in externem Link gefunden (URL: {href})") domain_found = True - break # Erster Treffer reicht + break else: self.logger.warning(f" -> Konnte HTML für Link-Prüfung von {page.url} nicht laden.") else: self.logger.debug(" -> Keine Website-Domain für Link-Prüfung vorhanden.") # 3. Entscheidung - threshold = getattr(Config, 'SIMILARITY_THRESHOLD', 0.65) # Standard-Schwelle aus Config + threshold = getattr(Config, 'SIMILARITY_THRESHOLD', 0.65) if domain_found: - # Schwelle signifikant lockern, wenn Domain passt - threshold = max(0.35, threshold - 0.3) # Beispiel: 0.65 -> 0.35 + threshold = max(0.35, threshold - 0.3) self.logger.debug(f" -> Domain gefunden, Ähnlichkeitsschwelle angepasst auf {threshold:.2f}") is_valid = similarity >= threshold - log_level = logging.INFO if is_valid else logging.DEBUG # Logge Erfolg als INFO + 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})") return is_valid @@ -1683,7 +1660,6 @@ class WikipediaScraper: else: self.logger.debug("Kein 'div#mw-normal-catlinks' gefunden.") except Exception as e: self.logger.error(f"Fehler beim Extrahieren der Kategorien: {e}") - return ", ".join(cats_filtered) if cats_filtered else "k.A." def _extract_first_paragraph_from_soup(self, soup): @@ -1691,40 +1667,30 @@ class WikipediaScraper: if not soup: return "k.A." paragraph_text = "k.A." try: - # Suche bevorzugt im Hauptinhalt content_div = soup.find('div', class_='mw-parser-output') - search_area = content_div if content_div else soup # Fallback auf ganzen Soup + search_area = content_div if content_div else soup - # Finde alle

-Tags direkt unter dem Suchbereich (oft relevanter) paragraphs = search_area.find_all('p', recursive=False) - if not paragraphs: # Fallback: Alle

-Tags suchen - paragraphs = search_area.find_all('p', recursive=True) + 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): - # Überspringe leere oder zu kurze Absätze if not p.get_text(strip=True): self.logger.debug(f" -> Überspringe leeres

Tag (Index {idx})") continue - # Überspringe Absätze, die nur Bilder/Tabellen etc. enthalten könnten if p.find(['img', 'table', 'figure', 'div'], recursive=False): self.logger.debug(f" -> Überspringe

Tag (Index {idx}), da er Blockelemente enthält.") continue - # Entferne Referenz-Links `[1]`, `[2]` etc. - for sup in p.find_all('sup', class_='reference'): - sup.decompose() - # Entferne Koordinaten (oft am Anfang in ) - for span in p.find_all('span', id='coordinates'): - span.decompose() + 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)) + text = clean_text(p.get_text(separator=' ', strip=True)) # Annahme: clean_text existiert - # Prüfe Mindestlänge - if text and len(text) > 40: # Mindestlänge etwas erhöht + if text and len(text) > 40: self.logger.debug(f" -> Ersten gültigen Absatz (Index {idx}) gefunden: {text[:100]}...") - paragraph_text = text[:1000] # Begrenze Länge - break # Nimm den ersten passenden + paragraph_text = text[:1000] + break else: self.logger.debug(f" -> Überspringe

Tag (Index {idx}), Text zu kurz oder leer nach clean_text: '{text[:50]}...'") @@ -1738,7 +1704,7 @@ class WikipediaScraper: def _extract_infobox_value(self, soup, target): """ Extrahiert gezielt Branche, Umsatz oder Mitarbeiter aus der Infobox. - Berücksichtigt jetzt auch Header in -Tags (z.B. ). + Berücksichtigt Header in oder fett formatierten . """ self.logger.debug(f"--- Entering _extract_infobox_value for target '{target}' ---") @@ -1761,28 +1727,23 @@ class WikipediaScraper: self.logger.debug(f" -> Analysiere {len(rows)} Zeilen in der Infobox.") 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) # Finde ALLE th oder td Zellen direkt unter tr + cells = row.find_all(['th', 'td'], recursive=False) - # --- NEUE LOGIK ZUR IDENTIFIZIERUNG VON HEADER UND WERT --- header_text = None value_cell = None - # Fall 1: Klassisch + + # 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] self.logger.debug(f" -> Zeile {idx}: Struktur TH + TD erkannt.") - # Fall 2: Header in (oft mit style="font-weight:bold;") + elif len(cells) == 2 and cells[0].name == 'td' and cells[1].name == 'td': - # Prüfe, ob die erste Zelle wie ein Header aussieht (fett oder starker Text) first_cell_is_header_like = False style = cells[0].get('style', '').lower() if 'font-weight' in style and ('bold' in style or '700' in style or '800' in style or '900' in style): first_cell_is_header_like = True - # Fallback: Prüfe, ob der direkte Text fett ist (, ) elif cells[0].find(['b', 'strong'], recursive=False): first_cell_is_header_like = True - if first_cell_is_header_like: header_text = cells[0].get_text(strip=True) value_cell = cells[1] @@ -1792,9 +1753,7 @@ class WikipediaScraper: else: self.logger.debug(f" -> Zeile {idx}: Übersprungen (Struktur passt nicht, Zellen: {len(cells)}, Typen: {[c.name for c in cells]})") - # --- ENDE NEUE LOGIK --- - - # Verarbeite nur, wenn Header und Wert gefunden wurden + # 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() @@ -1808,35 +1767,31 @@ class WikipediaScraper: if matched_keyword: self.logger.debug(f" --> Keyword '{matched_keyword}' gefunden in Header '{header_text}'!") - # Störende Elemente entfernen 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() - # Text extrahieren und bereinigen 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) + cleaned_raw_value = clean_text(raw_value_text) # Annahme: existiert - # Spezifische Verarbeitung 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}'") # INFO statt DEBUG für gefundene Werte + self.logger.info(f" --> Branche extrahiert: '{value_found}'") # Logge Fund als INFO elif target == 'umsatz': - numeric_val = extract_numeric_value(cleaned_raw_value, is_umsatz=True) + numeric_val = extract_numeric_value(cleaned_raw_value, is_umsatz=True) # Annahme: existiert 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) + numeric_val = extract_numeric_value(cleaned_raw_value, is_umsatz=False) # Annahme: existiert value_found = numeric_val self.logger.info(f" --> Mitarbeiter extrahiert (aus '{cleaned_raw_value}'): '{value_found}'") - # WICHTIG: Schleife für dieses Ziel beenden, da Wert gefunden wurde - break + break # Ersten Treffer nehmen # Ende der Zeilenschleife if value_found != "k.A.": @@ -1852,7 +1807,7 @@ class WikipediaScraper: def extract_company_data(self, page_url): """ - Extrahiert Firmendaten von einer Wikipedia-URL (v1.6.5 Logik). + Extrahiert Firmendaten von einer Wikipedia-URL. """ 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(): @@ -1864,10 +1819,10 @@ class WikipediaScraper: if not soup: self.logger.error(f" -> Fehler: Konnte Seite {page_url} nicht laden oder parsen.") - default_result['url'] = page_url # Behalte URL im Ergebnis + default_result['url'] = page_url return default_result - # --- Extrahiere Daten aus dem Soup-Objekt --- + # 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...") @@ -1879,7 +1834,6 @@ class WikipediaScraper: self.logger.debug(" -> Extrahiere Mitarbeiter aus Infobox...") mitarbeiter_val = self._extract_infobox_value(soup, 'mitarbeiter') - # --- Ergebnis zusammenstellen --- result = { 'url': page_url, 'first_paragraph': first_paragraph, @@ -1888,14 +1842,14 @@ class WikipediaScraper: 'mitarbeiter': mitarbeiter_val, 'categories': categories_val } - # Logge das Ergebnis (INFO Level für Übersicht) 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 @retry_on_failure # Annahme: Decorator existiert def search_company_article(self, company_name, website=None): """ - Sucht einen passenden Wikipedia-Artikel und gibt das page-Objekt zurück. (v1.6.5 Logik) + Sucht einen passenden Wikipedia-Artikel und gibt das page-Objekt zurück. + Behandelt jetzt explizit Begriffsklärungsseiten. """ if not company_name: self.logger.warning("Wikipedia search skipped: No company name provided.") @@ -1907,87 +1861,91 @@ class WikipediaScraper: 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 - # Versuche direkten Match zuerst - try: - self.logger.debug(f" -> Versuche direkten Match für '{company_name}'...") - page = wikipedia.page(company_name, auto_suggest=False, preload=True) - self.logger.debug(f" -> Direkten Match gefunden: '{page.title}'. Validiere...") - if self._validate_article(page, company_name, website): - # Erfolg bereits hier geloggt durch _validate_article - return page - else: - self.logger.debug(f" -> Direkter Match '{page.title}' nicht validiert. Fahre mit Suche fort.") - except wikipedia.exceptions.PageError: - self.logger.debug(f" -> Kein direkter Artikel für '{company_name}' gefunden.") - except wikipedia.exceptions.DisambiguationError as e: - self.logger.debug(f" -> '{company_name}' ist eine Begriffsklärungsseite. Optionen: {e.options[:3]}...") - # Optional: Prüfe die erste Option - if e.options: - try: - option_title = e.options[0] - self.logger.debug(f" -> Prüfe erste Option der Begriffsklärung: '{option_title}'") - page = wikipedia.page(option_title, auto_suggest=False, preload=True) - if self._validate_article(page, company_name, website): - return page # Erfolg wird von _validate_article geloggt - else: - self.logger.debug(f" -> Erste Option '{page.title}' nicht validiert.") - except Exception as e_disamb: - self.logger.warning(f" -> Fehler beim Laden/Validieren der Disambiguation-Option '{e.options[0]}': {e_disamb}") - except Exception as e_direct: - # Fehler beim direkten Zugriff loggen - self.logger.error(f" -> Unerwarteter Fehler beim direkten Zugriff auf '{company_name}': {e_direct}") - # Fehler weitergeben, da es ein Problem mit der Lib/Verbindung sein könnte - raise e_direct + # --- 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.") + return None + processed_titles.add(title_to_check) + try: + self.logger.debug(f" -> Prüfe potenziellen Artikel: '{title_to_check}'") + page = wikipedia.page(title_to_check, auto_suggest=False, preload=True) + if self._validate_article(page, company_name, website): + return page # Erfolg wird von _validate_article geloggt + else: + self.logger.debug(f" -> Titel '{title_to_check}' nicht validiert.") + return None + except wikipedia.exceptions.PageError: + self.logger.debug(f" -> Seite '{title_to_check}' nicht gefunden (PageError).") + return None + except wikipedia.exceptions.DisambiguationError as e_inner: + self.logger.info(f" -> Begriffsklärung '{title_to_check}' gefunden. Prüfe Optionen...") + self.logger.debug(f" Optionen: {e_inner.options}") + best_option_page = None + for option in e_inner.options: + option_lower = option.lower() + is_company_candidate = False + 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']): + 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}'") - # Wenn direkter Match fehlschlägt, nutze die generierten Suchbegriffe - self.logger.debug(f" -> Starte Suche mit generierten Begriffen: {search_terms}") + if is_company_candidate: + validated_option_page = check_page(option) # Rekursiver Check + if validated_option_page: + self.logger.info(f" -> Option '{option}' erfolgreich validiert!") + if best_option_page is None: # Nimm die erste validierte Unternehmensoption + 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: + self.logger.warning(f" -> Keine passende/validierte Unternehmens-Option in Begriffsklärung '{title_to_check}' gefunden.") + return None + 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) + 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 + + # --- Haupt-Suchlogik --- + self.logger.debug(f" -> Versuche direkten Match für '{company_name}'...") + validated_page = check_page(company_name) + if validated_page: return validated_page + + self.logger.debug(f" -> Kein direkter Treffer/validiert. Starte Suche mit generierten Begriffen: {search_terms}") for term in search_terms: try: self.logger.debug(f" -> Suche mit Begriff: '{term}'...") - # Begrenze Anzahl Suchergebnisse 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: - # Heuristische Filterung - if any(ignore in title.lower() for ignore in ['liste von', 'begriffsklärung', '(Begriffsklärung)']): - self.logger.debug(f" -> Überspringe wahrscheinlichen Nicht-Artikel/BK: '{title}'") - continue - try: - self.logger.debug(f" -> Prüfe potenziellen Artikel: '{title}'") - page = wikipedia.page(title, auto_suggest=False, preload=True) - if self._validate_article(page, company_name, website): - # Erfolg wird von _validate_article geloggt - return page - time.sleep(0.1) # Kleines Delay - - except wikipedia.exceptions.PageError: - self.logger.debug(f" -> Seite '{title}' nicht gefunden (PageError).") - continue - except wikipedia.exceptions.DisambiguationError as e: - self.logger.debug(f" -> Seite '{title}' ist Begriffsklärung: {e.options[:3]}...") - continue - except requests.exceptions.RequestException as e_req: - self.logger.warning(f" -> Netzwerkfehler beim Laden/Validieren von '{title}': {e_req}. Überspringe Titel.") - time.sleep(1) - continue - except Exception as e_page: - self.logger.error(f" -> Fehler bei Verarbeitung von Titel '{title}': {type(e_page).__name__} - {e_page}") - continue # Zum nächsten Titel + validated_page = check_page(title) + if validated_page: return validated_page + time.sleep(0.1) 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) # Längere Pause bei Suchfehler - # Fehler weitergeben für Retry + time.sleep(2) raise e_search_req except Exception as e_search: self.logger.error(f"Allgemeiner Fehler während Wikipedia-Suche für '{term}': {e_search}") - # Fehler weitergeben für Retry? Eher nicht, nächsten Begriff versuchen. continue - self.logger.warning(f"Kein passender & validierter Wikipedia-Artikel für '{company_name}' gefunden nach Prüfung aller Begriffe.") + self.logger.warning(f"Kein passender & validierter Wikipedia-Artikel für '{company_name}' gefunden nach Prüfung aller Begriffe und Optionen.") return None @@ -3624,171 +3582,195 @@ class DataProcessor: self.wiki_scraper = WikipediaScraper() # Eigene Instanz des Scrapers # @retry_on_failure # Vorsicht mit Retry auf dieser Ebene für die ganze 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): """ - Verarbeitet die Daten für eine einzelne Zeile, prüft Timestamps für jeden Teilbereich - und stellt sicher, dass aktuelle Wiki-Daten für Branch-Eval verwendet werden. + Verarbeitet die Daten für eine einzelne Zeile. + Priorisiert jetzt die Wiki-Artikelsuche/-Validierung VOR der Extraktion. + Prüft Timestamps für jeden Teilbereich. """ - debug_print(f"--- Starte Verarbeitung für Zeile {row_num_in_sheet} ---") + logging.info(f"--- Starte Verarbeitung für Zeile {row_num_in_sheet} ---") updates = [] now_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") any_processing_done = False + wiki_data_updated_in_this_run = False # Flag, ob Wiki-Daten (M-R) neu geschrieben wurden - # Hilfsfunktion + # Hilfsfunktion für sicheren Zellenzugriff def get_cell_value(key): idx = COLUMN_MAP.get(key) if idx is not None and len(row_data) > idx: return row_data[idx] return "" - # Lese initiale Werte + # Lese initiale Werte für spätere Verwendung 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") - crm_wiki_url = get_cell_value("CRM Vorschlag Wiki URL") 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." - # Initialisiere wiki_data mit Werten aus dem Sheet (Fallback) - 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.' + + # Initialisiere finale Wiki-Daten (werden evtl. überschrieben) + 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.' } - wiki_data_updated_in_this_run = False # Flag, ob Wiki neu geparst wurde + final_page_object = None # Das validierte Page-Objekt # --- 1. Website Handling (prüft AT) --- website_ts_needed = process_website and not get_cell_value("Website Scrape Timestamp").strip() if website_ts_needed: - any_processing_done = True; debug_print(f"Zeile {row_num_in_sheet}: Starte Website Verarbeitung...") - # --- Lookup & Scraping --- + any_processing_done = True + logging.info(f"Zeile {row_num_in_sheet}: Starte Website Verarbeitung (Lookup, Scrape, Summarize)...") if not website_url or website_url.strip().lower() == "k.a.": - new_website = serp_website_lookup(company_name) - if new_website != "k.A.": website_url = new_website; - if website_url != original_website: updates.append({'range': f'D{row_num_in_sheet}', 'values': [[website_url]]}) + logging.debug(" -> Suche Website via SERP...") + new_website = serp_website_lookup(company_name) # Annahme: nutzt logging + 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.": - new_website_raw = get_website_raw(website_url); new_website_summary = summarize_website_content(new_website_raw) - if new_website_raw != website_raw: updates.append({'range': f'AR{row_num_in_sheet}', 'values': [[new_website_raw]]}); website_raw = new_website_raw - if new_website_summary != website_summary: updates.append({'range': f'AS{row_num_in_sheet}', 'values': [[new_website_summary]]}); website_summary = new_website_summary + logging.debug(f" -> Scrape Rohtext von {website_url}...") + new_website_raw = get_website_raw(website_url) # Annahme: nutzt logging + logging.debug(f" -> Fasse Rohtext zusammen (Länge: {len(new_website_raw)})...") + new_website_summary = summarize_website_content(new_website_raw) # Annahme: nutzt logging + # Aktualisiere globale Variablen für spätere Schritte (ChatGPT) + website_raw = new_website_raw + website_summary = new_website_summary + # Füge Updates für AR und AS hinzu + 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]]}) else: - if website_raw != "k.A.": updates.append({'range': f'AR{row_num_in_sheet}', 'values': [['k.A.']]}) - if website_summary != "k.A.": updates.append({'range': f'AS{row_num_in_sheet}', 'values': [['k.A.']]}) + 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'AT{row_num_in_sheet}', 'values': [[now_timestamp]]}) - elif process_website: debug_print(f"Zeile {row_num_in_sheet}: Überspringe Website (AT vorhanden).") + 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.']]}) + # Setze AT Timestamp + 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).") - # --- 2. Wikipedia Handling (prüft AN oder S='X (URL Copied)') --- + + # --- 2. Wikipedia Artikel Findung/Validierung (prüft AN oder S='X(Copied)') --- + # Diese Logik bestimmt, OB und WELCHE Seite extrahiert werden soll. wiki_ts_an_missing = not get_cell_value("Wikipedia Timestamp").strip() status_s_indicates_reparse = konsistenz_s.strip().upper() == "X (URL COPIED)" - reparse_wiki_needed = process_wiki and (wiki_ts_an_missing or status_s_indicates_reparse) + wiki_processing_needed = process_wiki and (wiki_ts_an_missing or status_s_indicates_reparse) + url_to_potentially_parse = get_cell_value("Wiki URL").strip() # Die URL, die aktuell in Spalte M steht - if reparse_wiki_needed: - any_processing_done = True; debug_print(f"Zeile {row_num_in_sheet}: Starte Wikipedia Verarbeitung (AN fehlt? {wiki_ts_an_missing}, S='X(Copied)'? {status_s_indicates_reparse})...") - new_wiki_data_extracted = None + if wiki_processing_needed: + any_processing_done = True + logging.info(f"Zeile {row_num_in_sheet}: Starte Wikipedia Artikel Findung/Validierung (AN fehlt? {wiki_ts_an_missing}, S='X(Copied)'? {status_s_indicates_reparse})...") + # --- NEUE LOGIK: Suche/Validierung zuerst --- + 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: + # Wir brauchen das Page-Objekt für _validate_article + 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): + 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...") + # Wenn M eine BKL ist, explizit neu suchen + validated_page = self.wiki_scraper.search_company_article(company_name, website_url) + except Exception as e_val_m: + logging.error(f" -> Fehler beim Prüfen der URL aus M '{url_to_potentially_parse}': {e_val_m}") - # --- Priorisiere URL aus Spalte M --- - url_to_parse = get_cell_value("Wiki URL").strip() # Holt die URL, die ggf. von update_wiki kopiert wurde - if url_to_parse and url_to_parse.lower() not in ["k.a.", "kein artikel gefunden"] and url_to_parse.lower().startswith("http"): - debug_print(f" -> Nutze vorhandene URL aus Spalte M: {url_to_parse}") - new_wiki_data_extracted = self.wiki_scraper.extract_company_data(url_to_parse) + # 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}'...") + validated_page = self.wiki_scraper.search_company_article(company_name, website_url) # Nutzt die verbesserte Suche inkl. Disambiguation + + # --- 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 # Speichere für spätere Verwendung + extracted_data = self.wiki_scraper.extract_company_data(validated_page.url) # Extrahiere Daten von der KORREKTEN Seite + final_wiki_data = extracted_data # Überschreibe die initialen Daten + wiki_data_updated_in_this_run = True # Setze Flag, da M-R neu geschrieben wird + logging.info(f" -> Datenextraktion für '{validated_page.title}' abgeschlossen.") else: - debug_print(f" -> Spalte M ('{url_to_parse}') ungültig/leer. Starte Wiki-Suche...") - valid_crm_wiki_url = crm_wiki_url if crm_wiki_url and crm_wiki_url.strip() not in ["", "k.A."] else None - article_page = None # Initialisiere article_page - current_website_for_validation = website_url if website_url and website_url != 'k.A.' else original_website + logging.warning(f" -> Konnte keinen validen Wikipedia Artikel für '{company_name}' finden/bestätigen.") + # Setze Wiki-Daten auf "Nicht gefunden" / "k.A." + 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 # Auch hier Flag setzen, da wir M-R überschreiben - # --- KORREKTE EINRÜCKUNG HIER --- - if valid_crm_wiki_url: - debug_print(f" -> Prüfe CRM Vorschlag L: {valid_crm_wiki_url}") - page = self.wiki_scraper._fetch_page_content(valid_crm_wiki_url.split('/')[-1]) - if page and self.wiki_scraper._validate_article(page, company_name, current_website_for_validation): - article_page = page - else: - debug_print(f" -> CRM Vorschlag L nicht validiert. Starte Suche...") - # Wenn CRM-Vorschlag nicht validiert, Suche trotzdem starten - article_page = self.wiki_scraper.search_company_article(company_name, current_website_for_validation) - else: - # --- DIESE ZEILE IST JETZT KORREKT EINGERÜCKT UNTER DEM ELSE --- - debug_print(f" -> Kein CRM Vorschlag L. Starte Suche...") - article_page = self.wiki_scraper.search_company_article(company_name, current_website_for_validation) - # --- ENDE KORREKTE EINRÜCKUNG --- + # 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 AN Timestamp - if article_page: - debug_print(f" -> Artikel gefunden durch Suche: {article_page.url}") - new_wiki_data_extracted = self.wiki_scraper.extract_company_data(article_page.url) - else: - debug_print(f" -> Kein passender Wikipedia Artikel durch Suche gefunden.") - new_wiki_data_extracted = {'url': 'Kein Artikel gefunden', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.'} - - # --- WICHTIG: Überschreibe wiki_data mit den NEUEN Ergebnissen --- - if new_wiki_data_extracted: - wiki_data = new_wiki_data_extracted # <-- Hier werden die Daten für den Branch-Teil aktualisiert! - wiki_data_updated_in_this_run = True # Setze Flag - # Füge Updates für M-R und AN hinzu - updates.append({'range': f'M{row_num_in_sheet}', 'values': [[wiki_data.get('url', 'k.A.')]]}) - updates.append({'range': f'N{row_num_in_sheet}', 'values': [[wiki_data.get('first_paragraph', 'k.A.')]]}) - # ... (Updates für O, P, Q, R) ... - updates.append({'range': f'O{row_num_in_sheet}', 'values': [[wiki_data.get('branche', 'k.A.')]]}) - updates.append({'range': f'P{row_num_in_sheet}', 'values': [[wiki_data.get('umsatz', 'k.A.')]]}) - updates.append({'range': f'Q{row_num_in_sheet}', 'values': [[wiki_data.get('mitarbeiter', 'k.A.')]]}) - updates.append({'range': f'R{row_num_in_sheet}', 'values': [[wiki_data.get('categories', 'k.A.')]]}) - updates.append({'range': f'AN{row_num_in_sheet}', 'values': [[now_timestamp]]}) # Setze AN neu - - # Wenn der Trigger "X (URL Copied)" war, setze S zurück - if status_s_indicates_reparse: - 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': [["?"]]}) - debug_print(f" -> Status S zurückgesetzt auf '?' für erneute Verifikation.") - else: - debug_print(f" -> FEHLER: Keine neuen Wiki-Daten extrahiert.") - # wiki_data behält die alten/default Werte + # Setze S zurück, wenn Trigger 'X(Copied)' war oder wenn URL sich geändert hat + if status_s_indicates_reparse or (url_to_potentially_parse != 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: - debug_print(f"Zeile {row_num_in_sheet}: Überspringe Wikipedia Verarbeitung (AN vorhanden UND S != 'X (URL Copied)').") + logging.debug(f"Zeile {row_num_in_sheet}: Überspringe Wikipedia Verarbeitung (AN vorhanden UND S != 'X (URL Copied)').") + # WICHTIG: Obwohl wir nicht neu parsen, müssen wir die final_wiki_data + # mit den bereits im Sheet stehenden Werten für den nächsten Schritt befüllen. + # Das ist oben bei der Initialisierung von final_wiki_data bereits geschehen. # --- 3. ChatGPT Evaluationen (Branch etc.) --- - # Trigger: AO fehlt ODER Wiki wurde in DIESEM Lauf neu geparsed chat_ts_ao_missing = not get_cell_value("Timestamp letzte Prüfung").strip() - run_chat_eval = process_chatgpt and (chat_ts_ao_missing or wiki_data_updated_in_this_run) # <-- Nutze das neue Flag + # Trigger: AO fehlt ODER Wiki-Daten wurden in DIESEM Lauf neu geschrieben (M-R, AN) + run_chat_eval = process_chatgpt and (chat_ts_ao_missing or wiki_data_updated_in_this_run) if run_chat_eval: - debug_print(f"Zeile {row_num_in_sheet}: Starte ChatGPT Evaluationen (Grund: AO fehlt? {chat_ts_ao_missing}, Wiki gerade aktualisiert? {wiki_data_updated_in_this_run})...") + logging.info(f"Zeile {row_num_in_sheet}: Starte ChatGPT Evaluationen (Grund: AO fehlt? {chat_ts_ao_missing}, Wiki gerade aktualisiert? {wiki_data_updated_in_this_run})...") any_processing_done = True - # 3.1 Branchenevaluierung (Nutzt IMMER die aktuelle 'wiki_data' Variable) + # Nutze IMMER die 'final_wiki_data' für die Evaluation branch_result = evaluate_branche_chatgpt( crm_branche, crm_beschreibung, - wiki_data.get('branche', 'k.A.'), # Nimmt die potenziell neuen Daten - wiki_data.get('categories', 'k.A.'),# Nimmt die potenziell neuen Daten - website_summary - ) - updates.append({'range': f'W{row_num_in_sheet}', 'values': [[branch_result.get('branch', 'Fehler')]]}) - updates.append({'range': f'X{row_num_in_sheet}', 'values': [[branch_result.get('consistency', 'Fehler')]]}) - updates.append({'range': f'Y{row_num_in_sheet}', 'values': [[branch_result.get('justification', 'Fehler')]]}) + final_wiki_data.get('branche', 'k.A.'), + final_wiki_data.get('categories', 'k.A.'), + website_summary # Kommt aus Schritt 1 oder initialen Werten + ) # Annahme: nutzt logging + 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 weitere ChatGPT Evaluationen) ... + # ... (Hier weitere ChatGPT Evaluationen, immer mit final_wiki_data und aktuellen website_*) ... # Setze Timestamp letzte Prüfung (AO) - updates.append({'range': f'AO{row_num_in_sheet}', 'values': [[now_timestamp]]}) + 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: - debug_print(f"Zeile {row_num_in_sheet}: Überspringe ChatGPT Evaluationen (AO vorhanden UND Wiki nicht gerade aktualisiert).") + logging.debug(f"Zeile {row_num_in_sheet}: Überspringe ChatGPT Evaluationen (AO vorhanden UND Wiki nicht gerade aktualisiert).") # --- 4. Abschließende Updates --- if any_processing_done: - updates.append({'range': f'AP{row_num_in_sheet}', 'values': [[Config.VERSION]]}) + # Setze Version nur, wenn *irgendetwas* in dieser Zeile gemacht wurde + updates.append({'range': f'{self.sheet_handler._get_col_letter(COLUMN_MAP["Version"] + 1)}{row_num_in_sheet}', 'values': [[Config.VERSION]]}) - # --- 5. Batch Update --- + # --- 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) - if success: debug_print(f"Zeile {row_num_in_sheet}: Batch-Update erfolgreich ({len(updates)} Zellen/Bereiche).") - else: debug_print(f"Zeile {row_num_in_sheet}: FEHLER beim Batch-Update.") + if not success: logging.error(f"Zeile {row_num_in_sheet}: FEHLER beim Batch-Update.") else: - debug_print(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben.") + logging.info(f"Zeile {row_num_in_sheet}: Keine Updates zum Schreiben.") - debug_print(f"--- Verarbeitung für Zeile {row_num_in_sheet} abgeschlossen ---") - time.sleep(max(0.1, Config.RETRY_DELAY / 25)) # Noch kürzere Pause hier + logging.info(f"--- Verarbeitung für Zeile {row_num_in_sheet} abgeschlossen ---") + # Kurze Pause zwischen den Zeilen im sequenziellen Modus + time.sleep(max(0.1, getattr(Config, 'RETRY_DELAY', 5) / 20)) # Kleine Pause def process_rows_sequentially(self, start_row_index, num_rows_to_process, process_wiki=True, process_chatgpt=True, process_website=True):