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.
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:
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.
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()
.
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
).
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.
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.
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.
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.