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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user