diff --git a/brancheneinstufung.py b/brancheneinstufung.py index bd4e2611..9d4a9c10 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -1505,13 +1505,24 @@ class GoogleSheetHandler: class WikipediaScraper: """ Handles searching Wikipedia articles and extracting relevant company data. - Version: 1.6.5 - Improved infobox parsing reliability. + Version: 1.6.5 logic - Improved infobox parsing reliability and detailed logging. """ - def __init__(self, user_agent=None): # Optional User-Agent übergeben - # Verwende User-Agent aus Config, falls nicht anders angegeben - self.user_agent = user_agent or getattr(Config, 'USER_AGENT', 'Mozilla/5.0 (compatible; YourBotName/1.0; +http://yourwebsite.com/botinfo)') + 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 = { @@ -1520,348 +1531,351 @@ class WikipediaScraper: 'mitarbeiter': ['mitarbeiter', 'mitarbeiterzahl', 'beschäftigte', 'employees', 'number of employees', 'personal', 'belegschaft'] } try: - wikipedia.set_lang(Config.LANG) + wiki_lang = getattr(Config, 'LANG', 'de') # Sprache aus Config holen + wikipedia.set_lang(wiki_lang) wikipedia.set_rate_limiting(True) # Respektiere Wikipedia API Limits - debug_print(f"Wikipedia library language set to '{Config.LANG}'.") + self.logger.info(f"Wikipedia library language set to '{wiki_lang}'. Rate limiting enabled.") except Exception as e: - debug_print(f"WARNUNG: Fehler beim Setzen der Wikipedia-Sprache oder Rate Limiting: {e}") + self.logger.warning(f"Fehler beim Setzen der Wikipedia-Sprache oder Rate Limiting: {e}") - # Beibehalten: Hilfsfunktion für Domain def _get_full_domain(self, website): + """Extrahiert die normalisierte Domain (ohne www, ohne Pfad) aus einer URL.""" if not website or not isinstance(website, str): return "" - website = website.lower().strip() - website = re.sub(r'^https?:\/\/', '', website) - website = re.sub(r'^www\.', '', website) - return website.split('/')[0] + 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 - # Beibehalten: Suchbegriffe generieren def _generate_search_terms(self, company_name, website): - terms = set(); full_domain = self._get_full_domain(website) - if full_domain: terms.add(full_domain) - normalized_name = normalize_company_name(company_name) # Annahme: normalize_company_name existiert + """Generiert eine Liste von Suchbegriffen für die Wikipedia-Suche.""" + if not company_name: return [] + terms = set() + full_domain = self._get_full_domain(website) + if full_domain: terms.add(full_domain) # Domain als Suchbegriff + + # Normalisierten Namen hinzufügen (Annahme: Funktion 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) - # Füge Originalnamen hinzu, falls er signifikant anders ist - if company_name and company_name.lower() != normalized_name and company_name.lower() not in terms: - terms.add(company_name.lower()) + 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 - # Nimm nur die ersten paar sinnvollen Begriffe + # 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 - debug_print(f"Generierte Suchbegriffe für '{company_name}': {final_terms}") + self.logger.debug(f"Generierte Suchbegriffe für '{company_name}': {final_terms}") return final_terms - # retry_on_failure Decorator hier hinzufügen! - @retry_on_failure + @retry_on_failure # Annahme: Decorator existiert und behandelt Exceptions 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"): - logger.warning(f"_get_page_soup: Ungültige URL '{url}'.") + self.logger.warning(f"_get_page_soup: Ungültige URL '{url}'.") return None try: - logger.debug(f"_get_page_soup: Rufe URL ab: {url}") - response = self.session.get(url, timeout=15) # Timeout erhöhen + 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, da Wikipedia dies verwendet + # Encoding explizit auf UTF-8 setzen (Standard für Wikipedia) response.encoding = 'utf-8' - soup = BeautifulSoup(response.text, Config.HTML_PARSER) # Nutze Parser aus Config - logger.debug(f"_get_page_soup: Parsen von {url} erfolgreich.") + soup = BeautifulSoup(response.text, getattr(Config, 'HTML_PARSER', 'html.parser')) # Nutze Parser aus Config + self.logger.debug(f"_get_page_soup: Parsen von {url} erfolgreich.") return soup except requests.exceptions.Timeout: - logger.error(f"_get_page_soup: Timeout beim Abrufen von {url}") - return None + self.logger.error(f"_get_page_soup: Timeout beim Abrufen von {url}") + raise # Fehler weitergeben für Retry except requests.exceptions.RequestException as e: - logger.error(f"_get_page_soup: Fehler beim Abrufen von HTML von {url}: {e}") - # Hier keinen Fehler weitergeben, damit der Prozess weiterläuft, aber None zurückgeben - return None + self.logger.error(f"_get_page_soup: Netzwerkfehler beim Abrufen von HTML von {url}: {e}") + raise e # Fehler weitergeben für Retry except Exception as e: - logger.error(f"_get_page_soup: Fehler beim Parsen von HTML von {url}: {e}") - return None + 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 - # Überarbeitete Validierung 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. - Nutzt jetzt _get_page_soup für die Link-Prüfung. """ if not page or not company_name: return False - debug_print(f"Validiere Artikel '{page.title}' für Firma '{company_name}' (Website: {website})...") + self.logger.debug(f"Validiere Artikel '{page.title}' für Firma '{company_name}' (Website: {website})...") full_domain = self._get_full_domain(website) - normalized_company = normalize_company_name(company_name) # Annahme: normalize_company_name existiert + normalized_company = normalize_company_name(company_name) normalized_title = normalize_company_name(page.title) - # 1. Titelähnlichkeit prüfen + # 1. Titelähnlichkeit similarity = SequenceMatcher(None, normalized_title, normalized_company).ratio() - debug_print(f" -> Titelähnlichkeit: {similarity:.2f} ('{normalized_title}' vs '{normalized_company}')") + self.logger.debug(f" -> Titelähnlichkeit: {similarity:.2f} ('{normalized_title}' vs '{normalized_company}')") # 2. Link-Prüfung (nur wenn Domain vorhanden) domain_found = False if full_domain: - debug_print(f" -> Suche nach Domain '{full_domain}' in Links...") + 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 if soup: - # Suche zuerst in der Infobox (häufigster Ort für offizielle Links) - infobox = soup.select_one('table[class*="infobox"]') # Flexibler Selektor + # 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', '') - # Prüfe, ob der Link auf eine externe Seite geht und die Domain enthält - if href.startswith('http') and full_domain in href.lower(): - # Zusätzliche Prüfung: Ist der Link-Text relevant? (Optional) + if href.startswith('http') and full_domain in self._get_full_domain(href): # Vergleiche Domains link_text = link.get_text(strip=True).lower() - if any(kw in link_text for kw in ['website', 'webseite', 'offizielle']): - debug_print(f" -> Domain '{full_domain}' in Infobox-Link gefunden (Text: '{link_text}', URL: {href})") + # 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 \ + 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})") domain_found = True break - else: # Auch akzeptieren, wenn der Text nicht passt, aber die URL klar ist - debug_print(f" -> Domain '{full_domain}' in Infobox-Link gefunden (URL: {href})") + else: # Akzeptiere auch ohne Keyword, wenn URL eindeutig scheint + 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 des Artikels + # Wenn nicht in Infobox, suche in allen externen Links if not domain_found: - # Die wikipedia library liefert externe Links manchmal nicht zuverlässig, daher besser alle Links parsen - all_links = soup.find_all('a', href=True) + 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) + for link in all_links: href = link.get('href', '') - # Klasse 'external text' ist ein guter Indikator, aber nicht immer vorhanden - is_external = link.has_attr('class') and any(c in ['external', 'text'] for c in link['class']) - # Prüfe, ob es ein externer Link ist und die Domain enthält - if href.startswith('http') and full_domain in href.lower(): + 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']): - debug_print(f" -> Domain '{full_domain}' in externem Link gefunden (URL: {href}, External Class? {is_external})") + 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 else: - debug_print(f" -> Konnte HTML für Link-Prüfung von {page.url} nicht laden.") + self.logger.warning(f" -> Konnte HTML für Link-Prüfung von {page.url} nicht laden.") else: - debug_print(" -> Keine Website-Domain für Link-Prüfung vorhanden.") + self.logger.debug(" -> Keine Website-Domain für Link-Prüfung vorhanden.") - # 3. Entscheidung basierend auf Ähnlichkeit und Link - threshold = Config.SIMILARITY_THRESHOLD # Standard-Schwelle + # 3. Entscheidung + threshold = getattr(Config, 'SIMILARITY_THRESHOLD', 0.65) # Standard-Schwelle aus Config if domain_found: - threshold = max(0.4, threshold - 0.2) # Schwelle lockern, wenn Domain passt (aber nicht zu sehr) - debug_print(f" -> Domain gefunden, Schwelle angepasst auf {threshold:.2f}") + # Schwelle signifikant lockern, wenn Domain passt + threshold = max(0.35, threshold - 0.3) # Beispiel: 0.65 -> 0.35 + self.logger.debug(f" -> Domain gefunden, Ähnlichkeitsschwelle angepasst auf {threshold:.2f}") is_valid = similarity >= threshold - if is_valid: - debug_print(f" => Artikel '{page.title}' VALIDiert (Ähnlichkeit >= {threshold:.2f}, Domain gefunden? {domain_found})") - else: - debug_print(f" => Artikel '{page.title}' NICHT validiert (Ähnlichkeit < {threshold:.2f}, Domain gefunden? {domain_found})") + log_level = logging.INFO if is_valid else logging.DEBUG # Logge Erfolg als INFO + 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 - # Beibehalten: Extrahiere Kategorien def extract_categories(self, soup): + """Extrahiert Wikipedia-Kategorien aus dem Soup-Objekt.""" if not soup: return "k.A." + cats_filtered = [] try: cat_div = soup.find('div', id="mw-normal-catlinks") if cat_div: ul = cat_div.find('ul') if ul: - # Extrahiere Text, bereinige ihn und filtere den "Kategorien:" Link selbst cats = [clean_text(li.get_text()) for li in ul.find_all('li')] cats_filtered = [c for c in cats if c and "kategorien:" not in c.lower()] - return ", ".join(cats_filtered) if cats_filtered else "k.A." + self.logger.debug(f"Kategorien gefunden: {cats_filtered}") + else: self.logger.debug("Kein 'ul' in 'mw-normal-catlinks' gefunden.") + else: self.logger.debug("Kein 'div#mw-normal-catlinks' gefunden.") except Exception as e: - logger.error(f"Fehler beim Extrahieren der Kategorien: {e}") - return "k.A." + self.logger.error(f"Fehler beim Extrahieren der Kategorien: {e}") + + return ", ".join(cats_filtered) if cats_filtered else "k.A." - # Beibehalten: Extrahiere ersten Absatz def _extract_first_paragraph_from_soup(self, soup): + """Extrahiert den ersten aussagekräftigen Absatz aus dem Soup-Objekt.""" if not soup: return "k.A." + paragraph_text = "k.A." try: - # Finde das Haupt-Inhaltsdiv (üblich bei MediaWiki) + # Suche bevorzugt im Hauptinhalt content_div = soup.find('div', class_='mw-parser-output') - if not content_div: - content_div = soup.find('div', id='bodyContent') # Fallback - if not content_div: - content_div = soup # Fallback auf ganzen Soup + search_area = content_div if content_div else soup # Fallback auf ganzen Soup - # Suche nach dem ersten
-Tag, der direkten Text enthält (keine leeren
) - paragraphs = content_div.find_all('p', recursive=False) # Nur direkte Kinder suchen? Oft besser - if not paragraphs: - paragraphs = content_div.find_all('p', recursive=True) # Fallback: alle
+ # 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) + self.logger.debug(f"Suche ersten Absatz in {len(paragraphs)} gefundenen
-Tags...") - for p in paragraphs: - # Ignoriere
-Tags, die nur ein Bild oder eine Tabelle enthalten (heuristisch) - if p.find(['img', 'table', 'figure'], recursive=False): continue - # Entferne Referenz-Links [1], [2] etc. VOR der Textextraktion + 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() - # Extrahiere und bereinige Text - text = p.get_text(separator=' ', strip=True) - text = clean_text(text) # Annahme: clean_text entfernt u.a. doppelte Leerzeichen - # Nimm den ersten Absatz mit signifikanter Länge - if text and len(text) > 30: # Mindestlänge, um leere oder irrelevante
zu überspringen
- logger.debug(f"Ersten Absatz gefunden: {text[:100]}...")
- return text[:1000] # Begrenze Länge
- except Exception as e:
- logger.error(f"Fehler beim Extrahieren des ersten Absatzes: {e}")
- return "k.A."
+ # Entferne Koordinaten (oft am Anfang in )
+ for span in p.find_all('span', id='coordinates'):
+ span.decompose()
+
+ text = clean_text(p.get_text(separator=' ', strip=True))
+
+ # Prüfe Mindestlänge
+ if text and len(text) > 40: # Mindestlänge etwas erhöht
+ 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
+ else:
+ self.logger.debug(f" -> Überspringe Tag (Index {idx}), Text zu kurz oder leer nach clean_text: '{text[:50]}...'")
+
+ except Exception as e:
+ self.logger.error(f"Fehler beim Extrahieren des ersten Absatzes: {e}")
+
+ if paragraph_text == "k.A.":
+ self.logger.warning("Kein passender erster Absatz gefunden.")
+ return paragraph_text
- # --- NEUE, VERBESSERTE Infobox-Extraktion ---
def _extract_infobox_value(self, soup, target):
"""
- Extrahiert gezielt Branche, Umsatz oder Mitarbeiter aus der Infobox.
- Robuster gegen Variationen in HTML und Keyword-Bezeichnungen.
-
- Args:
- soup (BeautifulSoup): Das geparste HTML der Seite.
- target (str): Welcher Wert gesucht wird ('branche', 'umsatz', 'mitarbeiter').
-
- Returns:
- str: Der gefundene und bereinigte Wert oder "k.A.".
+ Extrahiert gezielt Branche, Umsatz oder Mitarbeiter aus der Infobox. (v1.6.5 Logik)
"""
+ # --- DEBUG LOG: Funktion betreten ---
+ self.logger.debug(f"--- Entering _extract_infobox_value for target '{target}' ---")
+
if not soup or target not in self.keywords_map:
- logger.debug(f"_extract_infobox_value: Ungültiger Input (Soup: {soup is not None}, Target: {target})")
+ 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]
- logger.debug(f"_extract_infobox_value: Suche nach '{target}' mit Keywords: {keywords}")
+ self.logger.debug(f"_extract_infobox_value: Suche nach '{target}' mit Keywords: {keywords}")
# Flexiblere Suche nach der Infobox
infobox = soup.select_one('table[class*="infobox"]')
- if not infobox:
- logger.debug(" -> Keine Infobox mit Klasse '*infobox*' gefunden.")
- # Fallback: Suche nach Tabellen mit typischen Attributen, falls Klasse fehlt
- infobox = soup.find('table', {'summary': re.compile(r'Infobox', re.I)})
- if not infobox:
- logger.debug(" -> Keine Infobox über summary='Infobox...' gefunden.")
- return "k.A."
- else:
- logger.debug(" -> Infobox über summary='Infobox...' gefunden.")
+ # --- DEBUG LOG: Infobox gefunden? ---
+ if infobox:
+ self.logger.debug(f" -> Infobox gefunden (via select_one 'table[class*=\"infobox\"]')")
else:
- logger.debug(f" -> Infobox gefunden (Selektor 'table[class*=\"infobox\"]'). Erste Zeilen: {str(infobox.find('tr'))[:100]}...")
+ self.logger.debug(" -> KEINE Infobox via select_one 'table[class*=\"infobox\"]' gefunden.")
+ # Optional: Fallback-Suche (hier ausgelassen für Klarheit)
+ return "k.A." # Frühzeitiger Ausstieg
-
- value_found = "k.A." # Standardwert
-
- # Iteriere durch alle Zeilen (tr) der Infobox
+ value_found = "k.A."
try:
rows = infobox.find_all('tr')
- logger.debug(f" -> Analysiere {len(rows)} Zeilen in der Infobox.")
+ self.logger.debug(f" -> Analysiere {len(rows)} Zeilen in der Infobox.")
for idx, row in enumerate(rows):
- header = row.find('th') # Finde die Header-Zelle
- value_cell = row.find('td') # Finde die Daten-Zelle
+ header_cells = row.find_all('th', recursive=False) # Nur direkte th Kinder
+ value_cells = row.find_all('td', recursive=False) # Nur direkte td Kinder
+
+ # Wir erwarten meistens ein th und ein td pro Zeile für Key-Value Paare
+ if len(header_cells) == 1 and len(value_cells) == 1:
+ header = header_cells[0]
+ value_cell = value_cells[0]
- if header and value_cell:
- # Normalisiere den Header-Text für den Keyword-Vergleich
header_text = header.get_text(strip=True)
header_text_lower = header_text.lower()
- # Entferne Doppelpunkte etc. für besseren Match
+ # Normalisierte Version für flexibleren Match
header_text_normalized = re.sub(r'[\s:]', '', header_text_lower)
- # Detailliertes Log für jede geprüfte Zeile (Header+Value)
- # Zeige den Wert nur gekürzt an, um Logs nicht zu fluten
raw_value_preview = value_cell.get_text(strip=True)[:50]
- logger.debug(f" -> Prüfe Zeile {idx}: TH='{header_text}' (Norm: '{header_text_normalized}') | TD Preview='{raw_value_preview}...'")
+ self.logger.debug(f" -> Prüfe Zeile {idx}: TH='{header_text}' (Norm: '{header_text_normalized}') | TD Preview='{raw_value_preview}...'")
- # Prüfe, ob *irgendein* Keyword im normalisierten Header enthalten ist
matched_keyword = None
for kw in keywords:
- # Suche nach dem Keyword als ganzes Wort oder am Anfang/Ende?
- # Einfacher: `in`-Operator prüft auf Substring
- if kw in header_text_lower: # Prüfe im weniger aggressiv normalisierten Text
+ # Prüfe ob Keyword im (nicht-normalisierten) Lowercase-Header vorkommt
+ if kw in header_text_lower:
matched_keyword = kw
break
if matched_keyword:
- logger.debug(f" --> Keyword '{matched_keyword}' gefunden in TH '{header_text}'!")
+ self.logger.debug(f" --> Keyword '{matched_keyword}' gefunden in TH '{header_text}'!")
- # --- Wert extrahieren und bereinigen ---
- # 1. Störende Elemente entfernen (Referenzen etc.)
- for sup in value_cell.find_all(['sup', 'span']): # Entferne und ggf. überflüssige
- # Spezifischer: Nur Referenzen entfernen?
- if sup.has_attr('class') and 'reference' in sup['class']:
- logger.debug(f" -> Entferne Referenz: {sup.get_text(strip=True)}")
- sup.decompose()
- # Entferne unsichtbare Elemente (manchmal in )
- elif sup.name == 'span' and sup.get('style') and 'display:none' in sup['style']:
- logger.debug(f" -> Entferne unsichtbares Span: {sup.get_text(strip=True)}")
- sup.decompose()
+ # Störende Elemente (Referenzen, unsichtbare Spans) im Value 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()
-
- # 2. Text extrahieren (mit Leerzeichen für
)
+ # Text extrahieren (mit Leerzeichen für
) und bereinigen
raw_value_text = value_cell.get_text(separator=' ', strip=True)
- logger.debug(f" -> Roher TD-Text nach Decompose: '{raw_value_text}'")
+ self.logger.debug(f" -> Roher TD-Text nach Decompose: '{raw_value_text}'")
+ cleaned_raw_value = clean_text(raw_value_text)
- # 3. Generelle Bereinigung (clean_text)
- cleaned_raw_value = clean_text(raw_value_text) # Annahme: clean_text ist definiert
-
- # 4. Spezifische Verarbeitung je nach Zieltyp
+ # Spezifische Verarbeitung
if target == 'branche':
- # Entferne oft vorkommende Zusätze wie (Stand: ...), [1], etc.
- clean_val = re.sub(r'\s*\[\d+\]', '', cleaned_raw_value) # Referenzen (falls clean_text sie nicht erwischt)
- clean_val = re.sub(r'\s*\([^)]*\)', '', clean_val).strip() # Klammerzusätze
- value_found = clean_val if clean_val else "k.A."
- logger.debug(f" --> Branche extrahiert: '{value_found}'")
-
+ # Einfache Bereinigung für Branche
+ clean_val = re.sub(r'\s*\([^)]*\)', '', cleaned_raw_value).strip() # Klammerzusätze entfernen
+ # Nimm nur den Teil vor einem möglichen Zeilenumbruch (manchmal gibt es Listen)
+ clean_val = clean_val.split('\n')[0].strip()
+ value_found = clean_val if clean_val else "k.A."
+ self.logger.debug(f" --> Branche extrahiert: '{value_found}'")
elif target == 'umsatz':
- # extract_numeric_value sollte robust sein
- numeric_val = extract_numeric_value(cleaned_raw_value, is_umsatz=True) # Annahme: existiert
- value_found = numeric_val
- logger.debug(f" --> Umsatz extrahiert (aus '{cleaned_raw_value}'): '{value_found}'")
-
+ numeric_val = extract_numeric_value(cleaned_raw_value, is_umsatz=True)
+ value_found = numeric_val
+ self.logger.debug(f" --> Umsatz extrahiert (aus '{cleaned_raw_value}'): '{value_found}'")
elif target == 'mitarbeiter':
- # extract_numeric_value sollte robust sein
- numeric_val = extract_numeric_value(cleaned_raw_value, is_umsatz=False) # Annahme: existiert
- value_found = numeric_val
- logger.debug(f" --> Mitarbeiter extrahiert (aus '{cleaned_raw_value}'): '{value_found}'")
+ numeric_val = extract_numeric_value(cleaned_raw_value, is_umsatz=False)
+ value_found = numeric_val
+ self.logger.debug(f" --> Mitarbeiter extrahiert (aus '{cleaned_raw_value}'): '{value_found}'")
- # Wenn ein Wert für das gesuchte Ziel gefunden wurde, beende die Schleife für dieses Ziel
- break # WICHTIG: Verhindert Überschreiben durch spätere Zeilen
-
- # else: # Optional: Loggen, wenn kein Keyword passte
- # logger.debug(f" --- Kein passendes Keyword in TH '{header_text}' gefunden.")
+ break # WICHTIG: Ersten Treffer für das gesuchte Ziel nehmen und Zeilenschleife verlassen
+ else:
+ # Logge Zeilen, die nicht dem Key-Value-Muster entsprechen (optional auf DEBUG)
+ # self.logger.debug(f" -> Überspringe Zeile {idx}: Struktur passt nicht (TH:{len(header_cells)}, TD:{len(value_cells)})")
+ pass
# Ende der Zeilenschleife
if value_found != "k.A.":
- logger.debug(f" -> Finaler Wert für '{target}' gefunden: '{value_found}'")
+ self.logger.debug(f" -> Finaler Wert für '{target}' gefunden: '{value_found}'")
else:
- logger.debug(f" -> Kein passender Eintrag für '{target}' in der gesamten Infobox gefunden.")
+ self.logger.debug(f" -> Kein passender Eintrag für '{target}' in der gesamten Infobox gefunden.")
except Exception as e:
- logger.error(f"Fehler beim Durchlaufen der Infobox-Zeilen für '{target}': {e}")
- # Gib "k.A." zurück, wenn Fehler beim Parsen auftreten
- return "k.A."
+ self.logger.exception(f"Fehler beim Durchlaufen der Infobox-Zeilen für '{target}': {e}")
+ return "k.A." # Fehler beim Parsen
return value_found
- # --- Kombinierte Datenextraktion (ruft Helfer auf) ---
def extract_company_data(self, page_url):
"""
- Extrahiert Firmendaten von einer Wikipedia-URL. Holt die Seite nur einmal
- und verwendet die verbesserte Infobox-Extraktion.
+ Extrahiert Firmendaten von einer Wikipedia-URL (v1.6.5 Logik).
"""
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:
- logger.warning(f"extract_company_data: Ungültige URL '{page_url}'.")
+ 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
- logger.info(f"Extrahiere Daten für Wiki-URL: {page_url}")
- soup = self._get_page_soup(page_url) # Hole und parse die Seite EINMAL
+ self.logger.info(f"Extrahiere Daten für Wiki-URL: {page_url}")
+ soup = self._get_page_soup(page_url)
if not soup:
- logger.error(f" -> Fehler: Konnte Seite {page_url} nicht laden oder parsen.")
- # Gib Default zurück, aber behalte die URL
- default_result['url'] = page_url
+ self.logger.error(f" -> Fehler: Konnte Seite {page_url} nicht laden oder parsen.")
+ default_result['url'] = page_url # Behalte URL im Ergebnis
return default_result
# --- Extrahiere Daten aus dem Soup-Objekt ---
- logger.debug(" -> Extrahiere ersten Absatz...")
+ self.logger.debug(" -> Extrahiere ersten Absatz...")
first_paragraph = self._extract_first_paragraph_from_soup(soup)
- logger.debug(" -> Extrahiere Kategorien...")
+ self.logger.debug(" -> Extrahiere Kategorien...")
categories_val = self.extract_categories(soup)
- logger.debug(" -> Extrahiere Branche aus Infobox...")
+ self.logger.debug(" -> Extrahiere Branche aus Infobox...")
branche_val = self._extract_infobox_value(soup, 'branche')
- logger.debug(" -> Extrahiere Umsatz aus Infobox...")
+ self.logger.debug(" -> Extrahiere Umsatz aus Infobox...")
umsatz_val = self._extract_infobox_value(soup, 'umsatz')
- logger.debug(" -> Extrahiere Mitarbeiter aus Infobox...")
+ self.logger.debug(" -> Extrahiere Mitarbeiter aus Infobox...")
mitarbeiter_val = self._extract_infobox_value(soup, 'mitarbeiter')
# --- Ergebnis zusammenstellen ---
@@ -1873,113 +1887,106 @@ class WikipediaScraper:
'mitarbeiter': mitarbeiter_val,
'categories': categories_val
}
- logger.info(f" -> Extrahierte Daten: P={first_paragraph[:30]}..., B='{branche_val}', U='{umsatz_val}', M='{mitarbeiter_val}', C={categories_val[:30]}...")
+ # 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
- # --- Artikelsuche (ruft Helfer auf) ---
- # retry_on_failure hier sinnvoll, da es API-Calls (wikipedia.search/page) macht
- @retry_on_failure
+ @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.
- Nutzt generierte Suchbegriffe und Validierung.
+ Sucht einen passenden Wikipedia-Artikel und gibt das page-Objekt zurück. (v1.6.5 Logik)
"""
if not company_name:
- logger.warning("Wikipedia search skipped: No company name provided.")
+ 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:
- logger.warning(f"Keine Suchbegriffe für '{company_name}' generiert.")
+ self.logger.warning(f"Keine Suchbegriffe für '{company_name}' generiert.")
return None
- logger.info(f"Starte Wikipedia-Suche für '{company_name}' (Website: {website}) mit Begriffen: {search_terms}")
+ self.logger.info(f"Starte Wikipedia-Suche für '{company_name}' (Website: {website}) mit Begriffen: {search_terms}")
- # Versuche direkten Match zuerst (oft der beste Treffer)
+ # Versuche direkten Match zuerst
try:
- logger.debug(f" -> Versuche direkten Match für '{company_name}'...")
+ self.logger.debug(f" -> Versuche direkten Match für '{company_name}'...")
page = wikipedia.page(company_name, auto_suggest=False, preload=True)
- logger.debug(f" -> Direkten Match gefunden: '{page.title}'. Validiere...")
+ self.logger.debug(f" -> Direkten Match gefunden: '{page.title}'. Validiere...")
if self._validate_article(page, company_name, website):
- logger.info(f" -> Direkter Match '{page.title}' ({page.url}) erfolgreich validiert.")
+ # Erfolg bereits hier geloggt durch _validate_article
return page
else:
- logger.debug(f" -> Direkter Match '{page.title}' nicht validiert. Fahre mit Suche fort.")
+ self.logger.debug(f" -> Direkter Match '{page.title}' nicht validiert. Fahre mit Suche fort.")
except wikipedia.exceptions.PageError:
- logger.debug(f" -> Kein direkter Artikel für '{company_name}' gefunden.")
+ self.logger.debug(f" -> Kein direkter Artikel für '{company_name}' gefunden.")
except wikipedia.exceptions.DisambiguationError as e:
- logger.debug(f" -> '{company_name}' ist eine Begriffsklärungsseite. Optionen: {e.options[:3]}...")
- # Optional: Prüfe die erste Option der Begriffsklärung?
+ self.logger.debug(f" -> '{company_name}' ist eine Begriffsklärungsseite. Optionen: {e.options[:3]}...")
+ # Optional: Prüfe die erste Option
if e.options:
try:
- first_option_title = e.options[0]
- logger.debug(f" -> Prüfe erste Option der Begriffsklärung: '{first_option_title}'")
- page = wikipedia.page(first_option_title, auto_suggest=False, preload=True)
+ 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):
- logger.info(f" -> Erste Option '{page.title}' ({page.url}) erfolgreich validiert.")
- return page
+ return page # Erfolg wird von _validate_article geloggt
else:
- logger.debug(f" -> Erste Option '{page.title}' nicht validiert.")
+ self.logger.debug(f" -> Erste Option '{page.title}' nicht validiert.")
except Exception as e_disamb:
- logger.warning(f" -> Fehler beim Laden/Validieren der ersten Disambiguation-Option '{e.options[0]}': {e_disamb}")
+ self.logger.warning(f" -> Fehler beim Laden/Validieren der Disambiguation-Option '{e.options[0]}': {e_disamb}")
except Exception as e_direct:
- logger.error(f" -> Unerwarteter Fehler beim direkten Zugriff auf '{company_name}': {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
- # Wenn direkter Match fehlschlägt oder nicht validiert, nutze die generierten Suchbegriffe
- logger.debug(f" -> Starte Suche mit generierten Begriffen: {search_terms}")
+ # Wenn direkter Match fehlschlägt, nutze die generierten Suchbegriffe
+ self.logger.debug(f" -> Starte Suche mit generierten Begriffen: {search_terms}")
for term in search_terms:
try:
- logger.debug(f" -> Suche mit Begriff: '{term}'...")
- results = wikipedia.search(term, results=Config.WIKIPEDIA_SEARCH_RESULTS)
- logger.debug(f" -> Suchergebnisse für '{term}': {results}")
- if not results: continue # Nächster Begriff, wenn keine Ergebnisse
+ 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 results:
- # Überspringe Titel, die wahrscheinlich keine Firmen sind (heuristisch)
- if any(ignore_word in title.lower() for ignore_word in ['liste von', 'disambiguation', 'begriffsklärung']):
- logger.debug(f" -> Überspringe wahrscheinlichen Nicht-Artikel: '{title}'")
+ 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:
- logger.debug(f" -> Prüfe potenziellen Artikel: '{title}'")
- # Lade Page-Objekt (preload=True für Effizienz bei Validierung)
+ self.logger.debug(f" -> Prüfe potenziellen Artikel: '{title}'")
page = wikipedia.page(title, auto_suggest=False, preload=True)
- # Validierung (nutzt jetzt intern _get_page_soup)
if self._validate_article(page, company_name, website):
- logger.info(f" -> Valider Artikel gefunden: '{page.title}' ({page.url})")
- return page # Gib das validierte Page-Objekt zurück
- # else: Artikel gefunden, aber nicht validiert -> nächsten Titel prüfen
- # Minimales Delay, um nicht zu schnell zu sein
- time.sleep(0.1)
+ # Erfolg wird von _validate_article geloggt
+ return page
+ time.sleep(0.1) # Kleines Delay
except wikipedia.exceptions.PageError:
- logger.debug(f" -> Seite '{title}' nicht gefunden (PageError).")
+ self.logger.debug(f" -> Seite '{title}' nicht gefunden (PageError).")
continue
except wikipedia.exceptions.DisambiguationError as e:
- logger.debug(f" -> Seite '{title}' ist Begriffsklärung: {e.options[:3]}...")
- # Optional: Versuche ersten Link der Begriffsklärung?
+ self.logger.debug(f" -> Seite '{title}' ist Begriffsklärung: {e.options[:3]}...")
continue
except requests.exceptions.RequestException as e_req:
- logger.warning(f" -> Netzwerkfehler beim Laden/Validieren von '{title}': {e_req}. Überspringe Titel.")
- # Kurze Pause bei Netzwerkfehler
+ self.logger.warning(f" -> Netzwerkfehler beim Laden/Validieren von '{title}': {e_req}. Überspringe Titel.")
time.sleep(1)
continue
except Exception as e_page:
- # Andere Fehler beim Laden/Validieren einer einzelnen Seite
- logger.error(f" -> Fehler bei Verarbeitung von Titel '{title}': {type(e_page).__name__} - {e_page}")
+ self.logger.error(f" -> Fehler bei Verarbeitung von Titel '{title}': {type(e_page).__name__} - {e_page}")
continue # Zum nächsten Titel
except requests.exceptions.RequestException as e_search_req:
- logger.error(f"Netzwerkfehler während Wikipedia-Suche für '{term}': {e_search_req}")
- # Bei Netzwerkfehler während der Suche ggf. kurz warten und nächsten Begriff versuchen
- time.sleep(2)
- continue
+ 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
+ raise e_search_req
except Exception as e_search:
- # Fehler bei der Suche selbst
- logger.error(f"Allgemeiner Fehler während Wikipedia-Suche für '{term}': {e_search}")
- # Hier nicht abbrechen, sondern nächsten Suchbegriff versuchen
- continue # Zum nächsten Suchbegriff
+ 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
- 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.")
return None