1.5.1: Integrierter hybrider Geschlechtsdetektor & aktualisierte Kontakte-Spalten

- 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
This commit is contained in:
2025-04-10 05:49:56 +00:00
parent 3e89898477
commit 33c9f76460

View File

@@ -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 WY) 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):