6 min de lecturaIngeniería

Cómo acortar una URL en Python con la librería requests

Acorta una URL en Python con unas pocas líneas de requests: haz un POST a la API de un acortador, recupera el enlace corto, y luego añade reintentos, idempotencia, procesamiento en bloque y async.

Marius Voß
DevRel · edge infra
Cómo acortar una URL en Python: un POST de requests que envía una URL de destino a la API de un acortador y recupera un enlace corto, con pasos de reintento e idempotencia

Acortar una URL en Python es un solo POST HTTP. Envías tu enlace largo a la API de un acortador con la librería requests, pasas tu clave de API como token Bearer, y lees el enlace corto de la respuesta JSON. Todo esto cabe en unas cinco líneas, y el resto de esta guía trata de lo que hay que añadir cuando lo haces en serio: manejo de errores, idempotencia, procesamiento en bloque y async.

Esta es la versión para desarrolladores de la guía general de cómo acortar una URL - esa cubre el flujo del panel y del navegador, esta es código que puedes pegar en un script. Los endpoints y los nombres de los campos que siguen usan la API de Elido, pero la forma (haces un POST de un destino, recibes de vuelta una URL corta) es la misma en la mayoría de los acortadores modernos, así que los patrones se trasladan sin problema.

Empieza por el resumen de la API gratuita de acortador de URL si aún no has elegido un servicio - ahí se explica la forma de la solicitud, el modelo de autenticación, y los límites del plan gratuito que este artículo da por hecho.

La forma más rápida: un solo POST con requests

Instala requests, pon tu clave de API en una variable de entorno, y haz un POST de la URL de destino al endpoint de enlaces:

import os
import requests

resp = requests.post(
    "https://api.elido.app/v1/links",
    headers={"Authorization": f"Bearer {os.environ['ELIDO_API_KEY']}"},
    json={"destination_url": "https://example.com/a-very-long-path?with=params"},
    timeout=10,
)
resp.raise_for_status()
print(resp.json()["short_url"])   # -> https://s.elido.me/ab12cd

Tres cosas hacen que esto sea apto para producción y no solo un juguete. La clave de API viene del entorno, nunca como literal en el código fuente. Se establece timeout, para que una conexión colgada falle rápido en lugar de bloquearse para siempre. Y raise_for_status() convierte un 4xx o un 5xx en una excepción que puedes capturar, en lugar de dejar que una llamada fallida parezca un éxito.

La respuesta es JSON. Lee short_url para obtener el enlace terminado; la mayoría de los acortadores también devuelven un id que puedes guardar si piensas editar o expirar el enlace más adelante.

Maneja los errores y los límites de tasa correctamente

El camino feliz son cinco líneas. Un script que se ejecuta sin supervisión necesita sobrevivir a los infelices: un 5xx transitorio, un 429 cuando vas demasiado rápido, un fallo momentáneo de red. Envuelve la llamada para que reintente en los errores que vale la pena reintentar y se rinda en los que nunca van a tener éxito.

import os
import time
import requests

def shorten(destination: str, *, retries: int = 3) -> str:
    headers = {"Authorization": f"Bearer {os.environ['ELIDO_API_KEY']}"}
    for attempt in range(retries):
        resp = requests.post(
            "https://api.elido.app/v1/links",
            headers=headers,
            json={"destination_url": destination},
            timeout=10,
        )
        if resp.status_code == 429:
            time.sleep(int(resp.headers.get("Retry-After", 2)))
            continue
        if resp.status_code >= 500:
            time.sleep(2 ** attempt)   # exponential backoff
            continue
        resp.raise_for_status()        # 4xx other than 429 -> raise
        return resp.json()["short_url"]
    raise RuntimeError(f"shorten failed after {retries} attempts")

Un 401 o un 403 nunca se arreglan solos con reintentos, así que caen directamente en raise_for_status() y detienen el script. Un 429 respeta la cabecera Retry-After que envía la API. Un 5xx retrocede de forma exponencial. El análisis a fondo de límites de tasa e idempotencia explica por qué reintentar a ciegas es la forma de convertir una caída en dos.

Una llamada de requests en Python que envía una URL de destino y un Idempotency-Key al endpoint de enlaces del acortador con un token Bearer, la API devolviendo HTTP 201 con un short_url, y un bucle de reintento que retrocede ante respuestas 429 y 5xx

Envía un Idempotency-Key para que los reintentos no dupliquen

Aquí está el bug sutil del bucle de reintento anterior. Si un POST tiene éxito en el servidor pero la respuesta se pierde en el camino de vuelta, tu código ve un timeout, reintenta, y crea un segundo enlace corto para la misma URL. Una clave de idempotencia soluciona esto: envía una clave estable y única con cada solicitud lógica, y la API devuelve el enlace original ante una repetición en lugar de acuñar uno nuevo.

import uuid

key = str(uuid.uuid4())   # one key per URL you want shortened once
resp = requests.post(
    "https://api.elido.app/v1/links",
    headers={
        "Authorization": f"Bearer {os.environ['ELIDO_API_KEY']}",
        "Idempotency-Key": key,
    },
    json={"destination_url": "https://example.com/launch"},
    timeout=10,
)

Genera la clave una vez por URL y reutilízala en los reintentos de esa misma URL - no por intento. Guárdala junto a la URL si el lote en sí podría volver a ejecutarse. Este es el hábito más importante de todos cuando pasas de acortar un enlace a acortar una lista, y está integrado en la API y los SDKs exactamente por esta razón.

Acorta URLs en bloque

