v1.2.8: Verbesserte numerische Parsing-Logik und robustere Umsatz-Vergleichslogik

Robuste numerische Extraktion:
Die Funktion extract_numeric_value wurde erweitert, um führende und umgebende Texte zu entfernen. Kommas werden je nach Kontext als Dezimaltrennzeichen oder Tausendertrennzeichen behandelt.

Verbesserte Vergleichslogik:
Vor dem Vergleich werden die bereinigten Werte geloggt. Falls einer der Werte nicht in einen Float konvertiert werden kann, wird "Daten unvollständig" zurückgegeben.

Erweiterte Debug-Ausgabe:
Log-Ausgaben zeigen jetzt explizit die bereinigten Vergleichswerte für CRM- und Wikipedia-Umsätze.

Pause zur Datensynchronisation:
Eine einsekündige Pause wurde nach dem Schreiben in Google Sheets eingeführt, um
This commit is contained in:
2025-04-02 05:58:22 +00:00
parent ff7d5dd278
commit c5bc203fab

View File

@@ -4,16 +4,17 @@ 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 = "1.2.8"
VERSION = "v1.2.8" # v1.2.8: Verbesserte numerische Extraktion für Umsatz und Mitarbeiter; robustere Vergleichslogik
LANG = "de"
CREDENTIALS_FILE = "service_account.json"
SHEET_URL = "https://docs.google.com/spreadsheets/d/1u_gHr9JUfmV1-iviRzbSe3575QEp7KLhK5jFV_gJcgo"
@@ -44,7 +45,7 @@ def debug_print(message):
def clean_text(text):
if not text:
return "k.A."
text = str(text)
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."
@@ -70,63 +71,88 @@ def normalize_company_name(name):
normalized = re.sub(r'\s+', ' ', normalized).strip()
return normalized.lower()
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)
def extract_numeric_value(raw_value, is_umsatz=False):
raw_value = raw_value.strip()
if not raw_value or raw_value.lower() in ["k.a.", "n.a."]:
return "k.A."
# Entferne Texte wie "ca.", "circa", etc.
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 Textteile in Klammern
raw_value = re.sub(r'\(.*?\)', '', raw_value).strip()
# Extrahiere den numerischen Teil
match = re.search(r'([\d.,]+)', raw_value)
if not match:
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('.', '')
debug_print(f"Keine numerischen Zeichen gefunden im Rohtext: '{raw_value}'")
return "k.A."
num_str = match.group(1)
# Wenn ein einzelnes Komma als Dezimaltrenner und kein Punkt vorhanden ist
if ',' in num_str and '.' not in num_str and len(num_str.split(',')[1]) <= 2:
num_str = num_str.replace(',', '.')
else:
# sonst entferne alle Punkte (als Tausendertrenner)
num_str = num_str.replace('.', '')
num_str = num_str.replace(',', '')
try:
value = float(num_str)
num = float(num_str)
except Exception as e:
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
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)))
else:
value *= scale
return value
return str(int(round(num)))
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(",", ".")
def compare_umsatz_values(crm, wiki):
debug_print(f"Vergleich CRM Umsatz: '{crm}' mit Wikipedia Umsatz: '{wiki}'")
try:
return int(float(raw))
crm_val = float(crm)
wiki_val = float(wiki)
except Exception as e:
debug_print(f"Fehler beim Parsen von Mitarbeiterzahl: {raw} - {e}")
return None
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."
# ==================== GOOGLE SHEET HANDLER ====================
class GoogleSheetHandler:
@@ -143,6 +169,26 @@ 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):
@@ -153,8 +199,7 @@ class WikipediaScraper:
website = website.lower().strip()
website = re.sub(r'^https?:\/\/', '', website)
website = re.sub(r'^www\.', '', website)
website = website.split('/')[0]
return website
return website.split('/')[0]
def _generate_search_terms(self, company_name, website):
terms = []
full_domain = self._get_full_domain(website)
@@ -213,6 +258,14 @@ 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:
@@ -220,7 +273,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']
'mitarbeiter': ['mitarbeiter', 'beschäftigte', 'personal', 'mitarbeiterzahl', 'angestellte', 'belegschaft', 'personalstärke']
}
keywords = keywords_map.get(target, [])
for row in infobox.find_all('tr'):
@@ -235,11 +288,9 @@ class WikipediaScraper:
clean_val = re.sub(r'\[.*?\]|\(.*?\)', '', raw_value)
return ' '.join(clean_val.split()).strip()
if target == 'umsatz':
parsed = parse_currency_value(raw_value)
return str(parsed) if parsed is not None else raw_value.strip()
return extract_numeric_value(raw_value, is_umsatz=True)
if target == 'mitarbeiter':
parsed = parse_employee_value(raw_value)
return str(parsed) if parsed is not None else raw_value.strip()
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']))
@@ -251,7 +302,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 token.lower() == field.lower():
if field.lower() in token.lower():
j = i + 1
while j < len(tokens) and not tokens[j]:
j += 1
@@ -259,26 +310,33 @@ 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.'}
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', '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'))
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': branche_val,
'branche': raw_branche,
'umsatz': umsatz_val,
'mitarbeiter': mitarbeiter_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.'}
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)
@@ -304,55 +362,95 @@ class DataProcessor:
def __init__(self):
self.sheet_handler = GoogleSheetHandler()
self.wiki_scraper = WikipediaScraper()
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_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_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.'}
# Beispiel: Schreibe die Wikipedia-Daten in Spalten J bis N (anpassen an das neue Schema)
self.sheet_handler.sheet.update(values=[[
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
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.')
]], 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
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) # Sicherstellen, dass Werte synchronisiert werden
wiki_umsatz = company_data.get('umsatz', 'k.A.')
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.)
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)
current_dt = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
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.')}")
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("----------------------------------------")
time.sleep(Config.RETRY_DELAY)
# ==================== MAIN ====================
if __name__ == "__main__":
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)
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
processor = DataProcessor()
processor.process_rows(num_rows)
print("\n✅ Wikipedia-Auswertung abgeschlossen")
print(f"\n✅ Wikipedia-Auswertung abgeschlossen ({Config.VERSION})")