HOWTO: Ressourcen aus dem Internet mit dem Paket urllib holen

Einführung

urllib.request ist ein Python-Modul zum Holen von URLs (Uniform Resource Locators). Es bietet eine sehr einfache Schnittstelle in Form der Funktion urlopen. Diese kann URLs über eine Vielzahl unterschiedlicher Protokolle holen. Es bietet außerdem eine etwas komplexere Schnittstelle, um verbreitete Fälle zu behandeln – etwa Basic Authentication, Cookies, Proxys und so weiter. Dafür gibt es Objekte namens Handler und Opener.

urllib.request kann URLs für viele „URL-Schemata“ holen (gekennzeichnet durch die Zeichenkette vor dem ":" in der URL – "ftp" ist zum Beispiel das URL-Schema von "ftp://python.org/"), und zwar über die zugehörigen Netzwerkprotokolle (etwa FTP, HTTP). Dieses Tutorial befasst sich mit dem häufigsten Fall, HTTP.

In einfachen Lagen ist urlopen sehr leicht zu verwenden. Sobald du beim Öffnen von HTTP-URLs aber auf Fehler oder nichttriviale Fälle stößt, brauchst du ein gewisses Verständnis des HyperText Transfer Protocol. Die umfassendste und maßgebliche Quelle zu HTTP ist RFC 2616. Das ist ein technisches Dokument und nicht als leichte Lektüre gedacht. Dieses HOWTO will die Verwendung von urllib veranschaulichen, mit genügend Details zu HTTP, um dir weiterzuhelfen. Es soll die Dokumentation zu urllib.request nicht ersetzen, sondern ergänzt sie.

URLs holen

Am einfachsten verwendest du urllib.request so:

import urllib.request
with urllib.request.urlopen('http://python.org/') as response:
   html = response.read()

Wenn du eine Ressource über eine URL abrufen und an einem vorübergehenden Ort speichern willst, geht das über die Funktionen shutil.copyfileobj() und tempfile.NamedTemporaryFile():

import shutil
import tempfile
import urllib.request

with urllib.request.urlopen('http://python.org/') as response:
    with tempfile.NamedTemporaryFile(delete=False) as tmp_file:
        shutil.copyfileobj(response, tmp_file)

with open(tmp_file.name) as html:
    pass

Viele Verwendungen von urllib sind so einfach (beachte, dass wir anstelle einer „http:“-URL auch eine URL hätten verwenden können, die mit „ftp:“, „file:“ usw. beginnt). Zweck dieses Tutorials ist es allerdings, die komplizierteren Fälle zu erklären, mit Schwerpunkt auf HTTP.

HTTP beruht auf Anfragen und Antworten – der Client stellt Anfragen und Server senden Antworten. urllib.request spiegelt das mit einem Request-Objekt, das die HTTP-Anfrage darstellt, die du stellst. In der einfachsten Form erzeugst du ein Request-Objekt, das die URL angibt, die du holen willst. Der Aufruf von urlopen mit diesem Request-Objekt gibt ein Antwortobjekt für die angefragte URL zurück. Diese Antwort ist ein dateiartiges Objekt. Das heißt, du kannst zum Beispiel .read() auf der Antwort aufrufen:

import urllib.request

req = urllib.request.Request('http://python.org/')
with urllib.request.urlopen(req) as response:
   the_page = response.read()

Beachte, dass urllib.request dieselbe Request-Schnittstelle für alle URL-Schemata verwendet. Eine FTP-Anfrage kannst du zum Beispiel so stellen:

req = urllib.request.Request('ftp://example.com/')

Bei HTTP gibt es zwei weitere Dinge, die Request-Objekte dir erlauben: Erstens kannst du Daten übergeben, die an den Server gesendet werden. Zweitens kannst du dem Server zusätzliche Informationen („Metadaten“) über die Daten oder über die Anfrage selbst übergeben – diese Informationen werden als HTTP-„Header“ gesendet. Sehen wir uns beides der Reihe nach an.

Daten

Manchmal willst du Daten an eine URL senden (oft verweist die URL auf ein CGI-Skript (Common Gateway Interface) oder eine andere Webanwendung). Bei HTTP geschieht das oft über eine sogenannte POST-Anfrage. Das ist oft genau das, was dein Browser tut, wenn du ein HTML-Formular abschickst, das du im Web ausgefüllt hast. Nicht alle POSTs müssen von Formularen kommen: Du kannst ein POST verwenden, um beliebige Daten an deine eigene Anwendung zu übertragen. Im häufigen Fall von HTML-Formularen müssen die Daten auf eine standardisierte Weise kodiert und dann als Argument data an das Request-Objekt übergeben werden. Die Kodierung erfolgt über eine Funktion aus der Bibliothek urllib.parse.

