1582 lines
80 KiB
Python
1582 lines
80 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Version: v1.4.8
|
||
Datum: {aktuelles Datum}
|
||
Git-Überschrift (max. 100 Zeichen):
|
||
v1.4.8 Verbesserter Website-Scraper: User-Agent gesetzt und SSL-Verifikation optional
|
||
|
||
|
||
Git-Änderungsbeschreibung:
|
||
- In get_website_raw() wurde ein User-Agent-Header hinzugefügt, um Blockaden zu vermeiden.
|
||
- SSL-Zertifikatüberprüfung kann jetzt optional durch den Parameter verify_cert deaktiviert werden (Standard: False).
|
||
- Zusätzliche Debug-Ausgaben protokollieren den Statuscode und erste 100 Zeichen des extrahierten Textes.
|
||
- Damit wird sichergestellt, dass beim Fehlen eines Wikipedia-Eintrags der Website-Fallback besser analysiert werden kann.
|
||
|
||
|
||
"""
|
||
|
||
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
|
||
|
||
# Optional: tiktoken für Token-Zählung (Modus 8)
|
||
try:
|
||
import tiktoken
|
||
except ImportError:
|
||
tiktoken = None
|
||
|
||
# ==================== KONFIGURATION ====================
|
||
class Config:
|
||
VERSION = "v1.4.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."
|
||
|
||
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 €"
|
||
|
||
# ==================== 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():
|
||
debug_print("Starte Verifizierungsmodus (Modus 51) im Batch-Prozess...")
|
||
try:
|
||
rows_limit = int(input("Wie viele Zeilen sollen im Batch verarbeitet werden? "))
|
||
except Exception:
|
||
rows_limit = Config.BATCH_SIZE
|
||
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()
|
||
batch_entries = []
|
||
row_indices = []
|
||
for i, row in enumerate(data[1:], start=2):
|
||
# Überspringe Zeilen, bei denen bereits in Spalte AO (Index 40) ein Timestamp steht
|
||
if len(row) > 40 and row[40].strip() != "":
|
||
continue
|
||
if len(row) <= 25 or row[24].strip() == "":
|
||
entry_text = f"Eintrag {i}:\nFirmenname: {row[1] if len(row)>1 else ''}\nCRM-Beschreibung: {row[7] if len(row)>7 else ''}\nWikipedia-URL: {row[11] if len(row)>11 and row[11].strip() not in ['', 'k.A.'] else 'k.A.'}\nWiki-Absatz: {row[12] if len(row)>12 else 'k.A.'}\nWiki-Kategorien: {row[16] if len(row)>16 else 'k.A.'}\n-----\n"
|
||
batch_entries.append(entry_text)
|
||
row_indices.append(i)
|
||
if len(batch_entries) == rows_limit:
|
||
break
|
||
if not batch_entries:
|
||
debug_print("Keine Einträge für die Verifizierung gefunden.")
|
||
return
|
||
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:\nEintrag <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(batch_entries)
|
||
debug_print("Aggregierter Prompt für Verifizierungs-Batch erstellt.")
|
||
agg_token_count = "n.v."
|
||
if tiktoken:
|
||
try:
|
||
enc = tiktoken.encoding_for_model(Config.TOKEN_MODEL)
|
||
agg_token_count = len(enc.encode(aggregated_prompt))
|
||
debug_print(f"Token-Zahl für Batch: {agg_token_count}")
|
||
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 (Verifizierung): {e}")
|
||
return
|
||
openai.api_key = api_key
|
||
try:
|
||
response = openai.ChatCompletion.create(
|
||
model=Config.TOKEN_MODEL,
|
||
messages=[{"role": "system", "content": aggregated_prompt}],
|
||
temperature=0.0
|
||
)
|
||
result = response.choices[0].message.content.strip()
|
||
debug_print(f"Antwort ChatGPT Verifizierung Batch: {result}")
|
||
except Exception as e:
|
||
debug_print(f"Fehler bei der ChatGPT Anfrage für Verifizierung: {e}")
|
||
return
|
||
answers = result.split("\n")
|
||
for idx, row_num in enumerate(row_indices):
|
||
answer = "k.A."
|
||
for line in answers:
|
||
if line.strip().startswith(f"Eintrag {row_num}:"):
|
||
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
|
||
main_sheet.update(values=[[wiki_confirm]], range_name=f"S{row_num}")
|
||
main_sheet.update(values=[[alt_article]], range_name=f"U{row_num}")
|
||
main_sheet.update(values=[[wiki_explanation]], range_name=f"V{row_num}")
|
||
# Branchenbewertung (Spalte W)
|
||
crm_branch = data[row_num-1][6] if len(data[row_num-1]) > 6 else "k.A."
|
||
ext_branch = data[row_num-1][7] if len(data[row_num-1]) > 7 else "k.A."
|
||
wiki_branch = data[row_num-1][14] if len(data[row_num-1]) > 14 else "k.A."
|
||
wiki_cats = data[row_num-1][17] if len(data[row_num-1]) > 17 else "k.A."
|
||
website_url = data[row_num-1][3] if len(data[row_num-1]) > 3 else "k.A."
|
||
website_raw = get_website_raw(website_url)
|
||
website_summary = summarize_website_content(website_raw)
|
||
branch_result = evaluate_branche_chatgpt(crm_branch, ext_branch, wiki_branch, wiki_cats, website_summary)
|
||
main_sheet.update(values=[[branch_result["branch"]]], range_name=f"W{row_num}")
|
||
main_sheet.update(values=[[branch_result["consistency"]]], range_name=f"X{row_num}")
|
||
main_sheet.update(values=[[branch_result["justification"]]], range_name=f"Y{row_num}")
|
||
current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
main_sheet.update(values=[[current_dt]], range_name=f"AO{row_num}")
|
||
main_sheet.update(values=[[Config.VERSION]], range_name=f"AP{row_num}")
|
||
main_sheet.update(values=[[str(agg_token_count)]], range_name=f"AQ{row_num}")
|
||
debug_print(f"Zeile {row_num} verifiziert: Antwort: {answer}")
|
||
time.sleep(Config.RETRY_DELAY)
|
||
debug_print("Verifizierungs-Batch abgeschlossen.")
|
||
|
||
# ==================== 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):
|
||
def load_target_branches():
|
||
try:
|
||
with open("ziel_Branchenschema.csv", "r", encoding="utf-8") as csvfile:
|
||
reader = csv.reader(csvfile)
|
||
branches = [row[0] for row in reader if row]
|
||
return branches
|
||
except Exception as e:
|
||
debug_print(f"Fehler beim Laden des Ziel-Branchenschemas: {e}")
|
||
return []
|
||
target_branches = load_target_branches()
|
||
target_branches_str = "\n".join(target_branches)
|
||
focus_branches = [
|
||
"Gutachter / Versicherungen > Baugutachter",
|
||
"Gutachter / Versicherungen > Technische Gutachten",
|
||
"Gutachter / Versicherungen > Versicherungsgutachten",
|
||
"Gutachter / Versicherungen > Medizinische Gutachten",
|
||
"Hersteller / Produzenten > Anlagenbau",
|
||
"Hersteller / Produzenten > Automaten (Vending, Slot)",
|
||
"Hersteller / Produzenten > Gebäudetechnik Allgemein",
|
||
"Hersteller / Produzenten > Gebäudetechnik Heizung, Lüftung, Klima",
|
||
"Hersteller / Produzenten > Maschinenbau",
|
||
"Hersteller / Produzenten > Medizintechnik",
|
||
"Service provider (Dienstleister) > Aufzüge und Rolltreppen",
|
||
"Service provider (Dienstleister) > Feuer- und Sicherheitssysteme",
|
||
"Service provider (Dienstleister) > Servicedienstleister / Reparatur ohne Produktion",
|
||
"Service provider (Dienstleister) > Facility Management",
|
||
"Versorger > Telekommunikation"
|
||
]
|
||
focus_branches_str = "\n".join(focus_branches)
|
||
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 (Branche): {e}")
|
||
return {"branch": "k.A.", "consistency": "k.A.", "justification": "k.A."}
|
||
openai.api_key = api_key
|
||
|
||
# Angepasst: Sobald kein Wikipedia-Artikel (Wiki-Branche) vorhanden ist, wird die Website-Zusammenfassung als Fallback genutzt.
|
||
if wiki_branche.strip().lower() == "k.a.":
|
||
debug_print("Kein Wikipedia-Artikel vorhanden – verwende Website-Zusammenfassung als Branchenbeschreibung-Fallback.")
|
||
used_description = website_summary
|
||
else:
|
||
used_description = beschreibung
|
||
debug_print(f"Verwendete Angaben: CRM-Branche='{crm_branche}', externe Beschreibung='{beschreibung}', Wiki-Branche='{wiki_branche}', Wiki-Kategorien='{wiki_kategorien}'")
|
||
|
||
system_prompt = (
|
||
"Du bist ein Experte im Field Service Management. Deine Aufgabe ist es, ein Unternehmen basierend auf folgenden Angaben einer Branche zuzuordnen.\n\n"
|
||
f"CRM-Branche (Spalte F): {crm_branche if crm_branche.strip() != '' else 'k.A.'}\n"
|
||
f"Branchenbeschreibung (Spalte G): {used_description if used_description.strip() != '' else 'k.A.'}\n"
|
||
f"Wikipedia-Branche (Spalte N): {wiki_branche}\n"
|
||
f"Wikipedia-Kategorien (Spalte Q): {wiki_kategorien}\n\n"
|
||
"Das Ziel-Branchenschema umfasst ALLE gültigen Branchen, also sowohl Fokusbranchen als auch weitere, z. B. 'Housing > Sozialbau Unternehmen'.\n"
|
||
"Das vollständige Ziel-Branchenschema lautet:\n"
|
||
f"{target_branches_str}\n\n"
|
||
"Falls das Unternehmen mehreren Branchen zugeordnet werden könnte, wähle bitte bevorzugt eine Branche aus der folgenden Fokusliste, sofern zutreffend:\n"
|
||
f"{focus_branches_str}\n\n"
|
||
"Gewichtung der Angaben:\n"
|
||
"1. Wikipedia-Branche (Spalte N) zusammen mit Wikipedia-Kategorien (Spalte Q) (höchste Priorität, wenn verifiziert, ansonsten erhöhte Gewichtung der Kategorien)\n"
|
||
"2. Branchenbeschreibung (Spalte G) – (hier wird die Website-Zusammenfassung genutzt, wenn kein Wikipedia-Artikel vorhanden ist)\n"
|
||
"3. CRM-Branche (Spalte F)\n\n"
|
||
"Ordne das Unternehmen exakt einer der oben genannten Branchen zu (keine zusätzlichen Branchen erfinden). "
|
||
"Bitte antworte im Format:\n"
|
||
"Branche: <vorgeschlagene Branche>\nÜbereinstimmung: <ok oder X>\nBegründung: <kurze Begründung, falls abweichend, ansonsten leer>"
|
||
)
|
||
|
||
try:
|
||
response = openai.ChatCompletion.create(
|
||
model="gpt-3.5-turbo",
|
||
messages=[{"role": "system", "content": system_prompt}],
|
||
temperature=0.0
|
||
)
|
||
result = response.choices[0].message.content.strip()
|
||
debug_print(f"Branchenabgleich ChatGPT Antwort: '{result}'")
|
||
branch = "k.A."
|
||
consistency = "k.A."
|
||
justification = ""
|
||
for line in result.split("\n"):
|
||
if line.lower().startswith("branche:"):
|
||
branch = line.split(":", 1)[1].strip()
|
||
elif line.lower().startswith("übereinstimmung:"):
|
||
consistency = line.split(":", 1)[1].strip()
|
||
elif line.lower().startswith("begründung:"):
|
||
justification = line.split(":", 1)[1].strip()
|
||
if branch.lower() not in [tb.lower() for tb in target_branches]:
|
||
justification = "Vorgeschlagene Branche entspricht nicht dem Ziel-Branchenschema."
|
||
branch = "k.A."
|
||
consistency = "X"
|
||
if crm_branche.strip() and branch.lower() == crm_branche.strip().lower():
|
||
justification = ""
|
||
consistency = "ok"
|
||
debug_print(f"Endergebnis Branchenbewertung: Branche='{branch}', Übereinstimmung='{consistency}', Begründung='{justification}'")
|
||
return {"branch": branch, "consistency": consistency, "justification": justification}
|
||
except Exception as e:
|
||
debug_print(f"Fehler beim Aufruf der ChatGPT API für Branchenabgleich: {e}")
|
||
return {"branch": "k.A.", "consistency": "k.A.", "justification": "k.A."}
|
||
|
||
|
||
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.":
|
||
# Extrahiere den Rohtext der Website
|
||
website_raw = get_website_raw(website_url)
|
||
# Erstelle eine Zusammenfassung des Website-Contents
|
||
website_summary = summarize_website_content(website_raw)
|
||
try:
|
||
self.sheet_handler.sheet.update(values=[[website_raw]], range_name=f"AR{row_num}")
|
||
debug_print(f"Zeile {row_num}: Spalte AR Update erfolgreich.")
|
||
except Exception as e:
|
||
debug_print(f"Zeile {row_num}: Fehler beim Update von Spalte AR: {e}")
|
||
try:
|
||
self.sheet_handler.sheet.update(values=[[website_summary]], range_name=f"AS{row_num}")
|
||
debug_print(f"Zeile {row_num}: Spalte AS Update erfolgreich.")
|
||
except Exception as e:
|
||
debug_print(f"Zeile {row_num}: Fehler beim Update von Spalte AS: {e}")
|
||
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.")
|
||
|
||
# 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.'
|
||
}
|
||
self.sheet_handler.sheet.update(values=[[
|
||
row_data[11] if len(row_data) > 11 and row_data[11].strip() not in ["", "k.A."] else "k.A.",
|
||
company_data.get('url', 'k.A.'),
|
||
company_data.get('first_paragraph', 'k.A.'),
|
||
company_data.get('branche', 'k.A.'),
|
||
company_data.get('umsatz', 'k.A.'),
|
||
company_data.get('mitarbeiter', 'k.A.'),
|
||
company_data.get('categories', 'k.A.')
|
||
]], range_name=wiki_update_range)
|
||
self.sheet_handler.sheet.update(values=[[datetime.now().strftime("%Y-%m-%d %H:%M:%S")]], range_name=dt_wiki_range)
|
||
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.'))
|
||
self.sheet_handler.sheet.update(values=[[abgleich_result]], range_name=f"AG{row_num}")
|
||
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)
|
||
self.sheet_handler.sheet.update(values=[[valid_result]], range_name=f"R{row_num}")
|
||
fsm_result = evaluate_fsm_suitability(company_name, company_data)
|
||
self.sheet_handler.sheet.update(values=[[fsm_result["suitability"]]], range_name=f"Y{row_num}")
|
||
self.sheet_handler.sheet.update(values=[[fsm_result["justification"]]], range_name=f"Z{row_num}")
|
||
st_estimate = evaluate_servicetechnicians_estimate(company_name, company_data)
|
||
self.sheet_handler.sheet.update(values=[[st_estimate]], range_name=f"AD{row_num}")
|
||
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"
|
||
self.sheet_handler.sheet.update(values=[[discrepancy]], range_name=f"AF{row_num}")
|
||
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)
|
||
self.sheet_handler.sheet.update(values=[[emp_estimate]], range_name=f"AB{row_num}")
|
||
self.sheet_handler.sheet.update(values=[[emp_consistency]], range_name=f"AC{row_num}")
|
||
revenue_result = evaluate_umsatz_chatgpt(company_name, company_data.get('umsatz', 'k.A.'))
|
||
self.sheet_handler.sheet.update(values=[[revenue_result]], range_name=f"AG{row_num}")
|
||
# Hier NICHT mehr neu einlesen! Verwende die bereits extrahierten Website-Daten.
|
||
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))}"
|
||
self.sheet_handler.sheet.update(values=[[total_tokens]], range_name=f"AQ{row_num}")
|
||
self.sheet_handler.sheet.update(values=[[datetime.now().strftime('%Y-%m-%d %H:%M:%S')]], range_name=dt_chat_range)
|
||
else:
|
||
debug_print(f"Zeile {row_num}: ChatGPT-Timestamp bereits gesetzt – überspringe ChatGPT-Auswertung.")
|
||
|
||
# Aktualisiere den Timestamp für die letzte Prüfung und die Version
|
||
current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
self.sheet_handler.sheet.update(values=[[current_dt]], range_name=ver_range)
|
||
self.sheet_handler.sheet.update(values=[[Config.VERSION]], range_name=ver_range)
|
||
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_contact(company_name, website, position_query):
|
||
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)
|
||
data = response.json()
|
||
if "organic_results" in data and len(data["organic_results"]) > 0:
|
||
result = data["organic_results"][0]
|
||
title = result.get("title", "")
|
||
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 = 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 = ""
|
||
return {"Firmenname": company_name, "Website": website, "Vorname": firstname, "Nachname": lastname, "Position": pos}
|
||
else:
|
||
return {"Firmenname": company_name, "Website": website, "Vorname": "", "Nachname": "", "Position": title}
|
||
else:
|
||
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):
|
||
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)
|
||
data = response.json()
|
||
if "organic_results" in data:
|
||
count = len(data["organic_results"])
|
||
debug_print(f"Anzahl Kontakte für Query '{query}': {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
|
||
|
||
# ----------------- 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
|
||
print("Bitte wählen Sie den Betriebsmodus:")
|
||
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("51: Batch-Verifizierung (alte Nummerierung beibehalten)")
|
||
print("6: Contact Research (LinkedIn)")
|
||
print("8: Batch Token-Zählung")
|
||
|
||
MODE = input("Geben Sie den Modus (Zahl) ein: ").strip()
|
||
if not MODE:
|
||
MODE = "1"
|
||
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()
|
||
|
||
if 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 == "51":
|
||
process_verification_only()
|
||
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() |