0%

Anfragen von Hand schreiben · Übung

Prozentkodiere, was du sendest

Angenommen, die Leseliste führt einen Datensatz pro Titel, und ein Client möchte den Datensatz für “Café & Co”. Den Titel von Hand ins Ziel einzufügen ergibt diese Anfragezeile:

Try it

Eine Anfragezeile hat genau drei Wörter, getrennt durch einzelne Leerzeichen: , Ziel, Version. Diese hier hat fünf, und ein Server, der sie liest, kann nicht erkennen, wo das Ziel endet. Dein request_line aus Kapitel 1 lehnt ein Ziel mit Leerzeichen genau aus diesem Grund ab, und make_request könnte é ohnehin nicht senden, weil es den Kopf als ASCII kodiert. Das & ist ein leiseres Problem: In einer Query trennt es einen vom nächsten, also würde ein Titel, der & enthält, in zwei Teile geschnitten.

Die Lösung ist, die Schreibweise solcher Zeichen zu ändern, damit sie den Server als Daten erreichen und nie als Struktur. Das ist die Prozentkodierung (percent-encoding), und in dieser Lektion schreibst du in wire.py die beiden , die sie erzeugen und lesen. Das dieser Lektion hat kein Netzwerk: Nichts hier wird irgendwohin gesendet, und jedes Ergebnis ist ein , den du ausgeben kannst.

Ein Byte als drei Zeichen

Die Prozentkodierung schreibt ein Byte als %, gefolgt vom Wert des Bytes in zwei Hexadezimalziffern. Ein Leerzeichen ist das Byte 32, hexadezimal 20, also wird ein Leerzeichen zu %20. Ein Schrägstrich wird zu %2F und ein kaufmännisches Und zu %26.

Ein Zeichen außerhalb von ASCII ist mehr als ein Byte. Wie Networks with Python gezeigt hat, schreibt é als zwei:

Try it

Jedes dieser Bytes wird für sich kodiert, also wird é zu %C3%A9 und Café zu Caf%C3%A9. Der kodierte Text ist reines ASCII, und jedes Byte des Originals bleibt erhalten.

ASCII-Buchstaben, Ziffern und die vier Zeichen -, ., _ und ~ müssen nie kodiert werden; sie bedeuten überall in einer dasselbe. Alles andere wird kodiert, wenn es Daten sind.

Ein Pfadsegment mit quote

Ein Pfad besteht aus Segmenten, getrennt durch /: /v1/items/42 hat die Segmente v1, items und 42. Wenn ein Wert in ein Segment kommt, muss jedes Zeichen darin, das keine einfachen Daten sind, kodiert werden, auch /.

urllib.parse hat die Funktion dafür, quote. Nur mit dem Text aufgerufen, lässt sie absichtlich ein Zeichen in Ruhe:

Try it

Standardmäßig behandelt quote das / als sicher, weil es einen ganzen Pfad erwartet, in dem Schrägstriche Segmente trennen. Für ein einzelnes Segment ist das falsch: /v1/items/AC/DC nennt ein Segment AC und danach ein Segment DC, was ein anderer Pfad ist. Die Übergabe von safe="" sagt, dass kein Zeichen sicher so bleiben darf, wie es ist, also wird der Schrägstrich als %2F kodiert, und das Segment bleibt ein Segment.

quote("AC/DC") gibt 'AC/DC' zurück. Warum ist das für einen Wert falsch, der in ein einzelnes Pfadsegment kommt?

Query-Werte mit urlencode

Eine Query ist eine von Paaren name=value, verbunden durch &, das Format, das Python in der Praxis mit params= gesendet hat. Sie von Hand zu bauen führt zum selben Problem: Ein Wert, der & oder = enthält, würde ein neues Paar beginnen. urlencode nimmt ein und kodiert jeden Namen und jeden Wert, wobei es eine Zahl zuerst in ihren Text verwandelt.

Ein Teil seiner Kodierung unterscheidet sich von quote: urlencode schreibt ein Leerzeichen in einem Wert als +, nicht als %20. Das kommt von HTML-Formularen: Wenn ein Browser ein Formular sendet, schreibt er jedes Leerzeichen in einem Wert als +, und ein Server, der eine Query als Formulardaten liest, macht aus + wieder ein Leerzeichen. urlencode folgt dieser Konvention, und die lesende Seite, etwa parse_qs, ebenso. Das gilt nur für die Query. Überall sonst in einer URL ist + ein gewöhnliches, erlaubtes Zeichen, das ein Pluszeichen bedeutet. In einem Pfadsegment schreibt quote ein Leerzeichen als %20 und lässt die Bedeutung von + in Ruhe.

Sag mit diesem Wissen voraus, was urlencode aus einem Titel macht, der sowohl ein & als auch ein Leerzeichen enthält:

Was gibt das aus?

from urllib.parse import urlencode

print(urlencode({"title": "R&D notes", "page": 2}))

Der Rückweg mit unquote

