Guía para enviar solicitudes HTTP en Python con urllib3

En esta guía, veremos cómo enviar solicitudes HTTP en Python con el módulo urllib3, y cómo habilitar conexiones seguras y cargar archivos.

Introducción

Los recursos en la Web se encuentran bajo algún tipo de dirección web (incluso si no son accesibles), a menudo denominada URL (Localizador uniforme de recursos). Estos recursos son, la mayoría de las veces, manipulados por un usuario final (recuperados, actualizados, eliminados, etc.) utilizando el protocolo HTTP a través de los respectivos métodos HTTP.

En esta guía, veremos cómo aprovechar la biblioteca urllib3, que nos permite enviar solicitudes HTTP a través de Python, mediante programación.

Nota: El módulo urllib3 solo se puede usar con Python 3.x.

¿Qué es HTTP?

HTTP (Protocolo de transferencia de hipertexto) es un protocolo de transferencia de datos que se utiliza, por lo general, para transmitir documentos hipermedia, como HTML, pero también se puede utilizar para transferir JSON, XML o formatos similares. Se aplica en la Capa de aplicación del Modelo OSI, junto con otros protocolos como FTP (Protocolo de transferencia de archivos) y ***SMTP (Protocolo simple de transferencia de correo) ***.

HTTP es la columna vertebral de la World Wide Web tal como la conocemos hoy y su tarea principal es habilitar un canal de comunicación entre navegadores web y servidores web, a través de un ciclo de vida de Solicitudes HTTP y * Respuestas HTTP*: los componentes de comunicación fundamentales de HTTP.

Se basa en el modelo cliente-servidor en el que un cliente solicita un recurso y el servidor responde con el recurso, o con la falta del mismo.

Una Solicitud HTTP típica puede parecerse a:

