HOWTO - Cómo obtener recursos de Internet con el paquete urllib

Autor

Michael Foord

Nota

Hay una traducción al francés de una revisión anterior de este HOWTO, disponible en urllib2 - Le Manuel manquant.

Introducción

urllib.request es un módulo Python para acceder y utilizar recursos de internet identificados por URLs (Uniform Resource Locators). Ofrece una interfaz muy simple, a través de la función urlopen. Esta función es capaz de acceder a URLs usando una variedad de protocolos diferentes. También ofrece una interfaz un poco más compleja para manejar situaciones comunes - como la autenticación básica, cookies y proxies, entre otros. Estos son proporcionados por los llamados objetos de apertura y gestores.

urllib.request soporta la obtención de recursos identificados por URLs para muchos «esquemas de URL» (identificados por la cadena de texto ubicada antes del ":" en el URL - por ejemplo "ftp" es el esquema de URL de "ftp://python.org/") usando sus protocolos de red asociados (por ejemplo FTP, HTTP). Este tutorial se centra en el caso más común, HTTP.

Para situaciones sencillas urlopen es muy fácil de usar. Pero tan pronto como encuentres errores o casos no triviales al abrir URLs HTTP, necesitarás entender el Protocolo de Transferencia de Hipertexto. La referencia más completa y autorizada para HTTP es RFC 2616. Este es un documento técnico y no pretende ser fácil de leer. Este HOWTO tiene como objetivo ilustrar el uso de la urllib, con suficientes detalles sobre HTTP para ayudarte a entenderlo. No pretende reemplazar los documentos urllib.request, pero es complementario a ellos.

Obtención de URLs

La forma más simple de usar urllib.request es la siguiente:

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

Si deseas recuperar un recurso a partir de la URL y almacenarlo en una ubicación temporal, puede hacerlo a través de las funciones shutil.copyfileobj() y 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

Muchos usos de urllib serán así de sencillos (nótese que en lugar de una URL “http:” podríamos haber usado una URL que empezara por “ftp:”, “file:”, etc.). Sin embargo, el propósito de este tutorial es explicar los casos más complicados, concentrándose en el HTTP.

HTTP se basa en peticiones y respuestas - el cliente hace peticiones y los servidores envían respuestas. urllib.request refleja esto con un objeto Request que representa la petición HTTP que estás haciendo. En su forma más simple se crea un objeto Request que especifica la URL que se quiere obtener. Llamar a urlopen con este objeto Request devuelve un objeto respuesta para la URL solicitada. Esta respuesta es un objeto tipo archivo, lo que significa que puedes por ejemplo llamar a .read() en la respuesta:

import urllib.request

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

Tenga en cuenta que urllib.request utiliza la misma interfaz de Request para gestionar todos los esquemas de URL. Por ejemplo, puedes hacer una petición FTP de la siguiente manera:

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

In the case of HTTP, there are two extra things that Request objects allow you to do: First, you can pass data to be sent to the server. Second, you can pass extra information («metadata») about the data or about the request itself, to the server - this information is sent as HTTP «headers». Let’s look at each of these in turn.

Datos

A veces quieres enviar datos a una URL (a menudo la URL se referirá a un script CGI (Common Gateway Interface) u otra aplicación web). Con HTTP, esto se hace a menudo usando lo que se conoce como una petición POST. Esto es a menudo lo que su navegador hace cuando envías un formulario HTML que has rellenado en la web. No todos los POSTs tienen que provenir de formularios: puedes usar un POST para transmitir datos arbitrarios a tu propia aplicación. En el caso común de los formularios HTML, los datos tienen que ser codificados de forma estándar, y luego pasados al objeto Request como el argumento data. La codificación se hace usando una función de la biblioteca 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()

Ten en cuenta que a veces se requieren otras codificaciones (por ejemplo, para la carga de archivos desde formularios HTML - ver HTML Specification, Form Submission para más detalles).

Si no pasas el argumento data, urllib usa una petición GET. Una de las formas en la que las peticiones GET y POST difieren entre sí es que las peticiones POST a menudo tienen «efectos secundarios»: cambian el estado del sistema de alguna manera (por ejemplo, haciendo una petición al sitio para que un centenar de spam chatarra sea entregado a tu dirección). Aunque el estándar HTTP deja claro que las solicitudes POST están siempre destinadas a causar efectos secundarios, y las solicitudes GET a nunca causar efectos secundarios, nada impide que una solicitud GET tenga efectos secundarios, ni que una solicitud POST no tenga efectos secundarios. Los datos también pueden ser pasados en una solicitud GET HTTP codificándolos en la propia URL.

Esto se hace de la siguiente manera:

>>> 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)

