# Discounter-Preisdatenbank ## Übersicht Automatisches Sammeln und Vergleichen von Wochenangeboten deutscher Discounter für PLZ 56068 (Koblenz). - **Datenquelle:** marktguru REST-API (inoffiziell) - **Umgebung:** Linux LXC 106 auf Proxmox pve - **IP:** 192.168.2.181 (DHCP, in FRITZ!Box reserviert) - **Stack:** Python 3.12 + SQLite + FastAPI + HTML/JS --- ## Architektur ``` marktguru API → Import-Service (Python, systemd timer 02:00 Uhr) → SQLite Datenbank (/opt/preisdb/data/preisdb.sqlite) → FastAPI Backend (Phase 4) → Web-Frontend HTML/JS (Phase 4) ``` --- ## Händler (PLZ 56068) | ID | Name | |----|------| | retailers/126679 | Lidl | | retailers/126735 | Netto Marken-Discount | | retailers/126654 | Kaufland | | retailers/126802 | REWE | | retailers/127153 | ALDI SÜD | | retailers/126765 | PENNY | | retailers/127093 | EDEKA | > **Hinweis:** `retailerId`-Filter der API funktioniert nicht — alle Händler kommen über Suchbegriffe. --- ## marktguru API ``` URL: https://api.marktguru.de/api/v1/offers/search Header: x-clientkey: WU/RH+PMGDi+gkZer3WbMelt6zcYHSTytNB7VpTia90= x-apikey: 8Kk+pmbf7TgJ9nVj2cXeA7P5zBGv8iuutVVMRfOfvNE= Params: as=web, limit=24, offset=0, q=<Suchbegriff>, zipCode=56068 ``` **Wichtige Felder pro Angebot:** | Feld | Beschreibung | |------|-------------| | `id` | Angebots-ID | | `description` | Produkttext | | `price` | Angebotspreis | | `referencePrice` | Normpreis pro Einheit (€/kg oder €/l) | | `volume` + `unit` | Menge + Einheit | | `validityDates.from/to` | Gültigkeitszeitraum | | `advertisers[0].id/name` | Händler | | `brand.id/name` | Marke | | `product.id/name` | Produkt (Matching-Schlüssel) | | `categories[0].id/name` | Kategorie | --- ## Phase 1 — LXC einrichten ### LXC anlegen (auf pve) ```bash pveam update pveam download local ubuntu-24.04-standard_24.04-2_amd64.tar.zst pct create 106 local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst \ --hostname preisdb \ --memory 512 \ --swap 512 \ --cores 1 \ --rootfs local-lvm:8 \ --net0 name=eth0,bridge=vmbr0,ip=dhcp \ --unprivileged 1 \ --start 1 ``` ### System aktualisieren ```bash pct enter 106 apt-get update && apt-get upgrade -y --fix-missing ``` > **Hinweis:** Bei 404-Fehler (glibc-Pakete) `--fix-missing` verwenden. ### SSH einrichten ```bash passwd root apt install -y openssh-server sed -i 's/#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config systemctl enable ssh && systemctl start ssh ``` ### Python einrichten ```bash apt install -y python3 python3-pip python3-venv mkdir -p /opt/preisdb python3 -m venv /opt/preisdb/venv source /opt/preisdb/venv/bin/activate pip install requests fastapi uvicorn ``` ### Zeitzone prüfen ```bash timedatectl set-timezone Europe/Berlin timedatectl set-ntp true ``` --- ## Phase 2 — Datenbankschema ### Datei: `/opt/preisdb/schema.py` ```python import sqlite3 DB_PATH = "/opt/preisdb/data/preisdb.sqlite" conn = sqlite3.connect(DB_PATH) cur = conn.cursor() cur.executescript(""" PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; CREATE TABLE IF NOT EXISTS Haendler ( id INTEGER PRIMARY KEY AUTOINCREMENT, mg_retailer_id TEXT NOT NULL UNIQUE, name TEXT NOT NULL, unique_name TEXT ); CREATE TABLE IF NOT EXISTS Marke ( id INTEGER PRIMARY KEY AUTOINCREMENT, mg_brand_id INTEGER UNIQUE, name TEXT NOT NULL, unique_name TEXT ); CREATE TABLE IF NOT EXISTS Kategorie ( id INTEGER PRIMARY KEY AUTOINCREMENT, mg_category_id INTEGER UNIQUE, name TEXT NOT NULL, parent_id INTEGER REFERENCES Kategorie(id) ); CREATE TABLE IF NOT EXISTS Produkt ( id INTEGER PRIMARY KEY AUTOINCREMENT, mg_product_id INTEGER UNIQUE, name TEXT NOT NULL, kategorie_id INTEGER REFERENCES Kategorie(id), marke_id INTEGER REFERENCES Marke(id) ); CREATE TABLE IF NOT EXISTS Angebot ( id INTEGER PRIMARY KEY AUTOINCREMENT, mg_offer_id INTEGER NOT NULL UNIQUE, produkt_id INTEGER NOT NULL REFERENCES Produkt(id), haendler_id INTEGER NOT NULL REFERENCES Haendler(id), beschreibung TEXT, preis REAL NOT NULL, referenz_preis REAL, volumen REAL, einheit TEXT, is_multi INTEGER DEFAULT 0, gueltig_von TEXT, gueltig_bis TEXT, importiert_am TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS Preishistorie ( id INTEGER PRIMARY KEY AUTOINCREMENT, produkt_id INTEGER NOT NULL REFERENCES Produkt(id), haendler_id INTEGER NOT NULL REFERENCES Haendler(id), preis REAL NOT NULL, referenz_preis REAL, gueltig_von TEXT, gueltig_bis TEXT, erfasst_am TEXT DEFAULT (datetime('now')) ); CREATE TABLE IF NOT EXISTS ImportLog ( id INTEGER PRIMARY KEY AUTOINCREMENT, import_zeit TEXT DEFAULT (datetime('now')), suchbegriff TEXT, plz TEXT, anzahl_angebote INTEGER DEFAULT 0, status TEXT, fehler TEXT ); CREATE INDEX IF NOT EXISTS idx_angebot_produkt ON Angebot(produkt_id); CREATE INDEX IF NOT EXISTS idx_angebot_haendler ON Angebot(haendler_id); CREATE INDEX IF NOT EXISTS idx_angebot_gueltig ON Angebot(gueltig_bis); CREATE INDEX IF NOT EXISTS idx_historie_produkt ON Preishistorie(produkt_id); CREATE INDEX IF NOT EXISTS idx_historie_haendler ON Preishistorie(haendler_id); """) conn.commit() conn.close() print("Schema erfolgreich angelegt:", DB_PATH) ``` ### Schema anlegen ```bash mkdir -p /opt/preisdb/data source /opt/preisdb/venv/bin/activate python3 /opt/preisdb/schema.py ``` --- ## Phase 3 — Import-Service ### Dateistruktur ``` /opt/preisdb/ ├── venv/ ├── data/ │ └── preisdb.sqlite ├── config.py ├── schema.py ├── importer.py ├── run_import.py └── test_import.py ``` ### Datei: `/opt/preisdb/config.py` ```python DB_PATH = "/opt/preisdb/data/preisdb.sqlite" PLZ = "56068" API_URL = "https://api.marktguru.de/api/v1/offers/search" API_HEADERS = { "x-clientkey": "WU/RH+PMGDi+gkZer3WbMelt6zcYHSTytNB7VpTia90=", "x-apikey": "8Kk+pmbf7TgJ9nVj2cXeA7P5zBGv8iuutVVMRfOfvNE=", "Accept": "application/json" } API_LIMIT = 24 HAENDLER = { "retailers/126679": "Lidl", "retailers/126735": "Netto Marken-Discount", "retailers/126654": "Kaufland", "retailers/126802": "REWE", "retailers/127153": "ALDI SUED", "retailers/126765": "PENNY", "retailers/127093": "EDEKA", } SUCHBEGRIFFE = [ "milch", "butter", "kaese", "joghurt", "quark", "sahne", "schmand", "frischkaese", "eier", "fleisch", "wurst", "schinken", "geflügel", "hackfleisch", "speck", "salami", "fisch", "lachs", "thunfisch", "garnelen", "brot", "brötchen", "toast", "mehl", "backmischung", "obst", "gemüse", "kartoffeln", "tomaten", "paprika", "bananen", "äpfel", "salat", "tiefkühl", "pizza", "pommes", "wasser", "saft", "softdrinks", "kaffee", "tee", "bier", "wein", "reis", "nudeln", "zucker", "salz", "oel", "essig", "haferflocken", "müsli", "konserven", "suppe", "sosse", "tomatenmark", "schokolade", "kekse", "chips", "snacks", "eis", "marmelade", "honig", "babynahrung", "veganes", "bio" ] NON_FOOD_KATEGORIEN = { "Heimtextilien", "Sofas", "Tische", "Stühle", "Betten", "Kommoden", "Schränke", "Sessel", "Gartenmöbel", "Gartenstühle", "Küchen", "Leuchten", "Glasartikel", "Poolzubehör und -bau", "Aufbewahrungsbehälter", "Küchengeräte", "Küchengeschirr", "Pflanzen", "Damen Shirts", "Kinder- und Babybekleidung", "Elektronik", "Werkzeug", "Haushalt" } ``` ### Manueller Import ```bash cd /opt/preisdb && source venv/bin/activate && python3 run_import.py ``` ### Import-Status prüfen ```bash # Live-Log journalctl -u preisdb-import.service -f # DB-Statistik python3 -c " import sqlite3 conn = sqlite3.connect('/opt/preisdb/data/preisdb.sqlite') for t in ['Haendler','Marke','Kategorie','Produkt','Angebot','Preishistorie','ImportLog']: print(t, conn.execute(f'SELECT COUNT(*) FROM {t}').fetchone()[0]) " ``` --- ## Phase 3 — systemd Timer ### Datei: `/etc/systemd/system/preisdb-import.service` ```ini [Unit] Description=Discounter Preisdatenbank Import After=network.target [Service] Type=oneshot WorkingDirectory=/opt/preisdb ExecStart=/opt/preisdb/venv/bin/python3 /opt/preisdb/run_import.py StandardOutput=journal StandardError=journal User=root ``` ### Datei: `/etc/systemd/system/preisdb-import.timer` ```ini [Unit] Description=Preisdb Import taeglich 02:00 Uhr [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target ``` ### Timer aktivieren ```bash systemctl daemon-reload systemctl enable preisdb-import.timer systemctl start preisdb-import.timer systemctl status preisdb-import.timer ``` ### Timer manuell auslösen ```bash systemctl start preisdb-import.service journalctl -u preisdb-import.service -f ``` --- ## Preisvergleich-Strategie - **Aktueller Vergleich:** Wer hat Produkt X heute am günstigsten → `referenz_preis` (€/kg oder €/l) - **Historienauswertung:** Ist der aktuelle Preis ein echtes Angebot? - Durchschnittspreis aus `Preishistorie` berechnen - Abweichung: > 10% unter Durchschnitt = günstig, = Minimum = Bestpreis - **Matching:** `mg_product_id` für identische Produkte bei verschiedenen Händlern > Belastbare Auswertungen erst nach mehreren Wochen Historiedaten möglich. --- ## Bekannte Einschränkungen | Problem | Lösung | |---------|--------| | `retailerId`-Filter ignoriert | Suchbegriffe + Händlerfilter in DB | | Non-Food in Ergebnissen | `NON_FOOD_KATEGORIEN`-Filter in importer.py | | `isMultiProduct`-Gebinde | `referenz_preis` für Vergleich nutzen | | EDEKA wenig Angebote | Marktguru-Abdeckung begrenzt | --- ## Nächste Schritte - [ ] Phase 4: FastAPI Backend (REST-Endpunkte) - [ ] Phase 4: Web-Frontend (Preisvergleich-Tabelle, Preisverlauf Chart.js) - [ ] Auswertungs-Queries dokumentieren (separate Note) --- [[Impressum|Impressum]] | [[Datenschutzerklärung|Datenschutz]]