Automatischer Scraper für kleinanzeigen.de mit Benachrichtigungen bei neuen Anzeigen.
ek-scraper überwacht beliebig viele Kleinanzeigen-Suchen und benachrichtigt dich per Telegram, Pushover oder ntfy.sh, sobald neue Inserate erscheinen. Dank Headless-Browser (Playwright/Chromium) werden auch JavaScript-geschützte Seiten zuverlässig geladen.
- Headless-Browser-Scraping via Playwright (Chromium) — umgeht Bot-Detection und JavaScript-Schutz
- Mehrere Suchen parallel überwachen
- Automatische Pagination — alle Ergebnisseiten werden durchsucht
- Flexible Filter: Top-Ads ausblenden, Regex-Muster zum Ausschließen
- Benachrichtigungen via Telegram, Pushover und/oder ntfy.sh
- Persistenter Datenspeicher — nur wirklich neue Anzeigen lösen Benachrichtigungen aus
- Pruning — veraltete Anzeigen automatisch aus dem Datenspeicher entfernen
- Ideal für Cronjobs / regelmäßige Ausführung
- Docker-Support — ein Container, beliebig viele Configs, integrierter Cron, Prozess-Management mit init und Overlap-Schutz
Voraussetzung: Docker mit Docker Compose.
git clone https://github.com/elRicharde/KleinanzeigenScraper.git
cd KleinanzeigenScraper
# Configs-Ordner anlegen und Konfigurationen hinzufügen
mkdir -p configs
cp config-davinci-pc.example configs/config-pc.json
# configs/config-pc.json anpassen (Suchen, Telegram-Token etc.)
# Bauen & starten
docker compose up -d
# Logs ansehen
docker compose logs -fDer Container erkennt automatisch alle *.json-Dateien im configs/-Ordner und erstellt für jede einen Cron-Job mit eigenem Datenspeicher.
Neue Suche hinzufügen: JSON-Datei in configs/ legen → docker compose restart
Suche entfernen: JSON-Datei aus configs/ löschen → docker compose restart
Stabilität: Der Container nutzt init: true (tini als PID 1), damit Playwright/Chromium-Prozesse nach jedem Scrape-Lauf zuverlässig aufgeräumt werden. Zusätzlich verhindert flock pro Config, dass sich überlappende Cron-Läufe gegenseitig stören (z.B. wenn ein Lauf länger als das Cron-Intervall dauert).
Umgebungsvariablen (in docker-compose.yml):
| Variable | Standard | Beschreibung |
|---|---|---|
CRON_SCHEDULE |
*/15 * * * * |
Cron-Zeitplan für alle Configs |
RUN_ON_START |
false |
Beim Container-Start sofort einmal scrapen |
- Python >= 3.11
- uv (empfohlen) oder pip
uv tool install ek-scraperOder mit pip:
pip install ek-scraperNach der Installation muss einmalig der Chromium-Browser für Playwright heruntergeladen werden:
playwright install chromium
playwright install-deps chromium
install-depsinstalliert die benötigten System-Bibliotheken (Linux). Unter macOS/Windows ist dieser Schritt in der Regel nicht nötig.
ek-scraper create-config config.jsonBearbeite config.json — füge deine Suchen und Benachrichtigungseinstellungen hinzu (siehe Konfiguration).
ek-scraper run --no-notifications --data-store datastore.json config.jsonBeim ersten Lauf werden alle aktuellen Anzeigen erfasst und im Datenspeicher gespeichert. So bekommst du beim nächsten Lauf nur wirklich neue Anzeigen gemeldet.
ek-scraper run --data-store datastore.json config.jsonFührt den Scraper aus und sendet Benachrichtigungen für neue Anzeigen.
ek-scraper run [OPTIONEN] CONFIG_FILE
| Option | Beschreibung |
|---|---|
--data-store PFAD |
Pfad zur JSON-Datei für den Datenspeicher (Standard: ~/ek-scraper-datastore.json) |
--temp-data-store |
Temporären Datenspeicher verwenden (nicht persistent) |
--no-notifications |
Keine Benachrichtigungen senden |
--prune |
Veraltete Anzeigen beim Schließen aus dem Datenspeicher entfernen |
-v, --verbose |
Debug-Ausgabe aktivieren |
--data-storeund--temp-data-storeschließen sich gegenseitig aus.
Erstellt eine Beispiel-Konfigurationsdatei.
ek-scraper create-config CONFIG_FILE
Entfernt Anzeigen aus dem Datenspeicher, die in keiner Suche mehr auftauchen.
ek-scraper prune --data-store PFAD CONFIG_FILE
Die Konfiguration erfolgt über eine JSON-Datei mit drei Abschnitten:
{
"filter": { ... },
"notifications": { ... },
"searches": [ ... ]
}Ein Array von Suchen, die überwacht werden sollen.
| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---|---|---|
name |
string | ja | — | Beschreibender Name der Suche |
url |
string | ja | — | URL der ersten Ergebnisseite auf kleinanzeigen.de |
recursive |
boolean | nein | true |
Alle Ergebnisseiten (Pagination) durchsuchen |
Beispiel:
"searches": [
{
"name": "Wohnungen in Hamburg Altona",
"url": "https://www.kleinanzeigen.de/s-wohnung-mieten/altona/c203l9497",
"recursive": true
},
{
"name": "E-Bikes in Berlin",
"url": "https://www.kleinanzeigen.de/s-fahrraeder/berlin/e-bike/k0c217l3331",
"recursive": false
}
]Client-seitige Filter zum Ausschließen und Einschließen bestimmter Anzeigen.
| Feld | Typ | Standard | Beschreibung |
|---|---|---|---|
exclude_topads |
boolean | true |
Gesponserte Top-Anzeigen ausschließen |
exclude_patterns |
string[] | [] |
Regex-Muster — Anzeigen mit passendem Titel oder Beschreibung werden ignoriert |
require_all_patterns |
string[] | [] |
Regex-Muster — alle müssen im Titel oder der Beschreibung matchen (AND-Verknüpfung). Innerhalb eines Musters kann Regex-OR (` |
Logik:
exclude_patterns: Anzeige wird ausgeschlossen wenn irgendein Muster matcht (OR)require_all_patterns: Anzeige wird ausgeschlossen wenn nicht alle Muster matchen (AND)
So lassen sich komplexe Abfragen bauen: (Bedingung1_a ODER Bedingung1_b) UND (Bedingung2_a ODER Bedingung2_b)
Beispiel:
"filter": {
"exclude_topads": true,
"exclude_patterns": [
"(?i)\\b(defekt|bastler|tausch)\\b"
],
"require_all_patterns": [
"(?i)(64\\s*gb|96\\s*gb|128\\s*gb)",
"(?i)(nvme|m\\.?2|ssd)"
]
}Pattern-Referenz:
Die Patterns verwenden Python-Regex-Syntax. (?i) aktiviert Groß-/Kleinschreibung-ignorieren, \\s* matcht optionale Leerzeichen, | ist ODER innerhalb einer Gruppe.
| Pattern | Matcht | Matcht nicht |
|---|---|---|
(?i)(48\\s*gb|64\\s*gb|96\\s*gb|128\\s*gb) |
64GB, 64 GB, 128gb, 96 Gb |
32GB, viel RAM, 16gb |
(?i)(nvme|m\\.?2|ssd\\s*(1|2)\\s*tb) |
NVMe, M.2, M2, SSD 1TB, 2 TB SSD |
HDD, Festplatte, 256GB SSD |
(?i)\\b(defekt|bastler|tausch)\\b |
Defekt, BASTLER, Tausch |
defekter (da \\b Wortgrenzen prüft) |
Hinweis: Alle Patterns werden sowohl auf den Titel als auch auf die Beschreibung der Anzeige angewendet. Es reicht wenn der Suchbegriff in einem der beiden Felder vorkommt.
Alle Backends sind optional und können kombiniert werden. Pro Suche wird eine Benachrichtigung gesendet, wenn neue Anzeigen gefunden werden.
Benachrichtigungen über einen Telegram-Bot. Jede Nachricht enthält die einzelnen Anzeigen als klickbare Links mit Preis sowie einen Link zur Suchergebnisseite:
Dresden Workstation 64GB
🤖 Found 2 new ads
• iMac Pro 2017 5K 27" | 14-Core | 64GB | 2TB SSD Workstation PC – 1.399 €
• High-End Workstation PC - Ryzen 9 5950X|64GB Ram| X570|1TB NVMe – 1.150 € VB
Zur Suche
"notifications": {
"telegram": {
"bot_token": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"chat_id": "-1001234567890",
"link_preview": false
}
}| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---|---|---|
bot_token |
string | ja | — | Bot-Token vom BotFather |
chat_id |
string | ja | — | Chat-/Gruppen-/Kanal-ID |
link_preview |
boolean | nein | false |
Link-Vorschau in Nachrichten anzeigen |
Telegram einrichten:
- Öffne @BotFather in Telegram
- Sende
/newbotund folge den Anweisungen → du erhältst denbot_token - Starte eine Konversation mit deinem Bot oder füge ihn einer Gruppe hinzu
- Rufe
https://api.telegram.org/bot<DEIN_TOKEN>/getUpdatesauf → lies diechat_idaus demchat.idFeld ab
Push-Benachrichtigungen über Pushover.
"notifications": {
"pushover": {
"token": "<app-api-token>",
"user": "<user-api-token>",
"device": []
}
}| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---|---|---|
token |
string | ja | — | API-Token der Pushover-App |
user |
string | ja | — | API-Token des Pushover-Nutzers |
device |
string[] | nein | [] (alle) |
Gerätenamen, die benachrichtigt werden |
Push-Benachrichtigungen über ntfy.sh.
"notifications": {
"ntfy.sh": {
"topic": "ek-scraper-dein-geheimer-name",
"priority": 3
}
}| Feld | Typ | Pflicht | Standard | Beschreibung |
|---|---|---|---|---|
topic |
string | ja | — | Topic-Name (sollte schwer zu erraten sein) |
priority |
integer | nein | 3 |
Priorität: 1 (niedrig) bis 5 (dringend) |
Topic-Namen sind öffentlich. Verwende einen schwer erratbaren Namen, z.B.:
echo "ek-scraper-$(uuidgen)"
{
"filter": {
"exclude_topads": true,
"exclude_patterns": [".*[Mm]akler.*", ".*[Pp]rovision.*"]
},
"notifications": {
"telegram": {
"bot_token": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"chat_id": "-1001234567890",
"link_preview": false
}
},
"searches": [
{
"name": "Wohnungen in Hamburg Altona",
"url": "https://www.kleinanzeigen.de/s-wohnung-mieten/altona/c203l9497",
"recursive": true
}
]
}Bei der Docker-Installation ist der Cron bereits im Container integriert. Das Intervall wird über die CRON_SCHEDULE-Umgebungsvariable in der docker-compose.yml gesteuert:
environment:
- CRON_SCHEDULE=*/15 * * * *Nützliche Docker-Befehle:
# Status prüfen
docker compose ps
# Logs ansehen
docker compose logs -f
# Neustart (z.B. nach Config-Änderung)
docker compose restart
# Manuellen Scrape-Lauf auslösen
docker exec ek-scraper ek-scraper run --data-store /app/data/datastore-config-pc.json /app/configs/config-pc.jsonUm ek-scraper ohne Docker automatisch regelmäßig auszuführen, richte einen Cronjob ein:
crontab -eWichtig: Verwende in Cronjobs immer den vollen Pfad zu ek-scraper, da Cron eine eingeschränkte PATH-Umgebung hat. Den Pfad findest du mit which ek-scraper.
Beispiel — alle 30 Minuten ausführen, mit vollem Pfad und Logging:
*/30 * * * * cd ~/ek-scraper && /home/user/.local/bin/ek-scraper run --data-store datastore.json config.json >> ~/ek-scraper.log 2>&1
Beispiel — mehrere Configs mit unterschiedlichen Intervallen:
*/15 * * * * cd ~/ek-scraper && /home/user/.local/bin/ek-scraper run --data-store datastore-pc.json config-pc.json >> ~/ek-scraper-pc.log 2>&1
*/30 * * * * cd ~/ek-scraper && /home/user/.local/bin/ek-scraper run --data-store datastore-moebel.json config-moebel.json >> ~/ek-scraper-moebel.log 2>&1
Hinweis:
>> ~/ek-scraper.log 2>&1leitet sowohl stdout als auch stderr in eine Logdatei um. Ohne dieses Redirect gehen Fehlermeldungen verloren und der Scraper scheitert still.
Führe den Scraper nicht zu häufig aus, um eine IP-Sperre durch kleinanzeigen.de zu vermeiden. Ein Intervall von 15–30 Minuten ist empfehlenswert.
Ein nützliches Tool zur Erstellung von Cron-Ausdrücken: crontab.guru
Komplette Anleitung für einen frischen Linux-Server mit Docker.
# Docker installieren (Ubuntu/Debian)
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Neu einloggen damit die Gruppenänderung greiftgit clone https://github.com/elRicharde/KleinanzeigenScraper.git
cd KleinanzeigenScrapermkdir -p configs
# Beispiel-Config kopieren und anpassen
cp config-davinci-pc.example configs/config-pc.json
# configs/config-pc.json bearbeiten: Suchen, Telegram-Token, Chat-ID etc.Pflichtfelder in der Config:
- Mindestens eine Suche mit
nameundurl - Benachrichtigungs-Credentials (z.B. Telegram
bot_token+chat_id)
Tipp: Mehrere Config-Dateien im
configs/-Ordner anlegen — jede wird automatisch erkannt.
Wenn du von einem bestehenden Setup umziehst, kannst du die bisherigen Datenspeicher in das Docker-Volume kopieren:
# Container einmal starten damit das Volume erstellt wird
docker compose up -d && docker compose down
# Datenspeicher ins Volume kopieren
docker run --rm -v kleinanzeigenscraper_ek-scraper-data:/data -v $(pwd):/src alpine \
cp /src/datastore-pc.json /data/datastore-config-pc.jsonOhne Datenspeicher werden beim ersten Lauf alle aktuellen Anzeigen als "neu" erfasst — du bekommst dann einmalig viele Benachrichtigungen.
docker compose up -d# Container-Status
docker compose ps
# Logs ansehen
docker compose logs -f
# Datenspeicher prüfen (im Volume)
docker exec ek-scraper ls -la /app/data/Zusammenfassung: Docker installieren → Repo klonen → Configs in
configs/anlegen →docker compose up -d. Fertig.
Komplette Anleitung, um ek-scraper ohne Docker auf einem frischen Linux-Server einzurichten.
# Python 3.11+ prüfen
python3 --version
# uv installieren (falls noch nicht vorhanden)
curl -LsSf https://astral.sh/uv/install.sh | shuv tool install ek-scraperplaywright install chromium
playwright install-deps chromium # installiert System-Bibliotheken (nur Linux)# Beispielconfig erzeugen und anpassen
ek-scraper create-config config.jsonOder eine vorhandene Config-Datei vom alten Server kopieren.
Pflichtfelder in der Config:
- Mindestens eine Suche mit
nameundurl - Benachrichtigungs-Credentials (z.B. Telegram
bot_token+chat_id)
Tipp: Mehrere Configs für verschiedene Suchkategorien anlegen (z.B.
config-pc.json,config-moebel.json).
Wenn du von einem anderen Server umziehst, kopiere die Datenspeicher-Datei mit:
scp alter-server:~/ek-scraper-datastore.json ~/Ohne Datenspeicher werden beim ersten Lauf alle aktuellen Anzeigen als "neu" erfasst — du bekommst dann einmalig viele Benachrichtigungen.
# Ohne Benachrichtigungen — füllt nur den Datenspeicher
ek-scraper run --no-notifications --data-store datastore.json config.json
# Mit Benachrichtigungen — prüfen ob alles funktioniert
ek-scraper run --data-store datastore.json config.json# Vollen Pfad ermitteln
which ek-scraper
# z.B. /home/techniker/.local/bin/ek-scraper
crontab -eBeispiel — alle 30 Minuten, mit vollem Pfad und Logging:
*/30 * * * * cd ~/ek-scraper && /home/techniker/.local/bin/ek-scraper run --data-store datastore.json config.json >> ~/ek-scraper.log 2>&1
Beispiel — mehrere Configs mit unterschiedlichen Intervallen:
*/15 * * * * cd ~/ek-scraper && /home/techniker/.local/bin/ek-scraper run --data-store datastore-pc.json config-pc.json >> ~/ek-scraper-pc.log 2>&1
*/30 * * * * cd ~/ek-scraper && /home/techniker/.local/bin/ek-scraper run --data-store datastore-moebel.json config-moebel.json >> ~/ek-scraper-moebel.log 2>&1
# Cron-Ausführungen im Syslog prüfen
sudo grep ek-scraper /var/log/syslog | tail -10
# Datenspeicher prüfen — wann zuletzt geändert?
ls -la ~/ek-scraper/datastore*.json
# Logdatei prüfen (falls Logging eingerichtet)
tail -50 ~/ek-scraper.logZusammenfassung:
uv+playwrightinstallieren → Config anlegen → optional Datenspeicher mitnehmen → Cronjob mit vollem Pfad + Logging einrichten. Fertig.
Container-Status prüfen:
docker compose ps
docker compose logs -fManueller Testlauf im Container:
docker exec ek-scraper ek-scraper run --data-store /app/data/datastore-config-pc.json /app/configs/config-pc.jsonContainer neu bauen (nach Code-Updates):
git pull
docker compose up -d --buildDatenspeicher einsehen:
docker exec ek-scraper ls -la /app/data/| Problem | Ursache | Lösung |
|---|---|---|
| Container startet, aber keine Scrapes | Keine Config-Dateien gefunden | Prüfen ob configs/-Ordner existiert und JSON-Dateien enthält |
docker compose up schlägt fehl |
Image-Build-Fehler | docker compose build --no-cache versuchen |
| Alte Anzeigen werden nochmal gemeldet | Neues Volume ohne bisherigen Datenspeicher | Datenspeicher ins Volume kopieren (siehe Server-Einrichtung) |
BlockingIOError: Resource temporarily unavailable |
Zombie-Prozesse akkumulieren (fehlende init: true) |
docker compose down && docker compose up -d --build — ab v1.0.0 durch init: true behoben |
Hohe PID-Anzahl im Container (docker stats) |
Überlappende Cron-Läufe oder Zombie-Prozesse | Container neustarten; ab v1.0.0 durch flock und init: true behoben |
Läuft der Scraper überhaupt?
1. Cronjobs prüfen:
# Sind die Cronjobs eingerichtet?
crontab -l
# Werden sie ausgeführt?
sudo grep ek-scraper /var/log/syslog | tail -202. Datenspeicher prüfen:
# Wann wurde zuletzt geschrieben?
ls -la ~/ek-scraper/datastore*.jsonWenn das Änderungsdatum weit zurückliegt, aber die Cronjobs laut Syslog laufen, scheitert der Scraper still (siehe nächster Punkt).
3. Manueller Testlauf mit Debug-Output:
ek-scraper --verbose run --data-store datastore.json config.jsonHinweis:
--verbosemuss vor dem Subcommandrunstehen.
| Problem | Ursache | Lösung |
|---|---|---|
| Cronjob läuft, aber Datenspeicher wird nicht aktualisiert | Cron findet ek-scraper nicht (eingeschränkter PATH) |
Vollen Pfad verwenden: /home/user/.local/bin/ek-scraper (ermitteln mit which ek-scraper) |
| Cronjob-Fehler nicht sichtbar | stdout/stderr werden nicht erfasst | >> ~/ek-scraper.log 2>&1 an die Crontab-Zeile anhängen |
| Scraper findet keine Anzeigen | Chromium/Playwright veraltet oder kaputt | playwright install chromium erneut ausführen |
| Scraper findet Anzeigen, aber keine Benachrichtigungen | Filter zu streng — alle Anzeigen werden rausgefiltert | Im --verbose-Output nach does not match required pattern suchen und Filter in der Config lockern |
| IP-Sperre durch kleinanzeigen.de | Zu häufige Abfragen | Intervall auf mindestens 15–30 Minuten erhöhen |
| Telegram-Benachrichtigung kommt nicht an | Bot-Token oder Chat-ID falsch | Token und Chat-ID in der Config prüfen, Bot muss der Gruppe hinzugefügt sein |
Im --verbose-Modus zeigt der Scraper für jede gefilterte Anzeige den Grund an:
scraper: Ad '3373532998' 'Ryzen 5950x rtx 3080 ti Gaming pc'
does not match required pattern '(?i)(48\s*gb|64\s*gb|...)'
Das bedeutet: Die Anzeige wurde gefunden, aber der require_all_patterns-Filter hat sie ausgeschlossen. Wenn zu wenige Ergebnisse durchkommen, die Filter-Patterns in der Config anpassen.
Wenn der Datenspeicher zu groß wird oder du Benachrichtigungen für bereits gesehene Anzeigen erneut erhalten möchtest:
# Veraltete Anzeigen entfernen (die nicht mehr in den Suchergebnissen sind)
ek-scraper prune --data-store datastore.json config.json
# Komplett neu starten (alle aktuellen Anzeigen werden als "neu" erkannt)
echo '{}' > datastore.json
ek-scraper run --no-notifications --data-store datastore.json config.jsonKonfigurationsdatei (JSON)
│
▼
┌─────────────────────┐
│ Suchen (parallel) │
│ │
│ Playwright startet │
│ Headless Chromium │──▶ kleinanzeigen.de
│ │◀── HTML (mit JS gerendert)
│ BeautifulSoup │
│ parst die Anzeigen │
│ │
│ Pagination: │
│ Alle Seiten auto- │
│ matisch abarbeiten │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ Filter anwenden │
│ • Top-Ads │
│ • Regex-Muster │
└─────────┬───────────┘
│
▼
┌─────────────────────┐
│ Datenspeicher │
│ prüfen │──▶ Bekannte Anzeigen überspringen
└─────────┬───────────┘
│ nur neue Anzeigen
▼
┌─────────────────────┐
│ Benachrichtigungen │
│ • Telegram │
│ • Pushover │
│ • ntfy.sh │
└─────────────────────┘
Der Datenspeicher ist eine JSON-Datei, die alle bereits gesehenen Anzeigen enthält. So wird sichergestellt, dass nur neue Anzeigen Benachrichtigungen auslösen.
- Standardpfad:
~/ek-scraper-datastore.json - Format: JSON mit Anzeigen-IDs als Schlüssel
- Pruning: Mit
--pruneoder demprune-Befehl können Anzeigen entfernt werden, die nicht mehr in den Suchergebnissen erscheinen
git clone git@github.com:jonasehrlich/ek-scraper.git
cd ek-scraper
uv sync
playwright install chromiumuv run ruff check ek_scraper/
uv run mypy ek_scraper/
uv run isort --check ek_scraper/MIT