From 33c9f764608bfae466ed01e2d7e9782c8a6a4483 Mon Sep 17 00:00:00 2001 From: Floke Date: Thu, 10 Apr 2025 05:49:56 +0000 Subject: [PATCH] 1.5.1: Integrierter hybrider Geschlechtsdetektor & aktualisierte Kontakte-Spalten MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Umstellung der Geschlechtsbestimmung: Zuerst gender-guesser, Fallback zu Genderize API - Geschlecht wird jetzt in Spalte D gespeichert (alle folgenden Felder rutschen um eine Spalte nach rechts) - Aktualisierte Header und Kontaktzeilen im Contacts-Blatt, inklusive API-Key aus "genderize_API_Key.txt" - Anpassung der Contact Research-Funktion zur Verarbeitung der geänderten Spalten --- brancheneinstufung.py | 135 ++++++++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 46 deletions(-) diff --git a/brancheneinstufung.py b/brancheneinstufung.py index ba185c15..194cd29a 100644 --- a/brancheneinstufung.py +++ b/brancheneinstufung.py @@ -1,28 +1,16 @@ #!/usr/bin/env python3 """ -Version: v1.5.0 +Version: v1.5.1 Datum: {aktuelles Datum} Git-Überschrift (max. 100 Zeichen): -Version 1.5.0 – Verbesserung der Website-Detail-Extraktion und Kontaktsuche +1.5.1: Integrierter hybrider Geschlechtsdetektor & aktualisierte Kontakte-Spalten Git-Änderungsbeschreibung: -- **Website-Extraktion verbessert:** - - Fix: Ausgabe in Spalte AR (Website Rohtext) und AS (Website Zusammenfassung) wird nun zwingend in jedem Datensatz geschrieben – dabei wird nun *unabhängig* vom Vorhandensein eines "x" in Spalte A gearbeitet. - - Erweiterte Debug-Ausgaben: Zusätzliche Log-Meldungen protokollieren nun explizit, welchen Text (insb. die ersten 100 Zeichen) der Website extrahiert wurde und welcher Zusammenfassungstext generiert wird. Dies erleichtert die Fehlersuche und bestätigt, was in Spalte AR/AS geschrieben wird. - - User-Agent-Header und optionale SSL-Überprüfung wurden in `get_website_raw()` implementiert, um Blockierungen zu vermeiden und Fehler besser zu diagnostizieren. - -- **Kontaktsuche (Modus 6) optimiert:** - - Überarbeitet: Die Suche nutzt nun explizit die CRM-Kurzform (Spalte C) als Filter, sodass nur Ergebnisse berücksichtigt werden, bei denen diese als Teil des Titels enthalten ist. - - Es wird in ein separates Arbeitsblatt "Contacts" geschrieben und die gefundenen Kontakte werden mit detaillierten Debug-Ausgaben protokolliert – dabei wird außerdem die Anzahl der Treffer pro Position (Serviceleiter, IT-Leiter, Geschäftsführer, Disponent) im Hauptblatt aktualisiert. - - Es wird nun darauf geachtet, dass der Suchlauf ohne Überschreiben bereits vorhandener Zeitstempel (ab Zeile AM7) durchgeführt wird. - -- **Allgemeine Verbesserungen und Debugging:** - - Detaillierte Log-Ausgaben in allen kritischen Funktionen (Web-Extraktion, LinkedIn-Suche, Branchenabgleich) wurden erweitert, um Nachvollziehbarkeit zu gewährleisten. - - Debug-Ausgaben wurden verbessert, um exakte Abfragen, Ergebnisse und die daraus resultierenden Schreibvorgänge in den jeweiligen Spalten (z. B. für die Branchenbewertung in den Spalten W–Y) zu protokollieren. - - Anpassungen im Code haben sicher gestellt, dass keine wichtigen Funktionen entfernt wurden – Fokus lag ausschließlich auf den von Dir beanstandeten Bereichen. - -Dieser Commit stellt sicher, dass die Website-Daten korrekt in den vorgesehenen Spalten abgelegt werden und die Kontaktsuche präziser und robuster arbeitet. +- Umstellung der Geschlechtsbestimmung: Zuerst gender-guesser, Fallback zu Genderize API +- Geschlecht wird jetzt in Spalte D gespeichert (alle folgenden Felder rutschen um eine Spalte nach rechts) +- Aktualisierte Header und Kontaktzeilen im Contacts-Blatt, inklusive API-Key aus "genderize_API_Key.txt" +- Anpassung der Contact Research-Funktion zur Verarbeitung der geänderten Spalten """ @@ -48,7 +36,7 @@ except ImportError: # ==================== KONFIGURATION ==================== class Config: - VERSION = "v1.4.9" + VERSION = "v1.5.1" LANG = "de" CREDENTIALS_FILE = "service_account.json" SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo" @@ -109,6 +97,36 @@ def simple_normalize_url(url): except Exception as e: return "k.A." +def get_gender(firstname): + """ + Ermittelt das Geschlecht anhand des Vornamens. + Zunächst wird gender-guesser genutzt. Ergibt sich ein unsicheres Ergebnis ("andy" oder "unknown"), + so wird als Fallback die Genderize API (mit API-Key aus der Datei "genderize_API_Key.txt") angefragt. + """ + d = gender.Detector() + result = d.get_gender(firstname) + if result in ["andy", "unknown"]: + try: + with open("genderize_API_Key.txt", "r") as f: + genderize_api_key = f.read().strip() + except Exception as e: + debug_print("Fehler beim Lesen des Genderize API-Schlüssels: " + str(e)) + return result + params = {"name": firstname, "apikey": genderize_api_key} + try: + response = requests.get("https://api.genderize.io", params=params, timeout=10) + data = response.json() + new_gender = data.get("gender") + if new_gender: + return new_gender + else: + return result + except Exception as e: + debug_print("Fehler bei der Genderize API-Anfrage: " + str(e)) + return result + else: + return result + def is_valid_company_article(wiki_categories): """ Prüft, ob in den Wikipedia-Kategorien ein Hinweis auf einen Unternehmensartikel enthalten ist. @@ -1464,7 +1482,7 @@ def search_linkedin_contact(company_name, website, position_query, crm_kurzform) crm_kurzform (str): Die manuell gepflegte Kurzform des Firmennamens. Returns: - dict oder None: Ein Dictionary mit den Kontaktdaten (Vorname, Nachname, Position) oder None, falls kein passender Kontakt gefunden wurde. + dict oder None: Ein Dictionary mit den Kontaktdaten (Vorname, Nachname, Position, LinkedInURL) oder None, falls kein passender Kontakt gefunden wurde. """ try: with open("serpApiKey.txt", "r") as f: @@ -1484,11 +1502,9 @@ def search_linkedin_contact(company_name, website, position_query, crm_kurzform) response = requests.get("https://serpapi.com/search", params=params, timeout=10) data = response.json() if "organic_results" in data and len(data["organic_results"]) > 0: - # Gehe die Ergebnisse durch und prüfe, ob der Titel den crm_kurzform-String enthält for result in data["organic_results"]: title = result.get("title", "") if crm_kurzform.lower() in title.lower(): - # Aufteilen des Titels in Namens- und Positionsbestandteile if "–" in title: parts = title.split("–") elif "-" in title: @@ -1504,13 +1520,15 @@ def search_linkedin_contact(company_name, website, position_query, crm_kurzform) else: firstname = name_part lastname = "" + linkedin_url = result.get("link", "") debug_print(f"Gefundener Kontakt: {firstname} {lastname}, Position: {pos_part}") return { "Firmenname": company_name, "Website": website, "Vorname": firstname, "Nachname": lastname, - "Position": pos_part + "Position": pos_part, + "LinkedInURL": linkedin_url } debug_print("Kein Treffer mit CRM-Kurzform in Titel gefunden.") return None @@ -1523,7 +1541,7 @@ def search_linkedin_contact(company_name, website, position_query, crm_kurzform) def count_linkedin_contacts(company_name, website, position_query, crm_kurzform): """ - Zählt über SERPAPI, wieviele LinkedIn-Kontakte für einen bestimmten Positionsbegriff existieren, + Zählt über SERPAPI, wie viele LinkedIn-Kontakte für einen bestimmten Positionsbegriff existieren, wobei als Filter zusätzlich geprüft wird, ob der Titel Teile der CRM-Kurzform enthält. Args: @@ -1567,6 +1585,7 @@ def count_linkedin_contacts(company_name, website, position_query, crm_kurzform) debug_print(f"Fehler bei der SerpAPI-Suche (Count): {e}") return 0 + def search_linkedin_contact(company_name, website, position_query, crm_kurzform): """ Sucht über SERPAPI einen einzelnen LinkedIn-Kontakt basierend auf der Positionsbezeichnung und der CRM-Kurzform des Unternehmens. @@ -1687,10 +1706,24 @@ def count_linkedin_contacts(company_name, website, position_query, crm_kurzform) def process_contact_research(): """ Sucht mithilfe der SerpAPI Kontakte für bestimmte Positionen für jedes Unternehmen. - Es werden zunächst die CRM-Daten (insbesondere CRM Kurzform in Spalte C) und die Firma sowie die Website aus dem Hauptblatt genommen. - Die gefundenen Kontakte, die den Filter (CRM Kurzform muss im Titel enthalten sein) erfüllen, werden in das Kontakt-Tabellenblatt eingetragen. - Zusätzlich werden die Trefferzahlen (als Summen pro Position) in das Hauptblatt in den entsprechenden Spalten (z. B. AI, AJ, AK, AL) - sowie ein Timestamp in Spalte AM geschrieben. + Es werden zunächst die CRM-Daten (insbesondere CRM Kurzform in Spalte C) sowie Firma und Website aus dem Hauptblatt genommen. + Die gefundenen Kontakte, welche den Filter (CRM Kurzform muss im Titel enthalten sein) erfüllen, werden in das Kontakte-Blatt eingetragen. + + Zusätzlich werden die Trefferzahlen (als Summen pro Position) in das Hauptblatt in den Spalten AI, AJ, AK, AL geschrieben + und ein Timestamp in Spalte AM gesetzt. + + Im Kontakte-Blatt wird folgende Spaltenstruktur verwendet: + A: Firmenname + B: CRM Kurzform + C: Website + D: Geschlecht (ermittelt aus dem Vornamen) + E: Vorname + F: Nachname + G: Position + H: Suchbegriffskategorie + I: LinkedIn-Link + J: Timestamp + Detaillierte Debug-Ausgaben sorgen für Transparenz bei der Ausführung. """ debug_print("Starte Contact Research (Modus 6)...") @@ -1702,33 +1735,32 @@ def process_contact_research(): data = main_sheet.get_all_values() # Ermittle die letzte Zeile in Spalte AM (Spalte 39), in der ein Timestamp eingetragen wurde - col_am = main_sheet.col_values(39) # Spalte AM hat den Index 39 (A=1, ..., AM=39) + col_am = main_sheet.col_values(39) # Spalte AM = 39. Spalte last_filled_row = 1 # Header-Zeile for idx, cell in enumerate(col_am): if cell.strip() != "": - last_filled_row = idx + 1 # idx ist 0-basiert, Zeilennummern beginnen bei 1 + last_filled_row = idx + 1 # 0-basierter Index -> Zeilennummer start_row = last_filled_row + 1 # Verarbeitung ab der nächsten Zeile debug_print(f"Letzter Timestamp in Spalte AM wurde in Zeile {last_filled_row} gefunden. Starte Verarbeitung ab Zeile {start_row}.") - # Falls start_row größer ist als die Anzahl der vorhandenen Zeilen, gibt es nichts weiter zu verarbeiten + # Falls start_row größer ist als die Anzahl der vorhandenen Zeilen, gibt es nichts zu verarbeiten. if start_row > len(data): debug_print("Keine neuen Zeilen zu verarbeiten, da Timestamp in Spalte AM bereits bis zum Ende vorhanden ist.") return - # Versuche, das Kontakte-Blatt zu öffnen; falls nicht vorhanden, erstelle es + # Kontakte-Blatt öffnen oder erstellen try: contacts_sheet = sh.worksheet("Contacts") except gspread.exceptions.WorksheetNotFound: contacts_sheet = sh.add_worksheet(title="Contacts", rows="1000", cols="10") - # Header um eine zusätzliche Spalte für Suchbegriffskategorie, LinkedIn-Link und Timestamp angepasst - header = ["Firmenname", "CRM Kurzform", "Website", "Vorname", "Nachname", "Position", + header = ["Firmenname", "CRM Kurzform", "Website", "Geschlecht", "Vorname", "Nachname", "Position", "Suchbegriffskategorie", "LinkedIn-Link", "Timestamp"] - contacts_sheet.update(values=[header], range_name="A1:I1") + contacts_sheet.update(values=[header], range_name="A1:J1") debug_print("Neues Blatt 'Contacts' erstellt und Header eingetragen.") - # Verarbeite jede Zeile im Hauptblatt ab der festgelegten Startzeile + # Verarbeite jede Zeile ab der ermittelten Startzeile for i in range(start_row, len(data) + 1): - row = data[i - 1] # Weil data 0-basiert indiziert ist + row = data[i - 1] # data ist 0-basiert company_name = row[1] if len(row) > 1 else "" crm_kurzform = row[2] if len(row) > 2 else "" website = row[3] if len(row) > 3 else "" @@ -1738,26 +1770,38 @@ def process_contact_research(): positions = ["Serviceleiter", "IT-Leiter", "Geschäftsführer", "Disponent"] contact_counts = {} - # Für jeden Positionsbegriff: zähle Kontakte und suche exemplarisch einen Kontakt, der die CRM Kurzform im Titel enthält + # Für jeden Positionsbegriff: Kontakte zählen und exemplarisch einen Kontakt suchen for pos in positions: count = count_linkedin_contacts(crm_kurzform, website, pos, crm_kurzform) contact_counts[pos] = count - # Suche und speichere den ersten passenden Kontakt contact = search_linkedin_contact(crm_kurzform, website, pos, crm_kurzform) if contact: - # In Spalte G wird der Suchbegriff (Kategorie) eingetragen, - # in Spalte H der LinkedIn-Link und in Spalte I der Timestamp. + # Ermittle das Geschlecht anhand des Vornamens + firstname = contact.get("Vorname", "") + gender_value = get_gender(firstname) if firstname else "unknown" + # Erstelle den Kontaktzeile-Eintrag: + # Spalte A: Firmenname + # Spalte B: CRM Kurzform + # Spalte C: Website + # Spalte D: Geschlecht + # Spalte E: Vorname + # Spalte F: Nachname + # Spalte G: Position + # Spalte H: Suchbegriffskategorie + # Spalte I: LinkedIn-Link + # Spalte J: Timestamp timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") contact_row = [ company_name, crm_kurzform, website, + gender_value, # Geschlecht (Spalte D) contact.get("Vorname", ""), contact.get("Nachname", ""), contact.get("Position", ""), - pos, # Suchbegriffskategorie in Spalte G - contact.get("LinkedInURL", ""), # LinkedIn-Link in Spalte H - timestamp # Timestamp in Spalte I + pos, # Suchbegriffskategorie (Spalte H) + contact.get("LinkedInURL", ""), + timestamp # Timestamp (Spalte J) ] try: contacts_sheet.append_row(contact_row) @@ -1767,7 +1811,7 @@ def process_contact_research(): else: debug_print(f"Zeile {i}: Kein passender Kontakt für Position '{pos}' gefunden.") - # Aktualisiere die Hauptblatt-Zeile mit der Summe der Treffer in den Spalten AI, AJ, AK, AL und schreibe den Timestamp in AM + # Aktualisierung des Hauptblatts try: main_sheet.update(values=[[str(contact_counts.get("Serviceleiter", 0))]], range_name=f"AI{i}") main_sheet.update(values=[[str(contact_counts.get("IT-Leiter", 0))]], range_name=f"AJ{i}") @@ -1782,7 +1826,6 @@ def process_contact_research(): debug_print("Contact Research abgeschlossen.") - # ----------------- DataProcessor-Klasse inklusive neuer SERP-API Website Lookup-Methode ----------------- class DataProcessor: def __init__(self):