import urllib.parse
import urllib.request

url = 'http://www.someserver.com/cgi-bin/register.cgi'
values = {'name' : 'Michael Foord',
          'location' : 'Northampton',
          'language' : 'Python' }

data = urllib.parse.urlencode(values)
data = data.encode('ascii') # data should be bytes
req = urllib.request.Request(url, data)
with urllib.request.urlopen(req) as response:
   the_page = response.read()

Beachte, dass manchmal andere Kodierungen nötig sind (etwa beim Hochladen von Dateien aus HTML-Formularen – Näheres steht in der HTML Specification, Form Submission).

Wenn du das Argument data nicht übergibst, verwendet urllib eine GET-Anfrage. GET- und POST-Anfragen unterscheiden sich unter anderem darin, dass POST-Anfragen oft „Nebenwirkungen“ haben: Sie ändern den Zustand des Systems auf irgendeine Weise (zum Beispiel, indem sie bei der Website einen Auftrag über einen Zentner Dosen-Spam zur Lieferung an deine Haustür aufgeben). Der HTTP-Standard stellt zwar klar, dass POSTs immer Nebenwirkungen haben sollen und GET-Anfragen nie, doch nichts hindert eine GET-Anfrage daran, Nebenwirkungen zu haben, und eine POST-Anfrage daran, keine zu haben. Daten lassen sich auch in einer HTTP-GET-Anfrage übergeben, indem man sie in die URL selbst kodiert.

Das geht so:

>>> import urllib.request
>>> import urllib.parse
>>> data = {}
>>> data['name'] = 'Somebody Here'
>>> data['location'] = 'Northampton'
>>> data['language'] = 'Python'
>>> url_values = urllib.parse.urlencode(data)
>>> print(url_values)  # The order may differ from below.
name=Somebody+Here&language=Python&location=Northampton
>>> url = 'http://www.example.com/example.cgi'
>>> full_url = url + '?' + url_values
>>> data = urllib.request.urlopen(full_url)

Beachte, dass die vollständige URL entsteht, indem der URL ein ? angehängt wird, gefolgt von den kodierten Werten.

Header

Wir erörtern hier einen bestimmten HTTP-Header, um zu veranschaulichen, wie du Header zu deiner HTTP-Anfrage hinzufügst.

Manche Websites [1] mögen es nicht, von Programmen besucht zu werden, oder senden unterschiedlichen Browsern unterschiedliche Fassungen [2]. Standardmäßig weist sich urllib als Python-urllib/x.y aus (wobei x und y die Haupt- und Nebenversionsnummer der Python-Veröffentlichung sind, etwa Python-urllib/2.5), was die Seite verwirren oder einfach nicht funktionieren kann. Ein Browser weist sich über den Header User-Agent aus [3]. Wenn du ein Request-Objekt erzeugst, kannst du ein Wörterbuch mit Headern übergeben. Das folgende Beispiel stellt dieselbe Anfrage wie oben, weist sich aber als eine Fassung des Internet Explorer aus [4].

import urllib.parse
import urllib.request

url = 'http://www.someserver.com/cgi-bin/register.cgi'
user_agent = 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)'
values = {'name': 'Michael Foord',
          'location': 'Northampton',
          'language': 'Python' }
headers = {'User-Agent': user_agent}

data = urllib.parse.urlencode(values)
data = data.encode('ascii')
req = urllib.request.Request(url, data, headers)
with urllib.request.urlopen(req) as response:
   the_page = response.read()

Die Antwort hat außerdem zwei nützliche Methoden. Siehe den Abschnitt info and geturl, der nach dem Blick darauf kommt, was passiert, wenn etwas schiefgeht.

Behandlung von Ausnahmen

urlopen löst URLError aus, wenn es eine Antwort nicht behandeln kann (wobei wie üblich bei Python-APIs auch integrierte Ausnahmen wie ValueError, TypeError usw. ausgelöst werden können).

HTTPError ist die Unterklasse von URLError, die im besonderen Fall von HTTP-URLs ausgelöst wird.

Die Ausnahmeklassen werden aus dem Modul urllib.error exportiert.

