Worum es geht
Feeds sind Alltag in jeder Überwachungs-Pipeline, und sie sind unzuverlässiger, als die Spezifikation vermuten lässt. Es gibt RSS 2.0, Atom 1.0 und Mischformen. Dazu kommen falsche Content-Types, fehlende Felder, unterschiedliche Datumsformate und Encoding-Probleme.
Für viele Fälle ist feedparser die richtige Wahl. Wer die Abhängigkeit vermeiden oder genau steuern will, was bei einem Fehler passiert, kommt mit xml.etree.ElementTree und urllib aus der Standardbibliothek erstaunlich weit.
Abrufen und Antwort prüfen
Bevor du parst, prüfst du, ob überhaupt ein Feed angekommen ist. Manche Server liefern bei Fehlern eine HTML-Seite mit Status 200.
import logging
import urllib.request
log = logging.getLogger("feeds")
def fetch(url: str, timeout: int = 15) -> bytes | None:
req = urllib.request.Request(url, headers={"User-Agent": "pipeline/1.0"})
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
ctype = resp.headers.get("Content-Type", "")
body = resp.read()
except OSError as exc:
log.warning("Abruf fehlgeschlagen für %s: %s", url, exc)
return None
if "html" in ctype.lower() and not body.lstrip().startswith(b"<?xml"):
log.warning("Kein Feed, sondern HTML: %s", url)
return None
return body
Wer die Bytes direkt an den XML-Parser gibt, lässt ihn die Zeichenkodierung aus der XML-Deklaration bestimmen. Das ist zuverlässiger als selbst zu dekodieren.
RSS und Atom unterscheiden
RSS 2.0 hat ein Wurzelelement rss mit channel und item. Atom nutzt den Namespace http://www.w3.org/2005/Atom mit feed und entry. Am einfachsten liest du das Wurzelelement und verzweigst.
import xml.etree.ElementTree as ET
ATOM = "{http://www.w3.org/2005/Atom}"
def parse_feed(data: bytes, feed_name: str) -> list[dict]:
try:
root = ET.fromstring(data)
except ET.ParseError as exc:
log.warning("%s: XML ungültig: %s", feed_name, exc)
return []
if root.tag == "rss":
nodes = root.findall("./channel/item")
get = _rss_item
elif root.tag == ATOM + "feed":
nodes = root.findall(ATOM + "entry")
get = _atom_item
else:
log.warning("%s: unbekanntes Format %s", feed_name, root.tag)
return []
items = []
for index, node in enumerate(nodes):
try:
items.append(get(node))
except Exception as exc: # ein kaputtes Item darf nichts abbrechen
log.warning("%s: Item %d übersprungen: %s", feed_name, index, exc)
return items
Der entscheidende Punkt ist die Schleife mit Fehlerbehandlung pro Item: Ein einzelnes kaputtes Element wird protokolliert und übersprungen, der Rest des Feeds bleibt verwertbar.
Fehlende Felder abfangen
Titel, Link und Datum können fehlen. Setze vernünftige Vorgaben, statt zu scheitern, und verlange nur das, ohne das ein Eintrag wertlos wäre, meist den Link.
def _text(node, path: str, default: str = "") -> str:
found = node.find(path)
return (found.text or "").strip() if found is not None else default
def _rss_item(node) -> dict:
link = _text(node, "link")
if not link:
raise ValueError("Item ohne Link")
return {"title": _text(node, "title", "(ohne Titel)"), "link": link,
"date": _text(node, "pubDate")}
Für Atom liegt der Link als Attribut href an einem link-Element, eine Besonderheit, die man beim ersten Mal leicht übersieht.
Datumsformate
RSS verwendet meist RFC 822, Atom ISO 8601. Die Standardbibliothek deckt beides ab: email.utils.parsedate_to_datetime für RSS und datetime.fromisoformat für Atom. Unlesbare Daten sollten nicht zum Abbruch führen, sondern auf den Abrufzeitpunkt zurückfallen.
Strukturierte Fehlermeldungen
Eine Meldung wie „Feed kaputt“ hilft nicht. Protokolliere Feed-Name, Item-Index und die Art des Fehlers, wie im Beispiel oben. Dann siehst du im Journal sofort, welcher Feed Probleme macht, und ein Heartbeat kann melden, wie viele Feeds oder Items übersprungen wurden.
Wann sich das lohnt (und wann nicht)
Der Eigenbau lohnt sich, wenn du wenige Feeds hast, die Abhängigkeiten klein halten willst oder detaillierte Fehlerdiagnose brauchst. Der Code ist überschaubar und tut genau, was du erwartest.
feedparser ist die bessere Wahl, wenn du viele unterschiedliche, teils schlecht formatierte Feeds verarbeitest, denn die Bibliothek fängt viele Sonderfälle ab, die du sonst selbst nachbauen müsstest. Auch Erweiterungen wie Medien-Anhänge oder Namespaces fremder Dienste sind dort bereits abgedeckt.