HOWTO: Ressourcen aus dem Internet mit dem Paket urllib holen
*************************************************************


Einführung
==========


Related Articles
^^^^^^^^^^^^^^^^

Vielleicht ist auch der folgende Artikel über das Holen von
Webressourcen mit Python nützlich für dich:

* Basic Authentication

     Ein Tutorial über *Basic Authentication*, mit Beispielen in
     Python.

**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.

[1] Zum Beispiel Google.

[2] Browser-Sniffing ist eine ganz schlechte Praxis beim Entwurf von
    Websites – Seiten nach Webstandards zu bauen ist viel
    vernünftiger. Leider senden viele Seiten unterschiedlichen
    Browsern noch immer unterschiedliche Fassungen.

[3] Der User-Agent für MSIE 6 ist *'Mozilla/4.0 (compatible; MSIE 6.0;
    Windows NT 5.1; SV1; .NET CLR 1.1.4322)'*

[4] Näheres zu weiteren HTTP-Anfrage-Headern steht in der Quick
    Reference to HTTP Headers.

[5] In meinem Fall muss ich bei der Arbeit einen Proxy verwenden, um
    ins Internet zu kommen. Wenn du versuchst, *localhost*-URLs über
    diesen Proxy zu holen, blockiert er sie. IE ist auf die Verwendung
    des Proxys eingestellt, was urllib übernimmt. Um Skripte mit einem
    localhost-Server zu testen, muss ich urllib daran hindern, den
    Proxy zu verwenden.

[6] urllib-Opener für SSL-Proxy (CONNECT-Methode): ASPN Cookbook
    Recipe.
