PCPP1 Block 4 (Teil b): XML und REST-Client mit requests
Track Python · PCPP1 Block 4 (4.5 und 4.6) · ca. 55 Min.
Worum es geht
Netzwerkprogrammierung (network programming) ist laut Quelle PCPP1 Block 4 mit 18 % der Prüfung und 8 Fragen (Stand der Quelle: 1. Oktober 2026, bitte vor der Buchung prüfen). Diese Lektion ist Teil b und deckt 4.5 und 4.6 ab: XML (Aufbau und Parsen mit ElementTree) und den REST-Client mit requests, der an einigen Stellen anders denkt als fetch.
Was du aus Teil a brauchst: Du kennst HTTP-Methoden, Statuscodes (404, 204 und so weiter) und die Idempotenz, und du weißt, dass json.dumps und json.loads Typen verändern. Das steht in Teil a: Sockets, HTTP und JSON.
Nicht behandelt: HTTPS und Zertifikate, Authentifizierung. Das Schreiben von XML (Elemente bauen) gehört laut Quelle zu Block 5.
Ablauf: Tabelle JS/TS gegen Python, zwei kurze Schritte, danach drei Übungen. Zwei laufen im Browser, eine läuft lokal (echtes requests braucht deinen Rechner). Im Browser arbeiten wir mit einer nachgebauten Antwort (Fake-Response).
Von JS/TS her gedacht
| Thema | JS/TS | Python |
|---|---|---|
| HTTP-Anfrage | fetch(url, { method, headers, body }) |
requests.get(url, headers=..., timeout=...) |
| Query-Parameter | new URLSearchParams({ a: 1 }) |
params={"a": 1} |
| JSON senden | body: JSON.stringify(obj) plus Header setzen |
json=obj (serialisiert und setzt den Content-Type) |
| JSON lesen | await r.json() |
r.json() (ohne await) |
| Statuscode prüfen | r.ok, r.status; fetch wirft bei 404 nicht |
r.ok, r.status_code; requests wirft bei 404 nicht |
| Fehler erzwingen | if (!r.ok) throw ... |
r.raise_for_status() |
| Zeitlimit | AbortController mit Timer |
timeout=5 |
| XML parsen | new DOMParser().parseFromString(s, "text/xml") |
ET.fromstring(s) (Fehler: ParseError) |
Zwei Dinge sind in beiden Welten gleich und werden in der Prüfung gern als Falle benutzt: Eine HTTP-Antwort mit 404 ist für den Client kein Programmfehler, und ohne Zeitlimit wartet der Client unter Umständen ewig.
Konzept in kleinen Schritten
Schritt 1: XML (4.5)
- Wohlgeformt (well-formed): genau ein Wurzelelement, Tags korrekt verschachtelt und geschlossen, Attributwerte in Anführungszeichen, Groß-/Kleinschreibung beachten.
- Gültig (valid): wohlgeformt und entspricht einer DTD (Document Type Definition) oder einem Schema.
- Baum: Ein Element hat Tag, Attribute, Text und Kindelemente.
xml.etree.ElementTree(meist alsETimportiert) behandelt ein Dokument als Baum. Die Quelle verschiebt Parsen und Bauen in Block 5, die Grundaufrufe unten sind daher Vorgriff (bitte prüfen, wie tief Block 4 sie fragt).
Beachte: Attribute und Text sind immer Strings ('1', nicht 1). Und bei kaputtem XML gibt es ET.ParseError, nicht ValueError:
Wohlgeformt ist nicht gültig: Der Parser prüft nur die Syntax. Ob <price> vorkommen darf, sagt erst eine DTD oder ein Schema (die Quelle behandelt hier nur den Unterschied, nicht die Prüfung selbst). Das Lesen eines Katalogs, auch mit kaputtem XML, übst du in Übung 1.
Schritt 2: REST-Client mit requests (4.6)
requests ist ein Drittpaket (pip install requests).
| Funktion oder Attribut | Zweck |
|---|---|
requests.get(url, params=..., headers=..., timeout=...) |
Lesen, params wird an die URL angehängt |
requests.post(url, json=...) |
Anlegen. json= serialisiert und setzt den Content-Type, data= sendet Formulardaten |
requests.put(url, json=...) |
Ersetzen |
requests.delete(url) |
Löschen |
r.status_code, r.ok |
Statuscode, r.ok ist True bei Codes unter 400 |
r.text, r.content, r.json() |
Antwort als String, als Bytes, als Python-Objekt |
r.headers, r.url |
Antwort-Header, endgültige URL |
r.raise_for_status() |
Löst bei 4xx und 5xx einen HTTPError aus |
Die Ausnahmen: requests.exceptions.RequestException ist die Basis, darunter ConnectionError, Timeout, HTTPError. Setze immer ein timeout. Ein 404 ist keine Exception, erst raise_for_status() macht daraus eine.
Der Unterschied zwischen json= und data= ist ein Klassiker. Mit echtem requests (lokal geprüft, Version 2.32.5) entstehen diese Anfragen:
import requests
requests.Request("POST", "http://x.test/items", json={"a": 1}).prepare()
# Content-Type: application/json Body: b'{"a": 1}'
requests.Request("POST", "http://x.test/items", data={"a": 1, "b": "x y"}).prepare()
# Content-Type: application/x-www-form-urlencoded Body: a=1&b=x+y
requests.Request("GET", "http://x.test/items", params={"min_preis": 5, "q": "a b"}).prepare().url
# http://x.test/items?min_preis=5&q=a+bIm Browser gibt es kein echtes requests. Für die Übung 2 arbeiten wir deshalb mit einer Fake-Response: einem Objekt, das status_code, json() und raise_for_status() hat wie das echte, und mit einer Funktion http_get(url, timeout=...), die wie requests.get aufgerufen wird. Die Logik, die du übst (404 ist None, andere Fehler werfen, Timeout wiederholen), ist dieselbe wie gegen einen echten Server. Der Vorteil für Tests: Du kannst jeden Fehlerfall gezielt erzeugen. Ein Entwurfsmuster dahinter: Wer die HTTP-Funktion als Parameter übergibt, kann sie im Test durch eine Fälschung ersetzen (dependency injection).
Falle: die Prüfungs- und Praxisklassiker
requests.post(url, data=dict)sendet Formulardaten,json=dictsendet JSON. Der Server sieht einen anderen Content-Type.- 404 ist für
requestskeine Exception, erstraise_for_status()macht daraus eine. Und ohnetimeoutwartet der Aufruf unter Umständen unendlich. - Wohlgeformt ist nicht gültig.
Übungen
Übung 1: Katalog aus XML lesen
Schreibe lies_katalog(xml_text). Das Dokument hat eine beliebige Wurzel. Darunter stehen <item>-Elemente mit einem Attribut id, einem Kind <name> und optional einem Kind <price>:
<katalog>
<item id="1"><name>Kabel</name><price>5.5</price></item>
<item id="2"><name> Maus </name></item>
</katalog>Rückgabe: Liste von dicts {"id": int, "name": str, "preis": float oder None} in Dokumentreihenfolge.
idalsint,nameohne umgebende Leerzeichen (leeres<name/>ergibt"").- Fehlt
<price>, istpreisgleichNone. - Es zählen nur direkte Kinder der Wurzel. Ein
<item>tief in einem anderen Element (z. B. in einem<archiv>) wird ignoriert. - Ist der Text nicht wohlgeformt, gibst du
Nonezurück (und wirfst nichts).
Schritt 1 zeigt, welche Ausnahme ein kaputtes Dokument auslöst und welche Methode nur direkte Kinder liefert (und welche den ganzen Baum durchsucht). Denke an die Typen: Attribute und Text sind immer Strings. Und was liefert .text bei einem leeren Element?
import xml.etree.ElementTree as ET
def lies_katalog(xml_text):
try:
root = ET.fromstring(xml_text)
except ET.ParseError:
return None
ergebnis = []
for item in root.findall("item"):
name = (item.findtext("name") or "").strip()
preis_text = item.findtext("price")
preis = float(preis_text) if preis_text is not None else None
ergebnis.append({"id": int(item.get("id")), "name": name, "preis": preis})
return ergebnis
lies_katalogÜbung 2: REST-Client-Logik gegen eine Fake-Response
Du schreibst die Logik eines REST-Clients. Die HTTP-Funktion bekommst du als Parameter (http_get), sie wird wie requests.get(url, timeout=...) aufgerufen. Hinter den Kulissen stehen (nur zum Lesen):
BASE = "https://api.example.com/artikel"
class Timeout(Exception): ... # wie requests.exceptions.Timeout
class HTTPError(Exception): ... # wie requests.exceptions.HTTPError
class FakeResponse: # status_code, json(), raise_for_status()
...raise_for_status() löst HTTPError aus, wenn status_code 400 oder höher ist. Schreibe lade_artikel(http_get, artikel_id):
- Aufruf:
http_get(f"{BASE}/{artikel_id}", timeout=5). Dastimeoutist Pflicht. - Status 404: Rückgabe
None. - Andere Fehlerstatus (400 und höher):
HTTPErrorentsteht überraise_for_status(), ohne Wiederholung. - Bei Erfolg:
r.json()zurückgeben. - Löst
http_geteinTimeoutaus, versuchst du es genau einmal erneut. Klappt auch der zweite Versuch nicht, bleibt dasTimeoutbestehen (nicht verschlucken). Insgesamt also höchstens zwei Aufrufe.
Gehe die Fälle einzeln durch: Welcher Statuscode ist ein erwarteter Sonderfall und wird zu None, welche anderen Fehler sollen laufen lassen, was raise_for_status auslöst? Für den Timeout brauchst du einen Versuch mehr als einen, und die Frage ist, was beim letzten Fehlversuch mit der Ausnahme passiert.
def lade_artikel(http_get, artikel_id):
url = f"{BASE}/{artikel_id}"
for versuch in range(2):
try:
r = http_get(url, timeout=5)
except Timeout:
if versuch == 1:
raise
continue
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()
lade_artikelÜbung 3 (lokal): REST-Client gegen einen lokalen Server
Auch das läuft nur auf deinem Rechner. Die Aufgabe liegt in lernlabor/uebung/python/python_16_requests_lokal.py. Im Skript läuft ein kleiner http.server (Thread, 127.0.0.1, Port 50717), du schreibst fünf Funktionen mit requests: Artikel holen (404 ist None), Liste mit params=, Anlegen mit json=, Löschen (204 oder 404) und ein Aufruf mit Timeout. requests ist nicht Teil des Projekts, deshalb mit --with:
cd lernlabor && uv run --with requests python uebung/python/python_16_requests_lokal.pyDer Server lehnt POST ohne JSON-Content-Type mit 400 ab. Wer data= statt json= nimmt, sieht den Fehler also sofort. Wie bei der lokalen Socket-Übung aus Teil a steht bei Erfolg “Aufgabe erfüllt.” in der Ausgabe, sonst eine Liste mit “Noch nicht erfüllt”.
Pro Funktion zwei Fragen: Welcher Statuscode ist hier ein normaler Fall, der keine Ausnahme sein soll, und was muss alles andere auslösen? Und bei Anlegen und Filtern: Welcher Parameter von requests übernimmt das Serialisieren beziehungsweise das Anhängen an die URL?
def hole_artikel(item_id):
r = requests.get(f"{BASE}/items/{item_id}", timeout=5)
if r.status_code == 404:
return None
r.raise_for_status()
return r.json()
def hole_liste(min_preis):
r = requests.get(f"{BASE}/items", params={"min_preis": min_preis}, timeout=5)
r.raise_for_status()
return r.json()
def lege_an(daten):
r = requests.post(f"{BASE}/items", json=daten, timeout=5)
r.raise_for_status()
return r.json()
def loesche(item_id):
r = requests.delete(f"{BASE}/items/{item_id}", timeout=5)
if r.status_code == 404:
return False
r.raise_for_status()
return r.status_code == 204
def langsam_oder_none():
try:
return requests.get(f"{BASE}/langsam", timeout=0.5).json()
except requests.exceptions.Timeout:
return NoneMerksatz und Prüfstein
Merksatz: Wohlgeformt ist nicht gültig, Attribute und Text in XML sind Strings, und bei requests ist 404 keine Exception (erst raise_for_status()), json= sendet JSON und data= ein Formular, ein timeout gehört immer dazu.
Prüfstein (offene Frage): Ein Client schickt ein POST zum Anlegen einer Bestellung, die Antwort kommt wegen eines Timeout nie an, und dein Code wiederholt die Anfrage automatisch. Was kann schiefgehen, und warum wäre es bei einem PUT anders? Welche Zeile in der Methodentabelle (siehe Teil a) ist dafür entscheidend?
Zurück zu Teil a: Sockets, HTTP und JSON.
Quelle: quellen/python-glossar-pcap-pcpp1.md, Abschnitt “PCPP1 Block 4: Netzwerkprogrammierung”, 4.5 und 4.6 und “Typische Fallen in Block 4”. Blockgewicht und Fragenzahl laut Quelle (Stand 1. Oktober 2026, bitte prüfen). Hinweis: Die Grundaufrufe ET.fromstring, findall, findtext, ParseError und das Beispiel mit Request(...).prepare() stehen nicht (oder nicht so) in der Block-4-Quelle (XML-Parsen gehört dort zu Block 5), wurden aber mit Python 3.13 und requests 2.32.5 ausgeführt und geprüft (bitte prüfen, wie tief die PCPP1 sie in Block 4 fragt). Der lokale Server, die Wiederholung bei Timeout und die Fake-Response sind Übungskonstruktionen, nicht Prüfungsstoff der Quelle.