Nota que la URL completa se crea añadiendo un ? a la URL, seguido de los valores codificados.

Encabezados (Headers)

Discutiremos aquí un encabezado HTTP en particular, para ilustrar cómo agregar encabezados a su solicitud HTTP.

A algunos sitios web 1 no les gusta ser navegados por programas, o envían diferentes versiones a diferentes navegadores 2. Por defecto urllib se identifica como Python-urllib/x.y (donde x y y son los números mayor y menor de la versión de Python, por ejemplo Python-urllib/2.5), lo que puede confundir el sitio, o simplemente no funcionar. La forma en que un navegador se identifica a sí mismo es a través del encabezado User-Agent 3. Cuando creas un objeto Request puedes pasarle un diccionario de encabezados. El siguiente ejemplo hace la misma petición que arriba, pero se identifica como una versión de Internet Explorer 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()

La respuesta también tiene dos métodos útiles. Ver la sección de info y geturl que viene después de que echemos un vistazo a lo que pasa cuando las cosas van mal.

Gestión de excepciones

urlopen genera URLError cuando no puede gestionar una respuesta (aunque como es habitual en las APIs de Python, también se pueden generar excepciones predefinidas tales como ValueError, TypeError etc.).

HTTPError es la subclase de URLError generada en el caso específico de las URLs HTTP.

Las clases de excepción se exportan desde el módulo urllib.error.

URLError

A menudo, URLError se genera porque no hay conexión de red (no se encuentra ruta al servidor especificado), o el servidor especificado no existe. En este caso, la excepción generada tendrá un atributo «reason», que es una tupla que contiene un código de error y un mensaje de error de texto.

por ejemplo

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

Cada respuesta HTTP del servidor contiene un «código de estado» numérico. A veces el código de estado indica que el servidor es incapaz de satisfacer la petición. Los gestores predeterminados se encargarán de algunas de estas respuestas automáticamente (por ejemplo, si la respuesta es una «redirección» que solicita que el cliente obtenga el documento desde una URL diferente, urllib se encargará de eso por tí). Para aquellas respuestas que no puede manejar, urlopen generará un HTTPError. Los errores típicos incluyen “404” (página no encontrada), “403” (petición prohibida), y “401” (autenticación requerida).

Vea la sección 10 de RFC 2616 para una referencia sobre todos los códigos de error HTTP.

La instancia HTTPError generada tendrá un atributo de “código” numérico de tipo entero, que corresponde al error enviado por el servidor.

Códigos de Error

Debido a que los gestores por defecto gestionan redirecciones (códigos en el rango de 300), y que los códigos en el rango de 100–299 indican éxito, normalmente sólo verás códigos de error en el rango de 400–599.

http.server.BaseHTTPRequestHandler.responses es un diccionario útil de códigos de respuesta en el que se muestran todos los códigos de respuesta utilizados por RFC 2616. El diccionario se reproduce aquí por conveniencia

# Table mapping response codes to messages; entries have the
# form {code: (shortmessage, longmessage)}.
responses = {
    100: ('Continue', 'Request received, please continue'),
    101: ('Switching Protocols',
          'Switching to new protocol; obey Upgrade header'),

    200: ('OK', 'Request fulfilled, document follows'),
    201: ('Created', 'Document created, URL follows'),
    202: ('Accepted',
          'Request accepted, processing continues off-line'),
    203: ('Non-Authoritative Information', 'Request fulfilled from cache'),
    204: ('No Content', 'Request fulfilled, nothing follows'),
    205: ('Reset Content', 'Clear input form for further input.'),
    206: ('Partial Content', 'Partial content follows.'),

    300: ('Multiple Choices',
          'Object has several resources -- see URI list'),
    301: ('Moved Permanently', 'Object moved permanently -- see URI list'),
    302: ('Found', 'Object moved temporarily -- see URI list'),
    303: ('See Other', 'Object moved -- see Method and URL list'),
    304: ('Not Modified',
          'Document has not changed since given time'),
    305: ('Use Proxy',
          'You must use proxy specified in Location to access this '
          'resource.'),
    307: ('Temporary Redirect',
          'Object moved temporarily -- see URI list'),

    400: ('Bad Request',
          'Bad request syntax or unsupported method'),
    401: ('Unauthorized',
          'No permission -- see authorization schemes'),
    402: ('Payment Required',
          'No payment -- see charging schemes'),
    403: ('Forbidden',
          'Request forbidden -- authorization will not help'),
    404: ('Not Found', 'Nothing matches the given URI'),
    405: ('Method Not Allowed',
          'Specified method is invalid for this server.'),
    406: ('Not Acceptable', 'URI not available in preferred format.'),
    407: ('Proxy Authentication Required', 'You must authenticate with '
          'this proxy before proceeding.'),
    408: ('Request Timeout', 'Request timed out; try again later.'),
    409: ('Conflict', 'Request conflict.'),
    410: ('Gone',
          'URI no longer exists and has been permanently removed.'),
    411: ('Length Required', 'Client must specify Content-Length.'),
    412: ('Precondition Failed', 'Precondition in headers is false.'),
    413: ('Request Entity Too Large', 'Entity is too large.'),
    414: ('Request-URI Too Long', 'URI is too long.'),
    415: ('Unsupported Media Type', 'Entity body in unsupported format.'),
    416: ('Requested Range Not Satisfiable',
          'Cannot satisfy request range.'),
    417: ('Expectation Failed',
          'Expect condition could not be satisfied.'),

    500: ('Internal Server Error', 'Server got itself in trouble'),
    501: ('Not Implemented',
          'Server does not support this operation'),
    502: ('Bad Gateway', 'Invalid responses from another server/proxy.'),
    503: ('Service Unavailable',
          'The server cannot process the request due to a high load'),
    504: ('Gateway Timeout',
          'The gateway server did not receive a timely response'),
    505: ('HTTP Version Not Supported', 'Cannot fulfill request.'),
    }