URLError

Oft wird URLError ausgelöst, weil es keine Netzwerkverbindung gibt (keine Route zum angegebenen Server) oder der angegebene Server nicht existiert. In diesem Fall hat die ausgelöste Ausnahme ein Attribut „reason“, das ein Tupel aus einem Fehlercode und einer Fehlermeldung als Text ist.

etwa

>>> req = urllib.request.Request('http://www.pretend_server.org')
>>> try: urllib.request.urlopen(req)
... except urllib.error.URLError as e:
...     print(e.reason)
...
(4, 'getaddrinfo failed')

HTTPError

Jede HTTP-Antwort des Servers enthält einen numerischen „Statuscode“. Manchmal zeigt der Statuscode an, dass der Server die Anfrage nicht erfüllen kann. Die Standard-Handler behandeln einige dieser Antworten für dich (ist die Antwort zum Beispiel eine „Weiterleitung“, die den Client auffordert, das Dokument von einer anderen URL zu holen, erledigt urllib das für dich). Für die, die es nicht behandeln kann, löst urlopen einen HTTPError aus. Typische Fehler sind „404“ (Seite nicht gefunden), „403“ (Anfrage verboten) und „401“ (Authentifizierung erforderlich).

Abschnitt 10 von RFC 2616 ist eine Referenz für alle HTTP-Fehlercodes.

Die ausgelöste HTTPError-Instanz hat ein ganzzahliges Attribut „code“, das dem vom Server gesendeten Fehler entspricht.

Fehlercodes

Weil die Standard-Handler Weiterleitungen behandeln (Codes im Bereich 300) und Codes im Bereich 100–299 Erfolg anzeigen, siehst du üblicherweise nur Fehlercodes im Bereich 400–599.

http.server.BaseHTTPRequestHandler.responses ist ein nützliches Wörterbuch von Antwortcodes, das alle von RFC 2616 verwendeten Antwortcodes zeigt. Ein Auszug aus dem Wörterbuch ist unten zu sehen

responses = {
    ...
    <HTTPStatus.OK: 200>: ('OK', 'Request fulfilled, document follows'),
    ...
    <HTTPStatus.FORBIDDEN: 403>: ('Forbidden',
                                  'Request forbidden -- authorization will '
                                  'not help'),
    <HTTPStatus.NOT_FOUND: 404>: ('Not Found',
                                  'Nothing matches the given URI'),
    ...
    <HTTPStatus.IM_A_TEAPOT: 418>: ("I'm a Teapot",
                                    'Server refuses to brew coffee because '
                                    'it is a teapot'),
    ...
    <HTTPStatus.SERVICE_UNAVAILABLE: 503>: ('Service Unavailable',
                                            'The server cannot process the '
                                            'request due to a high load'),
    ...
    }

Wird ein Fehler ausgelöst, antwortet der Server mit einem HTTP-Fehlercode und einer Fehlerseite. Du kannst die HTTPError-Instanz als Antwort auf der zurückgegebenen Seite verwenden. Das heißt, dass sie neben dem Attribut code auch die Methoden read, geturl und info hat, wie sie das Modul urllib.response zurückgibt:

>>> req = urllib.request.Request('http://www.python.org/fish.html')
>>> try:
...     urllib.request.urlopen(req)
... except urllib.error.HTTPError as e:
...     print(e.code)
...     print(e.read())
...
404
b'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n\n\n<html
  ...
  <title>Page Not Found</title>\n
  ...

Alles zusammengenommen

Wenn du also auf HTTPError oder URLError vorbereitet sein willst, gibt es zwei grundlegende Ansätze. Ich bevorzuge den zweiten.

Nummer 1

from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
req = Request(someurl)
try:
    response = urlopen(req)
except HTTPError as e:
    print('The server couldn\'t fulfill the request.')
    print('Error code: ', e.code)
except URLError as e:
    print('We failed to reach a server.')
    print('Reason: ', e.reason)
else:
    # everything is fine

Bemerkung

Das except HTTPError muss zuerst kommen, sonst fängt except URLError auch einen HTTPError.

Nummer 2

from urllib.request import Request, urlopen
from urllib.error import URLError
req = Request(someurl)
try:
    response = urlopen(req)
except URLError as e:
    if hasattr(e, 'reason'):
        print('We failed to reach a server.')
        print('Reason: ', e.reason)
    elif hasattr(e, 'code'):
        print('The server couldn\'t fulfill the request.')
        print('Error code: ', e.code)
