# 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]]