urllib.request — Acceso a recursos en red

Propósito:Una biblioteca para abrir direcciones URL que se puede ampliar definiendo manejadores de protocolo personalizados.

El módulo urllib.request proporciona una interfaz de programación para usar recursos de Internet identificados por URLs. Está diseñado para ser extendida por aplicaciones individuales para soportar nuevos protocolos o agregar variaciones a protocolos existentes (como el manejo de autenticación básica HTTP).

HTTP GET

Nota

El servidor de prueba para estos ejemplos se encuentra en http_server_GET.py, de los ejemplos para el módulo http.server. Inicia el servidor en una ventana de terminal, luego ejecuta estos ejemplos en otra.

Una operación HTTP GET es el uso más simple de urllib.request. Pasa la URL a urlopen() para obtener un manejador «tipo archivo» a los datos remotos.

urllib_request_urlopen.py
from urllib import request

response = request.urlopen('http://localhost:8080/')
print('RESPONSE:', response)
print('URL     :', response.geturl())

headers = response.info()
print('DATE    :', headers['date'])
print('HEADERS :')
print('---------')
print(headers)

data = response.read().decode('utf-8')
print('LENGTH  :', len(data))
print('DATA    :')
print('---------')
print(data)

El servidor de ejemplo acepta los valores entrantes y formatea una respuesta de texto plano para enviar de vuelta. El valor de retorno de urlopen() da acceso a los encabezados desde el servidor HTTP a través del método info(), y los datos del recurso remoto a través de métodos como read() y readlines().

$ python3 urllib_request_urlopen.py

RESPONSE: <http.client.HTTPResponse object at 0x101744d68>
URL     : http://localhost:8080/
DATE    : Sat, 08 Oct 2016 18:08:54 GMT
HEADERS :
---------
Server: BaseHTTP/0.6 Python/3.5.2
Date: Sat, 08 Oct 2016 18:08:54 GMT
Content-Type: text/plain; charset=utf-8

LENGTH  : 349
DATA    :
---------
CLIENT VALUES:
client_address=('127.0.0.1', 58420) (127.0.0.1)
command=GET
path=/
real path=/
query=
request_version=HTTP/1.1

SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.2
protocol_version=HTTP/1.0

HEADERS RECEIVED:
Accept-Encoding=identity
Connection=close
Host=localhost:8080
User-Agent=Python-urllib/3.5

El objeto tipo archivo devuelto por urlopen() es iterable:

urllib_request_urlopen_iterator.py
from urllib import request

response = request.urlopen('http://localhost:8080/')
for line in response:
    print(line.decode('utf-8').rstrip())

Este ejemplo elimina las nuevas líneas finales y los retornos de carro antes de imprimir la salida.

$ python3 urllib_request_urlopen_iterator.py

CLIENT VALUES:
client_address=('127.0.0.1', 58444) (127.0.0.1)
command=GET
path=/
real path=/
query=
request_version=HTTP/1.1

SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.2
protocol_version=HTTP/1.0

HEADERS RECEIVED:
Accept-Encoding=identity
Connection=close
Host=localhost:8080
User-Agent=Python-urllib/3.5

Argumentos de codificación

Los argumentos se pueden pasar al servidor codificándolos con urllib.parse.urlencode() y anexándolos a la URL.

urllib_request_http_get_args.py
from urllib import parse
from urllib import request

query_args = {'q': 'query string', 'foo': 'bar'}
encoded_args = parse.urlencode(query_args)
print('Encoded:', encoded_args)

url = 'http://localhost:8080/?' + encoded_args
print(request.urlopen(url).read().decode('utf-8'))

La lista de valores de cliente devueltos en el resultado de ejemplo contiene los argumentos de consulta codificados.

$ python urllib_request_http_get_args.py
Encoded: q=query+string&foo=bar
CLIENT VALUES:
client_address=('127.0.0.1', 58455) (127.0.0.1)
command=GET
path=/?q=query+string&foo=bar
real path=/
query=q=query+string&foo=bar
request_version=HTTP/1.1

SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.2
protocol_version=HTTP/1.0

HEADERS RECEIVED:
Accept-Encoding=identity
Connection=close
Host=localhost:8080
User-Agent=Python-urllib/3.5

HTTP POST

Nota

El servidor de prueba para estos ejemplos se encuentra en http_server_POST.py, de los ejemplos para el módulo http.server. Inicia el servidor en una ventana de terminal, luego ejecute estos ejemplos en otra.