else:
    # everything is fine

info and geturl

Die von urlopen zurückgegebene Antwort (oder die HTTPError-Instanz) hat zwei nützliche Methoden, info() und geturl(), und ist im Modul urllib.response definiert.

  • geturl – gibt die wirkliche URL der geholten Seite zurück. Das ist nützlich, weil urlopen (oder das verwendete Opener-Objekt) einer Weiterleitung gefolgt sein kann. Die URL der geholten Seite muss nicht dieselbe sein wie die angefragte URL.

  • info – gibt ein wörterbuchartiges Objekt zurück, das die geholte Seite beschreibt, insbesondere die vom Server gesendeten Header. Derzeit ist es eine http.client.HTTPMessage-Instanz.

Typische Header sind „Content-length“, „Content-type“ und so weiter. Eine nützliche Auflistung von HTTP-Headern mit kurzen Erläuterungen zu Bedeutung und Verwendung findest du in der Quick Reference to HTTP Headers.

Opener und Handler

Wenn du eine URL holst, verwendest du einen Opener (eine Instanz der vielleicht verwirrend benannten urllib.request.OpenerDirector). Normalerweise haben wir bisher den Standard-Opener verwendet – über urlopen –, aber du kannst eigene Opener erzeugen. Opener verwenden Handler. Die ganze „Schwerarbeit“ leisten die Handler. Jeder Handler weiß, wie er URLs für ein bestimmtes URL-Schema (http, ftp usw.) öffnet oder wie er einen Aspekt des URL-Öffnens behandelt, zum Beispiel HTTP-Weiterleitungen oder HTTP-Cookies.

Du wirst Opener erzeugen wollen, wenn du URLs mit bestimmten installierten Handlern holen willst, zum Beispiel um einen Opener zu bekommen, der Cookies behandelt, oder einen, der Weiterleitungen nicht behandelt.

Um einen Opener zu erzeugen, erstelle eine Instanz von OpenerDirector und rufe dann wiederholt .add_handler(some_handler_instance) auf.

Du kannst auch build_opener verwenden, eine Hilfsfunktion, um Opener-Objekte mit einem einzigen Funktionsaufruf zu erzeugen. build_opener fügt standardmäßig mehrere Handler hinzu, bietet aber einen schnellen Weg, weitere hinzuzufügen und die Standard-Handler zu überschreiben.

Andere Arten von Handlern, die du vielleicht willst, können Proxys, Authentifizierung und weitere verbreitete, aber etwas speziellere Lagen behandeln.

Mit install_opener lässt sich ein opener-Objekt zum (globalen) Standard-Opener machen. Das heißt, dass Aufrufe von urlopen den von dir installierten Opener verwenden.

Opener-Objekte haben eine Methode open, die sich unmittelbar aufrufen lässt, um URLs genauso zu holen wie mit der Funktion urlopen: install_opener muss nicht aufgerufen werden, außer aus Bequemlichkeit.

Basic Authentication

Um das Erzeugen und Installieren eines Handlers zu veranschaulichen, verwenden wir den HTTPBasicAuthHandler. Eine ausführlichere Erörterung dieses Themas – einschließlich einer Erklärung, wie Basic Authentication funktioniert – findest du im Basic Authentication Tutorial.

Ist Authentifizierung erforderlich, sendet der Server (zusätzlich zum Fehlercode 401) einen Header, der Authentifizierung anfordert. Dieser gibt das Authentifizierungsschema und einen „Realm“ an. Der Header sieht so aus: WWW-Authenticate: SCHEME realm="REALM".

etwa

WWW-Authenticate: Basic realm="cPanel Users"

Der Client sollte die Anfrage dann erneut stellen, mit dem passenden Namen und Passwort für den Realm als Header in der Anfrage. Das ist „Basic Authentication“. Um diesen Vorgang zu vereinfachen, können wir eine Instanz von HTTPBasicAuthHandler und einen Opener erzeugen, der diesen Handler verwendet.

