5 мин чтенияИнженерия

Как сократить URL на Python с помощью библиотеки requests

Сократите URL на Python несколькими строками с requests: отправьте POST в API сокращателя, получите короткую ссылку в ответ, а затем добавьте повторные попытки, идемпотентность, массовую обработку и асинхронность.

Marius Voß
DevRel · edge infra
Как сократить URL на Python: POST-запрос через requests отправляет конечный URL в API сокращателя и получает в ответ короткую ссылку, с шагами повторных попыток и идемпотентности

Сократить URL на Python - это один HTTP POST-запрос. Вы отправляете свою длинную ссылку в API сокращателя с помощью библиотеки requests, передаёте API-ключ как Bearer-токен и получаете короткую ссылку из JSON-ответа. Всё это умещается примерно в пять строк, а остальная часть руководства - о том, что добавить, когда вы начинаете использовать это по-настоящему: обработку ошибок, идемпотентность, массовую обработку и асинхронность.

Это версия для разработчиков общего руководства о том, как сократить URL - там разобран сценарий через дашборд и браузер, а здесь код, который можно вставить прямо в скрипт. Эндпоинты и названия полей ниже взяты из API Elido, но сама схема (отправить конечный адрес через POST, получить короткий URL в ответ) одинакова у большинства современных сокращателей, так что паттерны переносятся без изменений.

Если вы ещё не выбрали сервис, начните с обзора бесплатного API сокращателя URL - там описаны форма запроса, модель авторизации и лимиты бесплатного тарифа, которые эта статья принимает как данность.

Самый быстрый способ: один POST через requests

Установите requests, поместите свой API-ключ в переменную окружения и отправьте конечный URL на эндпоинт links через POST:

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

Есть три детали, которые превращают это из игрушечного примера в код, готовый для продакшена. API-ключ берётся из окружения, а не прописан буквально в исходном коде. Указан timeout, поэтому зависшее соединение быстро завершается с ошибкой вместо бесконечного ожидания. А raise_for_status() превращает 4xx или 5xx в исключение, которое можно перехватить, вместо того чтобы неудачный вызов выглядел как успешный.

Ответ приходит в формате JSON. Читайте short_url, чтобы получить готовую ссылку; большинство сокращателей также возвращают id, который стоит сохранить, если вы планируете позже редактировать ссылку или завершать срок её действия.

Правильно обрабатывайте ошибки и лимиты запросов

Идеальный сценарий - это пять строк кода. Но скрипт, который работает без присмотра, должен переживать и неидеальные ситуации: временный 5xx, 429 при слишком высокой скорости запросов, кратковременный сбой сети. Оберните вызов так, чтобы он повторялся при ошибках, которые имеет смысл повторять, и сдавался при тех, что никогда не завершатся успехом.

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")

401 или 403 никогда не исправятся сами по себе от повторной попытки, поэтому они проваливаются до raise_for_status() и останавливают скрипт. 429 учитывает заголовок Retry-After, который присылает API. 5xx обрабатывается с экспоненциальной задержкой. Материал лимиты запросов и идемпотентность API - подробный разбор объясняет, почему слепые повторные попытки превращают один сбой в два.

Вызов Python requests отправляет конечный URL и Idempotency-Key на эндпоинт links сокращателя вместе с Bearer-токеном, API возвращает HTTP 201 с short_url, а цикл повторных попыток делает паузу при ответах 429 и 5xx

Отправляйте Idempotency-Key, чтобы повторы не создавали дубликаты

Вот скрытая проблема в цикле повторных попыток выше. Если POST-запрос успешно обработан на сервере, но ответ теряется на обратном пути, ваш код видит таймаут, повторяет запрос и создаёт вторую короткую ссылку для того же URL. Idempotency-ключ решает эту проблему: вы отправляете стабильный уникальный ключ с каждым логическим запросом, и при повторе API возвращает исходную ссылку вместо создания новой.

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,
)

Генерируйте ключ один раз на URL и используйте его повторно при всех попытках для этого же URL - а не на каждую отдельную попытку. Храните его рядом с URL, если сама партия может быть перезапущена. Это самая важная привычка при переходе от сокращения одной ссылки к сокращению целого списка, и именно поэтому она встроена в API и SDK.

Массовое сокращение URL

Имея надёжную функцию shorten(), партию можно обработать циклом - но циклом, который соблюдает лимит запросов и не теряет соответствие между входными и выходными данными:

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}")