Para enviar datos codificados como formulario al servidor remoto utilizando POST en lugar de GET, pasa los argumentos de consulta codificados como datos a urlopen().

urllib_request_urlopen_post.py
from urllib import parse
from urllib import request

query_args = {'q': 'query string', 'foo': 'bar'}
encoded_args = parse.urlencode(query_args).encode('utf-8')
url = 'http://localhost:8080/'
print(request.urlopen(url, encoded_args).read().decode('utf-8'))

El servidor puede decodificar los datos del formulario y acceder a los valores individuales por nombre.

$ python3 urllib_request_urlopen_post.py

Client: ('127.0.0.1', 58568)
User-agent: Python-urllib/3.5
Path: /
Form data:
    foo=bar
    q=query string

Agregar encabezados salientes

urlopen() es una función de conveniencia que oculta algunos de los detalles de cómo se realiza y maneja la solicitud. Control mas preciso es posible usando una instancia Request directamente. Por ejemplo, los encabezados personalizados se pueden agregar a la solicitud saliente para controlar el formato de los datos devueltos, especificar la versión de un documento almacenado en caché localmente, y decirle al servidor remoto el nombre del cliente de software que se comunica con él.

Como muestra la salida de los ejemplos anteriores, el valor predeterminado del encabezado User-agent se compone de la constante Python-urllib, seguido de la versión del intérprete de Python. Al crear una aplicación que accederá a los recursos web de propiedad de otra persona, es cortés incluir información de agente de usuario real en las solicitudes, para que puedan identificar la fuente de las vistas más fácilmente. El uso de un agente personalizado también permite controlar los rastreadores utilizando un archivo robots.txt (consulta el módulo http.robotparser).

urllib_request_request_header.py
from urllib import request

r = request.Request('http://localhost:8080/')
r.add_header(
    'User-agent',
    'PyMOTW (https://pymotw.com/)',
)

response = request.urlopen(r)
data = response.read().decode('utf-8')
print(data)

Después de crear un objeto Request, usa add_header() para establecer el valor de agente de usuario antes de abrir la solicitud. La ultima linea de La salida muestra el valor personalizado.

$ python3 urllib_request_request_header.py

CLIENT VALUES:
client_address=('127.0.0.1', 58585) (127.0.0.1)
command=GET
path=/
real path=/
query=
request_version=HTTP/1.1

SERVER VALUES:
server_version=BaseHTTP/0.6
sys_version=Python/3.5.2
protocol_version=HTTP/1.0