Con un shorten() seguro ya en mano, un lote es un bucle - pero un bucle que respeta el límite de tasa y no pierde la correspondencia entre entrada y salida:

urls = [
    "https://example.com/spring-sale",
    "https://example.com/newsletter",
    "https://example.com/docs/getting-started",
]

results = {}
for url in urls:
    try:
        results[url] = shorten(url)
    except Exception as err:      # log and keep going; one bad URL should not sink the batch
        results[url] = f"ERROR: {err}"

for original, short in results.items():
    print(f"{short}\t{original}")

Mantener el resultado en un dict indexado por la URL original hace que un fallo parcial sea visible y reejecutable, no un vacío silencioso. Para unos pocos cientos de enlaces, esta versión secuencial va bien. Para miles, la latencia de ida y vuelta se acumula, y ahí es donde async se gana su lugar.

Async a gran escala con httpx

Cuando el lote es grande, el cuello de botella es la espera de la red, no Python. Un cliente async como httpx envía muchas solicitudes de forma concurrente manteniendo un tope de cuántas están en curso a la vez, para que satures la conexión sin disparar el límite de tasa.

import asyncio
import os
import httpx

async def shorten_all(urls: list[str], concurrency: int = 10) -> dict[str, str]:
    headers = {"Authorization": f"Bearer {os.environ['ELIDO_API_KEY']}"}
    limit = asyncio.Semaphore(concurrency)
    out: dict[str, str] = {}

    async with httpx.AsyncClient(timeout=10) as client:
        async def one(url: str) -> None:
            async with limit:
                r = await client.post(
                    "https://api.elido.app/v1/links",
                    headers=headers,
                    json={"destination_url": url},
                )
                out[url] = r.json()["short_url"]

        await asyncio.gather(*(one(u) for u in urls))
    return out

# asyncio.run(shorten_all(my_urls))

El Semaphore(10) es todo el truco: limita las solicitudes concurrentes a diez para que vayas rápido sin machacar la API hasta forzar 429s. Ajústalo al límite documentado de tu plan. Para la forma completa de la solicitud, los nombres de los campos, y los límites actuales, los docs de la API son la referencia, y soluciones para desarrolladores tiene los SDKs si prefieres no construir el cliente a mano.

Acortamiento en bloque secuencial que envía una solicitud y espera cada respuesta por turno, comparado con un enfoque async que envía un número acotado de solicitudes de forma concurrente a través de un semaphore para acortar un lote grande más rápido

Qué enfoque elegir

Ajusta la herramienta al tamaño del trabajo. Un enlace en un script: la llamada de cinco líneas con requests. Un trabajo fiable que se ejecuta en un horario programado: la versión con reintento más idempotencia. Un lote puntual de unos pocos cientos: el bucle secuencial. Decenas de miles con una fecha límite: httpx con un semaphore.

Elijas lo que elijas, mantén la clave en el entorno, establece un timeout, y envía una clave de idempotencia en cualquier cosa que pueda reintentarse. Esos tres hábitos son la diferencia entre un fragmento que sirve para una demo y un script que puedes dejar corriendo.

Lee la serie pilar

Esto forma parte del clúster de ingeniería. El punto de partida es la guía de la API gratuita de acortador de URL para la forma del endpoint y la autenticación, y luego el artículo de límites de tasa e idempotencia para comportarte bien bajo carga. La referencia viva son los docs de la API.

Relacionado en el blog

Preguntas frecuentes

¿Cómo acorto una URL en Python?

Envía un POST HTTP a la API de un acortador de URL con la librería requests, pasando tu URL larga en el cuerpo JSON y tu clave de API como token Bearer, y luego lee el enlace corto de la respuesta JSON. Son unas cinco líneas: construye las cabeceras, envía la URL de destino al endpoint de enlaces, e imprime response.json()['short_url'].

¿Puedo acortar una URL en Python sin una librería externa?

Sí. El urllib.request de la librería estándar puede hacer POST de JSON sin instalar nada, lo cual es útil en un entorno restringido. Es más verboso que requests - codificas el cuerpo tú mismo, configuras las cabeceras manualmente, y lees la respuesta - pero no necesita ningún pip install.

¿Cómo acorto muchas URLs a la vez en Python?

Recorre la lista y envía un POST por cada una, pero envía un Idempotency-Key por URL para que un reintento nunca cree un duplicado, y respeta el límite de tasa de la API retrocediendo ante un HTTP 429. Para lotes grandes, un cliente async como httpx con un semaphore acotado acorta miles de URLs de forma concurrente en lugar de una por una.

¿Por qué mi solicitud de acortador de URL en Python devuelve 401?

Un 401 significa que la clave de API falta o es incorrecta en la cabecera Authorization. Confirma que estás enviando exactamente 'Authorization: Bearer YOUR_API_KEY', que la clave no ha sido revocada, y que la cargaste desde una variable de entorno en lugar de pegar una ya caducada. Un 403, en cambio, significa que la clave es válida pero no tiene alcance suficiente para esa acción.

Prueba Elido

Pega una URL, obtén un enlace corto

Sin registro. El enlace vive 30 días. Crea una cuenta para conservarlo.

Gratis, sin registro · 2 por día

Prueba Elido

Acortador de URL alojado en la UE: dominios personalizados, análisis profundo y API abierta. Plan gratuito - sin tarjeta de crédito.

Etiquetas
how to shorten a url in python
python url shortener
shorten url python requests
url shortener api python
python requests post json
bulk shorten urls python

Seguir leyendo