Ein Server, der AC%2FDC empfängt und den Text zurückhaben möchte, dekodiert ihn mit unquote, das jedes %XX wieder in sein Byte verwandelt und die Bytes als UTF-8 dekodiert. Es gibt auch unquote_plus, das zuerst jedes + in ein Leerzeichen verwandelt. Das ist richtig für Formulardaten in einer Query und falsch für ein Pfadsegment, in dem + ein Pluszeichen ist:

Try it

Die erste Zeile behält die Pluszeichen; die zweite macht aus dem letzten ein Leerzeichen.

Kodierung ist ein Versprechen zwischen dem Code, der kodiert, und dem Code, der dekodiert. Deine Seite davon, als Client, ist, jeden Wert zu kodieren, den du in ein Ziel setzt, damit der Server den Wert vollständig erhält, egal was er als Nächstes tut. Welche Teile ein Server dekodiert, und wann, entscheidet der Server; Lektion 5 zeigt, was practice-api mit einem kodierten Pfad macht.

Deine Aufgabe

Zwei Funktionen in wire.py gehören zu dieser Lektion, make_target und decode_segment, und beide lösen noch NotImplementedError aus. Die Importe oben in der Datei holen schon quote, unquote und urlencode herein.

Schreibe make_target(segments, params=None). Die Funktion bekommt eine Liste von Segment-Strings und optional ein Dict mit Query-Werten und gibt das Ziel als str zurück:

  1. Gib jedes Segment durch quote(segment, safe="") und sammle die Ergebnisse in einer Liste;

  2. verbinde sie mit "/" und setze ein "/" davor, sodass ["v1", "items"] den Wert /v1/items ergibt;

  3. wenn params etwas enthält, füge "?" und urlencode(params) hinzu. Wenn params als None stehen bleibt oder als leeres Dict übergeben wird, gibt es keine Query und kein "?". Der Test if params: ist für beides falsch.

Schreibe decode_segment(segment): Gib unquote(segment) zurück.

Probiere dann beide im Terminal aus:

python -c "import wire; print(wire.make_target(['v1', 'items', 'AC/DC live']))"
python -c "import wire; print(wire.make_target(['v1', 'items', 'Café']))"
python -c "import wire; print(wire.make_target(['v1', 'items'], {'q': 'R&D notes', 'page': 2}))"
python -c "import wire; print(wire.decode_segment('AC%2FDC%20live'))"

Wenn beide Funktionen geschrieben sind, geben die vier Zeilen Folgendes aus:

/v1/items/AC%2FDC%20live
/v1/items/Caf%C3%A9
/v1/items?q=R%26D+notes&page=2
AC/DC live

Jedes Ziel ist ein Wort ohne Leerzeichen darin, also würde request_line jedes davon akzeptieren.

Wenn eine Prüfung fehlschlägt, bitte Monty, die fehlschlagende Meldung mit dir zu lesen und auf die Zeile von make_target zu zeigen, um die es geht, ohne die Korrektur zu schreiben.

Wenn diese Zeilen wie gezeigt ausgegeben werden, drücke Check my work.

Die Ziele sind jetzt sicher, aber jede Anfrage trägt immer noch die Zeile Host, die Kapitel 1 mitgeliefert hat. Die nächste Lektion ersetzt sie durch eine, die du baust, und fügt eigene hinzu.

Aufgabe

Schreibe in wire.py die beiden , die mit # Lesson 3 markiert sind.

make_target(segments, params=None) gibt ein Anfrageziel als str zurück:

  • Jedes Segment geht durch quote(segment, safe=""), sodass ein Leerzeichen, /, ?, #, &, %, + oder ein Buchstabe außerhalb von ASCII innerhalb eines prozentkodiert wird;

  • die kodierten Segmente werden mit "/" verbunden, mit einem "/" davor;

  • wenn params etwas enthält, endet das Ziel mit "?" und urlencode(params); wenn params gleich None oder ein leeres ist, gibt es kein "?".

decode_segment(segment) gibt unquote(segment) zurück: Jedes %XX wird wieder zu seinem Byte, die Bytes werden als dekodiert, und ein + bleibt ein Pluszeichen.

Probiere sie im mit python -c "import wire; print(wire.make_target(['v1', 'items', 'AC/DC live']))" und den anderen Zeilen aus der Lektion aus.

Drücke dann Check my work. Die Prüfung ruft make_target mit Segmenten und Query-Werten auf, die du nicht gesehen hast, mit Leerzeichen, Schrägstrichen, kaufmännischen Unds, Pluszeichen, Prozentzeichen, Fragezeichen und Buchstaben außerhalb von ASCII, und liest jedes Ziel mit ihrem eigenen Decoder zurück: Jedes Segment muss zu seinem Wert dekodieren, der Pfad muss ein Segment pro Wert haben, die Query muss sich als die Werte zurücklesen lassen, die du übergeben hast (ein Leerzeichen als %20 oder als + ist beides in Ordnung), und ein Aufruf ohne Query-Werte darf kein "?" haben. Sie ruft decode_segment auf kodierten Segmenten auf, die du nicht gesehen hast, darunter eines mit einem + darin. Nichts wird gesendet, und sie liest dein Terminal nicht.