1
2
3
4
GET /tag/java/ HTTP/1.1
Host: wikihtp.com
Accept: */*
User-Agent: Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion

Si el servidor encuentra el recurso, el encabezado de la Respuesta HTTP contendrá datos sobre cómo le fue al ciclo de solicitud/respuesta:

1
2
3
4
5
6
HTTP/1.1 200 OK
Date: Thu, 22 Jul 2021 18:16:38 GMT
Content-Type: text/html; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive
...

Y el cuerpo de respuesta contendrá el recurso real, que en este caso es una página HTML:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!DOCTYPE html>
<html lang="en">
   <head>
      <meta name="twitter:title" content="Stack Abuse"/>
      <meta name="twitter:description" content="Learn Python, Java, JavaScript/Node, Machine Learning, and Web Development through articles, code examples, and tutorials for developers of all skill levels."/>
      <meta name="twitter:url" content="https://wikihtp.com"/>
      <meta name="twitter:site" content="@wikihtp"/>
      <meta name="next-head-count" content="16"/>
   </head>
...

El módulo urllib3

El módulo urllib3 es el último módulo relacionado con HTTP desarrollado para Python y el sucesor de urllib2. Admite cargas de archivos con codificación de varias partes, gzip, agrupación de conexiones y seguridad de subprocesos. Por lo general, viene preinstalado con Python 3.x, pero si ese no es tu caso, se puede instalar fácilmente con:

1
$ pip install urllib3

Puedes comprobar tu versión de urllib3 accediendo a la __version__ del módulo:

1
2
3
4
import urllib3

# This tutorial is done with urllib3 version 1.25.8
print(urrlib3.__version__)

Alternativamente, puede usar el módulo Solicitudes, que se basa en la parte superior de urllib3. Es más intuitivo y centrado en el ser humano, y permite una gama más amplia de solicitudes HTTP. Si desea leer más al respecto, lea nuestra Guía del Módulo de Solicitudes en Python.

Códigos de estado HTTP

Cada vez que se envía una solicitud HTTP, la respuesta, que no sea el recurso solicitado (si está disponible y accesible), también contiene un Código de estado HTTP, que indica cómo se desarrolló la operación. Es fundamental que sepa qué significa el código de estado que obtuvo, o al menos lo que implica en términos generales.

¿Hay algún problema? Si es así, ¿se debe a la solicitud, al servidor o a mí?*

Hay cinco grupos diferentes de códigos de respuesta:

  1. Códigos informativos (entre 100 y 199)
  2. Códigos exitosos (entre 200 y 299) - 200 es el más común
  3. Códigos de redirección (entre 300 y 399)
  4. Códigos de error del cliente (entre 400 y 499) - 404 es el más común
  5. Códigos de error del servidor (entre 500 y 599) - 500 es el más común

Para enviar solicitudes utilizando urllib3, usamos una instancia de la clase PoolManager, que se encarga de las solicitudes reales por nosotros, que se tratará en breve.

Todas las respuestas a estas solicitudes se empaquetan en una instancia HTTPResponse que, naturalmente, contiene el estado de esa respuesta:

1
2
3
4
5
6
import urllib3 

http = urllib3.PoolManager()

response = http.request("GET", "https://wikihtp.com")
print(response.status) # Prints 200

Puede usar estos estados para alterar la lógica del código: si el resultado es 200 OK, probablemente no sea necesario hacer mucho más. Sin embargo, si el resultado es una respuesta Método 405 no permitido, es probable que su solicitud esté mal construida.

Sin embargo, si un sitio web responde con un código de estado 418 Soy una tetera, aunque es raro, le está informando que no puede preparar café con una tetera. En la práctica, esto normalmente significa que el servidor no quiere responder a la solicitud y nunca lo hará. Si se tratara de una suspensión temporal de ciertas solicitudes, un código de estado “503 Servicio no disponible” sería mucho más apropiado.

Nota: El código de estado 418 Soy una tetera es un código de estado real pero divertido, agregado como una broma del Día de los Inocentes.

El administrador de la piscina

Un Grupo de conexiones es un caché de conexiones que se puede reutilizar cuando sea necesario en futuras solicitudes, utilizado para mejorar el rendimiento al ejecutar ciertos comandos varias veces. De manera similar, cuando se envían varias solicitudes, se crea un Grupo de conexiones para que se puedan reutilizar ciertas conexiones.

urllib3 realiza un seguimiento de las solicitudes y sus conexiones a través de las clases ConnectionPool y HTTPConnection. Dado que hacer esto a mano lleva a una gran cantidad de código repetitivo, podemos delegar la totalidad de la lógica al PoolManager, que crea conexiones automáticamente y las agrega al grupo. Al ajustar el argumento num_pools, podemos establecer la cantidad de grupos que usará:

1
2
3
4
5
6
import urllib3

http = urllib3.PoolManager(num_pools=3)

response1 = http.request("GET", "https://wikihtp.com")
response2 = http.request("GET", "http://www.google.com")

Solo a través del PoolManager, podemos enviar una solicitud(), pasando el Verbo HTTP y la dirección a la que estamos enviando la solicitud. Diferentes verbos significan diferentes intentos, ya sea que desee “OBTENER” algún contenido, “PUBLICAR” en un servidor, “PARCHE” un recurso existente o “ELIMINAR” uno.

Cómo enviar solicitudes HTTP en Python con urllib3

Finalmente, echemos un vistazo a cómo enviar diferentes tipos de solicitudes a través de urllib3 y cómo interpretar los datos que se devuelven.

Enviar solicitud HTTP GET

Una solicitud HTTP GET se utiliza cuando un cliente solicita recuperar datos de un servidor, sin modificarlos de ninguna manera o forma.

Para enviar una solicitud HTTP GET en Python, usamos el método request() de la instancia PoolManager, pasando el Verbo HTTP apropiado y el recurso para el que estamos enviando una solicitud:

1
2
3
4
5
6
7
import urllib3

http = urllib3.PoolManager()

response = http.request("GET", "http://jsonplaceholder.typicode.com/posts/")

print(response.data.decode("utf-8"))

Aquí, enviamos una solicitud GET a {JSON} Marcador de posición. Es un sitio web que genera datos JSON ficticios, que se envían en el cuerpo de la respuesta. Por lo general, el sitio web se usa para probar las solicitudes HTTP, agregando la respuesta.

La instancia HTTPResponse, es decir, nuestro objeto response contiene el cuerpo de la respuesta. Se puede acceder a él mediante la propiedad data, que es un flujo de bytes. Dado que un sitio web puede responder con una codificación para la que no somos adecuados, y dado que queremos convertir los bytes en str de todos modos, decodificamos() el cuerpo y lo codificamos en UTF- 8 para asegurarnos de que podemos analizar coherentemente los datos.

Si quieres leer más, lee nuestra guía sobre Conversión de bytes a cadenas en Python.

Finalmente, imprimimos el cuerpo de la respuesta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
[
  {
    "userId": 1,
    "id": 1,
    "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "qui est esse",
    "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
  },
...

Enviar solicitud HTTP GET con parámetros

Rara vez no añadimos ciertos parámetros a las solicitudes. Las variables de ruta y los parámetros de solicitud son muy comunes y permiten estructuras de enlace dinámicas y recursos de organización. Por ejemplo, es posible que deseemos buscar un comentario específico en una determinada publicación a través de una API: http://random.com/posts/get?id=1&commentId=1.

Naturalmente, urllib3 nos permite agregar parámetros a las solicitudes GET, a través del argumento fields. Acepta un diccionario de los nombres de los parámetros y sus valores:

1
2
3
4
5
6
7
8
9
import urllib3 

http = urllib3.PoolManager()

response = http.request("GET",
                        "http://jsonplaceholder.typicode.com/posts/", 
                        fields={"id": "1"})

print(response.data.decode("utf-8"))

Esto devolverá solo un objeto, con un id de 1:

1
2
3
4
5
6
7
8
[
    {
        "userId": 1,
        "id": 1,
        "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
        "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas                totam\nnostrum rerum est autem sunt rem eveniet architecto"
    }
]

Solicitud HTTP POST

Se utiliza una solicitud HTTP POST para enviar datos desde el lado del cliente al lado del servidor. Su uso más común es la carga de archivos o el llenado de formularios, pero se puede usar para enviar cualquier dato a un servidor, con una carga útil:

1
2
3
4
5
6
import urllib3

http = urllib3.PoolManager()
response = http.request("POST", "http://jsonplaceholder.typicode.com/posts", fields={"title": "Created Post", "body": "Lorem ipsum", "userId": 5})

print(response.data.decode("utf-8"))

Aunque nos estamos comunicando con la misma dirección web, porque estamos enviando una solicitud POST, el argumento fields ahora especificará los datos que se enviarán al servidor, no se recuperarán.

Hemos enviado una cadena JSON, que denota un objeto con un título, cuerpo e usuario. El servicio {JSON} Placeholder también agrega la funcionalidad para agregar entidades, por lo que devuelve una respuesta que nos informa si hemos podido "agregarlo" a la base de datos y devuelve el id de la publicación "creada":

1
2
3
{
  "id": 101
}

Solicitud de ELIMINACIÓN HTTP

Finalmente, para enviar solicitudes HTTP DELETE, simplemente modificamos el verbo a "DELETE" y apuntamos a una publicación específica a través de su id. Eliminemos todas las publicaciones con los ids de 1..5:

1
2
3
4
5
6
import urllib3

http = urllib3.PoolManager()
for i in range(1, 5):
    response = http.request("DELETE", "http://jsonplaceholder.typicode.com/posts", fields={"id": i})
    print(response.data.decode("utf-8"))

Se devuelve un cuerpo vacío, ya que se eliminan los recursos:

1
2
3
4
{}
{}
{}
{}

Al crear una API REST, probablemente querrá proporcionar un código de estado y un mensaje para que el usuario sepa que un recurso se eliminó correctamente.

Enviar solicitudes HTTP PATCH

Si bien podemos usar solicitudes POST para actualizar recursos, se considera una buena práctica si mantenemos las solicitudes POST solo para crear recursos. En cambio, podemos disparar una solicitud PATCH para actualizar un recurso existente.

Obtengamos la primera publicación y luego actualícela con un nuevo título y cuerpo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import urllib3

data = {
    'title': 'Updated title',
    'body': 'Updated body'
}

http = urllib3.PoolManager()

response = http.request("GET", "http://jsonplaceholder.typicode.com/posts/1")
print(response.data.decode('utf-8'))

response = http.request("PATCH", "https://jsonplaceholder.typicode.com/posts/1", fields=data)
print(response.data.decode('utf-8'))

Esto debería resultar en:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
  "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
}
{
  "userId": 1,
  "id": 1,
  "title": "Updated title",
  "body": "Updated body"
}

Envíe solicitudes HTTPS seguras en Python con urllib3

El módulo urllib3 también proporciona verificación SSL del lado del cliente para conexiones HTTP seguras. Podemos lograr esto con la ayuda de otro módulo, llamado certifi, que proporciona el paquete de certificados estándar de Mozilla.

Su instalación es bastante sencilla a través de pip:

1
$ pip install certifi

Con certifi.where(), hacemos referencia a la Autoridad de certificación (CA) instalada. Esta es una entidad que emite certificados digitales, en los que se puede confiar. Todos estos certificados de confianza están contenidos en el módulo certifi:

1
2
3
4
5
6
7
import urllib3
import certifi

http = urllib3.PoolManager(ca_certs=certifi.where())
response = http.request("GET", "https://httpbin.org/get")

print(response.status)

Ahora, podemos enviar una solicitud segura al servidor.

Carga de archivos con urllib3 {#carga de archivos con urllib3}

Usando urllib3, también podemos subir archivos a un servidor. Para cargar archivos, codificamos los datos como multipart/form-data y pasamos el nombre del archivo y su contenido como una tupla de file_name: file_data.

Para leer el contenido de un archivo, podemos usar el método integrado read() de Python:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import urllib3
import json

with open("file_name.txt") as f:
    file_data = f.read()

# Sending the request.
resp = urllib3.request(
    "POST",
    "https://reqbin.com/post-online",
    fields= {
       "file": ("file_name.txt", file_data),
    }
)

print(json.loads(resp.data.decode("utf-8"))["files"])

Para el propósito del ejemplo, vamos a crear un archivo llamado file_name.txt y agregue algo de contenido:

1
2
Some file data
And some more

Ahora, cuando ejecutamos el script, debería imprimirse:

1
{'file': 'Some file data\nAnd some more'}

Cuando enviamos archivos usando urllib3, los data de la respuesta contienen un atributo "files" adjunto, al que accedemos a través de resp.data.decode("utf-8")["files" ]. Para que la salida sea un poco más legible, usamos el módulo json para cargar la respuesta y mostrarla como una cadena.

También puede proporcionar un tercer argumento a la tupla, que especifica el tipo MIME del archivo cargado:

1
2
3
4
... previous code
fields={
  "file": ("file_name.txt", file_data, "text/plain"),
}

Conclusión

En esta guía, hemos analizado cómo enviar solicitudes HTTP usando urllib3, un poderoso módulo de Python para manejar solicitudes y respuestas HTTP.

También hemos analizado qué es HTTP, qué códigos de estado esperar y cómo interpretarlos, así como también cómo cargar archivos y enviar solicitudes seguras con certifi.