This commit is contained in:
2025-04-21 12:39:07 +00:00
parent 6015046c51
commit 166c87a451

View File

@@ -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 <p>-Tags direkt unter dem Suchbereich (oft relevanter)
paragraphs = search_area.find_all('p', recursive=False)
if not paragraphs: # Fallback: Alle <p>-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 <p>-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 <p> 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 <p> 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 <span>)
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 <p> 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 <td>-Tags (z.B. <td style="font-weight:bold">).
Berücksichtigt Header in <th> oder fett formatierten <td>.
"""
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 <th> + <td>
# 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 <td> (oft mit style="font-weight:bold;") + <td>
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 (<b>, <strong>)
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):