bundle erkennung verbessert und json vergleich

This commit is contained in:
Czechman 2025-02-16 14:24:45 +01:00
parent c38bff314d
commit 0dc125293d
1 changed files with 37 additions and 13 deletions

View File

@ -3,13 +3,14 @@ import requests
import json import json
import hashlib import hashlib
import logging import logging
import difflib
from datetime import datetime from datetime import datetime
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, ForeignKey, Text from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime, ForeignKey, Text
from sqlalchemy.orm import declarative_base, relationship, sessionmaker from sqlalchemy.orm import declarative_base, relationship, sessionmaker
# Konfiguriere Logging passe das Level bei Bedarf an (DEBUG, INFO, WARNING, ERROR) # Konfiguriere Logging ändere das Level bei Bedarf (DEBUG, INFO, WARNING, ERROR)
DEBUG_LEVEL = logging.DEBUG DEBUG_LEVEL = logging.DEBUG
logging.basicConfig( logging.basicConfig(
level=DEBUG_LEVEL, level=DEBUG_LEVEL,
@ -18,7 +19,7 @@ logging.basicConfig(
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Basis-Klasse für SQLAlchemy-Modelle (SQLAlchemy 2.0-konform) # Basis-Klasse für SQLAlchemy-Modelle (2.0-konform)
Base = declarative_base() Base = declarative_base()
# --------------------------- # ---------------------------
@ -29,10 +30,10 @@ class Bundle(Base):
__tablename__ = 'bundles' __tablename__ = 'bundles'
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
machine_name = Column(String, unique=True) machine_name = Column(String, unique=True)
human_name = Column(String) # Falls vorhanden; kann leer bleiben, wenn nicht extrahiert human_name = Column(String) # Kann leer bleiben, wenn nicht extrahiert
current_version_id = Column(Integer, ForeignKey('bundle_versions.id')) current_version_id = Column(Integer, ForeignKey('bundle_versions.id'))
# Beziehung zur aktuellen Version; post_update=True hilft bei zirkulären Abhängigkeiten # Beziehung zur aktuellen Version (post_update=True hilft bei zirkulären Abhängigkeiten)
current_version = relationship("BundleVersion", uselist=False, current_version = relationship("BundleVersion", uselist=False,
foreign_keys=[current_version_id], foreign_keys=[current_version_id],
post_update=True) post_update=True)
@ -41,7 +42,7 @@ class Bundle(Base):
foreign_keys=lambda: [BundleVersion.bundle_id]) foreign_keys=lambda: [BundleVersion.bundle_id])
# Verkaufshistorie # Verkaufshistorie
sales_history = relationship("BundleSalesHistory", back_populates="bundle") sales_history = relationship("BundleSalesHistory", back_populates="bundle")
# Einzelne Elemente des Bundles (z.B. die enthaltenen Bücher, Spiele etc.) # Einzelne Elemente des Bundles (z.B. enthaltene Bücher, Spiele etc.)
items = relationship("BundleItem", back_populates="bundle") items = relationship("BundleItem", back_populates="bundle")
class BundleVersion(Base): class BundleVersion(Base):
@ -49,7 +50,7 @@ class BundleVersion(Base):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
bundle_id = Column(Integer, ForeignKey('bundles.id')) bundle_id = Column(Integer, ForeignKey('bundles.id'))
version_hash = Column(String) version_hash = Column(String)
version_data = Column(Text) # Relevante Bundle-Daten als JSON-String version_data = Column(Text) # JSON-Daten als String
timestamp = Column(DateTime, default=datetime.utcnow) timestamp = Column(DateTime, default=datetime.utcnow)
# Explizite Angabe des Fremdschlüssels # Explizite Angabe des Fremdschlüssels
@ -70,7 +71,7 @@ class BundleItem(Base):
bundle_id = Column(Integer, ForeignKey('bundles.id')) bundle_id = Column(Integer, ForeignKey('bundles.id'))
title = Column(String) # Titel des Elements (z.B. Buch- oder Spielname) title = Column(String) # Titel des Elements (z.B. Buch- oder Spielname)
category = Column(String) # Kategorie, z.B. "book", "game" oder "software" category = Column(String) # Kategorie, z.B. "book", "game" oder "software"
details = Column(Text) # Optional: ganze Detaildaten als JSON-String details = Column(Text) # Optionale Detaildaten als JSON-String
bundle = relationship("Bundle", back_populates="items") bundle = relationship("Bundle", back_populates="items")
@ -79,16 +80,27 @@ class BundleItem(Base):
# --------------------------- # ---------------------------
def calculate_hash(data: dict) -> str: def calculate_hash(data: dict) -> str:
"""Berechnet einen SHA-256-Hash aus dem sortierten JSON-String der relevanten Daten.""" """Berechnet einen SHA-256-Hash aus dem sortierten JSON-String der Daten."""
json_string = json.dumps(data, sort_keys=True, ensure_ascii=False) json_string = json.dumps(data, sort_keys=True, ensure_ascii=False)
hash_value = hashlib.sha256(json_string.encode('utf-8')).hexdigest() hash_value = hashlib.sha256(json_string.encode('utf-8')).hexdigest()
logger.debug(f"Berechneter Hash: {hash_value}") logger.debug(f"Berechneter Hash: {hash_value}")
return hash_value return hash_value
def log_diff(old_data: dict, new_data: dict):
"""
Vergleicht zwei JSON-Daten (als dict) und gibt einen unified diff als String aus.
Nur im DEBUG-Level wird der detaillierte Vergleich ausgegeben.
"""
old_str = json.dumps(old_data, sort_keys=True, indent=4, ensure_ascii=False).splitlines()
new_str = json.dumps(new_data, sort_keys=True, indent=4, ensure_ascii=False).splitlines()
diff = difflib.unified_diff(old_str, new_str, fromfile="alte_version", tofile="neue_version", lineterm="")
diff_text = "\n".join(diff)
logger.debug("Unterschiede zwischen den Versionen:\n" + diff_text)
def fetch_bundle_data(url: str) -> dict: def fetch_bundle_data(url: str) -> dict:
""" """
Lädt die Detailseite eines Bundles und extrahiert den JSON-Inhalt aus dem <script>-Tag Lädt die Detailseite eines Bundles und extrahiert den JSON-Inhalt aus dem <script>-Tag
mit id="webpack-bundle-page-data" (dieser enthält die detaillierten Bundle-Daten). mit id="webpack-bundle-page-data" (diese Seite enthält die detaillierten Bundle-Daten).
""" """
logger.info(f"Rufe Bundle-Daten von {url} ab...") logger.info(f"Rufe Bundle-Daten von {url} ab...")
response = requests.get(url) response = requests.get(url)
@ -117,13 +129,15 @@ def process_bundle(session, url: str):
logger.error(f"Fehler beim Laden der Bundle-Daten von {url}: {e}") logger.error(f"Fehler beim Laden der Bundle-Daten von {url}: {e}")
return return
# Es wird angenommen, dass die Bundle-Daten unter dem Schlüssel "bundleData" stehen.
bundle_data = data.get("bundleData", {}) bundle_data = data.get("bundleData", {})
machine_name = bundle_data.get("machine_name", "") machine_name = bundle_data.get("machine_name", "")
human_name = bundle_data.get("human_name", "") human_name = bundle_data.get("human_name", "")
logger.info(f"Verarbeite Bundle '{human_name}' (machine_name: {machine_name})") logger.info(f"Verarbeite Bundle '{human_name}' (machine_name: {machine_name})")
# Hier definieren wir den relevanten Datenausschnitt (anpassen, falls nötig) # Definiere hier den relevanten Datenausschnitt (anpassen, falls nötig)
relevant_data = bundle_data relevant_data = bundle_data
new_hash = calculate_hash(relevant_data) new_hash = calculate_hash(relevant_data)
# Suche, ob das Bundle bereits existiert # Suche, ob das Bundle bereits existiert
@ -143,7 +157,17 @@ def process_bundle(session, url: str):
.first()) .first())
if latest_version is None or latest_version.version_hash != new_hash: if latest_version is None or latest_version.version_hash != new_hash:
logger.info(f"Neue Version für Bundle '{human_name}' wird gespeichert (alter Hash: {latest_version.version_hash if latest_version else 'None'}, neuer Hash: {new_hash}).") if latest_version:
if logger.isEnabledFor(logging.DEBUG):
try:
old_data = json.loads(latest_version.version_data)
log_diff(old_data, relevant_data)
except Exception as ex:
logger.debug(f"Fehler beim Diff-Vergleich: {ex}")
else:
logger.info("Hashabweichung festgestellt es gibt Änderungen in den Bundle-Daten.")
else:
logger.info("Keine vorherige Version gefunden neues Bundle wird gespeichert.")
new_version = BundleVersion( new_version = BundleVersion(
bundle_id=bundle.id, bundle_id=bundle.id,
version_hash=new_hash, version_hash=new_hash,
@ -178,7 +202,7 @@ def process_bundle(session, url: str):
# --------------------------- # ---------------------------
# Extraktion der Bundle-Elemente # Extraktion der Bundle-Elemente
# --------------------------- # ---------------------------
# Wir gehen hier davon aus, dass in den Bundle-Daten unter dem Schlüssel "items" eine Liste der enthaltenen Produkte steht. # Hier wird angenommen, dass in den Bundle-Daten unter "items" eine Liste der enthaltenen Produkte steht.
items = bundle_data.get("items", []) items = bundle_data.get("items", [])
if items: if items:
logger.info(f"Es wurden {len(items)} Elemente im Bundle '{human_name}' gefunden.") logger.info(f"Es wurden {len(items)} Elemente im Bundle '{human_name}' gefunden.")
@ -186,7 +210,7 @@ def process_bundle(session, url: str):
title = item.get("title", "Unbekannt") title = item.get("title", "Unbekannt")
category = item.get("category", "Unbekannt") category = item.get("category", "Unbekannt")
details = json.dumps(item, sort_keys=True, ensure_ascii=False) details = json.dumps(item, sort_keys=True, ensure_ascii=False)
# Prüfen, ob das Element bereits gespeichert wurde (zum Beispiel anhand des Titels) # Prüfen, ob das Element bereits gespeichert wurde (z.B. anhand des Titels)
existing_item = session.query(BundleItem).filter_by(bundle_id=bundle.id, title=title).first() existing_item = session.query(BundleItem).filter_by(bundle_id=bundle.id, title=title).first()
if existing_item: if existing_item:
logger.debug(f"Element '{title}' im Bundle '{human_name}' existiert bereits Überspringe.") logger.debug(f"Element '{title}' im Bundle '{human_name}' existiert bereits Überspringe.")