Der HTTPBasicAuthHandler verwendet ein Objekt namens Passwortverwalter, um die Zuordnung von URLs und Realms zu Passwörtern und Benutzernamen zu behandeln. Wenn du weißt, wie der Realm lautet (aus dem Authentifizierungsheader des Servers), kannst du einen HTTPPasswordMgr verwenden. Häufig ist der Realm einem gleichgültig. In diesem Fall ist es bequem, HTTPPasswordMgrWithDefaultRealm zu verwenden. Damit kannst du einen Standardbenutzernamen und ein Standardpasswort für eine URL angeben. Diese werden verwendet, sofern du für einen bestimmten Realm keine andere Kombination angibst. Wir zeigen das an, indem wir der Methode add_password None als Realm-Argument übergeben.

Die URL der obersten Ebene ist die erste URL, die Authentifizierung erfordert. URLs, die „tiefer“ liegen als die, die du an .add_password() übergibst, passen ebenfalls.

# create a password manager
password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()

# Add the username and password.
# If we knew the realm, we could use it instead of None.
top_level_url = "http://example.com/foo/"
password_mgr.add_password(None, top_level_url, username, password)

handler = urllib.request.HTTPBasicAuthHandler(password_mgr)

# create "opener" (OpenerDirector instance)
opener = urllib.request.build_opener(handler)

# use the opener to fetch a URL
opener.open(a_url)

# Install the opener.
# Now all calls to urllib.request.urlopen use our opener.
urllib.request.install_opener(opener)

Bemerkung

Im obigen Beispiel haben wir build_opener nur unseren HTTPBasicAuthHandler mitgegeben. Standardmäßig haben Opener die Handler für normale Lagen – ProxyHandler (wenn eine Proxy-Einstellung wie die Umgebungsvariable http_proxy gesetzt ist), UnknownHandler, HTTPHandler, HTTPDefaultErrorHandler, HTTPRedirectHandler, FTPHandler, FileHandler, DataHandler, HTTPErrorProcessor.

top_level_url ist tatsächlich entweder eine vollständige URL (einschließlich des Schemabestandteils „http:“, des Hostnamens und wahlweise der Portnummer), etwa "http://example.com/", oder eine „Authority“ (also der Hostname, wahlweise einschließlich der Portnummer), etwa "example.com" oder "example.com:8080" (das letzte Beispiel enthält eine Portnummer). Die Authority darf, wenn sie vorhanden ist, den Bestandteil „userinfo“ NICHT enthalten – "joe:password@example.com" ist zum Beispiel nicht korrekt.

Proxys

urllib erkennt deine Proxy-Einstellungen automatisch und verwendet sie. Das geschieht über den ProxyHandler, der Teil der normalen Handler-Kette ist, wenn eine Proxy-Einstellung erkannt wird. Normalerweise ist das gut, aber es gibt Gelegenheiten, bei denen es nicht hilfreich ist [5]. Ein Weg dazu ist, unseren eigenen ProxyHandler ohne definierte Proxys einzurichten. Das geht mit ähnlichen Schritten wie das Einrichten eines Basic Authentication-Handlers:

>>> proxy_support = urllib.request.ProxyHandler({})
>>> opener = urllib.request.build_opener(proxy_support)
>>> urllib.request.install_opener(opener)

Bemerkung

Derzeit unterstützt urllib.request das Holen von https-Orten über einen Proxy nicht. Das lässt sich aber einschalten, indem man urllib.request erweitert, wie im Rezept gezeigt [6].

Bemerkung

HTTP_PROXY wird ignoriert, wenn eine Variable REQUEST_METHOD gesetzt ist. Siehe die Dokumentation zu getproxies().

Sockets und Schichten

Die Unterstützung in Python für das Holen von Ressourcen aus dem Web ist geschichtet. urllib verwendet die Bibliothek http.client, die wiederum die socket-Bibliothek verwendet.

Seit Python 2.3 kannst du angeben, wie lange ein Socket auf eine Antwort warten soll, bevor die Zeit abläuft. Das kann in Anwendungen nützlich sein, die Webseiten holen müssen. Standardmäßig hat das socket-Modul keine Zeitbeschränkung und kann hängen. Derzeit ist die Socket-Zeitbeschränkung auf der Ebene von http.client oder urllib.request nicht zugänglich. Du kannst die Standardzeitbeschränkung aber global für alle Sockets setzen, und zwar so

import socket
import urllib.request

# timeout in seconds
timeout = 10
socket.setdefaulttimeout(timeout)

# this call to urllib.request.urlopen now uses the default timeout
# we have set in the socket module
req = urllib.request.Request('http://www.voidspace.org.uk')
response = urllib.request.urlopen(req)

Fußnoten

Dieses Dokument wurde von John Lee geprüft und überarbeitet.