Files
Brancheneinstufung2/brancheneinstufung.py
2025-04-15 11:48:24 +00:00

2520 lines
119 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Version: v1.5.8
Datum: {aktuelles Datum}
Git-Überschrift (max. 100 Zeichen):
v1.5.8: Externe Branchenzuordnung mittels Mapping verfeinert
Git-Änderungsbeschreibung:
- Mapping-Funktion load_branch_mapping() integriert, um aus der CSV "ziel_Branchenschema.csv" automatisch ein Mapping-Dictionary zu erstellen.
- Neue Funktion map_external_branch() implementiert, die den von ChatGPT gelieferten externen Branchenbegriff (nach Normalisierung) in das standardisierte Ziel-Branchenschema überführt.
- In evaluate_branche_chatgpt wird zuerst der ChatGPT-Vorschlag geparst, unerwünschte Präfixe entfernt und über map_external_branch() in den korrekten Standardwert transformiert.
- Optional wird der CRM-Präfix ergänzt, falls der Mapping-Wert kein hierarchisches Trennzeichen enthält.
- Damit wird der externe Input selbstbewusster übernommen, solange er durch das Mapping bestätigt wird.
"""
import os
import time
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 gender_guesser.detector as gender
from urllib.parse import urlparse, urlencode
from difflib import SequenceMatcher
import argparse
# Optional: tiktoken für Token-Zählung (Modus 8)
try:
import tiktoken
except ImportError:
tiktoken = None
# ==================== KONFIGURATION ====================
class Config:
VERSION = "v1.5.8"
LANG = "de"
CREDENTIALS_FILE = "service_account.json"
SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo"
MAX_RETRIES = 3
RETRY_DELAY = 5
LOG_CSV = "gpt_antworten_log.csv"
SIMILARITY_THRESHOLD = 0.65
DEBUG = True
WIKIPEDIA_SEARCH_RESULTS = 5
HTML_PARSER = "html.parser"
BATCH_SIZE = 10
TOKEN_MODEL = "gpt-3.5-turbo"
# ==================== RETRY-DECORATOR ====================
def retry_on_failure(func):
def wrapper(*args, **kwargs):
for attempt in range(Config.MAX_RETRIES):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"⚠️ Fehler bei {func.__name__} (Versuch {attempt+1}): {str(e)[:100]}")
time.sleep(Config.RETRY_DELAY)
return None
return wrapper
# ==================== LOGGING & HELPER FUNCTIONS ====================
def simple_normalize_url(url):
"""
Normalisiert einen URL-String und gibt nur 'www.domain.tld' zurück.
- Entfernt das Schema (http://, https://)
- Schneidet den Pfad und eventuelle Portinformationen ab
- Fügt 'www.' hinzu, falls es fehlt.
Args:
url (str): Der Original-URL-String.
Returns:
str: Normalisierte URL im Format 'www.domain.tld' oder "k.A.", falls etwas fehlschlägt.
"""
if not url:
return "k.A."
# Falls kein Schema vorhanden ist, hinzufügen
if not url.lower().startswith("http"):
url = "https://" + url
try:
# Entferne das Schema
parts = url.split("://", 1)
domain_part = parts[1] if len(parts) > 1 else parts[0]
# Entferne den Pfad (alles ab dem ersten "/")
domain_part = domain_part.split("/", 1)[0]
# Entferne einen eventuellen Port (z.B. ":8080")
domain_part = domain_part.split(":", 1)[0]
# Wenn die Domain nicht mit "www." beginnt, hinzufügen
if not domain_part.lower().startswith("www."):
domain_part = "www." + domain_part
return domain_part
except Exception as e:
return "k.A."
# ---------------------------------------------------------------------
# 1. Mapping-Funktion: Laden der Ziel-Branchenschema-Tabelle aus CSV
# ---------------------------------------------------------------------
def load_branch_mapping(file_path="ziel_Branchenschema.csv"):
"""
Lädt die Mapping-Tabelle mit zwei Spalten:
Spalte A: Externer (Wikipedia-)Brancheneintrag (z. B. "Getränkeabfüllung")
Spalte B: Ziel-Branchenschema (z. B. "Hersteller / Produzenten > Getränke")
Gibt ein Dictionary zurück, das alle Einträge (in Lowercase und normalisiert) enthält.
"""
mapping = {}
try:
with open(file_path, encoding="utf-8") as f:
reader = csv.reader(f)
for row in reader:
if len(row) >= 2:
key = row[0].strip().lower()
value = row[1].strip()
if key and value:
mapping[key] = value
except Exception as e:
debug_print("Fehler beim Laden des Branchen-Mappings: " + str(e))
return mapping
# Globales Mapping-Dictionary laden
BRANCH_MAPPING = load_branch_mapping()
def map_external_branch(external_branch):
"""
Normalisiert den externen Brancheneintrag und sucht im Mapping-Dictionary nach einer
entsprechenden Übersetzung in das Ziel-Branchenschema.
Falls kein exaktes Mapping gefunden wird, erfolgt eine Teilübereinstimmungsprüfung.
"""
norm = normalize_string(external_branch).lower()
if norm in BRANCH_MAPPING:
return BRANCH_MAPPING[norm]
for key in BRANCH_MAPPING:
if key in norm:
return BRANCH_MAPPING[key]
return norm
def process_wiki_batch(main_sheet, data, start_row, end_row):
"""
Batch-Prozess für Wikipedia-Verifizierung (Wiki-Modus):
- Verarbeitet alle Zeilen von start_row bis end_row in Gruppen (Batchgröße = Config.BATCH_SIZE).
- Ergebnisse werden in den Spalten S bis Y geschrieben.
"""
batch_size = Config.BATCH_SIZE
batches = []
row_numbers = []
for i in range(start_row, end_row + 1):
row = data[i - 1]
entry_text = (
f"Eintrag {i}:\n"
f"Firmenname: {row[1] if len(row) > 1 else ''}\n"
f"CRM-Beschreibung: {row[7] if len(row) > 7 else ''}\n"
f"Wikipedia-URL: {row[11] if len(row) > 11 and row[11].strip() not in ['', 'k.A.'] else 'k.A.'}\n"
f"Wiki-Absatz: {row[12] if len(row) > 12 else 'k.A.'}\n"
f"Wiki-Kategorien: {row[16] if len(row) > 16 else 'k.A.'}\n"
"-----\n"
)
batches.append(entry_text)
row_numbers.append(i)
if len(batches) == batch_size:
_process_batch(main_sheet, batches, row_numbers)
batches = []
row_numbers = []
if batches:
_process_batch(main_sheet, batches, row_numbers)
debug_print("Wiki batch processing completed.")
def process_website_batch(main_sheet, data, start_row, end_row):
"""
Batch-Prozess für Website-Scraping (Website-Modus):
- Für jede Zeile von start_row bis end_row werden Website-Rohtext (get_website_raw) und
Zusammenfassung (summarize_website_content) abgerufen.
- Ergebnisse werden in Spalte AR (Rohtext) und AS (Zusammenfassung) geschrieben.
- Am Ende jeder Zeile wird der Zeitstempel (Spalte AO) und Version (Spalte AP) gesetzt.
"""
for i in range(start_row, end_row + 1):
row = data[i - 1]
website = row[3] if len(row) >= 4 else ""
if website.strip() == "" or website.strip().lower() == "k.a.":
debug_print(f"Zeile {i}: Kein gültiger Website-Eintrag.")
continue
raw_text = get_website_raw(website)
summary = summarize_website_content(raw_text)
try:
main_sheet.update(values=[[raw_text]], range_name=f"AR{i}")
main_sheet.update(values=[[summary]], range_name=f"AS{i}")
current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
main_sheet.update(values=[[current_timestamp]], range_name=f"AO{i}")
main_sheet.update(values=[[Config.VERSION]], range_name=f"AP{i}")
debug_print(f"Zeile {i}: Website-Daten aktualisiert | Zeitstempel: {current_timestamp}, Version: {Config.VERSION}")
except Exception as e:
debug_print(f"Fehler beim Updaten der Website-Daten in Zeile {i}: {e}")
time.sleep(Config.RETRY_DELAY)
debug_print("Website batch processing completed.")
def process_branch_batch(main_sheet, data, start_row, end_row):
"""
Batch-Prozess für Brancheneinschätzung (Branch-Modus):
- Für jede Zeile von start_row bis end_row werden relevante Felder ausgelesen und
evaluate_branche_chatgpt aufgerufen.
- Das Ergebnis (Dictionary mit "branch", "consistency", "justification") wird in
Spalte W (Branch), X (Konsistenz) und Y (Begründung) geschrieben.
- Für jede verarbeitete Zeile werden zudem der Zeitstempel (Spalte AO) und Version (Spalte AP) gesetzt.
"""
for i in range(start_row, end_row + 1):
row = data[i - 1]
crm_branche = row[6] if len(row) > 6 else ""
beschreibung = row[7] if len(row) > 7 else ""
wiki_branche = row[14] if len(row) > 14 else ""
wiki_kategorien = row[17] if len(row) > 17 else ""
website_summary = row[44] if len(row) > 44 else ""
result = evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kategorien, website_summary)
try:
main_sheet.update(values=[[result["branch"]]], range_name=f"W{i}")
main_sheet.update(values=[[result["consistency"]]], range_name=f"X{i}")
main_sheet.update(values=[[result["justification"]]], range_name=f"Y{i}")
current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
main_sheet.update(values=[[current_timestamp]], range_name=f"AO{i}")
main_sheet.update(values=[[Config.VERSION]], range_name=f"AP{i}")
debug_print(f"Zeile {i}: Branch-Einschätzung aktualisiert: {result} | Zeitstempel: {current_timestamp}, Version: {Config.VERSION}")
except Exception as e:
debug_print(f"Fehler beim Updaten der Branch-Daten in Zeile {i}: {e}")
time.sleep(Config.RETRY_DELAY)
debug_print("Branch batch processing completed.")
def run_dispatcher(mode, row_limit=None):
debug_print(f"Starte Dispatcher im Modus '{mode}' mit row_limit={row_limit}.")
gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name(
Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"]))
sh = gc.open_by_url(Config.SHEET_URL)
main_sheet = sh.sheet1
data = main_sheet.get_all_values()
start_row = None
for i in range(7, len(data) + 1):
row = data[i - 1]
if len(row) < 41 or row[40].strip() == "":
start_row = i
break
if start_row is None:
debug_print("Keine Zeile ohne Zeitstempel in Spalte AO gefunden. Dispatcher beendet.")
return
debug_print(f"Dispatcher: Verarbeitung startet ab Zeile {start_row}.")
if row_limit is not None:
end_row = start_row + row_limit - 1
else:
end_row = len(data)
debug_print(f"Dispatcher: Es werden Zeilen {start_row} bis {end_row} bearbeitet.")
if mode == "wiki":
process_wiki_batch(main_sheet, data, start_row, end_row)
elif mode == "website":
process_website_batch(main_sheet, data, start_row, end_row)
elif mode == "branch":
process_branch_batch(main_sheet, data, start_row, end_row)
elif mode == "combined":
process_wiki_batch(main_sheet, data, start_row, end_row)
process_website_batch(main_sheet, data, start_row, end_row)
process_branch_batch(main_sheet, data, start_row, end_row)
else:
debug_print("Ungültiger Modus im Dispatcher.")
def normalize_string(s):
"""
Normalisiert Sonderzeichen in einem String anhand eines umfangreichen Mappings.
Ersetzt beispielsweise:
- Deutsche Umlaute: ü -> ue, ö -> oe, ä -> ae, ß -> ss
- Verschiedene diakritische Zeichen: č, ć -> c; š -> s; ž -> z; etc.
- Auch weitere Buchstaben mit Akzenten werden konvertiert.
"""
replacements = {
# Deutsche Umlaute und spezielle Buchstaben
'Ä': 'Ae', 'Ö': 'Oe', 'Ü': 'Ue', 'ß': 'ss',
'ä': 'ae', 'ö': 'oe', 'ü': 'ue',
# Lateinische Buchstaben mit Akzenten
'À': 'A', 'Á': 'A', 'Â': 'A', 'Ã': 'A', 'Å': 'A', 'Æ': 'AE',
'à': 'a', 'á': 'a', 'â': 'a', 'ã': 'a', 'å': 'a', 'æ': 'ae',
'Ç': 'C', 'ç': 'c',
'È': 'E', 'É': 'E', 'Ê': 'E', 'Ë': 'E',
'è': 'e', 'é': 'e', 'ê': 'e', 'ë': 'e',
'Ì': 'I', 'Í': 'I', 'Î': 'I', 'Ï': 'I',
'ì': 'i', 'í': 'i', 'î': 'i', 'ï': 'i',
'Ñ': 'N', 'ñ': 'n',
'Ò': 'O', 'Ó': 'O', 'Ô': 'O', 'Õ': 'O', 'Ø': 'O',
'ò': 'o', 'ó': 'o', 'ô': 'o', 'õ': 'o', 'ø': 'o',
'Œ': 'OE', 'œ': 'oe',
'Š': 'S', 'š': 's',
'Ž': 'Z', 'ž': 'z',
'Ý': 'Y', 'ý': 'y', 'ÿ': 'y',
# Zusätzliche spezifische Buchstaben
'Đ': 'D', 'đ': 'd',
'č': 'c', 'Č': 'C', 'ć': 'c', 'Ć': 'C',
'ł': 'l', 'Ł': 'L',
'ğ': 'g', 'Ğ': 'G',
'ş': 's', 'Ş': 'S',
# Weitere diakritische Zeichen (z. B. aus osteuropäischen Sprachen)
'ă': 'a', 'Ă': 'A',
'ı': 'i', 'İ': 'I',
'ň': 'n', 'Ň': 'N',
'ř': 'r', 'Ř': 'R',
'ő': 'o', 'Ű': 'U', 'ű': 'u', 'Ű': 'U',
'ț': 't', 'Ț': 'T',
'ș': 's', 'Ș': 'S'
}
for src, target in replacements.items():
s = s.replace(src, target)
return s
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 get_email_address(firstname, lastname, website):
"""
Generiert eine E-Mail-Adresse im Format vorname.nachname@domain.tld.
Dabei wird der Domainname aus der Website extrahiert und Vor- und Nachname
zunächst mittels normalize_string normalisiert.
"""
# Falls Website nicht mit http/https beginnt, Protokoll hinzufügen
url = website if website.startswith("http") else "http://" + website
parsed = urlparse(url)
domain = parsed.netloc
if domain.startswith("www."):
domain = domain[4:]
# Vor- und Nachname normalisieren, in Kleinbuchstaben umwandeln und nicht-alphanumerische Zeichen entfernen
normalized_first = re.sub(r'\W+', '', normalize_string(firstname.lower()))
normalized_last = re.sub(r'\W+', '', normalize_string(lastname.lower()))
if normalized_first and normalized_last:
return f"{normalized_first}.{normalized_last}@{domain}"
else:
return ""
def is_valid_company_article(wiki_categories):
"""
Prüft, ob in den Wikipedia-Kategorien ein Hinweis auf einen Unternehmensartikel enthalten ist.
Wir suchen nach den Stichwörtern 'unternehmen', 'firma', 'betrieb' und 'konzern'.
Args:
wiki_categories (str): Die Liste oder der String der Wikipedia-Kategorien.
Returns:
bool: True, wenn eines der Keywords gefunden wird, sonst False.
"""
if wiki_categories == "k.A.":
return False
keywords = ["unternehmen", "firma", "betrieb", "konzern"]
wiki_cats_lower = wiki_categories.lower()
for word in keywords:
if word in wiki_cats_lower:
return True
return False
def load_target_schema(csv_filepath="ziel_Branchenschema.csv"):
"""
Liest das Ziel-Branchenschema aus der CSV-Datei ein.
Die CSV-Datei sollte in Spalte A den externen (Wikipedia-)Branchenbegriff
und in Spalte B den zugehörigen Zielwert enthalten.
Returns:
mapping (dict): Ein Dictionary, das externe Branchenbegriffe (lowercase) auf
die zugehörigen Zielwerte (lowercase) abbildet.
schema_string (str): Eine formattierte Zeichenkette als Aufzählung der gültigen
Zielbranchen, die im Prompt übergeben werden kann.
allowed_targets (list): Eine sortierte Liste der eindeutigen Zielwerte.
"""
import csv
mapping = {}
valid_targets = set()
try:
with open(csv_filepath, encoding="utf-8") as f:
reader = csv.reader(f, delimiter=";")
for row in reader:
if len(row) >= 2:
external = row[0].strip().lower()
target = row[1].strip().lower()
if target:
mapping[external] = target
valid_targets.add(target)
except Exception as e:
debug_print("Fehler beim Einlesen des Ziel-Branchenschemas: " + str(e))
return {}, "Ziel-Branchenschema nicht verfügbar.", []
sorted_targets = sorted(valid_targets, key=lambda s: s.lower())
schema_string = (
"Ziel-Branchenschema: Folgende Branchenbereiche sind gültig:\n" +
"\n".join(f"- {value}" for value in sorted_targets) +
"\nBitte ordne das Unternehmen ausschließlich in einen dieser Bereiche ein."
)
return mapping, schema_string, sorted_targets
def serp_website_lookup(company_name):
"""
Ermittelt über SERPAPI (Google-Suche) die Website zum Unternehmen.
- Verwendet als Query den Firmennamen mit dem Zusatz "Website".
- Filtert Ergebnisse anhand einer Blacklist (z.B. bloomberg.com, northdata.de, finanzen.net, handelsblatt.com).
- Gibt die normalisierte Website-URL (im Format "www.domain.tld") zurück.
Returns:
str: Normalisierte Website-URL oder "k.A.", falls kein passendes Ergebnis gefunden wurde.
"""
# Blacklist unerwünschter Domains
blacklist = ["bloomberg.com", "northdata.de", "finanzen.net", "handelsblatt.com"]
try:
with open("serpApiKey.txt", "r") as f:
serp_key = f.read().strip()
except Exception as e:
debug_print(f"Fehler beim Lesen des SerpAPI-Schlüssels: {e}")
return "k.A."
query = f"{company_name} Website"
params = {
"engine": "google",
"q": query,
"api_key": serp_key,
"hl": "de"
}
try:
response = requests.get("https://serpapi.com/search", params=params, timeout=10)
data = response.json()
if "organic_results" in data:
for result in data["organic_results"]:
url = result.get("link", "")
# Überprüfen, ob die URL nicht in der Blacklist enthalten ist
if url and not any(bad in url for bad in blacklist):
normalized_url = simple_normalize_url(url)
debug_print(f"SERP-Website Lookup: Gefundene Website '{normalized_url}' für {company_name}")
return normalized_url
return "k.A."
except Exception as e:
debug_print(f"Fehler beim SERP-API Website Lookup für {company_name}: {e}")
return "k.A."
def create_log_filename(mode):
now = datetime.now().strftime("%d-%m-%Y_%H-%M")
ver_short = Config.VERSION.replace(".", "")
return os.path.join("Log", f"{now}_{ver_short}_Modus{mode}.txt")
if not os.path.exists("Log"):
os.makedirs("Log")
LOG_FILE = None
def debug_print(message):
global LOG_FILE
if Config.DEBUG:
print(f"[DEBUG] {message}")
if LOG_FILE:
try:
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"[DEBUG] {datetime.now().isoformat()} - {message}\n")
except Exception as e:
print(f"[DEBUG] Log-Schreibfehler: {e}")
def clean_text(text):
if not text:
return "k.A."
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."
def normalize_company_name(name):
if not name:
return ""
forms = [
r'gmbh', r'g\.m\.b\.h\.', r'ug', r'u\.g\.', r'ug \(haftungsbeschränkt\)',
r'u\.g\. \(haftungsbeschränkt\)', r'ag', r'a\.g\.', r'ohg', r'o\.h\.g\.',
r'kg', r'k\.g\.', r'gmbh & co\.?\s*kg', r'g\.m\.b\.h\. & co\.?\s*k\.g\.',
r'ag & co\.?\s*kg', r'a\.g\. & co\.?\s*k\.g\.', r'e\.k\.', r'e\.kfm\.',
r'e\.kfr\.', r'ltd\.', r'ltd & co\.?\s*kg', r's\.a r\.l\.', r'stiftung',
r'genossenschaft', r'ggmbh', r'gug', r'partg', r'partgmbb', r'kgaa', r'se',
r'og', r'o\.g\.', r'e\.u\.', r'ges\.n\.b\.r\.', r'genmbh', r'verein',
r'kollektivgesellschaft', r'kommanditgesellschaft', r'einzelfirma', r'sàrl',
r'sa', r'sagl', r'gmbh & co\.?\s*ohg', r'ag & co\.?\s*ohg', r'gmbh & co\.?\s*kgaa',
r'ag & co\.?\s*kgaa', r's\.a\.', r's\.p\.a\.', r'b\.v\.', r'n\.v\.'
]
pattern = r'\b(' + '|'.join(forms) + r')\b'
normalized = re.sub(pattern, '', name, flags=re.IGNORECASE)
normalized = re.sub(r'[\-]', ' ', normalized)
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."
raw_value = re.sub(r'\b(ca\.?|circa|über)\b', '', raw_value, flags=re.IGNORECASE)
raw = raw_value.lower().replace("\xa0", " ")
match = re.search(r'([\d.,]+)', raw, flags=re.UNICODE)
if not match or not match.group(1).strip():
debug_print(f"Keine numerischen Zeichen gefunden im Rohtext: '{raw_value}'")
return "k.A."
num_str = match.group(1)
if ',' in num_str:
num_str = num_str.replace('.', '').replace(',', '.')
try:
num = float(num_str)
except Exception as e:
debug_print(f"Fehler bei der Umwandlung von '{num_str}' (Rohtext: '{raw_value}'): {e}")
return raw_value
else:
num_str = num_str.replace(' ', '').replace('.', '')
try:
num = 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:
if "mrd" in raw or "milliarden" in raw:
num *= 1000
elif "mio" in raw or "millionen" in raw:
pass
else:
num /= 1e6
return str(int(round(num)))
else:
return str(int(round(num)))
def compare_umsatz_values(crm, wiki):
debug_print(f"Vergleich CRM Umsatz: '{crm}' mit Wikipedia Umsatz: '{wiki}'")
try:
crm_val = float(crm)
wiki_val = float(wiki)
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 is_valid_branch(branch):
"""
Prüft, ob der übergebene Branch-String grundsätzlich gültig ist.
Gültig ist ein String, der:
- nicht leer und nicht "k.A." (unabhängig von Groß-/Kleinschreibung) ist,
- mindestens ein hierarchisches Trennzeichen ">" enthält.
"""
if not branch or branch.strip() == "":
return False
if branch.strip().lower() == "k.a.":
return False
if ">" not in branch:
return False
return True
def branch_matches_target_schema(branch):
"""
Überprüft, ob der Branch zum Ziel-Branchenschema passt.
Als Fokus werden die definierten Präfixe verwendet (laut Alignment Demo):
- "service provider"
- "hersteller / produzenten"
- "sonstige"
"""
allowed_prefixes = [
"service provider",
"hersteller / produzenten",
"sonstige"
]
branch_lower = branch.lower()
for prefix in allowed_prefixes:
if branch_lower.startswith(prefix):
return True
return False
def extract_suffix(branch):
"""
Extrahiert den Teil hinter dem ">" als Suffix.
Falls kein ">" vorhanden ist, wird der gesamte String zurückgegeben.
"""
if ">" in branch:
return branch.split(">")[-1].strip()
return branch.strip()
def merge_with_prefix(suggestion, crm_branch):
"""
Falls der ChatGPT-Vorschlag kein ">" enthält, wird der Präfix aus dem CRM-Branchenwert übernommen.
Beispiel: CRM: "Hersteller / Produzenten > Automaten (Vending, Slot)" und
Vorschlag: "Automaten (Vending, Slot)" ergeben "Hersteller / Produzenten > Automaten (Vending, Slot)".
"""
if ">" in crm_branch:
prefix = crm_branch.split(">")[0].strip()
return prefix + " > " + suggestion.strip()
return suggestion.strip()
def fuzzy_similarity(str1, str2):
"""
Gibt den Ähnlichkeitswert (zwischen 0 und 1) der beiden Strings zurück.
"""
return SequenceMatcher(None, str1, str2).ratio()
# ==================== TOKEN COUNT FUNCTION ====================
def token_count(text):
if tiktoken:
try:
enc = tiktoken.encoding_for_model(Config.TOKEN_MODEL)
return len(enc.encode(text))
except Exception as e:
debug_print(f"Fehler beim Token-Counting mit tiktoken: {e}")
return len(text.split())
else:
return len(text.split())
# ==================== PROMPT-ÜBERSICHT ====================
def prompt_overview():
prompts = [
["Funktion", "Verwendeter Prompt"],
["process_wiki_verification", "Bitte verifiziere den Wikipedia-Artikel für {company_name}. Wenn 'k.A.' vorliegt, suche mit den vorliegenden Informationen nach einem passenden Artikel. (Nur 'Skipped (k.A.)', wenn wirklich keine Daten gefunden werden.)"],
["process_employee_estimation", "Schätze die Mitarbeiterzahl für {company_name} basierend auf Wikipedia-Daten. Bei 'k.A.' liefere 'Skipped (k.A.)'."],
["process_employee_consistency", "Vergleiche CRM-, Wiki- und ChatGPT-Mitarbeiterzahlen. Gib die prozentuale Differenz und eine Begründung zurück."],
["evaluate_umsatz_chatgpt", "Schätze den Umsatz in Mio. Euro für {company_name} basierend auf Wikipedia-Daten, antworte nur mit der Zahl."],
["evaluate_fsm_suitability", "Bewerte, ob {company_name} für Field Service Management geeignet ist; antworte ausschließlich mit 'Ja' oder 'Nein' und einer kurzen Begründung."],
["evaluate_branche_chatgpt", "Ordne {company_name} exakt einer Branche des Ziel-Branchenschemas zu. Antworte im Format:\nBranche: <vorgeschlagene Branche>\nÜbereinstimmung: <ok oder X>\nBegründung: <kurze Begründung>."]
]
return prompts
# ==================== TIMESTAMP HANDLING ====================
processed_timestamps = {}
def should_process(field):
return field not in processed_timestamps
def mark_processed(field):
processed_timestamps[field] = datetime.now().isoformat()
# ==================== NEUE FUNKTION: Website-Rohtext extrahieren ====================
def get_website_raw(url, max_length=1000, verify_cert=False):
"""
Ruft die Website ab und gibt den bereinigten Text des <body>-Inhalts zurück (maximal max_length Zeichen).
Zusätzliche Verbesserungen:
- Falls kein Schema vorhanden ist, wird "https://" ergänzt.
- Es werden zusätzliche Header (insbesondere ein User-Agent) mitgeschickt.
- Optional wird die Zertifikatüberprüfung deaktiviert (verify_cert=False).
Args:
url (str): Die URL der Website.
max_length (int): Maximale Länge des zurückgegebenen Texts.
verify_cert (bool): Gibt an, ob SSL-Zertifikate verifiziert werden sollen.
Returns:
str: Extrahierter Text oder "k.A.", wenn Fehler auftreten.
"""
if not url.lower().startswith("http"):
url = "https://" + url
headers = {
"User-Agent": "Mozilla/5.0 (compatible; AcmeInc/1.0; +http://example.com/bot)"
}
try:
response = requests.get(url, timeout=10, headers=headers, verify=verify_cert)
if response.status_code != 200:
debug_print(f"Fehler: Website {url} lieferte Statuscode {response.status_code}")
return "k.A."
soup = BeautifulSoup(response.text, Config.HTML_PARSER)
body = soup.find('body')
if body:
text = body.get_text(separator=' ', strip=True)
text = re.sub(r'\s+', ' ', text)
result = text[:max_length]
debug_print(f"Website {url} erfolgreich gescrapt. Extrahierter Text (Länge {len(result)}): {result[:100]}...")
return result
else:
debug_print(f"Kein <body>-Tag gefunden in {url}")
return "k.A."
except Exception as e:
debug_print(f"Fehler beim Abrufen der Website {url}: {e}")
return "k.A."
# ==================== NEUE FUNKTION: Website-Zusammenfassung erstellen ====================
def summarize_website_content(raw_text):
if raw_text == "k.A." or raw_text.strip() == "":
return "k.A."
prompt = (
"Fasse den folgenden Text der Unternehmensstartseite zusammen. "
"Beschreibe kurz das Tätigkeitsfeld, die Produkte und Leistungen des Unternehmens:\n\n"
f"{raw_text}\n\n"
"Zusammenfassung:"
)
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 für Website-Zusammenfassung: {e}")
return "k.A."
openai.api_key = api_key
try:
response = openai.ChatCompletion.create(
model=Config.TOKEN_MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.3
)
result = response.choices[0].message.content.strip()
return result
except Exception as e:
debug_print(f"Fehler beim Erstellen der Website-Zusammenfassung: {e}")
return "k.A."
# ==================== NEUE FUNKTION: Website-Suche bei fehlender Website ====================
# Neue Funktion: SERP-API Website Lookup in DataProcessor
class DataProcessor:
def __init__(self):
self.sheet_handler = GoogleSheetHandler()
self.wiki_scraper = WikipediaScraper()
def process_serp_website_lookup(self):
debug_print("Starte SERP-API Website Lookup für alle Zeilen ohne CRM-Website (Spalte D).")
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
current_website = row[3] if len(row) > 3 else ""
if current_website.strip() == "":
company_name = row[1] if len(row) > 1 else ""
new_website = serp_website_lookup(company_name)
if new_website != "k.A.":
self.sheet_handler.sheet.update(values=[[new_website]], range_name=f"D{i}")
debug_print(f"Zeile {i}: Neue Website gefunden und in Spalte D eingetragen: {new_website}")
else:
debug_print(f"Zeile {i}: Keine Website gefunden für {company_name}.")
time.sleep(Config.RETRY_DELAY)
else:
debug_print(f"Zeile {i}: CRM-Website bereits vorhanden, Überspringe.")
# Bestehende Funktion, die alle Zeilen verarbeitet
def process_rows(self, num_rows=None):
global MODE
if MODE == "1":
self.process_rows_complete() # Vollständige Verarbeitung (sofern definiert)
elif MODE == "11":
# Re-Evaluation markierter Zeilen (nur 'x' in Spalte A)
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
if row[0].strip().lower() == "x":
self._process_single_row(i, row)
elif MODE == "21":
# Testmodus: Nur Website-Scraping
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
self._process_single_row(i, row, process_wiki=False, process_chatgpt=False)
elif MODE == "22":
# SERP-API Website Lookup
self.process_serp_website_lookup()
elif MODE == "31":
# Nur ChatGPT-Auswertung
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
self._process_single_row(i, row, process_wiki=False, process_chatgpt=True)
elif MODE == "41":
# Nur Wikipedia-Scraping
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
self._process_single_row(i, row, process_wiki=True, process_chatgpt=False)
elif MODE == "51":
process_verification_only()
elif MODE == "6":
process_contact_research()
elif MODE == "8":
process_batch_token_count()
else:
start_index = self.sheet_handler.get_start_index()
print(f"Starte bei Zeile {start_index+1}")
rows_processed = 0
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
if i < start_index:
continue
if num_rows is not None and rows_processed >= num_rows:
break
self._process_single_row(i, row)
rows_processed += 1
# ==================== NEUE FUNKTION: process_verification_only ====================
def process_verification_only():
"""
Überarbeiteter BatchProzess (Version 1.5.10, Modus 51):
- Startet die Verarbeitung ab Zeile 7 und sucht ab dort die erste Zeile, in der Spalte AO (Index 41) leer ist.
- Falls der Nutzer eine Gesamtzeilenanzahl eingibt, die vor dem Startpunkt liegt,
wird dieser Wert ignoriert und alle Zeilen ab dem Startpunkt verarbeitet.
- Verarbeitet die Zeilen ab diesem Startpunkt in Paketen der Größe Config.BATCH_SIZE (z.B. 10 Zeilen).
- Für jedes Batch wird ein aggregierter Prompt erstellt, an ChatGPT gesendet und die Antwort
zeilenweise geparst.
- Die Ergebnisse werden in den Spalten S bis Y geschrieben:
S: Wiki-Validierung ("OK" oder "X")
T: Alternativer Wiki-Artikel (URL oder "Kein Wikipedia-Eintrag vorhanden.")
U: Wiki-Erklärung / Begründung
VY: Platzhalter (leer)
- Umfangreiche Log-Ausgaben unterstützen die Fehlerdiagnose.
"""
debug_print("Starte Verifizierungsmodus (Modus 51) im Batch-Prozess (Version 1.5.10)...")
# Ermittlung des Startpunkts: ab Zeile 7 die erste Zeile, in der Spalte AO (Index 41) leer ist.
gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name(
Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"]))
sh = gc.open_by_url(Config.SHEET_URL)
main_sheet = sh.sheet1
data = main_sheet.get_all_values()
start_row = None
for i in range(7, len(data) + 1):
row = data[i - 1]
if len(row) < 41 or row[40].strip() == "":
start_row = i
break
if start_row is None:
debug_print("Keine Zeile ohne Zeitstempel in Spalte AO gefunden. Es wird nichts verarbeitet.")
return
debug_print(f"Verarbeitung startet ab Zeile {start_row} (erste Zeile ohne Zeitstempel in Spalte AO).")
# Abfrage: Wie viele Zeilen sollen insgesamt verarbeitet werden?
try:
total_rows = int(input("Wie viele Zeilen sollen insgesamt bearbeitet werden? "))
except Exception as e:
debug_print(f"Fehler bei der Eingabe der Zeilenanzahl: {e}. Es werden alle verfügbaren Zeilen verarbeitet.")
total_rows = None
available_total = len(data) - 1 # ohne Header
# Wenn der Nutzer einen Wert eingibt, der vor dem Startpunkt liegt, wird dieser ignoriert.
if total_rows is not None and total_rows < start_row - 1:
debug_print("Die angegebene Zeilenanzahl liegt vor dem Startpunkt. Es werden alle Zeilen ab dem Startpunkt verarbeitet.")
available_rows = available_total
elif total_rows is not None:
available_rows = total_rows
else:
available_rows = available_total
if start_row > available_rows + 1:
debug_print("Es gibt keine Zeilen ohne Zeitstempel ab dem Startpunkt. Es wird nichts verarbeitet.")
return
batch_size = Config.BATCH_SIZE # z. B. 10
batches = []
row_numbers = []
for i in range(start_row, available_rows + 2): # +1 für Einbeziehung, +1 wegen 1-basierter Index
row = data[i - 1]
entry_text = (
f"Eintrag {i}:\n"
f"Firmenname: {row[1] if len(row) > 1 else ''}\n"
f"CRM-Beschreibung: {row[7] if len(row) > 7 else ''}\n"
f"Wikipedia-URL: {row[11] if len(row) > 11 and row[11].strip() not in ['', 'k.A.'] else 'k.A.'}\n"
f"Wiki-Absatz: {row[12] if len(row) > 12 else 'k.A.'}\n"
f"Wiki-Kategorien: {row[16] if len(row) > 16 else 'k.A.'}\n"
"-----\n"
)
batches.append(entry_text)
row_numbers.append(i)
if len(batches) == batch_size:
_process_batch(main_sheet, batches, row_numbers)
batches = []
row_numbers = []
if batches:
_process_batch(main_sheet, batches, row_numbers)
debug_print("Verifizierungs-Batch abgeschlossen.")
def _process_batch(main_sheet, batches, row_numbers):
"""
Hilfsfunktion: Bearbeitet einen Batch, indem ein aggregierter Prompt erstellt und
die aggregierte Antwort zeilenweise den entsprechenden Zeilennummern zugeordnet wird.
Die Ergebnisse werden in den Spalten S bis Y geschrieben, und anschließend wird
für jede Zeile der aktuelle Zeitstempel (Spalte AO) sowie die Versionsnummer (Spalte AP) eingetragen.
"""
aggregated_prompt = (
"Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln für Unternehmen. "
"Für jeden der folgenden Einträge prüfe, ob der vorhandene Wikipedia-Artikel (URL, Absatz, Kategorien) plausibel passt. "
"Gib das Ergebnis für jeden Eintrag im Format aus:\n"
"Eintrag <Zeilennummer>: <Antwort>\n"
"Regeln:\n"
"- Bei Übereinstimmung: 'OK'\n"
"- Bei Nichtübereinstimmung: 'Alternativer Wikipedia-Artikel vorgeschlagen: <URL> | X | <Begründung>'\n"
"- Falls kein Artikel gefunden wurde: 'Kein Wikipedia-Eintrag vorhanden.'\n\n"
)
aggregated_prompt += "\n".join(batches)
debug_print(f"Verarbeite Batch für Zeilen {row_numbers[0]} bis {row_numbers[-1]}.")
if tiktoken:
try:
enc = tiktoken.encoding_for_model(Config.TOKEN_MODEL)
debug_print(f"Token-Zahl für aktuellen Batch: {len(enc.encode(aggregated_prompt))}")
except Exception as e:
debug_print(f"Fehler beim Token-Counting: {e}")
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 für Verifizierung: {e}")
return
openai.api_key = api_key
try:
response = openai.ChatCompletion.create(
model=Config.TOKEN_MODEL,
messages=[{"role": "user", "content": aggregated_prompt}],
temperature=0.0
)
result = response.choices[0].message.content.strip()
debug_print(f"Aggregierte Antwort für Batch {row_numbers[0]}-{row_numbers[-1]}: {result}")
except Exception as e:
debug_print(f"Fehler bei der ChatGPT-Anfrage für Batch {row_numbers[0]}-{row_numbers[-1]}: {e}")
result = ""
answers = result.split("\n")
for current_row in row_numbers:
answer = "k.A."
for line in answers:
if line.strip().startswith(f"Eintrag {current_row}:"):
answer = line.split(":", 1)[1].strip()
break
if answer.upper() == "OK":
wiki_confirm = "OK"
alt_article = ""
wiki_explanation = ""
elif answer.upper() == "KEIN WIKIPEDIA-EINTRAG VORHANDEN.":
wiki_confirm = ""
alt_article = "Kein Wikipedia-Eintrag vorhanden."
wiki_explanation = ""
elif answer.startswith("Alternativer Wikipedia-Artikel vorgeschlagen:"):
parts = answer.split(":", 1)[1].split("|")
alt_article = parts[0].strip() if len(parts) > 0 else "k.A."
wiki_explanation = parts[2].strip() if len(parts) > 2 else ""
wiki_confirm = "X"
else:
wiki_confirm = ""
alt_article = answer
wiki_explanation = answer
try:
main_sheet.update(values=[[wiki_confirm]], range_name=f"S{current_row}")
main_sheet.update(values=[[alt_article]], range_name=f"T{current_row}")
main_sheet.update(values=[[wiki_explanation]], range_name=f"U{current_row}")
main_sheet.update(values=[["", "", "", ""]], range_name=f"V{current_row}:Y{current_row}")
# Neu: Setze Zeitstempel in Spalte AO und Version in Spalte AP
current_timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
main_sheet.update(values=[[current_timestamp]], range_name=f"AO{current_row}")
main_sheet.update(values=[[Config.VERSION]], range_name=f"AP{current_row}")
debug_print(f"Zeile {current_row} verifiziert: Antwort: {answer} | Zeitstempel: {current_timestamp}, Version: {Config.VERSION}")
except Exception as e:
debug_print(f"Fehler beim Updaten der Zeile {current_row}: {e}")
time.sleep(Config.RETRY_DELAY)
def process_verification_only():
"""
Überarbeiteter BatchProzess (Version 1.5.10, Modus 51):
- Startet die Verarbeitung ab Zeile 7 und sucht ab dort die erste Zeile, in der Spalte AO (Index 41) leer ist.
- Falls der Nutzer eine Gesamtzeilenanzahl eingibt, die vor dem Startpunkt liegt,
wird dieser Wert ignoriert und alle Zeilen ab dem Startpunkt verarbeitet.
- Verarbeitet die Zeilen ab diesem Startpunkt in Paketen der Größe Config.BATCH_SIZE (z.B. 10 Zeilen).
- Für jedes Batch wird ein aggregierter Prompt erstellt, an ChatGPT gesendet und die Antwort
zeilenweise geparst.
- Die Ergebnisse werden in den Spalten S bis Y geschrieben:
S: Wiki-Validierung ("OK" oder "X")
T: Alternativer Wiki-Artikel (URL oder "Kein Wikipedia-Eintrag vorhanden.")
U: Wiki-Erklärung / Begründung
VY: Platzhalter (leer)
- Umfangreiche Log-Ausgaben unterstützen die Fehlerdiagnose.
"""
debug_print("Starte Verifizierungsmodus (Modus 51) im Batch-Prozess (Version 1.5.10)...")
# Ermittlung des Startpunkts: ab Zeile 7 die erste Zeile, in der Spalte AO (Index 41) leer ist.
gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name(
Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"]))
sh = gc.open_by_url(Config.SHEET_URL)
main_sheet = sh.sheet1
data = main_sheet.get_all_values()
start_row = None
for i in range(7, len(data) + 1):
row = data[i - 1]
if len(row) < 41 or row[40].strip() == "":
start_row = i
break
if start_row is None:
debug_print("Keine Zeile ohne Zeitstempel in Spalte AO gefunden. Es wird nichts verarbeitet.")
return
debug_print(f"Verarbeitung startet ab Zeile {start_row} (erste Zeile ohne Zeitstempel in Spalte AO).")
# Abfrage: Wie viele Zeilen sollen insgesamt verarbeitet werden?
try:
total_rows = int(input("Wie viele Zeilen sollen insgesamt bearbeitet werden? "))
except Exception as e:
debug_print(f"Fehler bei der Eingabe der Zeilenanzahl: {e}. Es werden alle verfügbaren Zeilen verarbeitet.")
total_rows = None
available_total = len(data) - 1 # ohne Header
# Wenn der Nutzer einen Wert eingibt, der vor dem Startpunkt liegt, wird dieser ignoriert.
if total_rows is not None and total_rows < start_row - 1:
debug_print("Die angegebene Zeilenanzahl liegt vor dem Startpunkt. Es werden alle Zeilen ab dem Startpunkt verarbeitet.")
available_rows = available_total
elif total_rows is not None:
available_rows = total_rows
else:
available_rows = available_total
if start_row > available_rows + 1:
debug_print("Es gibt keine Zeilen ohne Zeitstempel ab dem Startpunkt. Es wird nichts verarbeitet.")
return
batch_size = Config.BATCH_SIZE # z. B. 10
batches = []
row_numbers = []
for i in range(start_row, available_rows + 2): # +1 für Einbeziehung, +1 wegen 1-basierter Index
row = data[i - 1]
entry_text = (
f"Eintrag {i}:\n"
f"Firmenname: {row[1] if len(row) > 1 else ''}\n"
f"CRM-Beschreibung: {row[7] if len(row) > 7 else ''}\n"
f"Wikipedia-URL: {row[11] if len(row) > 11 and row[11].strip() not in ['', 'k.A.'] else 'k.A.'}\n"
f"Wiki-Absatz: {row[12] if len(row) > 12 else 'k.A.'}\n"
f"Wiki-Kategorien: {row[16] if len(row) > 16 else 'k.A.'}\n"
"-----\n"
)
batches.append(entry_text)
row_numbers.append(i)
if len(batches) == batch_size:
_process_batch(main_sheet, batches, row_numbers)
batches = []
row_numbers = []
if batches:
_process_batch(main_sheet, batches, row_numbers)
debug_print("Verifizierungs-Batch abgeschlossen.")
def _process_batch(main_sheet, batches, row_numbers):
"""
Hilfsfunktion: Bearbeitet einen Batch, indem ein aggregierter Prompt erstellt und
die aggregierte Antwort zeilenweise den entsprechenden Zeilennummern zugeordnet wird.
Die Ergebnisse werden in Spalten S bis Y geschrieben.
"""
aggregated_prompt = (
"Du bist ein Experte in der Verifizierung von Wikipedia-Artikeln für Unternehmen. "
"Für jeden der folgenden Einträge prüfe, ob der vorhandene Wikipedia-Artikel (URL, Absatz, Kategorien) plausibel passt. "
"Gib das Ergebnis für jeden Eintrag im Format aus:\n"
"Eintrag <Zeilennummer>: <Antwort>\n"
"Regeln:\n"
"- Bei Übereinstimmung: 'OK'\n"
"- Bei Nichtübereinstimmung: 'Alternativer Wikipedia-Artikel vorgeschlagen: <URL> | X | <Begründung>'\n"
"- Falls kein Artikel gefunden wurde: 'Kein Wikipedia-Eintrag vorhanden.'\n\n"
)
aggregated_prompt += "\n".join(batches)
debug_print(f"Verarbeite Batch für Zeilen {row_numbers[0]} bis {row_numbers[-1]}.")
if tiktoken:
try:
enc = tiktoken.encoding_for_model(Config.TOKEN_MODEL)
debug_print(f"Token-Zahl für aktuellen Batch: {len(enc.encode(aggregated_prompt))}")
except Exception as e:
debug_print(f"Fehler beim Token-Counting: {e}")
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 für Verifizierung: {e}")
return
openai.api_key = api_key
try:
response = openai.ChatCompletion.create(
model=Config.TOKEN_MODEL,
messages=[{"role": "user", "content": aggregated_prompt}],
temperature=0.0
)
result = response.choices[0].message.content.strip()
debug_print(f"Aggregierte Antwort für Batch {row_numbers[0]}-{row_numbers[-1]}: {result}")
except Exception as e:
debug_print(f"Fehler bei der ChatGPT-Anfrage für Batch {row_numbers[0]}-{row_numbers[-1]}: {e}")
result = ""
answers = result.split("\n")
for current_row in row_numbers:
answer = "k.A."
for line in answers:
if line.strip().startswith(f"Eintrag {current_row}:"):
answer = line.split(":", 1)[1].strip()
break
if answer.upper() == "OK":
wiki_confirm = "OK"
alt_article = ""
wiki_explanation = ""
elif answer.upper() == "KEIN WIKIPEDIA-EINTRAG VORHANDEN.":
wiki_confirm = ""
alt_article = "Kein Wikipedia-Eintrag vorhanden."
wiki_explanation = ""
elif answer.startswith("Alternativer Wikipedia-Artikel vorgeschlagen:"):
parts = answer.split(":", 1)[1].split("|")
alt_article = parts[0].strip() if len(parts) > 0 else "k.A."
wiki_explanation = parts[2].strip() if len(parts) > 2 else ""
wiki_confirm = "X"
else:
wiki_confirm = ""
alt_article = answer
wiki_explanation = answer
try:
main_sheet.update(values=[[wiki_confirm]], range_name=f"S{current_row}")
main_sheet.update(values=[[alt_article]], range_name=f"T{current_row}")
main_sheet.update(values=[[wiki_explanation]], range_name=f"U{current_row}")
main_sheet.update(values=[["", "", "", ""]], range_name=f"V{current_row}:Y{current_row}")
debug_print(f"Zeile {current_row} verifiziert: Antwort: {answer}")
except Exception as e:
debug_print(f"Fehler beim Updaten der Zeile {current_row}: {e}")
time.sleep(Config.RETRY_DELAY)
# ==================== List Metatitel, Description und Überschriften aus Websiten aus ====================
def scrape_website_details(url):
"""
Ruft die Website ab und extrahiert folgende Informationen:
- Seitentitel (<title>)
- Meta-Description (<meta name="description">)
- Alle Überschriften h1, h2, h3 (als kommaseparierte Listen)
Die extrahierten Informationen werden in folgender Form kombiniert:
"Title: [Seitentitel] Description: [Meta-Description] H1: [h1-Überschriften] H2: [h2-Überschriften] H3: [h3-Überschriften]"
Args:
url (str): Die URL der Website.
Returns:
str: Die formatierte Zusammenfassung oder "k.A." bei Fehlern.
"""
# Falls URL kein Schema besitzt, ergänze "https://"
if not url.lower().startswith("http"):
url = "https://" + url
try:
response = requests.get(url, timeout=10)
soup = BeautifulSoup(response.text, Config.HTML_PARSER)
# Seitentitel extrahieren
title_tag = soup.find("title")
title = title_tag.get_text().strip() if title_tag and title_tag.get_text() else "k.A."
# Meta-Description extrahieren
meta_tag = soup.find("meta", attrs={"name": "description"})
description = meta_tag["content"].strip() if meta_tag and meta_tag.get("content") else "k.A."
# Überschriften h1, h2, h3 extrahieren und kommasepariert zusammenfassen
headers = {}
for tag in ["h1", "h2", "h3"]:
elements = soup.find_all(tag)
# Extrahiere den Text und filtere leere Ergebnisse
header_texts = [el.get_text().strip() for el in elements if el.get_text().strip()]
headers[tag] = ", ".join(header_texts) if header_texts else "k.A."
# Kombiniere alle extrahierten Daten in einen String
combined = (
f"Title: {title} "
f"Description: {description} "
f"H1: {headers['h1']} "
f"H2: {headers['h2']} "
f"H3: {headers['h3']}"
)
return combined
except Exception as e:
debug_print(f"Fehler beim Auslesen der Website {url}: {e}")
return "k.A."
# ==================== ALIGNMENT DEMO (Hauptblatt) ====================
def alignment_demo(sheet):
new_headers = [
[ # Spaltenname (Zeile 1)
"ReEval Flag", # A
"CRM Name", # B
"CRM Kurzform", # C
"CRM Website", # D
"CRM Ort", # E
"CRM Beschreibung", # F
"CRM Branche", # G
"CRM Beschreibung Branche extern", # H
"CRM Anzahl Techniker", # I
"CRM Umsatz", # J
"CRM Anzahl Mitarbeiter", # K
"CRM Vorschlag Wiki URL", # L
"Wiki URL", # M
"Wiki Absatz", # N
"Wiki Branche", # O
"Wiki Umsatz", # P
"Wiki Mitarbeiter", # Q
"Wiki Kategorien", # R
"Chat Wiki Konsistenzprüfung", # S
"Chat Begründung Wiki Inkonsistenz", # T
"Chat Vorschlag Wiki Artikel", # U
"Begründung bei Abweichung", # V
"Chat Vorschlag Branche", # W
"Chat Konsistenz Branche", # X
"Chat Begründung Abweichung Branche", # Y
"Chat Prüfung FSM Relevanz", # Z
"Chat Begründung für FSM Relevanz", # AA
"Chat Schätzung Anzahl Mitarbeiter", # AB
"Chat Konsistenzprüfung Mitarbeiterzahl", # AC
"Chat Begründung Abweichung Mitarbeiterzahl", # AD
"Chat Einschätzung Anzahl Servicetechniker", # AE
"Chat Begründung Abweichung Anzahl Servicetechniker", # AF
"Chat Schätzung Umsatz", # AG
"Chat Begründung Abweichung Umsatz", # AH
"Linked Serviceleiter gefunden", # AI
"Linked It-Leiter gefunden", # AJ
"Linked Management gefunden", # AK
"Linked Disponent gefunden", # AL
"Contact Search Timestamp", # AM
"Wikipedia Timestamp", # AN
"Timestamp letzte Prüfung", # AO
"Version", # AP
"Tokens", # AQ
"Website Rohtext", # AR
"Website Zusammenfassung" # AS
],
[ # Quelle der Daten (Zeile 2)
"CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM", "CRM",
"CRM", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper", "Wikipediascraper",
"Wikipediascraper", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API",
"Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API",
"Chat GPT API", "Chat GPT API", "Chat GPT API", "Chat GPT API",
"LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)", "LinkedIn (via SerpApi)",
"System", "System", "System", "System", "System",
"Web Scraper", # AR
"Chat GPT API" # AS
],
[ # Feldkategorie (Zeile 3)
"Prozess", "Firmenname", "Firmenname", "Website", "Ort", "Beschreibung (Text)",
"Branche", "Branche", "Anzahl Servicetechniker", "Umsatz", "Anzahl Mitarbeiter",
"Wikipedia Artikel URL", "Wikipedia Artikel", "Beschreibung (Text)", "Branche",
"Umsatz", "Anzahl Mitarbeiter", "Kategorien (Text)", "Verifizierung",
"Begründung bei Abweichung", "Wikipedia Artikel", "Wikipedia Artikel",
"Branche", "Branche", "Branche", "FSM Relevanz", "FSM Relevanz",
"Anzahl Mitarbeiter", "Anzahl Mitarbeiter", "Anzahl Mitarbeiter",
"Anzahl Servicetechniker", "Anzahl Servicetechniker", "Umsatz", "Umsatz",
"Kontakte zur Firma", "Kontakte zur Firma", "Kontakte zur Firma", "Kontakte zur Firma",
"Timestamp", "Timestamp", "Timestamp",
"Version des Skripts die verwendet wurde", "ChatGPT Tokens",
"Website-Content", # AR
"Website Zusammenfassung" # AS
],
[ # Kurze Beschreibung (Zeile 4)
"Systemspalte, irrelevant für den Prompt. Wird zur manuellen Neuprüfung genutzt.",
"Enthält den Firmennamen; Normalisierung erfolgt bei der Suche.",
"Manuell gepflegte Kurzform, meist die ersten 2 Worte.",
"Website des Unternehmens.",
"Ort des Unternehmens.",
"Kurze Beschreibung des Unternehmens.",
"Aktuelle Branchenzuweisung gemäß Ziel-Branchenschema.",
"Externe Branchenbeschreibung (z.B. von Dealfront).",
"Recherchierte Anzahl Servicetechniker.",
"Umsatz in Mio. € (CRM).",
"Anzahl Mitarbeiter (CRM).",
"Vorgeschlagene Wikipedia URL (Ausgangspunkt).",
"Wikipedia URL (Ergebnis der Suche).",
"Erster Absatz des Wikipedia-Artikels.",
"Wikipedia-Branche für den Branchenabgleich.",
"Wikipedia-Umsatz zur Validierung.",
"Wikipedia-Mitarbeiterzahl zur Validierung.",
"Liste der Wikipedia-Kategorien.",
"\"OK\" oder \"X\" Ergebnis der Wikipedia-Validierung.",
"Begründung bei Inkonsistenz (Wiki).",
"Chat-Vorschlag Wiki Artikel: Falls kein passender Artikel gefunden, alternativ vorschlagen.",
"Nicht genutzt, evtl. für zukünftige Funktionen.",
"Branchenvorschlag via ChatGPT (alternativer Vorschlag).",
"Vergleich: Übereinstimmung CRM vs. ChatGPT-Branche (OK/X).",
"Begründung bei abweichender Branchenzuordnung.",
"FSM-Relevanz: Bewertung, ob das Unternehmen für FSM geeignet ist (OK/X).",
"Begründung zur FSM-Bewertung.",
"Schätzung Anzahl Mitarbeiter via ChatGPT (nur falls Wiki-Daten fehlen).",
"Vergleich CRM vs. Wiki vs. ChatGPT Mitarbeiterzahl (OK/X).",
"Begründung bei Mitarbeiterabweichung (Prozentdifferenz).",
"Schätzung Servicetechniker via ChatGPT (in Kategorien, z.B. <50, >100, >200, >500).",
"Begründung bei Abweichung der Technikerzahl.",
"Schätzung Umsatz via ChatGPT.",
"Begründung bei Umsatzabweichung.",
"Anzahl Kontakte (Serviceleiter) gefunden.",
"Anzahl Kontakte (IT-Leiter) gefunden.",
"Anzahl Kontakte (Management) gefunden.",
"Anzahl Kontakte (Disponent) gefunden.",
"Timestamp der Kontaktsuche.",
"Timestamp der Wikipedia-Suche.",
"Timestamp der ChatGPT-Bewertung.",
"Ausgabe der Skriptversion, die das Ergebnis erzeugt hat.",
"Token-Zählung (separat pro Modul).",
"Roh extrahierter Text der Firmenwebsite (maximal 1000 Zeichen).",
"Zusammenfassung des Webseiteninhalts, fokussiert auf Tätigkeitsfeld, Produkte & Leistungen."
],
[ # Aufgabe / Funktion (exakte Vorgabe der Ausgangsversion, unverändert)
"Datenquelle",
"Datenquelle",
"Datenquelle",
"Datenquelle",
"Datenquelle",
"Datenquelle",
"Datenquelle",
"Datenquelle",
"Datenquelle",
"Datenquelle",
"Datenquelle",
"Datenquelle",
"Wird durch Wikipedia Scraper bereitgestellt",
"Wird zunächst nicht verwendet, kann aber zum Vergleich mit der CRM-Beschreibung genutzt werden.",
"Wird u.a. zur finalen Ermittlung der Branche im Ziel-Branchenschema genutzt und mit der CRM-Branche bzw. CRM-Beschreibung Branche Extern verglichen. Stimmen alle drei Einstufungen grob überein, bestärkt dies die ursprüngliche Einstufung. Laufen diese Branchen weit auseinander, soll sofern der Wikipedia-Artikel verifiziert ist die Branche von Wikipedia als zuverlässigste Quelle bewertet werden, danach folgen CRM-Beschreibung Branche Extern und CRM-Branche an dritter Stelle.",
"Wird u.a. mit CRM-Umsatz zur Validierung des Unternehmens verglichen bzw. zur Bewertung der Größe / Einschätzung der Technikerzahl bzw. FSM-Relevanz genutzt.",
"Wird u.a. mit CRM-Anzahl Mitarbeiter zur Validierung des Unternehmens verglichen bzw. zur Bewertung der Größe / Einschätzung der Technikerzahl bzw. FSM-Relevanz genutzt.",
"Wenn Website-Daten fehlen, wird in diesem Feld keine zusätzliche Information einbezogen; ansonsten als zusätzlicher Kontext.",
"\"Es soll durch ChatGPT geprüft werden, ob anhand der vorliegenden Daten bestätigt werden kann, dass der Wikipedia-Eintrag das Unternehmen sicher beschreibt. Dabei können alle Daten (Website, Umsatz, Mitarbeiterzahl etc.) berücksichtigt werden. Eine gewisse Toleranz (±30%) ist erlaubt. Insbesondere bei Konzernstrukturen muss großzügig bewertet werden. Abweichungen sollen in der Spalte 'Chat Begründung Wiki Inkonsistenz' begründet werden.\"",
"\"Liegt eine Inkonsistenz zwischen dem gefundenen Wikipedia-Artikel und dem Unternehmen vor, so soll dies kurz begründet werden. Wurde der Artikel als unpassend identifiziert, soll ChatGPT einen alternativen Wikipedia-Artikel vorschlagen und diesen in 'Chat Vorschlag Wiki Artikel' ausgeben.\"",
"\"Sollte durch die Wikipedia-Suche kein Artikel gefunden werden oder als unpassend bewertet werden, soll ChatGPT eigenständig nach einem passenden Artikel recherchieren. Der gefundene Artikel muss vom als unpassend bewerteten Artikel abweichen. Wird kein passender Artikel gefunden, soll 'kein Artikel verfügbar' ausgegeben werden.\"",
"XXX derzeit nicht verwendet, wird vermutlich gelöscht xxx",
"\"ChatGPT soll anhand der vorliegenden Informationen prüfen, welcher Branche des Ziel-Branchenschemas das Unternehmen am ehesten zugeordnet werden kann. Das Ziel-Branchenschema darf nicht verändert werden, sondern die Vorschläge müssen exakt diesem Schema entsprechen.\"",
"Die in Spalte CRM festgelegte Branche soll mit der von ChatGPT ermittelten Branche in 'Chat Vorschlag Branche' verglichen werden.",
"Weicht die von ChatGPT ermittelte Branche von der in CRM vorliegenden ab, so soll ChatGPT die Abweichung kurz begründen.",
"ChatGPT soll anhand der vorliegenden Daten prüfen, ob das Unternehmen für den Einsatz einer Field Service Management Lösung geeignet ist.",
"Die in 'Chat Begründung für FSM Relevanz' angegebene Begründung soll zur Bewertung der FSM-Eignung herangezogen werden.",
"Nur wenn kein Wikipedia-Eintrag vorhanden ist, soll ChatGPT basierend auf öffentlich verfügbaren Informationen die Mitarbeiterzahl schätzen. Falls keine Schätzung möglich ist, wird 'keine Schätzung möglich' ausgegeben.",
"Entspricht die durch ChatGPT ermittelte Mitarbeiterzahl ungefähr den in CRM und Wikipedia ermittelten Werten (±30%), wird 'OK' ausgegeben, andernfalls 'X' und eine Begründung in 'Chat Begründung Abweichung Mitarbeiterzahl'.",
"Weicht die von ChatGPT geschätzte Mitarbeiterzahl signifikant von den CRM- oder Wikipedia-Werten ab, soll dies kurz begründet werden.",
"ChatGPT soll auf Basis öffentlich zugänglicher Informationen eine Schätzung der Anzahl Servicetechniker abgeben (Kategorisierung: 0, <50, >100, >200, >500). Bei Abweichungen der Recherche-Werte soll 'X' ausgegeben werden, ansonsten 'OK'.",
"Weicht die von ChatGPT geschätzte Technikerzahl von den CRM-Werten ab, soll dies begründet werden.",
"Nur wenn kein Wikipedia-Eintrag vorhanden ist, soll ChatGPT den Umsatz anhand der Unternehmenswebsite oder anderer Daten schätzen. Bei fehlender Schätzung soll 'keine Schätzung möglich' ausgegeben werden.",
"ChatGPT soll signifikante Umsatzabweichungen zwischen den Schätzungen von Chat, Wikipedia und CRM begründen. Stimmen die Werte (±30%) überein, wird 'OK' ausgegeben.",
"Über SerpAPI wird zusammen mit der in 'CRM Kurzform' enthaltenen Information nach 'Serviceleiter' gesucht.",
"Über SerpAPI wird zusammen mit 'CRM Kurzform' nach 'Leiter IT' gesucht.",
"Über SerpAPI wird zusammen mit 'CRM Kurzform' nach 'Geschäftsführer' gesucht.",
"Über SerpAPI wird zusammen mit 'CRM Kurzform' erneut nach 'Serviceleiter' gesucht.",
"Wenn die Kontaktsuche gestartet wird, wird der erste Eintrag ohne Zeitstempel gesucht; Zeilen mit vorhandenem Zeitstempel werden übersprungen.",
"Wenn die Wikipedia-Suche gestartet wird, wird der erste Eintrag ohne Zeitstempel gesucht; Zeilen mit vorhandenem Zeitstempel werden übersprungen.",
"Wenn die ChatGPT-Bewertung gestartet wird, wird der erste Eintrag ohne Zeitstempel gesucht; Zeilen mit vorhandenem Zeitstempel werden übersprungen.",
"Wird durch das System befüllt",
"Wird durch tiktoken berechnet"
]
]
header_range = "A1:AS5"
sheet.update(values=new_headers, range_name=header_range)
print("Alignment-Demo abgeschlossen: Neues Spaltenschema in Zeilen A1 bis AS5 geschrieben.")
# ==================== WIKIPEDIA SCRAPER ====================
class WikipediaScraper:
def __init__(self):
wikipedia.set_lang(Config.LANG)
def _get_full_domain(self, website):
if not website:
return ""
website = website.lower().strip()
website = re.sub(r'^https?:\/\/', '', website)
website = re.sub(r'^www\.', '', website)
return website.split('/')[0]
def _generate_search_terms(self, company_name, website):
terms = []
full_domain = self._get_full_domain(website)
if full_domain:
terms.append(full_domain)
normalized_name = normalize_company_name(company_name)
candidate = " ".join(normalized_name.split()[:2]).strip()
if candidate and candidate not in terms:
terms.append(candidate)
if normalized_name and normalized_name not in terms:
terms.append(normalized_name)
debug_print(f"Generierte Suchbegriffe: {terms}")
return terms
def _validate_article(self, page, company_name, website):
full_domain = self._get_full_domain(website)
domain_found = False
if full_domain:
try:
html_raw = requests.get(page.url).text
soup = BeautifulSoup(html_raw, Config.HTML_PARSER)
infobox = soup.find('table', class_=lambda c: c and 'infobox' in c.lower())
if infobox:
links = infobox.find_all('a', href=True)
for link in links:
href = link.get('href').lower()
if href.startswith('/wiki/datei:'):
continue
if full_domain in href:
debug_print(f"Definitiver Link-Match in Infobox gefunden: {href}")
domain_found = True
break
if not domain_found and hasattr(page, 'externallinks'):
for ext_link in page.externallinks:
if full_domain in ext_link.lower():
debug_print(f"Definitiver Link-Match in externen Links gefunden: {ext_link}")
domain_found = True
break
except Exception as e:
debug_print(f"Fehler beim Extrahieren von Links: {str(e)}")
normalized_title = normalize_company_name(page.title)
normalized_company = normalize_company_name(company_name)
similarity = SequenceMatcher(None, normalized_title, normalized_company).ratio()
debug_print(f"Ähnlichkeit (normalisiert): {similarity:.2f} ({normalized_title} vs {normalized_company})")
threshold = 0.60 if domain_found else Config.SIMILARITY_THRESHOLD
return similarity >= threshold
def extract_first_paragraph(self, page_url):
try:
response = requests.get(page_url)
soup = BeautifulSoup(response.text, Config.HTML_PARSER)
paragraphs = soup.find_all('p')
for p in paragraphs:
text = clean_text(p.get_text())
if len(text) > 50:
return text
return "k.A."
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:
return "k.A."
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']
}
keywords = keywords_map.get(target, [])
for row in infobox.find_all('tr'):
header = row.find('th')
if header:
header_text = clean_text(header.get_text()).lower()
if any(kw in header_text for kw in keywords):
value = row.find('td')
if value:
raw_value = clean_text(value.get_text())
if target == 'branche':
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)
if target == 'mitarbeiter':
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']))
if not infobox:
return "k.A."
return clean_text(infobox.get_text(separator=' | '))
def extract_fields_from_infobox_text(self, infobox_text, field_names):
result = {}
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():
j = i + 1
while j < len(tokens) and not tokens[j]:
j += 1
result[field] = tokens[j] if j < len(tokens) else "k.A."
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.'
}
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)
first_paragraph = self.extract_first_paragraph(page_url)
return {
'url': page_url,
'first_paragraph': first_paragraph,
'branche': raw_branche,
'umsatz': umsatz_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.',
'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)
for term in search_terms:
try:
results = wikipedia.search(term, results=Config.WIKIPEDIA_SEARCH_RESULTS)
debug_print(f"Suchergebnisse für '{term}': {results}")
for title in results:
try:
page = wikipedia.page(title, auto_suggest=False)
if self._validate_article(page, company_name, website):
return page
except (wikipedia.exceptions.DisambiguationError, wikipedia.exceptions.PageError) as e:
debug_print(f"Seitenfehler: {str(e)}")
continue
except Exception as e:
debug_print(f"Suchfehler: {str(e)}")
continue
return None
# ==================== NEUE FUNKTION: Angepasste evaluate_branche_chatgpt ====================
def evaluate_branche_chatgpt(crm_branche, beschreibung, wiki_branche, wiki_kategorien, website_summary):
"""
Ordnet das Unternehmen basierend auf den angegebenen Informationen exakt einer Branche
des in der CSV-Datei hinterlegten Ziel-Branchenschemas zu.
Der System-Prompt enthält nun den erlaubten Branchenbereich, und der von ChatGPT gegebene Vorschlag
wird bereinigt und gegen die Einträge des Ziel-Schemas validiert.
Falls der Vorschlag nicht validiert werden kann, erfolgt ein Fallback auf den CRM-Wert.
Args:
crm_branche (str): Branche laut CRM
beschreibung (str): Unternehmensbeschreibung (CRM)
wiki_branche (str): Branche aus Wikipedia (falls vorhanden)
wiki_kategorien (str): Wikipedia-Kategorien
website_summary (str): Zusammenfassung des Website-Inhalts
Returns:
dict: Enthält "branch", "consistency" (ok oder X) und "justification".
"""
# Lade Mapping und Liste der erlaubten Ziel-Branchen
mapping, allowed_branches = load_target_branches()
# Baue den Text für das Ziel-Branchenschema, der im System-Prompt an ChatGPT übergeben wird
schema_lines = ["Ziel-Branchenschema: Folgende Branchenbereiche sind gültig:"]
for branch in allowed_branches:
schema_lines.append(f"- {branch}")
target_schema_info = "\n".join(schema_lines)
# Erstelle den System-Prompt inklusive der Zielvorgaben.
# Hier wird das Zielbranchenschema aus der CSV-Datei (via load_target_schema) eingebunden.
target_mapping, target_schema_string, allowed_targets = load_target_schema()
prompt = (
f"{target_schema_string}\n\n"
f"Ordne das Unternehmen anhand folgender Angaben exakt einer Branche des Ziel-Branchenschemas zu:\n"
f"CRM-Branche: {crm_branche}\n"
f"Beschreibung: {beschreibung}\n"
f"Wikipedia-Branche: {wiki_branche}\n"
f"Wikipedia-Kategorien: {wiki_kategorien}\n"
f"Website-Zusammenfassung: {website_summary}\n\n"
"Antworte im Format:\n"
"Branche: <vorgeschlagene Branche>\n"
"Übereinstimmung: <ok oder X>\n"
"Begründung: <kurze Begründung>"
)
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 für Brancheneinschätzung: {e}")
return {"branch": crm_branche, "consistency": "X", "justification": "API-Key Fehler"}
openai.api_key = api_key
try:
response = openai.ChatCompletion.create(
model=Config.TOKEN_MODEL,
messages=[{"role": "user", "content": prompt}],
temperature=0.0
)
chat_response = response.choices[0].message.content.strip()
except Exception as e:
debug_print(f"Fehler bei der ChatGPT-Anfrage für Brancheneinschätzung: {e}")
return {"branch": crm_branche, "consistency": "X", "justification": "API-Anfrage Fehler"}
# Erwarte ein Format:
# Branche: <vorgeschlagene Branche>
# Übereinstimmung: <ok oder X>
# Begründung: <kurze Begründung>
lines = chat_response.split("\n")
suggestion = ""
consistency = ""
explanation = ""
for line in lines:
lower_line = line.lower()
if lower_line.startswith("branche:"):
suggestion = line.split(":", 1)[1].strip()
elif lower_line.startswith("übereinstimmung:"):
consistency = line.split(":", 1)[1].strip()
elif lower_line.startswith("begründung:"):
explanation = line.split(":", 1)[1].strip()
# Bereinige den Vorschlag: Entferne unnötige Satzzeichen und konvertiere in Kleinbuchstaben
clean_suggestion = re.sub(r'[^\w\s/&-]', '', suggestion).strip().lower()
# Falls der bereinigte Vorschlag kein Hierarchie-Trennzeichen ">" enthält, übernehme den Präfix aus der CRM-Branche
if ">" not in clean_suggestion and ">" in crm_branche:
prefix = crm_branche.split(">")[0].strip().lower()
clean_suggestion = prefix + " > " + clean_suggestion
# Prüfe, ob der bereinigte Vorschlag mit einem erlaubten Eintrag (Fuzzy Matching) übereinstimmt
valid = False
for allowed in allowed_branches:
sim = fuzzy_similarity(clean_suggestion, allowed)
if sim > 0.95: # sehr hoher Ähnlichkeitswert (anpassbar)
valid = True
# Setze den Vorschlag exakt auf den Zielwert
clean_suggestion = allowed
break
if not valid:
debug_print(f"Mapping ungültig für Vorschlag: '{clean_suggestion}'. Fallback: CRM-Branche ('{crm_branche}') verwendet.")
return {"branch": crm_branche, "consistency": consistency, "justification": "Fallback: CRM-Wert verwendet aufgrund ungültiger ChatGPT-Zuweisung."}
return {"branch": clean_suggestion, "consistency": consistency, "justification": explanation}
def evaluate_servicetechnicians_estimate(company_name, company_data):
try:
with open("serpApiKey.txt", "r") as f:
serp_key = f.read().strip()
except Exception as e:
debug_print(f"Fehler beim Lesen des SerpAPI-Schlüssels (Servicetechniker): {e}")
return "k.A."
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 (Servicetechniker): {e}")
return "k.A."
openai.api_key = api_key
prompt = (
f"Bitte schätze auf Basis öffentlich zugänglicher Informationen (insbesondere verifizierte Wikipedia-Daten) "
f"die Anzahl der Servicetechniker für das Unternehmen '{company_name}' ein. "
"Gib die Antwort ausschließlich in einer der folgenden Kategorien aus: '<50 Techniker', '>100 Techniker', '>200 Techniker', '>500 Techniker'."
)
try:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "system", "content": prompt}],
temperature=0.0
)
result = response.choices[0].message.content.strip()
debug_print(f"Schätzung Servicetechniker ChatGPT: '{result}'")
return result
except Exception as e:
debug_print(f"Fehler beim Aufruf der ChatGPT API für Servicetechniker-Schätzung: {e}")
return "k.A."
def evaluate_servicetechnicians_explanation(company_name, st_estimate, company_data):
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 (ST-Erklärung): {e}")
return "k.A."
openai.api_key = api_key
prompt = (
f"Bitte erkläre, warum du für das Unternehmen '{company_name}' die Anzahl der Servicetechniker als '{st_estimate}' geschätzt hast. "
"Berücksichtige dabei öffentlich zugängliche Informationen (z.B. Branche, Umsatz, Mitarbeiterzahl)."
)
try:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "system", "content": prompt}],
temperature=0.0
)
result = response.choices[0].message.content.strip()
debug_print(f"Servicetechniker-Erklärung ChatGPT: '{result}'")
return result
except Exception as e:
debug_print(f"Fehler beim Aufruf der ChatGPT API für Servicetechniker-Erklärung: {e}")
return "k.A."
def map_internal_technicians(value):
try:
num = int(value)
except Exception:
return "k.A."
if num < 50:
return "<50 Techniker"
elif num < 100:
return ">100 Techniker"
elif num < 200:
return ">200 Techniker"
else:
return ">500 Techniker"
def process_batch_token_count():
debug_print("Batch Token Count Modus (Modus 8) wird ausgeführt.")
time.sleep(Config.RETRY_DELAY)
debug_print("Batch Token Count abgeschlossen.")
# ==================== GOOGLE SHEET HANDLER ====================
class GoogleSheetHandler:
def __init__(self):
self.sheet = None
self.sheet_values = []
self._connect()
def _connect(self):
scope = ["https://www.googleapis.com/auth/spreadsheets"]
creds = ServiceAccountCredentials.from_json_keyfile_name(Config.CREDENTIALS_FILE, scope)
self.sheet = gspread.authorize(creds).open_by_url(Config.SHEET_URL).sheet1
self.sheet_values = self.sheet.get_all_values()
def get_start_index(self):
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)
# ==================== DATA PROCESSOR ====================
class DataProcessor:
def __init__(self):
self.sheet_handler = GoogleSheetHandler()
self.wiki_scraper = WikipediaScraper()
def process_rows(self, num_rows=None):
global MODE
if MODE == "2":
print("Re-Evaluierungsmodus: Verarbeitung aller Zeilen mit 'x' in Spalte A.")
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
if row[0].strip().lower() == "x":
self._process_single_row(i, row)
elif MODE == "3":
print("Alignment-Demo-Modus: Schreibe neue Spaltenüberschriften in Hauptblatt und Contacts.")
alignment_demo_full()
elif MODE == "4":
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
if len(row) <= 39 or row[39].strip() == "":
self._process_single_row(i, row, process_wiki=True, process_chatgpt=False)
elif MODE == "5":
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
if len(row) <= 40 or row[40].strip() == "":
self._process_single_row(i, row, process_wiki=False, process_chatgpt=True)
elif MODE == "51":
process_verification_only()
elif MODE == "8":
process_batch_token_count()
else:
start_index = self.sheet_handler.get_start_index()
print(f"Starte bei Zeile {start_index+1}")
rows_processed = 0
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
if i < start_index:
continue
if num_rows is not None and rows_processed >= num_rows:
break
self._process_single_row(i, row)
rows_processed += 1
def _process_single_row(self, row_num, row_data, process_wiki=True, process_chatgpt=True):
# Hole den Firmennamen aus Spalte B
company_name = row_data[1] if len(row_data) > 1 else ""
# Hole die CRM-Website (Spalte D). Wenn diese leer ist, führe den SERP-API Lookup durch.
website_url = row_data[3] if len(row_data) > 3 else ""
if website_url.strip() == "" or website_url.strip().lower() == "k.a.":
new_website = serp_website_lookup(company_name)
if new_website != "k.A.":
website_url = new_website
try:
self.sheet_handler.sheet.update(values=[[website_url]], range_name=f"D{row_num}")
debug_print(f"Zeile {row_num}: CRM-Website war leer neue Website gefunden und in Spalte D eingetragen: {website_url}")
except Exception as e:
debug_print(f"Zeile {row_num}: Fehler beim Updaten der CRM-Website in Spalte D: {e}")
else:
debug_print(f"Zeile {row_num}: Keine Website gefunden für {company_name}.")
# Unabhängig vom process_wiki-Flag: Führe Website-Scraping durch, sofern eine Website vorliegt.
website_raw = "k.A."
website_summary = "k.A."
if website_url.strip() != "" and website_url.strip().lower() != "k.a.":
website_raw = get_website_raw(website_url)
website_summary = summarize_website_content(website_raw)
debug_print(f"Zeile {row_num}: Website-Daten gescrapt. Rohtext (Länge {len(website_raw)}): {website_raw[:100]}..., Zusammenfassung: {website_summary}")
else:
debug_print(f"Zeile {row_num}: Kein gültiger Website-URL vorhanden, Website-Scraping wird übersprungen.")
# Erstelle einen Dict mit allen Werten, die in dieser Zeile aktualisiert werden sollen.
# Dadurch können wir alle Updates in einem einzigen Aufruf zusammenfassen.
updates = {}
# Spalte AR: Website Rohtext
updates[f"AR{row_num}"] = website_raw
# Spalte AS: Website Zusammenfassung
updates[f"AS{row_num}"] = website_summary
# Weiterer Verarbeitungsteil: Wikipedia-Verarbeitung (falls process_wiki True)
wiki_update_range = f"L{row_num}:R{row_num}"
dt_wiki_range = f"AN{row_num}"
company_data = {}
if process_wiki:
if len(row_data) <= 39 or row_data[39].strip() == "":
if len(row_data) > 11 and row_data[11].strip() not in ["", "k.A."]:
wiki_url = row_data[11].strip()
try:
company_data = self.wiki_scraper.extract_company_data(wiki_url)
except Exception as e:
debug_print(f"Zeile {row_num}: Fehler beim Laden des vorgeschlagenen Wikipedia-Artikels: {e}")
article = self.wiki_scraper.search_company_article(company_name, website_url)
company_data = self.wiki_scraper.extract_company_data(article.url) if article else {
'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.',
'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.',
'full_infobox': 'k.A.'
}
else:
article = self.wiki_scraper.search_company_article(company_name, website_url)
company_data = self.wiki_scraper.extract_company_data(article.url) if article else {
'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.',
'umsatz': 'k.A.', 'mitarbeiter': 'k.A.', 'categories': 'k.A.',
'full_infobox': 'k.A.'
}
updates.update({
f"L{row_num}": row_data[11] if len(row_data) > 11 and row_data[11].strip() not in ["", "k.A."] else "k.A.",
f"M{row_num}": company_data.get('url', 'k.A.'),
f"N{row_num}": company_data.get('first_paragraph', 'k.A.'),
f"O{row_num}": company_data.get('branche', 'k.A.'),
f"P{row_num}": company_data.get('umsatz', 'k.A.'),
f"Q{row_num}": company_data.get('mitarbeiter', 'k.A.'),
f"R{row_num}": company_data.get('categories', 'k.A.')
})
updates[dt_wiki_range] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
debug_print(f"Zeile {row_num}: Wikipedia-Timestamp bereits gesetzt überspringe Wiki-Auswertung.")
# ChatGPT-Verarbeitung (Umsatz, FSM, Mitarbeiter, Branchenevaluierung)
dt_chat_range = f"AO{row_num}"
ver_range = f"AP{row_num}"
if process_chatgpt:
if len(row_data) <= 40 or row_data[40].strip() == "":
crm_umsatz = row_data[8] if len(row_data) > 8 else "k.A."
abgleich_result = compare_umsatz_values(crm_umsatz, company_data.get('umsatz', 'k.A.'))
updates[f"AG{row_num}"] = abgleich_result
crm_data = ";".join(row_data[1:10])
wiki_data_str = ";".join(row_data[11:18])
valid_result = process_wiki_verification(crm_data, wiki_data_str)
updates[f"R{row_num}"] = valid_result
fsm_result = evaluate_fsm_suitability(company_name, company_data)
updates[f"Y{row_num}"] = fsm_result["suitability"]
updates[f"Z{row_num}"] = fsm_result["justification"]
st_estimate = evaluate_servicetechnicians_estimate(company_name, company_data)
updates[f"AD{row_num}"] = st_estimate
internal_value = row_data[7] if len(row_data) > 7 else "k.A."
internal_category = map_internal_technicians(internal_value) if internal_value != "k.A." else "k.A."
if internal_category != "k.A." and st_estimate != internal_category:
explanation = evaluate_servicetechnicians_explanation(company_name, st_estimate, company_data)
discrepancy = explanation
else:
discrepancy = "ok"
updates[f"AF{row_num}"] = discrepancy
crm_employee = row_data[10] if len(row_data) > 10 else "k.A."
wiki_employee = company_data.get('mitarbeiter', 'k.A.')
emp_estimate = process_employee_estimation(company_name, company_data.get('first_paragraph', 'k.A.'), crm_employee)
emp_consistency = process_employee_consistency(crm_employee, wiki_employee, emp_estimate)
updates[f"AB{row_num}"] = emp_estimate
updates[f"AC{row_num}"] = emp_consistency
revenue_result = evaluate_umsatz_chatgpt(company_name, company_data.get('umsatz', 'k.A.'))
updates[f"AG{row_num}"] = revenue_result
total_tokens = f"Wiki: {token_count(str(company_data.get('first_paragraph', '')))}, Chat: {token_count(crm_data + wiki_data_str)}, Emp: {token_count(str(emp_estimate))}"
updates[f"AQ{row_num}"] = total_tokens
updates[dt_chat_range] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
else:
debug_print(f"Zeile {row_num}: ChatGPT-Timestamp bereits gesetzt überspringe ChatGPT-Auswertung.")
# Abschließende Updates: Timestamp für letzte Prüfung und Version
current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
updates[ver_range] = current_dt
updates["AP" + str(row_num)] = Config.VERSION
# Führe ein Batch-Update aller gesammelten Werte für diese Zeile durch
try:
batch_updates = []
for cell, value in updates.items():
batch_updates.append({
"range": cell,
"values": [[value]]
})
# Verwende die batch_update-Methode von gspread
self.sheet_handler.sheet.batch_update(batch_updates)
debug_print(f"Zeile {row_num}: Batch-Update erfolgreich durchgeführt. Geschriebene Werte: {updates}")
except Exception as e:
debug_print(f"Zeile {row_num}: Fehler beim Batch-Update: {e}")
debug_print(f"Zeile {row_num} abgeschlossen. URL: {company_data.get('url', 'k.A.')}, "
f"Branche: {company_data.get('branche', 'k.A.')}, Umsatz-Abgleich: {abgleich_result}, "
f"Validierung: {valid_result}, FSM: {fsm_result['suitability']}, "
f"Servicetechniker-Schätzung: {st_estimate}")
time.sleep(Config.RETRY_DELAY)
# ==================== ALIGNMENT DEMO FÜR HAUPTBLATT UND CONTACTS ====================
def alignment_demo_full():
alignment_demo(GoogleSheetHandler().sheet)
gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name(
Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"]))
sh = gc.open_by_url(Config.SHEET_URL)
try:
contacts_sheet = sh.worksheet("Contacts")
except gspread.exceptions.WorksheetNotFound:
contacts_sheet = sh.add_worksheet(title="Contacts", rows="1000", cols="10")
header = ["Firmenname", "Website", "Kurzform", "Vorname", "Nachname", "Position", "Anrede", "E-Mail"]
contacts_sheet.update(values=[header], range_name="A1:H1")
debug_print("Neues Blatt 'Contacts' erstellt und Header eingetragen.")
alignment_demo(contacts_sheet)
debug_print("Alignment-Demo für Hauptblatt und Contacts abgeschlossen.")
# ==================== NEUER MODUS: CONTACT RESEARCH (via SerpAPI) ====================
def process_contact_research():
debug_print("Starte Contact Research (Modus 6)...")
gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name(
Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"]))
sh = gc.open_by_url(Config.SHEET_URL)
main_sheet = sh.sheet1
data = main_sheet.get_all_values()
for i, row in enumerate(data[1:], start=2):
company_name = row[1] if len(row) > 1 else ""
search_name = row[2].strip() if len(row) > 2 and row[2].strip() not in ["", "k.A."] else company_name
website = row[3] if len(row) > 3 else ""
if not company_name or not website:
continue
count_service = count_linkedin_contacts(search_name, website, "Serviceleiter")
count_it = count_linkedin_contacts(search_name, website, "IT-Leiter")
count_management = count_linkedin_contacts(search_name, website, "Geschäftsführer")
count_disponent = count_linkedin_contacts(search_name, website, "Disponent")
current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
main_sheet.update(values=[[str(count_service)]], range_name=f"AI{i}")
main_sheet.update(values=[[str(count_it)]], range_name=f"AJ{i}")
main_sheet.update(values=[[str(count_management)]], range_name=f"AK{i}")
main_sheet.update(values=[[str(count_disponent)]], range_name=f"AL{i}")
main_sheet.update(values=[[current_dt]], range_name=f"AM{i}")
debug_print(f"Zeile {i}: Serviceleiter {count_service}, IT-Leiter {count_it}, Management {count_management}, Disponent {count_disponent} Contact Search Timestamp gesetzt.")
time.sleep(Config.RETRY_DELAY * 1.5)
debug_print("Contact Research abgeschlossen.")
# ==================== NEUER MODUS: CONTACTS (LinkedIn) ====================
def process_contacts():
debug_print("Starte LinkedIn-Kontaktsuche...")
gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name(
Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"]))
sh = gc.open_by_url(Config.SHEET_URL)
try:
contacts_sheet = sh.worksheet("Contacts")
except gspread.exceptions.WorksheetNotFound:
contacts_sheet = sh.add_worksheet(title="Contacts", rows="1000", cols="10")
header = ["Firmenname", "Website", "Kurzform", "Vorname", "Nachname", "Position", "Anrede", "E-Mail"]
contacts_sheet.update(values=[header], range_name="A1:H1")
debug_print("Neues Blatt 'Contacts' erstellt und Header eingetragen.")
alignment_demo(contacts_sheet)
debug_print("Alignment-Demo für Contacts abgeschlossen.")
# Weitere Verarbeitung der Kontakte folgt hier ...
# ==================== LINKEDIN HELPER ====================
def search_linkedin_contacts(company_name, website, position_query, crm_kurzform, num_results=100):
"""
Sucht über SERPAPI mehrere LinkedIn-Kontakte basierend auf der Positionsbezeichnung
und der CRM-Kurzform des Unternehmens. Es werden alle Treffer zurückgegeben, bei denen
die CRM-Kurzform (als Teilstring) im Titel auftaucht.
Args:
company_name (str): Der Firmenname.
website (str): Die Website des Unternehmens.
position_query (str): Die zu suchende Positionsbezeichnung (z. B. "Serviceleiter").
crm_kurzform (str): Die manuell gepflegte Kurzform des Firmennamens.
num_results (int): Anzahl der abzurufenden Suchergebnisse (hier standardmäßig 100).
Returns:
list: Eine Liste von Dictionaries mit den Kontaktdaten (Vorname, Nachname, Position, LinkedInURL)
oder eine leere Liste, wenn keine Treffer gefunden wurden.
"""
try:
with open("serpApiKey.txt", "r") as f:
serp_key = f.read().strip()
except Exception as e:
debug_print("Fehler beim Lesen des SerpAPI-Schlüssels: " + str(e))
return []
query = f'site:linkedin.com/in "{position_query}" "{company_name}"'
params = {
"engine": "google",
"q": query,
"api_key": serp_key,
"hl": "de",
"num": num_results
}
# Logge die vollständige Such-URL
request_url = "https://serpapi.com/search?" + urlencode(params)
debug_print(f"Such-URL: {request_url}")
try:
response = requests.get("https://serpapi.com/search", params=params, timeout=10)
data = response.json()
contacts = []
if "organic_results" in data and len(data["organic_results"]) > 0:
for result in data["organic_results"]:
title = result.get("title", "")
if crm_kurzform.lower() in title.lower():
if "" in title:
parts = title.split("")
elif "-" in title:
parts = title.split("-")
else:
parts = [title]
if len(parts) >= 2:
name_part = parts[0].strip()
pos_part = parts[1].split("|")[0].strip()
name_parts = name_part.split(" ", 1)
if len(name_parts) == 2:
firstname, lastname = name_parts
else:
firstname = name_part
lastname = ""
linkedin_url = result.get("link", "")
debug_print(f"Gefundener Kontakt: {firstname} {lastname}, Position: {pos_part}")
contacts.append({
"Firmenname": company_name,
"Website": website,
"Vorname": firstname,
"Nachname": lastname,
"Position": pos_part,
"LinkedInURL": linkedin_url
})
if not contacts:
debug_print("Kein Treffer mit CRM-Kurzform in Titel gefunden.")
else:
debug_print("Keine organic_results für Query gefunden.")
return contacts
except Exception as e:
debug_print(f"Fehler bei der SERPAPI-Suche: {e}")
return []
def count_linkedin_contacts(company_name, website, position_query, crm_kurzform):
"""
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:
company_name (str): Firmenname.
website (str): Website des Unternehmens.
position_query (str): Gewünschte Position (z. B. "Serviceleiter").
crm_kurzform (str): Kurzform des Firmennamens.
Returns:
int: Anzahl der Treffer, die den Filter erfüllen.
"""
try:
with open("serpApiKey.txt", "r") as f:
serp_key = f.read().strip()
except Exception as e:
debug_print("Fehler beim Lesen des SerpAPI-Schlüssels: " + str(e))
return 0
query = f'site:linkedin.com/in "{position_query}" "{company_name}"'
params = {
"engine": "google",
"q": query,
"api_key": serp_key,
"hl": "de"
}
try:
response = requests.get("https://serpapi.com/search", params=params, timeout=10)
data = response.json()
count = 0
if "organic_results" in data:
for result in data["organic_results"]:
title = result.get("title", "")
if crm_kurzform.lower() in title.lower():
count += 1
debug_print(f"Anzahl Kontakte für Query '{query}' mit CRM-Kurzform-Filter: {count}")
return count
else:
debug_print(f"Keine Ergebnisse für Query: {query}")
return 0
except Exception as e:
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.
Es wird nur ein Treffer zurückgegeben, wenn der Titel auch die CRM-Kurzform (als Teilstring) enthält.
Args:
company_name (str): Der Firmenname.
website (str): Die Website des Unternehmens.
position_query (str): Die zu suchende Positionsbezeichnung (z. B. "Serviceleiter").
crm_kurzform (str): Die manuell gepflegte Kurzform des Firmennamens.
Returns:
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:
serp_key = f.read().strip()
except Exception as e:
debug_print("Fehler beim Lesen des SerpAPI-Schlüssels: " + str(e))
return None
query = f'site:linkedin.com/in "{position_query}" "{company_name}"'
params = {
"engine": "google",
"q": query,
"api_key": serp_key,
"hl": "de"
}
try:
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:
parts = title.split("-")
else:
parts = [title]
if len(parts) >= 2:
name_part = parts[0].strip()
pos_part = parts[1].split("|")[0].strip()
name_parts = name_part.split(" ", 1)
if len(name_parts) == 2:
firstname, lastname = name_parts
else:
firstname = name_part
lastname = ""
linkedin_url = result.get("link", "") # LinkedIn-Profil-Link aus dem Ergebnis
debug_print(f"Gefundener Kontakt: {firstname} {lastname}, Position: {pos_part}")
return {
"Firmenname": company_name,
"Website": website,
"Vorname": firstname,
"Nachname": lastname,
"Position": pos_part,
"LinkedInURL": linkedin_url
}
debug_print("Kein Treffer mit CRM-Kurzform in Titel gefunden.")
return None
else:
debug_print("Keine organic_results für Query gefunden.")
return None
except Exception as e:
debug_print(f"Fehler bei der SerpAPI-Suche: {e}")
return None
def count_linkedin_contacts(company_name, website, position_query, crm_kurzform):
"""
Zählt über SERPAPI, wieviele 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:
company_name (str): Firmenname.
website (str): Website des Unternehmens.
position_query (str): Gewünschte Position (z. B. "Serviceleiter").
crm_kurzform (str): Kurzform des Firmennamens.
Returns:
int: Anzahl der Treffer, die den Filter erfüllen.
"""
try:
with open("serpApiKey.txt", "r") as f:
serp_key = f.read().strip()
except Exception as e:
debug_print("Fehler beim Lesen des SerpAPI-Schlüssels: " + str(e))
return 0
query = f'site:linkedin.com/in "{position_query}" "{company_name}"'
params = {
"engine": "google",
"q": query,
"api_key": serp_key,
"hl": "de"
}
try:
response = requests.get("https://serpapi.com/search", params=params, timeout=10)
data = response.json()
count = 0
if "organic_results" in data:
for result in data["organic_results"]:
title = result.get("title", "")
if crm_kurzform.lower() in title.lower():
count += 1
debug_print(f"Anzahl Kontakte für Query '{query}' mit CRM-Kurzform-Filter: {count}")
return count
else:
debug_print(f"Keine Ergebnisse für Query: {query}")
return 0
except Exception as e:
debug_print(f"Fehler bei der SerpAPI-Suche (Count): {e}")
return 0
def process_contact_research():
"""
Sucht mithilfe der SERPAPI Kontakte für bestimmte Positionen für jedes Unternehmen.
Die gefundenen Kontakte werden im Kontakte-Blatt eingetragen pro Kategorie werden alle
Treffer (die den Filter (CRM-Kurzform muss im Titel enthalten sein) erfüllen) verarbeitet.
Im Kontakte-Blatt wird folgende Spaltenstruktur verwendet:
A: Firmenname
B: CRM Kurzform
C: Website
D: Geschlecht
E: Vorname
F: Nachname
G: Position
H: Suchbegriffskategorie
I: E-Mail-Adresse
J: LinkedIn-Link
K: Timestamp
"""
debug_print("Starte Contact Research (Modus 6)...")
# Verbinde zum Hauptblatt
gc = gspread.authorize(ServiceAccountCredentials.from_json_keyfile_name(
Config.CREDENTIALS_FILE, ["https://www.googleapis.com/auth/spreadsheets"]))
sh = gc.open_by_url(Config.SHEET_URL)
main_sheet = sh.sheet1
data = main_sheet.get_all_values()
# Ermittle die letzte Zeile in Spalte AM (Spalte 39) mit einem Timestamp
col_am = main_sheet.col_values(39)
last_filled_row = 1
for idx, cell in enumerate(col_am):
if cell.strip() != "":
last_filled_row = idx + 1
start_row = last_filled_row + 1
debug_print(f"Letzter Timestamp in Spalte AM in Zeile {last_filled_row}. Starte Verarbeitung ab Zeile {start_row}.")
if start_row > len(data):
debug_print("Keine neuen Zeilen zu verarbeiten, da Timestamp in Spalte AM bis zum Ende vorhanden ist.")
return
# Kontakte-Blatt öffnen oder erstellen (Header: A-K)
try:
contacts_sheet = sh.worksheet("Contacts")
except gspread.exceptions.WorksheetNotFound:
contacts_sheet = sh.add_worksheet(title="Contacts", rows="1000", cols="12")
header = ["Firmenname", "CRM Kurzform", "Website", "Geschlecht", "Vorname", "Nachname", "Position",
"Suchbegriffskategorie", "E-Mail-Adresse", "LinkedIn-Link", "Timestamp"]
contacts_sheet.update(values=[header], range_name="A1:K1")
debug_print("Neues Blatt 'Contacts' erstellt und Header eingetragen.")
# Gehe alle Zeilen im Hauptblatt ab der Startzeile durch
for i in range(start_row, len(data) + 1):
row = data[i - 1]
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 ""
if not company_name or not website or not crm_kurzform:
debug_print(f"Zeile {i}: Fehlende essentielle CRM-Daten, überspringe.")
continue
positions = ["Serviceleiter", "IT-Leiter", "Geschäftsführer", "Disponent"]
contact_counts = {}
for pos in positions:
count = count_linkedin_contacts(crm_kurzform, website, pos, crm_kurzform)
contact_counts[pos] = count
# Abfrage: Es sollen nun alle Treffer (bis zu 100) verarbeitet werden
contacts = search_linkedin_contacts(crm_kurzform, website, pos, crm_kurzform, num_results=100)
for contact in contacts:
firstname = contact.get("Vorname", "")
lastname = contact.get("Nachname", "")
gender_value = get_gender(firstname) if firstname else "unknown"
email = get_email_address(firstname, lastname, website)
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Spaltenanordnung: A: Firmenname, B: CRM Kurzform, C: Website, D: Geschlecht, E: Vorname,
# F: Nachname, G: Position, H: Suchbegriffskategorie, I: E-Mail-Adresse, J: LinkedIn-Link, K: Timestamp
contact_row = [
company_name,
crm_kurzform,
website,
gender_value,
firstname,
lastname,
contact.get("Position", ""),
pos,
email,
contact.get("LinkedInURL", ""),
timestamp
]
try:
contacts_sheet.append_row(contact_row)
debug_print(f"Zeile {i}: Kontakt für '{pos}' gespeichert: {contact_row}")
except Exception as e:
debug_print(f"Zeile {i}: Fehler beim Speichern des Kontakts für '{pos}': {e}")
# Aktualisiere Trefferzahlen und Timestamp im Hauptblatt
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}")
main_sheet.update(values=[[str(contact_counts.get("Geschäftsführer", 0))]], range_name=f"AK{i}")
main_sheet.update(values=[[str(contact_counts.get("Disponent", 0))]], range_name=f"AL{i}")
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
main_sheet.update(values=[[timestamp]], range_name=f"AM{i}")
debug_print(f"Zeile {i}: Kontaktzahlen aktualisiert: {contact_counts} Timestamp in AM gesetzt.")
except Exception as e:
debug_print(f"Zeile {i}: Fehler beim Aktualisieren der CRM-Kontaktsummen: {e}")
time.sleep(Config.RETRY_DELAY)
debug_print("Contact Research abgeschlossen.")
# ----------------- DataProcessor-Klasse inklusive neuer SERP-API Website Lookup-Methode -----------------
class DataProcessor:
def __init__(self):
self.sheet_handler = GoogleSheetHandler()
self.wiki_scraper = WikipediaScraper()
def process_serp_website_lookup(self):
debug_print("Starte SERP-API Website Lookup für alle Zeilen ohne CRM-Website (Spalte D).")
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
current_website = row[3] if len(row) > 3 else ""
if current_website.strip() == "":
company_name = row[1] if len(row) > 1 else ""
new_website = serp_website_lookup(company_name)
if new_website != "k.A.":
self.sheet_handler.sheet.update(values=[[new_website]], range_name=f"D{i}")
debug_print(f"Zeile {i}: Neue Website gefunden und in Spalte D eingetragen: {new_website}")
else:
debug_print(f"Zeile {i}: Keine Website gefunden für {company_name}.")
time.sleep(Config.RETRY_DELAY)
else:
debug_print(f"Zeile {i}: CRM-Website bereits vorhanden, überspringe.")
def process_website_details(self):
"""
Neuer Modus 23:
Für alle Zeilen, in denen das Re-Evaluation-Flag in Spalte A "x" steht
und ein gültiger Website-URL in Spalte D vorhanden ist, wird die Funktion
scrape_website_details(url) aufgerufen. Das kombinierte Ergebnis (Title, Meta-Description,
h1, h2, h3) wird in Spalte AR geschrieben.
"""
debug_print("Starte Modus 23: Website Detail Extraction für Zeilen mit 'x' in Spalte A.")
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
# Hier verarbeiten wir nur die Zeilen, die gezielt zur Re-Evaluation markiert sind:
if row[0].strip().lower() != "x":
continue
website_url = row[3] if len(row) > 3 else ""
if website_url.strip() == "" or website_url.strip().lower() == "k.a.":
debug_print(f"Zeile {i}: Keine gültige Website in Spalte D vorhanden, überspringe.")
continue
details = scrape_website_details(website_url)
# Speichere das Detail-Ergebnis in Spalte AR (Website Rohtext)
self.sheet_handler.sheet.update(values=[[details]], range_name=f"AR{i}")
debug_print(f"Zeile {i}: Website Detail Extraction abgeschlossen, Ergebnis in Spalte AR geschrieben.")
time.sleep(Config.RETRY_DELAY)
def process_rows(self, num_rows=None):
global MODE
if MODE == "1":
# Vollständige Verarbeitung (alle Funktionen)
self.process_rows_complete() # Falls diese Methode bereits implementiert ist.
elif MODE == "11":
# Re-Evaluation markierter Zeilen (nur "x" in Spalte A)
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
if row[0].strip().lower() == "x":
self._process_single_row(i, row)
elif MODE == "21":
# Website-Scraping Testmodus: Nur Website-Rohtext & Zusammenfassung extrahieren
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
self._process_single_row(i, row, process_wiki=False, process_chatgpt=False)
elif MODE == "22":
# SERP-API Website Lookup: Füllt Spalte D, falls leer
self.process_serp_website_lookup()
elif MODE == "23":
# Neuer Modus 23: Detaillierte Website-Auswertung (nur für Zeilen mit "x" in Spalte A)
self.process_website_details()
elif MODE == "31":
# Nur ChatGPT-Auswertung: Alle ChatGPT-Routinen (ohne Wikipedia und Website)
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
self._process_single_row(i, row, process_wiki=False, process_chatgpt=True)
elif MODE == "41":
# Nur Wikipedia-Scraping
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
self._process_single_row(i, row, process_wiki=True, process_chatgpt=False)
elif MODE == "51":
process_verification_only()
elif MODE == "6":
process_contact_research()
elif MODE == "8":
process_batch_token_count()
else:
# Falls ein unbekannter Modus gewählt wird
start_index = self.sheet_handler.get_start_index()
print(f"Starte bei Zeile {start_index+1}")
rows_processed = 0
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
if i < start_index:
continue
if num_rows is not None and rows_processed >= num_rows:
break
self._process_single_row(i, row)
rows_processed += 1
# ----------------- Main-Funktion -----------------
def main():
global MODE, LOG_FILE
# Argumentparser initialisieren
parser = argparse.ArgumentParser(description="Brancheneinstufung Skript")
parser.add_argument("--mode", type=str, help="Betriebsmodus: wiki, website, branch, combined, etc.")
parser.add_argument("--row_limit", type=int, help="Anzahl der zu verarbeitenden Zeilen/Accounts", default=None)
args = parser.parse_args()
# Betriebsmodus aus Kommandozeile oder interaktiv ermitteln
if args.mode:
MODE = args.mode.strip().lower()
print(f"Betriebsmodus (aus Kommandozeile): {MODE}")
else:
print("Bitte wählen Sie den Betriebsmodus:")
print("wiki: Nur Wikipedia-Verifizierung (Batch)")
print("website: Nur Website-Scraping (Batch)")
print("branch: Nur Brancheneinschätzung (Batch)")
print("combined: Alle Funktionen (Wikipedia, Website, Branch) in einem Durchlauf")
print("1: Vollständige Verarbeitung (alle Funktionen)")
print("11: Re-Evaluation markierter Zeilen (nur 'x' in Spalte A)")
print("21: Website-Scraping Testmodus (nur Website-Rohtext & Zusammenfassung)")
print("22: SERP-API Website Lookup (nur Website-Daten ermitteln)")
print("23: Website Detail Extraction (nur für Zeilen mit 'x')")
print("31: Nur ChatGPT-Auswertung (alle ChatGPT-Routinen)")
print("41: Nur Wikipedia-Scraping")
print("6: Contact Research (LinkedIn)")
print("8: Batch Token-Zählung")
MODE = input("Geben Sie den Modus ein (z.B. wiki, website, branch, combined oder alte Zahl): ").strip().lower()
if not MODE:
MODE = "combined"
LOG_FILE = create_log_filename(MODE)
debug_print(f"Start Betriebsmodus {MODE}")
for entry in prompt_overview()[1:]:
debug_print(f"{entry[0]}: {entry[1]}")
dp = DataProcessor()
# Row_limit aus Kommandozeile oder interaktiv ermitteln
if args.row_limit is not None:
row_limit = args.row_limit
print(f"Zeilenlimit (aus Kommandozeile): {row_limit}")
else:
try:
row_limit = int(input("Wie viele Zeilen sollen insgesamt bearbeitet werden? "))
except Exception as e:
debug_print(f"Fehler bei der Eingabe der Zeilenanzahl: {e}. Es werden alle Zeilen verarbeitet.")
row_limit = None
# Auswahl des Arbeitsmodus
if MODE in ["wiki", "website", "branch", "combined"]:
run_dispatcher(MODE, row_limit)
elif MODE == "1":
dp.process_rows()
elif MODE == "11":
for i, row in enumerate(dp.sheet_handler.sheet_values[1:], start=2):
if row[0].strip().lower() == "x":
dp._process_single_row(i, row)
elif MODE == "21":
for i, row in enumerate(dp.sheet_handler.sheet_values[1:], start=2):
dp._process_single_row(i, row, process_wiki=False, process_chatgpt=False)
elif MODE == "22":
dp.process_serp_website_lookup()
elif MODE == "23":
dp.process_website_details()
elif MODE == "31":
for i, row in enumerate(dp.sheet_handler.sheet_values[1:], start=2):
dp._process_single_row(i, row, process_wiki=False, process_chatgpt=True)
elif MODE == "41":
for i, row in enumerate(dp.sheet_handler.sheet_values[1:], start=2):
dp._process_single_row(i, row, process_wiki=True, process_chatgpt=False)
elif MODE == "6":
process_contact_research()
elif MODE == "8":
process_batch_token_count()
else:
start_index = dp.sheet_handler.get_start_index()
print(f"Starte bei Zeile {start_index+1}")
rows_processed = 0
for i, row in enumerate(dp.sheet_handler.sheet_values[1:], start=2):
if i < start_index:
continue
if rows_processed >= 1:
break
dp._process_single_row(i, row)
rows_processed += 1
print(f"Verarbeitung abgeschlossen. Logfile: {LOG_FILE}")
if __name__ == '__main__':
main()