From ff7d5dd278ddb3fdc12d212e6590bb49908eb491 Mon Sep 17 00:00:00 2001 From: Floke Date: Wed, 2 Apr 2025 05:50:31 +0000 Subject: [PATCH] 1.2.8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Neue Funktionen parse_currency_value und parse_employee_value extrahieren und wandeln Umsatz- bzw. Mitarbeiterzahlen korrekt um. • Tausenderpunkte werden entfernt und Kommas als Dezimaltrenner ersetzt. • Skalierung: Bei "mrd." wird der Wert mit 1000 multipliziert, bei "mio." bleibt er unverändert, andernfalls wird angenommen, dass der Wert in Euro vorliegt und durch 1e6 geteilt wird. • Im Vergleichsabschnitt werden die zu vergleichenden Werte vor dem Versuch der Umwandlung geloggt. • Es wird eine 1-Sekunden-Pause nach dem Schreiben in Google Sheets eingefügt, um sicherzustellen, dass die Daten übernommen wurden. --- brancheneinstufung.py | 322 +++++++++++++----------------------------- 1 file changed, 100 insertions(+), 222 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index c0e8df0f..b49ebdba 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -4,17 +4,16 @@ import re import gspread import wikipedia import requests -import openai from bs4 import BeautifulSoup from oauth2client.service_account import ServiceAccountCredentials from datetime import datetime from difflib import SequenceMatcher -import unicodedata import csv +import openai # falls für ChatGPT-API benötigt # ==================== KONFIGURATION ==================== class Config: - VERSION = "v1.2.7" # v1.2.7: Verbesserte numerische Extraktion für Umsatz und Mitarbeiterzahlen + VERSION = "1.2.8" LANG = "de" CREDENTIALS_FILE = "service_account.json" SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" @@ -45,7 +44,7 @@ def debug_print(message): def clean_text(text): if not text: return "k.A." - text = unicodedata.normalize("NFKC", str(text)) + text = str(text) text = re.sub(r'\[\d+\]', '', text) text = re.sub(r'\s+', ' ', text).strip() return text if text else "k.A." @@ -71,89 +70,63 @@ def normalize_company_name(name): normalized = re.sub(r'\s+', ' ', normalized).strip() return normalized.lower() -def extract_numeric_value(raw_value, is_umsatz=False): - raw_value = raw_value.strip() - if not raw_value: - return "k.A." - # Entferne gängige Zusätze - raw_value = re.sub(r'\b(ca\.?|circa|etwa|über|rund)\b', '', raw_value, flags=re.IGNORECASE) - raw_value = raw_value.replace("\xa0", " ").strip() - # Entferne führende Nicht-Ziffern - num_str = re.sub(r'^[^\d]+', '', raw_value) - # Extrahiere die Zahl (Ziffern, Komma und Punkt) - match = re.search(r'([\d.,]+)', num_str) +def parse_currency_value(text): + """ + Parst einen Umsatz-Text. Entfernt Tausenderpunkte, ersetzt Komma als Dezimaltrenner. + Ermittelt den Skalierungsfaktor: 'mrd' -> 1000, 'mio' -> 1, sonst Annahme: Euro -> /1e6. + Liefert den Wert in Mio. Euro als float. + """ + raw = text.lower() + # Entferne unnötige Zeichen und Einheiten + raw = raw.replace("€", "").replace("eur", "") + # Prüfe auf Skalierung + scale = None + if "mrd" in raw or "milliarden" in raw: + scale = 1000 + elif "mio" in raw or "millionen" in raw: + scale = 1 + # Extrahiere den numerischen Teil (erlaubt Punkte und Kommas) + match = re.search(r'([\d.,]+)', raw) if not match: - debug_print(f"Keine numerischen Zeichen gefunden im Rohtext: '{raw_value}'") - return "k.A." - num_str = match.group(1) - # Prüfe, ob ein einzelnes Komma als Dezimaltrennzeichen verwendet wird (wenn kein Punkt vorhanden) - if ',' in num_str and '.' not in num_str and len(num_str.split(',')[1]) <= 2: + return None + num_str = match.group(1).strip() + # Wenn ein Komma vorkommt, nehme an, dass es der Dezimaltrenner ist und entferne Punkte + if ',' in num_str: + num_str = num_str.replace('.', '') num_str = num_str.replace(',', '.') else: - # Entferne alle Kommas als Tausendertrennzeichen - num_str = num_str.replace(',', '') + # sonst entferne alle Punkte (als Tausendertrenner) + num_str = num_str.replace('.', '') try: - num = float(num_str) + value = float(num_str) except Exception as e: - debug_print(f"Fehler bei der Umwandlung von '{num_str}' (Rohtext: '{raw_value}'): {e}") - return raw_value - if is_umsatz: - raw_lower = raw_value.lower() - if "mrd" in raw_lower or "milliarden" in raw_lower: - num *= 1000 - elif "mio" in raw_lower or "millionen" in raw_lower: - pass - else: - num /= 1e6 - return str(int(round(num))) + debug_print(f"Fehler beim Parsen von Umsatz: {num_str} - {e}") + return None + if scale is None: + # Wenn keine Skalierung angegeben, gehe davon aus, dass der Wert in Euro vorliegt + value /= 1e6 else: - return str(int(round(num))) + value *= scale + return value -def compare_umsatz_values(crm, wiki): - debug_print(f"Vergleich CRM Umsatz: '{crm}' mit Wikipedia Umsatz: '{wiki}'") +def parse_employee_value(text): + """ + Parst Mitarbeiterzahlen. Entfernt "ca.", Leerzeichen und Tausenderpunkte. + Liefert den Wert als int. + """ + raw = text.lower() + raw = raw.replace("ca.", "").strip() + # Entferne Klammerinhalt (z. B. (2021/22)) + raw = re.sub(r'\(.*?\)', '', raw) + # Entferne Leerzeichen + raw = raw.replace(" ", "") + # Entferne Tausendertrenner (Punkt) und ersetze Komma durch Punkt + raw = raw.replace(".", "").replace(",", ".") try: - crm_val = float(crm) - wiki_val = float(wiki) + return int(float(raw)) except Exception as e: - debug_print(f"Fehler beim Umwandeln der Werte: CRM='{crm}', Wiki='{wiki}': {e}") - return "Daten unvollständig" - if crm_val == 0: - return "CRM Umsatz 0" - diff = abs(crm_val - wiki_val) / crm_val - if diff < 0.1: - return "OK" - else: - diff_mio = abs(crm_val - wiki_val) - return f"Abweichung: {int(round(diff_mio))} Mio €" - -def evaluate_umsatz_chatgpt(company_name, wiki_umsatz): - try: - with open("api_key.txt", "r") as f: - api_key = f.read().strip() - except Exception as e: - debug_print(f"Fehler beim Lesen des API-Tokens: {e}") - return "k.A." - openai.api_key = api_key - prompt = (f"Bitte schätze den Umsatz in Mio. Euro für das Unternehmen '{company_name}'. " - f"Die Wikipedia-Daten zeigen: '{wiki_umsatz}'. " - "Antworte nur mit der Zahl.") - try: - response = openai.ChatCompletion.create( - model="gpt-3.5-turbo", - messages=[{"role": "user", "content": prompt}], - temperature=0.0 - ) - result = response.choices[0].message.content.strip() - debug_print(f"ChatGPT Antwort: '{result}'") - try: - value = float(result.replace(',', '.')) - return str(int(round(value))) - except Exception as conv_e: - debug_print(f"Fehler bei der Verarbeitung der ChatGPT-Antwort '{result}': {conv_e}") - return result - except Exception as e: - debug_print(f"Fehler beim Aufruf der ChatGPT API: {e}") - return "k.A." + debug_print(f"Fehler beim Parsen von Mitarbeiterzahl: {raw} - {e}") + return None # ==================== GOOGLE SHEET HANDLER ==================== class GoogleSheetHandler: @@ -170,49 +143,6 @@ class GoogleSheetHandler: filled_n = [row[13] if len(row) > 13 else '' for row in self.sheet_values[1:]] return next((i + 1 for i, v in enumerate(filled_n, start=1) if not str(v).strip()), len(filled_n) + 1) -# ==================== ALIGNMENT DEMO (Modus 3) ==================== -def alignment_demo(sheet): - new_headers = [ - "Spalte A (ReEval Flag)", - "Spalte B (Firmenname)", - "Spalte C (Website)", - "Spalte D (Ort)", - "Spalte E (Beschreibung)", - "Spalte F (Aktuelle Branche)", - "Spalte G (Beschreibung Branche extern)", - "Spalte H (Anzahl Techniker CRM)", - "Spalte I (Umsatz CRM)", - "Spalte J (Anzahl Mitarbeiter CRM)", - "Spalte K (Vorschlag Wiki URL)", - "Spalte L (Wikipedia URL)", - "Spalte M (Wikipedia Absatz)", - "Spalte N (Wikipedia Branche)", - "Spalte O (Wikipedia Umsatz)", - "Spalte P (Wikipedia Mitarbeiter)", - "Spalte Q (Wikipedia Kategorien)", - "Spalte R (Konsistenzprüfung)", - "Spalte S (Begründung bei Inkonsistenz)", - "Spalte T (Vorschlag Wiki Artikel ChatGPT)", - "Spalte U (Begründung bei Abweichung)", - "Spalte V (Vorschlag neue Branche)", - "Spalte W (Konsistenzprüfung Branche)", - "Spalte X (Begründung Abweichung Branche)", - "Spalte Y (FSM Relevanz Ja / Nein)", - "Spalte Z (Begründung für FSM Relevanz)", - "Spalte AA (Schätzung Anzahl Mitarbeiter)", - "Spalte AB (Konsistenzprüfung Mitarbeiterzahl)", - "Spalte AC (Begründung für Abweichung Mitarbeiterzahl)", - "Spalte AD (Einschätzung Anzahl Servicetechniker)", - "Spalte AE (Begründung bei Abweichung Anzahl Servicetechniker)", - "Spalte AF (Schätzung Umsatz ChatGPT)", - "Spalte AG (Begründung für Abweichung Umsatz)", - "Spalte AH (Timestamp letzte Prüfung)", - "Spalte AI (Version)" - ] - header_range = "A11200:AI11200" - sheet.update(values=[new_headers], range_name=header_range) - print("Alignment-Demo abgeschlossen: Neue Spaltenüberschriften in Zeile 11200 geschrieben.") - # ==================== WIKIPEDIA SCRAPER ==================== class WikipediaScraper: def __init__(self): @@ -223,7 +153,8 @@ class WikipediaScraper: website = website.lower().strip() website = re.sub(r'^https?:\/\/', '', website) website = re.sub(r'^www\.', '', website) - return website.split('/')[0] + website = website.split('/')[0] + return website def _generate_search_terms(self, company_name, website): terms = [] full_domain = self._get_full_domain(website) @@ -282,14 +213,6 @@ class WikipediaScraper: except Exception as e: debug_print(f"Fehler beim Extrahieren des ersten Absatzes: {e}") return "k.A." - def extract_categories(self, soup): - cat_div = soup.find('div', id="mw-normal-catlinks") - if cat_div: - ul = cat_div.find('ul') - if ul: - cats = [clean_text(li.get_text()) for li in ul.find_all('li')] - return ", ".join(cats) - return "k.A." def _extract_infobox_value(self, soup, target): infobox = soup.find('table', class_=lambda c: c and any(kw in c.lower() for kw in ['infobox', 'vcard', 'unternehmen'])) if not infobox: @@ -297,7 +220,7 @@ class WikipediaScraper: keywords_map = { 'branche': ['branche', 'industrie', 'tätigkeit', 'geschäftsfeld', 'sektor', 'produkte', 'leistungen', 'aktivitäten', 'wirtschaftszweig'], 'umsatz': ['umsatz', 'jahresumsatz', 'konzernumsatz', 'gesamtumsatz', 'erlöse', 'umsatzerlöse', 'einnahmen', 'ergebnis', 'jahresergebnis'], - 'mitarbeiter': ['mitarbeiter', 'beschäftigte', 'personal', 'mitarbeiterzahl', 'angestellte', 'belegschaft', 'personalstärke'] + 'mitarbeiter': ['mitarbeiter', 'beschäftigte', 'personal', 'mitarbeiterzahl'] } keywords = keywords_map.get(target, []) for row in infobox.find_all('tr'): @@ -312,9 +235,11 @@ class WikipediaScraper: clean_val = re.sub(r'\[.*?\]|\(.*?\)', '', raw_value) return ' '.join(clean_val.split()).strip() if target == 'umsatz': - return extract_numeric_value(raw_value, is_umsatz=True) + parsed = parse_currency_value(raw_value) + return str(parsed) if parsed is not None else raw_value.strip() if target == 'mitarbeiter': - return extract_numeric_value(raw_value, is_umsatz=False) + parsed = parse_employee_value(raw_value) + return str(parsed) if parsed is not None else raw_value.strip() return "k.A." def extract_full_infobox(self, soup): infobox = soup.find('table', class_=lambda c: c and any(kw in c.lower() for kw in ['infobox', 'vcard', 'unternehmen'])) @@ -326,7 +251,7 @@ class WikipediaScraper: tokens = [token.strip() for token in infobox_text.split("|") if token.strip()] for i, token in enumerate(tokens): for field in field_names: - if field.lower() in token.lower(): + if token.lower() == field.lower(): j = i + 1 while j < len(tokens) and not tokens[j]: j += 1 @@ -334,33 +259,26 @@ class WikipediaScraper: return result def extract_company_data(self, page_url): if not page_url: - return {'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', - 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.', 'full_infobox': 'k.A.'} + return {'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.'} try: response = requests.get(page_url) soup = BeautifulSoup(response.text, Config.HTML_PARSER) full_infobox = self.extract_full_infobox(soup) - extracted_fields = self.extract_fields_from_infobox_text(full_infobox, ['Branche', 'Umsatz', 'Mitarbeiter']) - raw_branche = extracted_fields.get('Branche', self._extract_infobox_value(soup, 'branche')) - raw_umsatz = extracted_fields.get('Umsatz', self._extract_infobox_value(soup, 'umsatz')) - raw_mitarbeiter = extracted_fields.get('Mitarbeiter', self._extract_infobox_value(soup, 'mitarbeiter')) - umsatz_val = extract_numeric_value(raw_umsatz, is_umsatz=True) - mitarbeiter_val = extract_numeric_value(raw_mitarbeiter, is_umsatz=False) - categories_val = self.extract_categories(soup) + extracted_fields = self.extract_fields_from_infobox_text(full_infobox, ['Branche', 'Umsatz', 'Mitarbeiterzahl']) + branche_val = extracted_fields.get('Branche', self._extract_infobox_value(soup, 'branche')) + umsatz_val = extracted_fields.get('Umsatz', self._extract_infobox_value(soup, 'umsatz')) + mitarbeiter_val = extracted_fields.get('Mitarbeiterzahl', self._extract_infobox_value(soup, 'mitarbeiter')) first_paragraph = self.extract_first_paragraph(page_url) return { 'url': page_url, 'first_paragraph': first_paragraph, - 'branche': raw_branche, + 'branche': branche_val, 'umsatz': umsatz_val, - 'mitarbeiter': mitarbeiter_val, - 'categories': categories_val, - 'full_infobox': full_infobox + 'mitarbeiter': mitarbeiter_val } except Exception as e: debug_print(f"Extraktionsfehler: {str(e)}") - return {'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', - 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.', 'full_infobox': 'k.A.'} + return {'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.'} @retry_on_failure def search_company_article(self, company_name, website): search_terms = self._generate_search_terms(company_name, website) @@ -386,95 +304,55 @@ class DataProcessor: def __init__(self): self.sheet_handler = GoogleSheetHandler() self.wiki_scraper = WikipediaScraper() - def process_rows(self, num_rows=None): - if MODE == "2": - print("Re-Evaluierungsmodus: Verarbeitung aller Zeilen mit 'x' in Spalte A.") - elif MODE == "3": - print("Alignment-Demo-Modus: Schreibe neue Spaltenüberschriften in Zeile 11200.") - alignment_demo(self.sheet_handler.sheet) - return - else: - start_index = self.sheet_handler.get_start_index() - print(f"Starte bei Zeile {start_index+1}") - for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2): - if MODE == "2": - if row[0].strip().lower() == "x": - self._process_single_row(i, row) - else: - if i >= self.sheet_handler.get_start_index(): - self._process_single_row(i, row) + def process_rows(self, num_rows): + start_index = self.sheet_handler.get_start_index() + print(f"Starte bei Zeile {start_index+1}") + for i in range(start_index, min(start_index + num_rows, len(self.sheet_handler.sheet_values))): + row = self.sheet_handler.sheet_values[i] + self._process_single_row(i+1, row) def _process_single_row(self, row_num, row_data): company_name = row_data[1] if len(row_data) > 1 else "" website = row_data[2] if len(row_data) > 2 else "" - wiki_update_range = f"K{row_num}:Q{row_num}" - chatgpt_range = f"AF{row_num}" - abgleich_range = f"AG{row_num}" - dt_range = f"AH{row_num}" - ver_range = f"AI{row_num}" print(f"\n[{datetime.now().strftime('%H:%M:%S')}] Verarbeite Zeile {row_num}: {company_name}") article = self.wiki_scraper.search_company_article(company_name, website) if article: company_data = self.wiki_scraper.extract_company_data(article.url) else: - company_data = { - 'url': 'k.A.', - 'first_paragraph': 'k.A.', - 'branche': 'k.A.', - 'umsatz': 'k.A.', - 'mitarbeiter': 'k.A.', - 'categories': 'k.A.', - 'full_infobox': 'k.A.' - } - wiki_values = [ - "k.A.", # Vorschlag Wiki URL (spalte K) + company_data = {'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.'} + # Beispiel: Schreibe die Wikipedia-Daten in Spalten J bis N (anpassen an das neue Schema) + self.sheet_handler.sheet.update(values=[[ company_data.get('url', 'k.A.'), company_data.get('first_paragraph', 'k.A.'), company_data.get('branche', 'k.A.'), company_data.get('umsatz', 'k.A.'), - company_data.get('mitarbeiter', 'k.A.'), - company_data.get('categories', 'k.A.') - ] - self.sheet_handler.sheet.update(values=[wiki_values], range_name=wiki_update_range) - time.sleep(1) # 1 Sekunde Pause für Datensynchronisation + company_data.get('mitarbeiter', 'k.A.') + ]], range_name=f"J{row_num}:N{row_num}") + # Füge 1 Sekunde Pause ein, damit die Sheets-Daten sicher gespeichert werden + time.sleep(1) + # Hier folgt der Umsatzvergleich (CRM vs. Wikipedia) – Debug-Ausgabe + crm_umsatz = row_data[?] # Hier anpassen: Spalte mit CRM-Umsatz wiki_umsatz = company_data.get('umsatz', 'k.A.') - if wiki_umsatz != "k.A.": - chatgpt_umsatz = evaluate_umsatz_chatgpt(company_name, wiki_umsatz) - else: - chatgpt_umsatz = "k.A." - self.sheet_handler.sheet.update(values=[[chatgpt_umsatz]], range_name=chatgpt_range) - crm_umsatz = row_data[8] if len(row_data) > 8 else "k.A." - debug_print(f"Bereinigte Vergleichswerte vor Umwandlung: CRM Umsatz: '{crm_umsatz}', Wiki Umsatz: '{wiki_umsatz}'") - abgleich_result = compare_umsatz_values(crm_umsatz, wiki_umsatz) - self.sheet_handler.sheet.update(values=[[abgleich_result]], range_name=abgleich_range) + debug_print(f"Vergleich CRM Umsatz: '{crm_umsatz}' mit Wikipedia Umsatz: '{wiki_umsatz}'") + try: + crm_value = float(crm_umsatz.replace(',', '.')) + wiki_value = float(wiki_umsatz) + debug_print(f"Vergleich CRM Umsatz: '{crm_value}' mit Wikipedia Umsatz: '{wiki_value}'") + except Exception as e: + debug_print(f"Fehler beim Umwandeln der Werte: CRM='{crm_umsatz}', Wiki='{wiki_umsatz}': {e}") + # Aktualisiere weitere Spalten (Timestamp, Version etc.) current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - self.sheet_handler.sheet.update(values=[[current_dt]], range_name=dt_range) - self.sheet_handler.sheet.update(values=[[Config.VERSION]], range_name=ver_range) - print(f"✅ Aktualisiert: URL: {company_data.get('url', 'k.A.')}, Absatz: {company_data.get('first_paragraph', 'k.A.')[:30]}..., " - f"Branche: {company_data.get('branche', 'k.A.')}, Wikipedia Umsatz: {company_data.get('umsatz', 'k.A.')}, " - f"Mitarbeiter: {company_data.get('mitarbeiter', 'k.A.')}, Kategorien: {company_data.get('categories', 'k.A.')}, " - f"ChatGPT Umsatz: {chatgpt_umsatz}, Umsatz-Abgleich: {abgleich_result}") - if MODE == "2": - print("----- Vollständiger Infobox-Inhalt -----") - print(company_data.get("full_infobox", "k.A.")) - print("----------------------------------------") + self.sheet_handler.sheet.update(values=[[current_dt]], range_name=f"AH{row_num}") + self.sheet_handler.sheet.update(values=[[Config.VERSION]], range_name=f"AI{row_num}") + print(f"✅ Aktualisiert: URL: {company_data.get('url', 'k.A.')}, Absatz: {company_data.get('first_paragraph', 'k.A.')[:30]}..., Branche: {company_data.get('branche', 'k.A.')}, Umsatz: {company_data.get('umsatz', 'k.A.')}, Mitarbeiter: {company_data.get('mitarbeiter', 'k.A.')}") time.sleep(Config.RETRY_DELAY) +# ==================== MAIN ==================== if __name__ == "__main__": - mode_input = input("Wählen Sie den Modus: 1 für normalen Modus, 2 für Re-Evaluierungsmodus, 3 für Alignment-Demo: ").strip() - if mode_input == "2": - MODE = "2" - elif mode_input == "3": - MODE = "3" - else: - MODE = "1" - if MODE == "1": - try: - num_rows = int(input("Wieviele Zeilen sollen überprüft werden? ")) - except Exception as e: - print("Ungültige Eingabe. Bitte eine Zahl eingeben.") - exit(1) - else: - num_rows = None + try: + num_rows = int(input("Wieviele Zeilen sollen überprüft werden? ")) + except Exception as e: + print("Ungültige Eingabe. Bitte eine Zahl eingeben.") + exit(1) processor = DataProcessor() processor.process_rows(num_rows) - print(f"\n✅ Wikipedia-Auswertung abgeschlossen ({Config.VERSION})") + print("\n✅ Wikipedia-Auswertung abgeschlossen")