diff --git a/brancheneinstufung.py b/brancheneinstufung.py index b49ebdba..24559b49 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -4,16 +4,17 @@ 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 = "1.2.8" + VERSION = "v1.2.8" # v1.2.8: Verbesserte numerische Extraktion für Umsatz und Mitarbeiter; robustere Vergleichslogik LANG = "de" CREDENTIALS_FILE = "service_account.json" SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" @@ -44,7 +45,7 @@ def debug_print(message): def clean_text(text): if not text: return "k.A." - text = str(text) + text = unicodedata.normalize("NFKC", str(text)) text = re.sub(r'\[\d+\]', '', text) text = re.sub(r'\s+', ' ', text).strip() return text if text else "k.A." @@ -70,63 +71,88 @@ def normalize_company_name(name): normalized = re.sub(r'\s+', ' ', normalized).strip() return normalized.lower() -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) +def extract_numeric_value(raw_value, is_umsatz=False): + raw_value = raw_value.strip() + if not raw_value or raw_value.lower() in ["k.a.", "n.a."]: + return "k.A." + # Entferne Texte wie "ca.", "circa", etc. + 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 Textteile in Klammern + raw_value = re.sub(r'\(.*?\)', '', raw_value).strip() + # Extrahiere den numerischen Teil + match = re.search(r'([\d.,]+)', raw_value) if not match: - 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('.', '') + debug_print(f"Keine numerischen Zeichen gefunden im Rohtext: '{raw_value}'") + return "k.A." + num_str = match.group(1) + # Wenn ein einzelnes Komma als Dezimaltrenner und kein Punkt vorhanden ist + if ',' in num_str and '.' not in num_str and len(num_str.split(',')[1]) <= 2: num_str = num_str.replace(',', '.') else: - # sonst entferne alle Punkte (als Tausendertrenner) - num_str = num_str.replace('.', '') + num_str = num_str.replace(',', '') try: - value = float(num_str) + num = float(num_str) except Exception as e: - 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 + 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))) else: - value *= scale - return value + return str(int(round(num))) -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(",", ".") +def compare_umsatz_values(crm, wiki): + debug_print(f"Vergleich CRM Umsatz: '{crm}' mit Wikipedia Umsatz: '{wiki}'") try: - return int(float(raw)) + crm_val = float(crm) + wiki_val = float(wiki) except Exception as e: - debug_print(f"Fehler beim Parsen von Mitarbeiterzahl: {raw} - {e}") - return None + 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." # ==================== GOOGLE SHEET HANDLER ==================== class GoogleSheetHandler: @@ -143,6 +169,26 @@ 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): @@ -153,8 +199,7 @@ class WikipediaScraper: website = website.lower().strip() website = re.sub(r'^https?:\/\/', '', website) website = re.sub(r'^www\.', '', website) - website = website.split('/')[0] - return website + return website.split('/')[0] def _generate_search_terms(self, company_name, website): terms = [] full_domain = self._get_full_domain(website) @@ -213,6 +258,14 @@ 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: @@ -220,7 +273,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'] + 'mitarbeiter': ['mitarbeiter', 'beschäftigte', 'personal', 'mitarbeiterzahl', 'angestellte', 'belegschaft', 'personalstärke'] } keywords = keywords_map.get(target, []) for row in infobox.find_all('tr'): @@ -235,11 +288,9 @@ class WikipediaScraper: clean_val = re.sub(r'\[.*?\]|\(.*?\)', '', raw_value) return ' '.join(clean_val.split()).strip() if target == 'umsatz': - parsed = parse_currency_value(raw_value) - return str(parsed) if parsed is not None else raw_value.strip() + return extract_numeric_value(raw_value, is_umsatz=True) if target == 'mitarbeiter': - parsed = parse_employee_value(raw_value) - return str(parsed) if parsed is not None else raw_value.strip() + return extract_numeric_value(raw_value, is_umsatz=False) 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'])) @@ -251,7 +302,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 token.lower() == field.lower(): + if field.lower() in token.lower(): j = i + 1 while j < len(tokens) and not tokens[j]: j += 1 @@ -259,26 +310,33 @@ 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.'} + return {'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', + 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.', 'full_infobox': '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', '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')) + 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) first_paragraph = self.extract_first_paragraph(page_url) return { 'url': page_url, 'first_paragraph': first_paragraph, - 'branche': branche_val, + 'branche': raw_branche, 'umsatz': umsatz_val, - 'mitarbeiter': mitarbeiter_val + 'mitarbeiter': mitarbeiter_val, + 'categories': categories_val, + 'full_infobox': full_infobox } 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.'} + return {'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', + 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.', 'full_infobox': 'k.A.'} @retry_on_failure def search_company_article(self, company_name, website): search_terms = self._generate_search_terms(company_name, website) @@ -304,55 +362,95 @@ class DataProcessor: def __init__(self): self.sheet_handler = GoogleSheetHandler() self.wiki_scraper = WikipediaScraper() - 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_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_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.'} - # Beispiel: Schreibe die Wikipedia-Daten in Spalten J bis N (anpassen an das neue Schema) - self.sheet_handler.sheet.update(values=[[ + 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 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.') - ]], 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 + 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) # Sicherstellen, dass Werte synchronisiert werden wiki_umsatz = company_data.get('umsatz', 'k.A.') - 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.) + 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) current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - 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.')}") + 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("----------------------------------------") time.sleep(Config.RETRY_DELAY) -# ==================== MAIN ==================== if __name__ == "__main__": - 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) + 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 processor = DataProcessor() processor.process_rows(num_rows) - print("\n✅ Wikipedia-Auswertung abgeschlossen") + print(f"\n✅ Wikipedia-Auswertung abgeschlossen ({Config.VERSION})")