Playwright-basierter Scraper für WebUntis. Lädt Stundenplan, Prüfungen / Klausuren, Hausaufgaben, Absenzen und Nachrichten und speichert sie als strukturiertes JSON.
WebUntis hat zwei relevante APIs:
- JSON-RPC unter
/WebUntis/jsonrpc.do(klassisch, gut dokumentiert, z.B.getOwnTimetableForWeek,getExamsForRange,getHomeWorkForRange). - REST v1 unter
/WebUntis/api/rest/view/v1/...(das neuere UI2020-Backend mit/timetable/entries,/app/data).
Beide erfordern einen gültigen Session-Cookie. Statt die komplexe
React-SPA von UI2020 mit Form-Selectors anzufassen (race-conditions
mit der JS-Hydration, instabile Selektoren), machen wir den Login
direkt gegen den JSON-RPC-authenticate-Endpoint. Das ist schnell,
zuverlässig und unabhängig vom gerenderten DOM.
Playwright wird nur kurz benutzt um die school-Cookies zu bootstrappen
(JSESSIONID etc.), die der Server bei einem GET auf die Login-URL setzt.
Diese Cookies + Username/Passwort gehen dann in den
authenticate-RPC → Session-ID. Alle weiteren Calls laufen über httpx.
Falls deine Schule SSO/2FA/Captcha vorschaltet, fällt der Scraper
automatisch auf den Form-Login zurück (Playwright klickt sich durch).
--form-login erzwingt diesen Pfad dauerhaft.
Bonus: playwright-stealth patcht typische Bot-Detection-Vektoren
(navigator.webdriver, navigator.plugins, navigator.languages, …).
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -r requirements.txt
playwright install chromium-
config.example.jsonnachconfig.jsonkopieren und anpassen:{ "server": "nese", // Subdomain vor .webuntis.com "school": "htbla_kaindorf",// Wert hinter ?school= "username": "max.muster" }server+schoolfindest du, indem du auf webuntis.com deine Schule suchst - die Redirect-URL enthält beides, z.B.https://nese.webuntis.com/WebUntis/?school=htbla_kaindorf. -
.env.examplenach.envkopieren und das Passwort eintragen:UNTIS_PASSWORD=deinPasswort
# Standard-Lauf (JSON-RPC-Login, headless, Session wiederverwendet)
python -m src
# Erzwinge Form-Login (z.B. bei 2FA / SSO)
python -m src --form-login --no-headless --clear-session
# Anderes Zeitfenster
python -m src --days-back 7 --days-forward 30
# Rohdaten der API zusätzlich behalten
python -m src --keep-raw -vOutput landet in out/untis_<timestamp>.json sowie out/latest.json.
In sessions/storage_state.json werden Cookies gespeichert, damit
Folge-Läufe kein erneutes Login brauchen.
Falls du eine Fehlermeldung wie Authenticate failed: Invalid username or password (code=-1) bekommst, obwohl die Credentials stimmen, prüfe:
- Server + Slug korrekt? Auf
webuntis.comdeine Schule suchen - die Redirect-URL lautethttps://<server>.webuntis.com/WebUntis/?school=<slug>. - Sonderzeichen im Passwort?
.envunterstützt=und Quotes, aber führende Whitespaces werden getrimmt. Test mitpython -c "import os; print(repr(os.environ['UNTIS_PASSWORD']))". - CAPTCHA / SSO / 2FA? →
python -m src --form-login --no-headless - Verbose-Output:
python -m src -vzeigt den HTTP-Verkehr.
- 2FA / Captcha: Falls deine Schule OTP verlangt, einmalig mit
--no-headless --clear-sessionlaufen lassen, Code eintippen, dann ab sofort headless. - Prüfungen: Der Endpoint
getExamsist nur für Admins/Lehrer verfügbar. Für Schüler leiten wir Klausuren aus dem Stundenplan ab (actTypeenthält "Klausur") - siehetimetable_fallbackin der Output-Source. - Rate-Limit: Wir senden höchstens eine Anfrage alle 400 ms.
Verzögern mit
--days-forwardreizen ist kein Problem. - Speicherort:
sessions/undout/sind in.gitignore.
src/
__init__.py
main.py # CLI
config.py # config.json + .env laden
browser.py # Playwright + stealth
untis_client.py # Login + JSON-RPC + REST v1
normalize.py # Rohdaten -> saubere Dicts
scraper.py # Orchestrierung
exporter.py # JSON-Ausgabe
{ "meta": { "school": "...", "server": "...", "user": "...", "generated_at": "2026-06-02", "window": {"start": "...", "end": "..."} }, "timetable": { "source": "rest_v1" | "jsonrpc", "start": "2026-06-02", "end": "2026-06-16", "days": [ {"date": "2026-06-02", "entries": [ { "start": "2026-06-02T08:00", "end": "2026-06-02T08:45", "status": "REGULAR", "is_cancelled": false, "is_exam": false, "lesson_text": "", "subjects": [{"short":"M","long":"Math"}], "teachers": [...], "rooms": [...] } ]} ], "lessons": [...] // bei jsonrpc-Fallback }, "exams": { "source": "jsonrpc" | "timetable_fallback", "exams": [ { "id": 123, "date": "2026-06-10", "name": "Klausur", ... } ] }, "homework": { "items": [...] }, "absences": { "items": [...] }, "messages": { "items": [...] } }