• Neue Funktionen parse_currency_value und parse_employee_value extrahieren und wandeln Umsatz- bzw. Mitarbeiterzahlen korrekt um.
• Tausenderpunkte werden entfernt und Kommas als Dezimaltrenner ersetzt.
• Skalierung: Bei "mrd." wird der Wert mit 1000 multipliziert, bei "mio." bleibt er unverändert, andernfalls wird angenommen, dass der Wert in Euro vorliegt und durch 1e6 geteilt wird.
• Im Vergleichsabschnitt werden die zu vergleichenden Werte vor dem Versuch der Umwandlung geloggt.
• Es wird eine 1-Sekunden-Pause nach dem Schreiben in Google Sheets eingefügt, um sicherzustellen, dass die Daten übernommen wurden.
This commit is contained in:
2025-04-02 05:50:31 +00:00
parent 491ec4a379
commit ff7d5dd278

View File

@@ -4,17 +4,16 @@ 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 openai # falls für ChatGPT-API benötigt
# ==================== KONFIGURATION ====================
class Config:
VERSION = "v1.2.7" # v1.2.7: Verbesserte numerische Extraktion für Umsatz und Mitarbeiterzahlen
VERSION = "1.2.8"
LANG = "de"
CREDENTIALS_FILE = "service_account.json"
SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo"
@@ -45,7 +44,7 @@ def debug_print(message):
def clean_text(text):
if not text:
return "k.A."
text = unicodedata.normalize("NFKC", str(text))
text = str(text)
text = re.sub(r'\[\d+\]', '', text)
text = re.sub(r'\s+', ' ', text).strip()
return text if text else "k.A."
@@ -71,89 +70,63 @@ def normalize_company_name(name):
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."
# Entferne gängige Zusätze
raw_value = re.sub(r'\b(ca\.?|circa|etwa|über|rund)\b', '', raw_value, flags=re.IGNORECASE)
raw_value = raw_value.replace("\xa0", " ").strip()
# Entferne führende Nicht-Ziffern
num_str = re.sub(r'^[^\d]+', '', raw_value)
# Extrahiere die Zahl (Ziffern, Komma und Punkt)
match = re.search(r'([\d.,]+)', num_str)
def parse_currency_value(text):
"""
Parst einen Umsatz-Text. Entfernt Tausenderpunkte, ersetzt Komma als Dezimaltrenner.
Ermittelt den Skalierungsfaktor: 'mrd' -> 1000, 'mio' -> 1, sonst Annahme: Euro -> /1e6.
Liefert den Wert in Mio. Euro als float.
"""
raw = text.lower()
# Entferne unnötige Zeichen und Einheiten
raw = raw.replace("", "").replace("eur", "")
# Prüfe auf Skalierung
scale = None
if "mrd" in raw or "milliarden" in raw:
scale = 1000
elif "mio" in raw or "millionen" in raw:
scale = 1
# Extrahiere den numerischen Teil (erlaubt Punkte und Kommas)
match = re.search(r'([\d.,]+)', raw)
if not match:
debug_print(f"Keine numerischen Zeichen gefunden im Rohtext: '{raw_value}'")
return "k.A."
num_str = match.group(1)
# Prüfe, ob ein einzelnes Komma als Dezimaltrennzeichen verwendet wird (wenn kein Punkt vorhanden)
if ',' in num_str and '.' not in num_str and len(num_str.split(',')[1]) <= 2:
return None
num_str = match.group(1).strip()
# Wenn ein Komma vorkommt, nehme an, dass es der Dezimaltrenner ist und entferne Punkte
if ',' in num_str:
num_str = num_str.replace('.', '')
num_str = num_str.replace(',', '.')
else:
# Entferne alle Kommas als Tausendertrennzeichen
num_str = num_str.replace(',', '')
# sonst entferne alle Punkte (als Tausendertrenner)
num_str = num_str.replace('.', '')
try:
num = float(num_str)
value = 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:
raw_lower = raw_value.lower()
if "mrd" in raw_lower or "milliarden" in raw_lower:
num *= 1000
elif "mio" in raw_lower or "millionen" in raw_lower:
pass
else:
num /= 1e6
return str(int(round(num)))
debug_print(f"Fehler beim Parsen von Umsatz: {num_str} - {e}")
return None
if scale is None:
# Wenn keine Skalierung angegeben, gehe davon aus, dass der Wert in Euro vorliegt
value /= 1e6
else:
return str(int(round(num)))
value *= scale
return value
def compare_umsatz_values(crm, wiki):
debug_print(f"Vergleich CRM Umsatz: '{crm}' mit Wikipedia Umsatz: '{wiki}'")
def parse_employee_value(text):
"""
Parst Mitarbeiterzahlen. Entfernt "ca.", Leerzeichen und Tausenderpunkte.
Liefert den Wert als int.
"""
raw = text.lower()
raw = raw.replace("ca.", "").strip()
# Entferne Klammerinhalt (z. B. (2021/22))
raw = re.sub(r'\(.*?\)', '', raw)
# Entferne Leerzeichen
raw = raw.replace(" ", "")
# Entferne Tausendertrenner (Punkt) und ersetze Komma durch Punkt
raw = raw.replace(".", "").replace(",", ".")
try:
crm_val = float(crm)
wiki_val = float(wiki)
return int(float(raw))
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 evaluate_umsatz_chatgpt(company_name, wiki_umsatz):
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: {e}")
return "k.A."
openai.api_key = api_key
prompt = (f"Bitte schätze den Umsatz in Mio. Euro für das Unternehmen '{company_name}'. "
f"Die Wikipedia-Daten zeigen: '{wiki_umsatz}'. "
"Antworte nur mit der Zahl.")
try:
response = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[{"role": "user", "content": prompt}],
temperature=0.0
)
result = response.choices[0].message.content.strip()
debug_print(f"ChatGPT Antwort: '{result}'")
try:
value = float(result.replace(',', '.'))
return str(int(round(value)))
except Exception as conv_e:
debug_print(f"Fehler bei der Verarbeitung der ChatGPT-Antwort '{result}': {conv_e}")
return result
except Exception as e:
debug_print(f"Fehler beim Aufruf der ChatGPT API: {e}")
return "k.A."
debug_print(f"Fehler beim Parsen von Mitarbeiterzahl: {raw} - {e}")
return None
# ==================== GOOGLE SHEET HANDLER ====================
class GoogleSheetHandler:
@@ -170,49 +143,6 @@ class GoogleSheetHandler:
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)
# ==================== ALIGNMENT DEMO (Modus 3) ====================
def alignment_demo(sheet):
new_headers = [
"Spalte A (ReEval Flag)",
"Spalte B (Firmenname)",
"Spalte C (Website)",
"Spalte D (Ort)",
"Spalte E (Beschreibung)",
"Spalte F (Aktuelle Branche)",
"Spalte G (Beschreibung Branche extern)",
"Spalte H (Anzahl Techniker CRM)",
"Spalte I (Umsatz CRM)",
"Spalte J (Anzahl Mitarbeiter CRM)",
"Spalte K (Vorschlag Wiki URL)",
"Spalte L (Wikipedia URL)",
"Spalte M (Wikipedia Absatz)",
"Spalte N (Wikipedia Branche)",
"Spalte O (Wikipedia Umsatz)",
"Spalte P (Wikipedia Mitarbeiter)",
"Spalte Q (Wikipedia Kategorien)",
"Spalte R (Konsistenzprüfung)",
"Spalte S (Begründung bei Inkonsistenz)",
"Spalte T (Vorschlag Wiki Artikel ChatGPT)",
"Spalte U (Begründung bei Abweichung)",
"Spalte V (Vorschlag neue Branche)",
"Spalte W (Konsistenzprüfung Branche)",
"Spalte X (Begründung Abweichung Branche)",
"Spalte Y (FSM Relevanz Ja / Nein)",
"Spalte Z (Begründung für FSM Relevanz)",
"Spalte AA (Schätzung Anzahl Mitarbeiter)",
"Spalte AB (Konsistenzprüfung Mitarbeiterzahl)",
"Spalte AC (Begründung für Abweichung Mitarbeiterzahl)",
"Spalte AD (Einschätzung Anzahl Servicetechniker)",
"Spalte AE (Begründung bei Abweichung Anzahl Servicetechniker)",
"Spalte AF (Schätzung Umsatz ChatGPT)",
"Spalte AG (Begründung für Abweichung Umsatz)",
"Spalte AH (Timestamp letzte Prüfung)",
"Spalte AI (Version)"
]
header_range = "A11200:AI11200"
sheet.update(values=[new_headers], range_name=header_range)
print("Alignment-Demo abgeschlossen: Neue Spaltenüberschriften in Zeile 11200 geschrieben.")
# ==================== WIKIPEDIA SCRAPER ====================
class WikipediaScraper:
def __init__(self):
@@ -223,7 +153,8 @@ class WikipediaScraper:
website = website.lower().strip()
website = re.sub(r'^https?:\/\/', '', website)
website = re.sub(r'^www\.', '', website)
return website.split('/')[0]
website = website.split('/')[0]
return website
def _generate_search_terms(self, company_name, website):
terms = []
full_domain = self._get_full_domain(website)
@@ -282,14 +213,6 @@ class WikipediaScraper:
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:
@@ -297,7 +220,7 @@ class WikipediaScraper:
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']
'mitarbeiter': ['mitarbeiter', 'beschäftigte', 'personal', 'mitarbeiterzahl']
}
keywords = keywords_map.get(target, [])
for row in infobox.find_all('tr'):
@@ -312,9 +235,11 @@ class WikipediaScraper:
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)
parsed = parse_currency_value(raw_value)
return str(parsed) if parsed is not None else raw_value.strip()
if target == 'mitarbeiter':
return extract_numeric_value(raw_value, is_umsatz=False)
parsed = parse_employee_value(raw_value)
return str(parsed) if parsed is not None else raw_value.strip()
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']))
@@ -326,7 +251,7 @@ class WikipediaScraper:
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():
if token.lower() == field.lower():
j = i + 1
while j < len(tokens) and not tokens[j]:
j += 1
@@ -334,33 +259,26 @@ class WikipediaScraper:
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.'}
return {'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': '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)
extracted_fields = self.extract_fields_from_infobox_text(full_infobox, ['Branche', 'Umsatz', 'Mitarbeiterzahl'])
branche_val = extracted_fields.get('Branche', self._extract_infobox_value(soup, 'branche'))
umsatz_val = extracted_fields.get('Umsatz', self._extract_infobox_value(soup, 'umsatz'))
mitarbeiter_val = extracted_fields.get('Mitarbeiterzahl', self._extract_infobox_value(soup, 'mitarbeiter'))
first_paragraph = self.extract_first_paragraph(page_url)
return {
'url': page_url,
'first_paragraph': first_paragraph,
'branche': raw_branche,
'branche': branche_val,
'umsatz': umsatz_val,
'mitarbeiter': mitarbeiter_val,
'categories': categories_val,
'full_infobox': full_infobox
'mitarbeiter': mitarbeiter_val
}
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.'}
return {'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.'}
@retry_on_failure
def search_company_article(self, company_name, website):
search_terms = self._generate_search_terms(company_name, website)
@@ -386,95 +304,55 @@ class DataProcessor:
def __init__(self):
self.sheet_handler = GoogleSheetHandler()
self.wiki_scraper = WikipediaScraper()
def process_rows(self, num_rows=None):
if MODE == "2":
print("Re-Evaluierungsmodus: Verarbeitung aller Zeilen mit 'x' in Spalte A.")
elif MODE == "3":
print("Alignment-Demo-Modus: Schreibe neue Spaltenüberschriften in Zeile 11200.")
alignment_demo(self.sheet_handler.sheet)
return
else:
start_index = self.sheet_handler.get_start_index()
print(f"Starte bei Zeile {start_index+1}")
for i, row in enumerate(self.sheet_handler.sheet_values[1:], start=2):
if MODE == "2":
if row[0].strip().lower() == "x":
self._process_single_row(i, row)
else:
if i >= self.sheet_handler.get_start_index():
self._process_single_row(i, row)
def process_rows(self, num_rows):
start_index = self.sheet_handler.get_start_index()
print(f"Starte bei Zeile {start_index+1}")
for i in range(start_index, min(start_index + num_rows, len(self.sheet_handler.sheet_values))):
row = self.sheet_handler.sheet_values[i]
self._process_single_row(i+1, row)
def _process_single_row(self, row_num, row_data):
company_name = row_data[1] if len(row_data) > 1 else ""
website = row_data[2] if len(row_data) > 2 else ""
wiki_update_range = f"K{row_num}:Q{row_num}"
chatgpt_range = f"AF{row_num}"
abgleich_range = f"AG{row_num}"
dt_range = f"AH{row_num}"
ver_range = f"AI{row_num}"
print(f"\n[{datetime.now().strftime('%H:%M:%S')}] Verarbeite Zeile {row_num}: {company_name}")
article = self.wiki_scraper.search_company_article(company_name, website)
if article:
company_data = self.wiki_scraper.extract_company_data(article.url)
else:
company_data = {
'url': 'k.A.',
'first_paragraph': 'k.A.',
'branche': 'k.A.',
'umsatz': 'k.A.',
'mitarbeiter': 'k.A.',
'categories': 'k.A.',
'full_infobox': 'k.A.'
}
wiki_values = [
"k.A.", # Vorschlag Wiki URL (spalte K)
company_data = {'url': 'k.A.', 'first_paragraph': 'k.A.', 'branche': 'k.A.', 'umsatz': 'k.A.', 'mitarbeiter': 'k.A.'}
# Beispiel: Schreibe die Wikipedia-Daten in Spalten J bis N (anpassen an das neue Schema)
self.sheet_handler.sheet.update(values=[[
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.')
]
self.sheet_handler.sheet.update(values=[wiki_values], range_name=wiki_update_range)
time.sleep(1) # 1 Sekunde Pause für Datensynchronisation
company_data.get('mitarbeiter', 'k.A.')
]], range_name=f"J{row_num}:N{row_num}")
# Füge 1 Sekunde Pause ein, damit die Sheets-Daten sicher gespeichert werden
time.sleep(1)
# Hier folgt der Umsatzvergleich (CRM vs. Wikipedia) Debug-Ausgabe
crm_umsatz = row_data[?] # Hier anpassen: Spalte mit CRM-Umsatz
wiki_umsatz = company_data.get('umsatz', 'k.A.')
if wiki_umsatz != "k.A.":
chatgpt_umsatz = evaluate_umsatz_chatgpt(company_name, wiki_umsatz)
else:
chatgpt_umsatz = "k.A."
self.sheet_handler.sheet.update(values=[[chatgpt_umsatz]], range_name=chatgpt_range)
crm_umsatz = row_data[8] if len(row_data) > 8 else "k.A."
debug_print(f"Bereinigte Vergleichswerte vor Umwandlung: CRM Umsatz: '{crm_umsatz}', Wiki Umsatz: '{wiki_umsatz}'")
abgleich_result = compare_umsatz_values(crm_umsatz, wiki_umsatz)
self.sheet_handler.sheet.update(values=[[abgleich_result]], range_name=abgleich_range)
debug_print(f"Vergleich CRM Umsatz: '{crm_umsatz}' mit Wikipedia Umsatz: '{wiki_umsatz}'")
try:
crm_value = float(crm_umsatz.replace(',', '.'))
wiki_value = float(wiki_umsatz)
debug_print(f"Vergleich CRM Umsatz: '{crm_value}' mit Wikipedia Umsatz: '{wiki_value}'")
except Exception as e:
debug_print(f"Fehler beim Umwandeln der Werte: CRM='{crm_umsatz}', Wiki='{wiki_umsatz}': {e}")
# Aktualisiere weitere Spalten (Timestamp, Version etc.)
current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.sheet_handler.sheet.update(values=[[current_dt]], range_name=dt_range)
self.sheet_handler.sheet.update(values=[[Config.VERSION]], range_name=ver_range)
print(f"✅ Aktualisiert: URL: {company_data.get('url', 'k.A.')}, Absatz: {company_data.get('first_paragraph', 'k.A.')[:30]}..., "
f"Branche: {company_data.get('branche', 'k.A.')}, Wikipedia Umsatz: {company_data.get('umsatz', 'k.A.')}, "
f"Mitarbeiter: {company_data.get('mitarbeiter', 'k.A.')}, Kategorien: {company_data.get('categories', 'k.A.')}, "
f"ChatGPT Umsatz: {chatgpt_umsatz}, Umsatz-Abgleich: {abgleich_result}")
if MODE == "2":
print("----- Vollständiger Infobox-Inhalt -----")
print(company_data.get("full_infobox", "k.A."))
print("----------------------------------------")
self.sheet_handler.sheet.update(values=[[current_dt]], range_name=f"AH{row_num}")
self.sheet_handler.sheet.update(values=[[Config.VERSION]], range_name=f"AI{row_num}")
print(f"✅ Aktualisiert: URL: {company_data.get('url', 'k.A.')}, Absatz: {company_data.get('first_paragraph', 'k.A.')[:30]}..., Branche: {company_data.get('branche', 'k.A.')}, Umsatz: {company_data.get('umsatz', 'k.A.')}, Mitarbeiter: {company_data.get('mitarbeiter', 'k.A.')}")
time.sleep(Config.RETRY_DELAY)
# ==================== MAIN ====================
if __name__ == "__main__":
mode_input = input("Wählen Sie den Modus: 1 für normalen Modus, 2 für Re-Evaluierungsmodus, 3 für Alignment-Demo: ").strip()
if mode_input == "2":
MODE = "2"
elif mode_input == "3":
MODE = "3"
else:
MODE = "1"
if MODE == "1":
try:
num_rows = int(input("Wieviele Zeilen sollen überprüft werden? "))
except Exception as e:
print("Ungültige Eingabe. Bitte eine Zahl eingeben.")
exit(1)
else:
num_rows = None
try:
num_rows = int(input("Wieviele Zeilen sollen überprüft werden? "))
except Exception as e:
print("Ungültige Eingabe. Bitte eine Zahl eingeben.")
exit(1)
processor = DataProcessor()
processor.process_rows(num_rows)
print(f"\n✅ Wikipedia-Auswertung abgeschlossen ({Config.VERSION})")
print("\n✅ Wikipedia-Auswertung abgeschlossen")