Cuando se genera un error, el servidor responde retornando un código de error HTTP y una página de error. Puedes usar la instancia HTTPError como respuesta en la página devuelta. Esto significa que además del atributo de código, también tiene los métodos read, geturl, e info, tal y como son devueltos por el módulo urllib.response:

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

Resumiéndolo

Si quieres estar preparado para HTTPError o URLError hay dos enfoques básicos. Prefiero el segundo enfoque.

Número 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

Nota

El error except HTTPError debe ser lo primero en venir, de lo contrario el except URLError también capturará un HTTPError.

Número 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 y geturl

La respuesta retornada por urlopen (o la instancia de HTTPError) tiene dos métodos útiles info() y geturl() y está definida en el módulo urllib.response..

geturl - devuelve la verdadera URL de la página obtenida. Esto es útil porque urlopen (o el objeto de apertura utilizado) puede haber seguido una redirección. El URL de la página obtenida puede no ser el mismo que el URL solicitado.

info - devuelve un objeto parecido a un diccionario que describe la página consultada, particularmente los encabezados enviados por el servidor. Actualmente es una instancia http.client.HTTPMessage.

Los encabezados típicos incluyen “Content-length”, “Content-type” y así sucesivamente. Mira la Quick Reference to HTTP Headers para un listado útil de encabezados de HTTP con breves explicaciones de su significado y uso.

Objetos de Apertura (Openers) y Gestores (Handlers)

Cuando buscas una URL usas un objeto de apertura (una instancia del quizás confusamente llamado urllib.request.OpenerDirector). Normalmente hemos estado usando el objeto de apertura por defecto - a través de urlopen - pero puedes crear objetos de apertura personalizados. Los objetos de apertura usan gestores. Todo el «trabajo pesado» es hecho por los gestores. Cada gestor sabe cómo abrir URLs para un esquema particular de URL (http, ftp, etc.), o cómo manejar un aspecto de la apertura de URLs, por ejemplo redirecciones HTTP o cookies HTTP.

Desearás crear objetos de apertura si deseas consultar URLs con gestores específicos instalados, por ejemplo para obtener un objeto de apertura que gestione cookies, o para obtener un objeto de apertura que no gestione redireccionamientos.

Para crear un objeto de apertura, debes instanciar un OpenerDirector, y luego llamar .add_handler(some_handler_instance) repetidamente.

Alternativamente, puedes usar build_opener, que es una función conveniente para crear objetos de apertura con una sola llamada a la función. build_opener añade varios gestores por defecto, pero proporciona una forma rápida de añadir más y/o sobrescribir los gestores por defecto.

Otros tipos de gestores que puedes querer permiten manejar proxies, autenticación, y otras situaciones comunes pero ligeramente especializadas.

install_opener puede ser usado para hacer que un objeto opener sea el objeto de apertura (global) por defecto. Esto significa que las llamadas a urlopen usarán el objeto de apertura que has instalado.

Los objetos de apertura tienen un método open, que puede ser llamado directamente para consultar urls de la misma manera que la función «urlopen»: no hay necesidad de llamar install_opener, excepto por conveniencia.

Autenticación Básica