Хранение результата в словаре с ключом по исходному URL означает, что частичный сбой заметен и его можно перезапустить, а не тихий пробел в данных. Для нескольких сотен ссылок такой последовательный вариант вполне подходит. Для тысяч задержка на каждый round-trip накапливается, и вот тут асинхронность оказывается кстати.

Асинхронность на масштабе с httpx

Когда партия большая, узким местом становится ожидание сети, а не сам Python. Асинхронный клиент вроде httpx отправляет множество запросов параллельно, при этом ограничивая, сколько из них выполняется одновременно, так что вы загружаете соединение по максимуму, не превышая лимит запросов.

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))

Весь фокус - в Semaphore(10): он ограничивает число параллельных запросов десятью, так что вы работаете быстро, но не забрасываете API до состояния 429. Настройте это значение под лимит, задокументированный для вашего тарифа. Полную форму запроса, названия полей и актуальные лимиты смотрите в документации API, а в решениях для разработчиков найдутся SDK, если вы не хотите писать клиент вручную.

Последовательное массовое сокращение, при котором один запрос отправляется и ожидается ответ, за другим по очереди, в сравнении с асинхронным подходом, отправляющим ограниченное число запросов параллельно через semaphore, чтобы быстрее обработать крупную партию

Какой подход выбрать

Подбирайте инструмент под масштаб задачи. Одна ссылка в скрипте - пятистрочный вызов requests. Надёжная задача, работающая по расписанию, - версия с повторными попытками и идемпотентностью. Разовая партия из нескольких сотен - последовательный цикл. Десятки тысяч к дедлайну - httpx с semaphore.

Что бы вы ни выбрали, держите ключ в переменной окружения, устанавливайте timeout и отправляйте idempotency-ключ на всё, что может повторяться. Эти три привычки - и есть разница между сниппетом для демонстрации и скриптом, который можно оставить работать без присмотра.

Читайте серию статей-основ

Эта статья входит в кластер engineering. Отправная точка - руководство по бесплатному API сокращателя URL о форме эндпоинтов и авторизации, затем материал лимиты запросов и идемпотентность о корректном поведении под нагрузкой. Актуальный справочник - документация API.

Похожие статьи в блоге

Частые вопросы

Как сократить URL на Python?

Отправьте HTTP POST-запрос в API сокращателя URL с помощью библиотеки requests, передав длинный URL в теле JSON, а API-ключ - как Bearer-токен, а затем прочитайте короткую ссылку из JSON-ответа. Это примерно пять строк кода: собрать заголовки, отправить конечный URL на эндпоинт links и вывести response.json()['short_url'].

Можно ли сократить URL на Python без внешней библиотеки?

Да. Модуль urllib.request из стандартной библиотеки умеет отправлять JSON через POST без установки чего-либо, что удобно в закрытом окружении. Кода получается больше, чем с requests, - тело запроса нужно кодировать самому, заголовки выставлять вручную, а ответ разбирать самостоятельно, - зато не требуется pip install.

Как сократить сразу много URL на Python?

Пройдитесь циклом по списку и отправляйте каждый URL отдельным POST-запросом, но передавайте свой Idempotency-Key на каждый URL, чтобы повтор запроса никогда не создавал дубликат, и соблюдайте лимит запросов API, делая паузу при HTTP 429. Для крупных партий асинхронный клиент вроде httpx с ограниченным semaphore сокращает тысячи URL параллельно, а не по одному.

Почему мой запрос к сокращателю URL на Python возвращает 401?

401 означает, что в заголовке Authorization отсутствует или неверен API-ключ. Убедитесь, что вы отправляете именно 'Authorization: Bearer YOUR_API_KEY', что ключ не был отозван и что вы загрузили его из переменной окружения, а не вставили просроченный ключ вручную. 403, в свою очередь, означает, что ключ действителен, но не имеет прав на это действие.

Попробуйте Elido

Вставьте URL - получите короткую ссылку

Без регистрации. Ссылка живёт 30 дней. Зарегистрируйтесь, чтобы оставить её навсегда.

Бесплатно, без регистрации · 2 в день

Попробуйте Elido

URL-сокращатель с хостингом в ЕС: собственные домены, глубокая аналитика, открытый API. Бесплатный тариф - без банковской карты.

Теги
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

Читать дальше