HEADERS RECEIVED:
Accept-Encoding=identity
Connection=close
Host=localhost:8080
User-Agent=PyMOTW (https://pymotw.com/)

Publicar datos de formulario de una solicitud

Los datos salientes se pueden especificar al crear el Request para tenerlo publicado en el servidor.

urllib_request_request_post.py
from urllib import parse
from urllib import request

query_args = {'q': 'query string', 'foo': 'bar'}

r = request.Request(
    url='http://localhost:8080/',
    data=parse.urlencode(query_args).encode('utf-8'),
)
print('Request method :', r.get_method())
r.add_header(
    'User-agent',
    'PyMOTW (https://pymotw.com/)',
)

print()
print('OUTGOING DATA:')
print(r.data)

print()
print('SERVER RESPONSE:')
print(request.urlopen(r).read().decode('utf-8'))

El método HTTP utilizado por el Request cambia de GET a POST automáticamente después de que se agregan los datos.

$ python3 urllib_request_request_post.py

Request method : POST

OUTGOING DATA:
b'q=query+string&foo=bar'

SERVER RESPONSE:
Client: ('127.0.0.1', 58613)
User-agent: PyMOTW (https://pymotw.com/)
Path: /
Form data:
    foo=bar
    q=query string

Cargar archivos

La codificación de archivos para cargar requiere un poco más de trabajo que el simple formulario. Un mensaje MIME completo debe construirse en el cuerpo de la solicitud, para que el servidor pueda distinguir los campos de formulario entrantes de archivos subidos.

urllib_request_upload_files.py
import io
import mimetypes
from urllib import request
import uuid


class MultiPartForm:
    """Accumulate the data to be used when posting a form."""

    def __init__(self):
        self.form_fields = []
        self.files = []
        # Use a large random byte string to separate
        # parts of the MIME data.
        self.boundary = uuid.uuid4().hex.encode('utf-8')
        return

    def get_content_type(self):
        return 'multipart/form-data; boundary={}'.format(
            self.boundary.decode('utf-8'))

    def add_field(self, name, value):
        """Add a simple field to the form data."""
        self.form_fields.append((name, value))

    def add_file(self, fieldname, filename, fileHandle,
                 mimetype=None):
        """Add a file to be uploaded."""
        body = fileHandle.read()
        if mimetype is None:
            mimetype = (
                mimetypes.guess_type(filename)[0] or
                'application/octet-stream'
            )
        self.files.append((fieldname, filename, mimetype, body))
        return

    @staticmethod
    def _form_data(name):
        return ('Content-Disposition: form-data; '
                'name="{}"\r\n').format(name).encode('utf-8')

    @staticmethod
    def _attached_file(name, filename):
        return ('Content-Disposition: file; '
                'name="{}"; filename="{}"\r\n').format(
                    name, filename).encode('utf-8')

    @staticmethod
    def _content_type(ct):
        return 'Content-Type: {}\r\n'.format(ct).encode('utf-8')

    def __bytes__(self):
        """Return a byte-string representing the form data,
        including attached files.
        """
        buffer = io.BytesIO()
        boundary = b'--' + self.boundary + b'\r\n'

        # Add the form fields
        for name, value in self.form_fields:
            buffer.write(boundary)
            buffer.write(self._form_data(name))
            buffer.write(b'\r\n')
            buffer.write(value.encode('utf-8'))
            buffer.write(b'\r\n')

        # Add the files to upload
        for f_name, filename, f_content_type, body in self.files:
            buffer.write(boundary)
            buffer.write(self._attached_file(f_name, filename))
            buffer.write(self._content_type(f_content_type))
            buffer.write(b'\r\n')
            buffer.write(body)
            buffer.write(b'\r\n')

        buffer.write(b'--' + self.boundary + b'--\r\n')
        return buffer.getvalue()


if __name__ == '__main__':
    # Create the form with simple fields
    form = MultiPartForm()
    form.add_field('firstname', 'Doug')
    form.add_field('lastname', 'Hellmann')

    # Add a fake file
    form.add_file(
        'biography', 'bio.txt',
        fileHandle=io.BytesIO(b'Python developer and blogger.'))

    # Build the request, including the byte-string
    # for the data to be posted.
    data = bytes(form)
    r = request.Request('http://localhost:8080/', data=data)
    r.add_header(
        'User-agent',
        'PyMOTW (https://pymotw.com/)',
    )
    r.add_header('Content-type', form.get_content_type())
    r.add_header('Content-length', len(data))

    print()
    print('OUTGOING DATA:')
    for name, value in r.header_items():
        print('{}: {}'.format(name, value))
    print()
    print(r.data.decode('utf-8'))

    print()
    print('SERVER RESPONSE:')
    print(request.urlopen(r).read().decode('utf-8'))

La clase MultiPartForm puede representar un formulario arbitrario como un mensaje MIME multiparte con archivos adjuntos.

$ python3 urllib_request_upload_files.py

OUTGOING DATA:
User-agent: PyMOTW (https://pymotw.com/)
Content-type: multipart/form-data;
    boundary=d99b5dc60871491b9d63352eb24972b4
Content-length: 389

--d99b5dc60871491b9d63352eb24972b4
Content-Disposition: form-data; name="firstname"

Doug
--d99b5dc60871491b9d63352eb24972b4
Content-Disposition: form-data; name="lastname"

Hellmann
--d99b5dc60871491b9d63352eb24972b4
Content-Disposition: file; name="biography";
    filename="bio.txt"
Content-Type: text/plain

Python developer and blogger.
--d99b5dc60871491b9d63352eb24972b4--

SERVER RESPONSE:
Client: ('127.0.0.1', 59310)
User-agent: PyMOTW (https://pymotw.com/)
Path: /
Form data:
    Uploaded biography as 'bio.txt' (29 bytes)
    firstname=Doug
    lastname=Hellmann

Crear controladores de protocolo personalizados

urllib.request tiene soporte incorporado para HTTP(S), FTP y acceso local a archivos. Para agregar soporte para otros tipos de URL, registra otro manejador de protocolo. Por ejemplo, para soportar URLs apuntando a archivos arbitrarios en servidores NFS remotos, sin requerir que los usuarios monten la ruta antes de acceder al archivo, crea una clase derivada de BaseHandler y con el método nfs_open().

Al método open() específico del protocolo se le da un solo argumento, la instancia Request, y debería devolver un objeto con un read() método que se puede usar para leer los datos, un método info() para devolver los encabezados de respuesta, y geturl() para devolver el URL real del archivo que se está leyendo. Una forma sencilla de lograrlo es crear una instancia de urllib.response.addinfourl, pasando los encabezados, el URL y el identificador de archivo abierto en el constructor.

urllib_request_nfs_handler.py
import io
import mimetypes
import os
import tempfile
from urllib import request
from urllib import response


class NFSFile:

    def __init__(self, tempdir, filename):
        self.tempdir = tempdir
        self.filename = filename
        with open(os.path.join(tempdir, filename), 'rb') as f:
            self.buffer = io.BytesIO(f.read())

    def read(self, *args):
        return self.buffer.read(*args)

    def readline(self, *args):
        return self.buffer.readline(*args)

    def close(self):
        print('\nNFSFile:')
        print('  unmounting {}'.format(
            os.path.basename(self.tempdir)))
        print('  when {} is closed'.format(
            os.path.basename(self.filename)))


class FauxNFSHandler(request.BaseHandler):

    def __init__(self, tempdir):
        self.tempdir = tempdir
        super().__init__()

    def nfs_open(self, req):
        url = req.full_url
        directory_name, file_name = os.path.split(url)
        server_name = req.host
        print('FauxNFSHandler simulating mount:')
        print('  Remote path: {}'.format(directory_name))
        print('  Server     : {}'.format(server_name))
        print('  Local path : {}'.format(
            os.path.basename(tempdir)))
        print('  Filename   : {}'.format(file_name))
        local_file = os.path.join(tempdir, file_name)
        fp = NFSFile(tempdir, file_name)
        content_type = (
            mimetypes.guess_type(file_name)[0] or
            'application/octet-stream'
        )
        stats = os.stat(local_file)
        size = stats.st_size
        headers = {
            'Content-type': content_type,
            'Content-length': size,
        }
        return response.addinfourl(fp, headers,
                                   req.get_full_url())


if __name__ == '__main__':
    with tempfile.TemporaryDirectory() as tempdir:
        # Populate the temporary file for the simulation
        filename = os.path.join(tempdir, 'file.txt')
        with open(filename, 'w', encoding='utf-8') as f:
            f.write('Contents of file.txt')

        # Construct an opener with our NFS handler
        # and register it as the default opener.
        opener = request.build_opener(FauxNFSHandler(tempdir))
        request.install_opener(opener)

        # Open the file through a URL.
        resp = request.urlopen(
            'nfs://remote_server/path/to/the/file.txt'
        )
        print()
        print('READ CONTENTS:', resp.read())
        print('URL          :', resp.geturl())
        print('HEADERS:')
        for name, value in sorted(resp.info().items()):
            print('  {:<15} = {}'.format(name, value))
        resp.close()

Las clases FauxNFSHandler y NFSFile imprimen mensajes para ilustrar dónde una implementación real agregaría llamadas de montaje y desmontaje. Dado que esto es sólo una simulación, FauxNFSHandler está cebado con el nombre de un directorio temporal donde debería buscar todos sus archivos.

$ python3 urllib_request_nfs_handler.py

FauxNFSHandler simulating mount:
  Remote path: nfs://remote_server/path/to/the
  Server     : remote_server
  Local path : tmprucom5sb
  Filename   : file.txt

READ CONTENTS: b'Contents of file.txt'
URL          : nfs://remote_server/path/to/the/file.txt
HEADERS:
  Content-length  = 20
  Content-type    = text/plain

NFSFile:
  unmounting tmprucom5sb
  when file.txt is closed

Ver también

  • Documentación de la biblioteca estándar para urllib.request
  • urllib.parse – Trabajar con la propia cadena de URL.
  • Tipos de contenido de formularios – Especificación W3C para publicar archivos o grandes cantidades de datos a través de formularios HTTP.
  • mimetypes – Asignar nombres de archivo a mimetype.
  • requests – Biblioteca HTTP de terceros con mejor soporte para conexiones seguras y una interfaz de programación más fácil de usar. El equipo de desarrollo núcleo de Python recomienda que la mayoría de los desarrolladores utilicen requests, en parte porque recibe actualizaciones de seguridad más frecuentes que la biblioteca estándar.