Para ilustrar la creación e instalación de un gestor usaremos HTTPBasicAuthHandler`. Para una discusión más detallada de este tema – incluyendo una explicación de cómo funciona la Autenticación Básica - ver Basic Authentication Tutorial.

Cuando se requiere la autenticación, el servidor envía un encabezado (así como el código de error 401) solicitando la autenticación. Esto especifica el esquema de autenticación y un “realm”. El encabezado tiene el siguiente aspecto: WWW-Authenticate: SCHEME realm="REALM".

por ejemplo.

WWW-Authenticate: Basic realm="cPanel Users"

El cliente debe entonces volver a intentar la solicitud con el nombre y la contraseña apropiados para el realm incluido como encabezamiento en la solicitud. Esto es “basic authentication”. Para simplificar este proceso podemos crear una instancia de HTTPBasicAuthHandler y un objeto de apertura para usar este manejador.

El HTTPBasicAuthHandler utiliza un objeto llamado administrador de contraseñas para gestionar el mapeo de URLs y realms con contraseñas y nombres de usuario. Si sabes cuál es el realm (por el encabezado de autenticación enviado por el servidor), entonces puedes usar un HTTPPasswordMgr. Frecuentemente a uno no le importa cuál es el realm. En ese caso, es conveniente usar «HTTPPasswordMgrWithDefaultRealm». Esto te permite especificar un nombre de usuario y una contraseña por defecto para una URL. Esto será suministrado en caso de que no proporciones una combinación alternativa para un realm específico. Lo indicamos proporcionando None como el argumento del realm al método add_password.

La URL de primer nivel es la primera URL que requiere autenticación. Las URLs «más profundas» que la URL que pasas a .add_password() también coincidirán.

# 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)

Nota

En el ejemplo anterior sólo suministramos nuestro HTTPBasicAuthHandler a build_opener. Por defecto, los objetos de apertura tienen los gestores para situaciones normales – ProxyHandler (si un ajuste de proxy tal como una variable de entorno http_proxy está establecida), UnknownHandler, HTTPHandler, HTTPDefaultErrorHandler, HTTPRedirectHandler, FTPHandler, FileHandler, DataHandler, HTTPErrorProcessor.

top_level_url es de hecho o una URL completa (incluyendo el componente del esquema “http:” y el nombre del host y opcionalmente el número de puerto) p.ej. "http://example.com/" o una «autoridad» (esto es, el nombre del host, incluyendo opcionalmente el número de puerto) por ejemplo "example.com" o "example.com:8080" (este último ejemplo incluye un número de puerto). La autoridad, si está presente, NO debe contener el componente «userinfo» - por ejemplo "joe:password@example.com" no es correcto.

Proxies

urllib detectará automáticamente tu configuración de proxy y la utilizará. Esto es a través de ProxyHandler, que es parte de la cadena de gestores normales cuando se detecta un ajuste de proxy. Normalmente esto es algo bueno, pero hay ocasiones en las que puede no ser útil 5. Una forma de hacerlo es configurar nuestro propio ProxyHandler, sin proxies definidos. Esto se hace usando pasos similares a la configuración de un gestor Basic Authentication:

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

Nota

Actualmente urllib.request no soporta la consulta de ubicaciones https a través de un proxy. Sin embargo, esto puede ser habilitado extendiendo urllib.request como se muestra en la receta 6.

Nota

HTTP_PROXY será ignorado si se establece una variable REQUEST_METHOD; ver la documentación en getproxies`().

Sockets y Capas

El soporte de Python para obtener recursos de la web funciona en capas. urllib utiliza la biblioteca http.client, que a su vez utiliza la biblioteca de sockets.

A partir de Python 2.3 se puede especificar cuánto tiempo debe esperar un socket para obtener una respuesta antes de que se agote el tiempo de espera. Esto puede ser útil en aplicaciones que tienen que consultar páginas web. Por defecto, el módulo socket no tiene tiempo de espera y puede colgarse. Actualmente, el tiempo de espera de la conexión no se expone en los niveles http.client o urllib.request. Sin embargo, puede establecerse el tiempo de espera por defecto de forma global para todas los sockets usando

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)

Notas a pie de página

Este documento fue examinado y revisado por John Lee.

1

Google por ejemplo.

2

El rastreo de navegadores es una práctica muy mala para el diseño de sitios web - construir sitios usando estándares web es mucho más sensato. Desafortunadamente muchos sitios siguen enviando versiones diferentes a diferentes navegadores.

3

El agente de usuario para MSIE 6 es “Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 1.1.4322)”

4

Para detalles de más encabezados de peticiones HTTP, ver Quick Reference to HTTP Headers.

5

En mi caso tengo que usar un proxy para acceder a internet en el trabajo. Si intentas consultar URLs de localhost a través de este proxy, las bloquea. IE está configurado para usar el proxy, que urllib recoge. Para poder probar los scripts con un servidor localhost, tengo que evitar que urllib use el proxy.

6

objeto de apertura de urllib para proxy SSL (método CONNECT): ASPN Cookbook Recipe.