This commit is contained in:
2025-04-22 12:21:33 +00:00
parent cca2191e2f
commit 564aef9d20

View File

@@ -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 <p>-Tags...")
for idx, p in enumerate(paragraphs):
if not p.get_text(strip=True):
self.logger.debug(f" -> Überspringe leeres <p> Tag (Index {idx})")
@@ -1970,22 +2020,18 @@ class WikipediaScraper:
if p.find(['img', 'table', 'figure', 'div'], recursive=False):
self.logger.debug(f" -> Überspringe <p> 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 <p> 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 <th> oder fett formatierten <td>.
"""
# ... (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,