Algroveon-Mini-SSG – Wie aus einem Skript ein Werkzeug wurde
Von einem einfachen Python-Skript zum vollständigen statischen Site-Generator mit Mehrsprachigkeit, Übersetzung und Admin-UI.
Warum ich einen eigenen RSS/Atom-Parser geschrieben habe, was ein robuster Feed-Parser leisten muss und welche Design-Entscheidungen dabei eine Rolle gespielt haben.
Die Entwicklung des Algroveon-Parsers zielt darauf ab, die Abhängigkeit von externen Bibliotheken durch eine maßgeschneiderte Python-Lösung für RSS- und Atom-Feeds zu ersetzen. Dabei werden die komplexen Herausforderungen durch unterschiedliche XML-Standards, Namespace-Variationen und Encoding-Probleme direkt adressiert.
Diese Zusammenfassung wurde mit KI-Unterstützung erstellt.
Wer RSS-Feeds in Python parsen will, greift normalerweise zu einer etablierten Library. Also zu fertigem Code, den man ins eigene Projekt einbindet, statt diese Funktion selbst zu entwickeln. Gemeint ist mit „parsen“ in diesem Fall schlicht: den XML-Inhalt eines Feeds einlesen, seine Struktur erkennen und Felder wie Titel, Link, Datum oder Beschreibung so herauslösen, dass der eigene Code damit weiterarbeiten kann. Das ist pragmatisch. Eine Zeit lang funktioniert das auch gut. Aber irgendwann kommt der Punkt, an dem es sich nicht mehr richtig anfühlt: Die Abhängigkeit sitzt tief im Kern des Projekts, die Library übernimmt Dinge automatisch, die nicht vollständig transparent sind, und jede neue Anforderung führt erst einmal in fremde Dokumentation statt direkt an den eigenen Code.
Das war der Ausgangspunkt für Algroveon-Parser. Nicht weil externe Lösungen das Problem wären – im Gegenteil, viele davon sind über Jahre gereift und für sehr viele Einsatzfälle sinnvoll.
RSS und Atom sind XML-Formate. Auf dem Papier klingt das erst einmal einfach: Die Datei wird eingelesen, die einzelnen Meldungen werden erkannt und dann die wichtigsten Informationen wie Titel, Link, Datum oder Beschreibung herausgeholt. In der Praxis ist das aber deutlich unordentlicher.
Format-Wildwuchs: RSS 2.0 ist heute die häufigste RSS-Variante, aber RSS 0.91 ist draußen weiterhin anzutreffen. Atom 1.0 taucht bei neueren Quellen auf, etwa bei The Verge. RDF/RSS 1.0 ist selten, sollte aber zumindest sauber erkannt oder abgefangen werden. Ein Parser, der nur RSS 2.0 sauber beherrscht, stößt bei realen Quellen schnell an Grenzen.
Namespace-Chaos: Fast kein Feed beschränkt sich auf das Basis-XML. content:encoded
für den vollständigen Artikeltext, dc:creator für den Autorennamen, media:thumbnail
für Bilder, media:content als Alternative – jeder Feed kombiniert das etwas anders.
Encoding-Lügen: Ein konkreter Feed (motorsport_magazin) deklariert in seiner
XML-Prolog-Zeile encoding="ISO-8859-1", liefert aber tatsächlich UTF-8. Python's
xml.etree.ElementTree vertraut dieser Deklaration, was in solchen Fällen zu Parse-Problemen führen kann.
Der pragmatische Fallback: die Deklaration ignorieren, den Inhalt testweise als UTF-8 verarbeiten und es erneut versuchen.
Datum-Vielfalt: RSS verwendet typischerweise RFC-2822-nahe Datumsangaben (Sat, 21 Mar 2026 20:03:31 +0100), Atom in der Regel ISO 8601 (2026-03-21T13:00:00-04:00). Beides kann mit Timezone-Offsets oder GMT beziehungsweise -0000 auftauchen. email.utils.parsedate_to_datetime hilft bei RFC 2822, löst -0000 aber zu einem naiven datetime auf – und genau das muss dann sauber korrigiert werden.
Bilder, die nirgendwo sauber stehen: Manche Feeds liefern Bilder über media:thumbnail,
andere über media:content, wieder andere verstecken das Bild als erstes <img> im
HTML-Body von content:encoded oder description. Ohne explizite Bildextraktion
bleibt das Bild bei vielen Feeds schlicht unentdeckt.
Das war die wichtigste und früheste Entscheidung. pyproject.toml hat dependencies = [].
Das klingt radikal, ist es im Kern aber nicht: Python's Standardbibliothek bringt alles mit,
was für diesen Parser nötig ist. xml.etree.ElementTree für das XML-Parsing,
html.parser für den HTML-Sanitizer, email.utils für RFC-2822-Datumsparsing,
re für ISO-8601 und Bildextraktion.
Der Vorteil ist sehr konkret: keine zusätzlichen pip-Abhängigkeiten, die sich mit anderen Projekten beißen können, und keine externen Updates, die das Verhalten unbemerkt verändern. Der Parser läuft überall dort, wo Python 3.12 läuft – ohne weitere Vorbereitung. Das macht ihn nicht grundsätzlich besser als etablierte Libraries, aber für diesen eng definierten Einsatzzweck bewusst überschaubar und gut kontrollierbar.
Die öffentliche API nimmt rohe Bytes entgegen – keine URL, kein HTTP-Client, kein automatisches Nachladen. Das ist eine bewusste Einschränkung. Sie sorgt dafür, dass der Transportweg – also HTTP, Datei oder Test-Fixture – vollständig außerhalb des Parsers liegt und dort separat testbar bleibt. Für Tests heißt das: Fixture-Dateien einlesen, direkt parsen, kein Netz nötig.
raw = urllib.request.urlopen(url).read()
feed = parse(raw, source_url=url)
Das Ergebnis ist immer ein Feed-Objekt mit typisierter Entry-Liste – keine Dicts,
keine optionalen Keys, die man überall erst defensiv prüfen muss. Entry hat feste Felder:
title, url, published (timezone-aware datetime oder None), summary,
summary_text, content, author, guid, image_url.
Die zwei Summary-Varianten sind bewusst getrennt: summary als bereinigtes HTML für die
Darstellung im Browser, summary_text als Plain Text für die Weitergabe an ein LLM.
Der Sanitizer in sanitize.py arbeitet mit einer Allowlist erlaubter Tags –
alles andere wird still entfernt, der Textinhalt bleibt erhalten. script, style,
iframe und form werden komplett mitsamt Inhalt gelöscht. Links werden auf
http- und https-URLs geprüft, javascript: wird verworfen. Bilder behalten nur ein
src mit sicherem Schema.
Das ist kein Extra, sondern Pflicht. Feed-Inhalte kommen aus beliebigen Quellen und werden im Browser gerendert. Ohne Sanitizer ist XSS keine theoretische Möglichkeit, sondern ein sehr naheliegendes Problem.
email.utils kann RFC 2822, Python's datetime.fromisoformat unterstützt seit 3.11
viel von ISO 8601. In der Praxis gibt es aber trotzdem genug Varianten, bei denen ich mich
nicht blind darauf verlassen wollte – gerade dann, wenn Millisekunden, Z oder verschiedene
Offset-Schreibweisen zusammenkommen. Die Entscheidung war deshalb: ein handgeschriebenes Regex, das genau die im Projekt relevanten Muster abdeckt und daraus ein timezone-aware datetime konstruiert. Überschaubar, kontrollierbar und für den konkreten Einsatzzweck ausreichend – ohne den Anspruch, damit eine allgemeine Referenzimplementierung für alle ISO-8601-Varianten zu liefern.
media:thumbnail mit korrekten Namespace-URIsFeeds deklarieren den media:-Namespace als http://search.yahoo.com/mrss/. ElementTree
löst das korrekt auf, aber nur dann, wenn man die vollständige Clark-Notation verwendet:
{http://search.yahoo.com/mrss/}thumbnail. Das ist nicht besonders intuitiv und war eine der
Stellen, an denen die ersten Testläufe still falsche Ergebnisse lieferten – die
Thumbnail-Extraktion gab None zurück, obwohl im Feed Bilder vorhanden waren.
rel="alternate" ist optionalIm Atom-Standard hat <link> ein rel-Attribut. rel="alternate" meint den
Artikel-Link. Viele Feeds lassen dieses Attribut aber weg – laut Spezifikation ist alternate
der Default. Eine XPath-Suche wie [@rel='alternate'] findet dann nichts.
Der Fallback ist entsprechend simpel: Wenn kein Link mit rel="alternate" gefunden wird,
wird der erste <link>-Tag mit href-Attribut genommen.
Der Parser ist per API in Algroveon-Agent eingebunden. Dort liest er die konfigurierten News-Feeds,
extrahiert Artikel und bereitet sie für die LLM-Zusammenfassung vor – summary_text
geht an Ollama, summary und image_url in die Darstellung.
Die Entkopplung war hier die richtige Entscheidung: Algroveon-Agent braucht keine XML-Kenntnisse, Algroveon-Parser keine Ahnung von Ollama. Beide Seiten bleiben dadurch einfacher, klarer und besser testbar, als wenn alles in einem gemeinsamen Block stecken würde.
Von einem einfachen Python-Skript zum vollständigen statischen Site-Generator mit Mehrsprachigkeit, Übersetzung und Admin-UI.
Wie und warum ich meinen eigenen Fußballmanager in Python gebaut habe – mit PyQt6, SQLite, einer eigenen Match-Engine und einem lokalen LLM als Pressedienst.