Session, reintentos y archivos con Requests [Python]

Cuando tu código hace muchas peticiones, conviene ser eficiente y resistente a fallos puntuales. Aquí ves cómo con sesiones, reintentos y transferencia de archivos.

Por qué importa la eficiencia

Cada llamada suelta a requests.get() abre una conexión nueva: resuelve el DNS, establece la conexión TCP y negocia TLS antes de enviar un solo byte útil.

Con una petición aislada apenas se nota, pero si consultas una API cien veces, ese trabajo repetido se acumula. Las sesiones, los reintentos y el manejo correcto de archivos hacen que tu código sea más rápido y más tolerante a los fallos de red.

Paso 7: Session para reutilizar conexiones y cabeceras

Si haces varias peticiones al mismo servidor, usa requests.Session(): reutiliza la conexión TCP (más rápido), conserva las cookies y comparte cabeceras y autenticación entre todas las llamadas.

import requests

with requests.Session() as s:
    # Se aplican a todas las peticiones de la sesión
    s.headers.update({"User-Agent": "agusbot-tutorial/1.0"})
    s.auth = ("agus", "secreto")

    # El servidor guarda una cookie...
    s.get("https://httpbin.org/cookies/set?sesion=abc123", timeout=10)

    # ...y la sesión la reenvía sola
    r = s.get("https://httpbin.org/cookies", timeout=10)
    print(r.json())   # {'cookies': {'sesion': 'abc123'}}

El bloque with cierra las conexiones al terminar. Una limitación a recordar: la sesión no tiene un timeout global, así que sigues pasándolo en cada llamada (o lo fijas con un adaptador, como en el paso siguiente).

Paso 8: Reintentos automáticos con Retry

Las redes fallan a veces: un 503 puntual o un límite de peticiones (429) no deberían romper tu script. requests delega los reintentos en urllib3, y se configuran una sola vez en la sesión.

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry


class TimeoutAdapter(HTTPAdapter):
    """Adaptador que aplica un timeout por defecto a toda la sesión."""

    def __init__(self, *args, timeout=10, **kwargs):
        self.timeout = timeout
        super().__init__(*args, **kwargs)

    def send(self, request, **kwargs):
        kwargs["timeout"] = kwargs.get("timeout") or self.timeout
        return super().send(request, **kwargs)


def crear_sesion():
    reintentos = Retry(
        total=3,                                  # máximo de reintentos
        backoff_factor=1,                         # espera creciente entre intentos
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["GET", "HEAD", "OPTIONS"],
    )
    adaptador = TimeoutAdapter(max_retries=reintentos, timeout=10)
    s = requests.Session()
    s.mount("https://", adaptador)
    s.mount("http://", adaptador)
    return s


sesion = crear_sesion()
r = sesion.get("https://httpbin.org/status/200")
print(r.status_code)

Por defecto solo se reintentan métodos idempotentes (GET, HEAD, OPTIONS…). No añadas POST a allowed_methods a menos que tu API garantice que repetir la petición no duplica datos, por ejemplo un pago o un pedido.

Cómo funciona la espera entre reintentos

Con backoff_factor la espera crece de forma exponencial en cada intento fallido, para no saturar más a un servidor que ya está en apuros. El primer reintento suele ser inmediato y los siguientes esperan cada vez más: con un factor de 1, aproximadamente 2 y luego 4 segundos, aunque el cálculo exacto depende de la versión de urllib3. Además, si el servidor responde con la cabecera Retry-After (típico de los códigos 429 y 503), Retry la respeta por defecto. Elige total y backoff_factor pensando en cuánto tiempo puede esperar realmente tu programa: tres reintentos con factor 1 ya suman varios segundos de espera.

Paso 9: Descargar y subir archivos

Para descargar archivos grandes usa stream=True: requests no carga todo en memoria, sino que lo lee por bloques.

import requests

url = "https://httpbin.org/bytes/102400"   # 100 KB de datos de prueba

with requests.get(url, stream=True, timeout=10) as r:
    r.raise_for_status()
    with open("descarga.bin", "wb") as f:
        for bloque in r.iter_content(chunk_size=8192):
            f.write(bloque)

print("Descarga completa")

Para subir un archivo usa files=; requests arma la petición multipart/form-data por ti:

import requests

with open("reporte.txt", "rb") as archivo:      # abrir en modo binario
    r = requests.post(
        "https://httpbin.org/post",
        files={"archivo": archivo},
        timeout=30,
    )

print(r.status_code)
print(list(r.json()["files"]))   # ['archivo']

Detalles que conviene conocer sobre archivos

  • Elige bien el chunk_size. Bloques de 8 KB a 1 MB funcionan bien; uno muy pequeño hace muchas iteraciones y uno enorme consume memoria innecesaria.
  • Usa siempre with. Con stream=True la conexión no se libera hasta que consumes todo el contenido o cierras la respuesta; el bloque with lo garantiza aunque ocurra un error.
  • Abre los archivos a subir en modo binario ("rb"). Si los abres en modo texto, un archivo con caracteres especiales puede corromperse o fallar.
  • Personaliza el nombre y el tipo. En files= puedes pasar una tupla (nombre, objeto_archivo, tipo_mime) cuando el servidor exige un nombre o un Content-Type concreto.
  • Comprueba el resultado. Tras una descarga, compara el tamaño del archivo con la cabecera Content-Length si el servidor la envía, para detectar descargas cortadas.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *