Saltar al contenido
🐍 Curso de Python
/

Módulo 5 · Lección 5 de 30

requests desde cero

Veinticinco líneas convertidas en una, la sesión que va casi tres veces más rápido, y el if respuesta: que parece correcto y se traga los errores.

⏱ 15 min de lectura

La lección anterior acabó con una función de veinticinco líneas para hacer bien lo que requests hace así:

python
requests.get(url, timeout=10).json()

requests no es la biblioteca estándar, y aun así es la respuesta correcta para casi todo. La usan Django, pip y medio Python. Esta lección enseña a usarla y, sobre todo, las tres trampas que tiene precisamente por ser cómoda: cuando algo es fácil, se escribe sin mirar.

Lo que trae puesto#

Lo primero que hace requests es arreglar los tres olvidos de la lección anterior. Estas son sus cabeceras de fábrica:

Salida en pantalla
Accept: */*
Accept-Encoding: gzip, deflate, zstd
User-Agent: python-requests/2.34.2

Pide compresión y descomprime sola:

Salida en pantalla
content-encoding: gzip
bytes por el cable: 1501  |  tras descomprimir: 6125

Cuatro veces menos tráfico sin escribir nada. Con urllib eso eran cinco líneas.

Lo del agente sigue igual de mal, y sigue conviniendo cambiarlo por algo que te identifique.

Las peticiones#

python
import requests

requests.get(url, params={'q': 'django', 'per_page': 50}, timeout=10)
requests.post(url, json={'titulo': 'Hola'}, timeout=10)
requests.put(url, json={...}, timeout=10)
requests.patch(url, json={...}, timeout=10)
requests.delete(url, timeout=10)

params= codifica la consulta por ti, que era la función de la lección anterior. Y la respuesta trae todo lo que hace falta:

python
respuesta.status_code     # 200
respuesta.headers         # un diccionario que no distingue mayúsculas
respuesta.json()          # el cuerpo ya analizado
respuesta.text            # el cuerpo como texto
respuesta.content         # el cuerpo como bytes
respuesta.url             # la dirección final, tras las redirecciones
respuesta.elapsed         # cuánto tardó

La sesión#

Esta es la diferencia que más se nota y la que casi nadie usa. Ocho peticiones seguidas a la misma máquina:

Salida en pantalla
urllib                8 peticiones: 3,46 s  (433 ms cada una)
requests.get          8 peticiones: 3,27 s  (409 ms)
requests.Session      8 peticiones: 1,30 s  (162 ms)   <- 2,7 veces más rápido

Fíjate en que requests.get suelto no es más rápido que urllib: las dos abren una conexión nueva cada vez, con su negociación de TLS, que es lo que se lleva los 250 milisegundos de diferencia.

Una Session mantiene la conexión abierta y la reutiliza:

python
with requests.Session() as sesion:
        sesion.headers['User-Agent'] = 'mi-proyecto/1.0 (contacto@ejemplo.es)'
    sesion.headers['Accept'] = 'application/vnd.github+json'

    for nombre in repositorios:
                datos = sesion.get(f'https://api.github.com/repos/{nombre}', timeout=10).json()

Y además de la velocidad, guarda las cabeceras y las cookies, así que las de autenticación se ponen una vez y valen para todas. Eso es lo que hace que la sesión sea el objeto natural cuando escribes un cliente de una API, que es el proyecto de la lección 10.

La regla es sencilla: más de una petición, sesión.

Los errores#

Aquí está la trampa principal, y es de diseño:

python
respuesta = requests.get('https://api.github.com/repos/django/no-existe-xyz')

Eso no lanza ninguna excepción. Devuelve una respuesta con status_code 404 y sigue como si nada. requests considera que un 404 es una respuesta legítima del servidor, y tiene razón, y el resultado es que el código que no comprueba nada continúa con un cuerpo de error entre las manos.

La comprobación es una línea:

python
respuesta.raise_for_status()
Salida en pantalla
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: https://api.github.com/...

Ponla siempre, salvo cuando estés tratando el código a mano.

Las excepciones que sí lanza están bien organizadas:

Salida en pantalla
HTTPError          <- RequestException <- OSError
ConnectionError    <- RequestException <- OSError
Timeout            <- RequestException <- OSError
ConnectTimeout     <- ConnectionError <- Timeout
ReadTimeout        <- Timeout <- RequestException
TooManyRedirects   <- RequestException <- OSError
JSONDecodeError    <- InvalidJSONError <- RequestException

Lo práctico de esa jerarquía es que todo cuelga de RequestException, así que un guion se protege entero con un solo except:

python
try:
        respuesta = sesion.get(url, timeout=(3, 10))
    respuesta.raise_for_status()
except requests.RequestException as e:
        registro.error('Falló la petición: %s', e)

Y fíjate en ConnectTimeout, que hereda de las dos cosas a la vez. Eso está pensado: agotar el tiempo al conectar es un problema de conexión y es un tiempo de espera, y así lo atrapan los dos except.

El tiempo de espera#

Salida en pantalla
timeout por defecto: None

requests no pone tiempo de espera. Igual que urllib. Es la crítica más repetida a la biblioteca y no la van a cambiar, porque rompería código ajeno.

Y aquí es peor, porque es tan cómoda que se escribe requests.get(url) sin pensar, y eso es un programa que se puede quedar colgado para siempre contra un servidor ajeno que acepta la conexión y no contesta.

El parámetro admite dos números, que es lo que conviene usar:

python
requests.get(url, timeout=(3, 10))
Salida en pantalla
timeout=(conexión 3, lectura 1) -> ReadTimeout tras 1,3 s

El primero es cuánto esperar a conectar, el segundo cuánto esperar entre trozos de la respuesta. Separarlos permite ser estricto con la conexión, que o va rápido o no va, y generoso con la lectura, que puede tardar si la respuesta es grande.