Как исправить HTTP 429 Too Many Requests при скрейпинге

http 429 too many requests - How to Fix HTTP 429 Too Many Requests While Scraping

HTTP 429 Too Many Requests означает «сбавь темп», а не «уходи». Сервер сообщает, что по-прежнему готов принимать ваш трафик, только пореже, и обычно прямо говорит, сколько ждать. Поэтому решение почти никогда не в прокси и не в новом user agent. Нужно прочитать один заголовок, правильно поспать и ограничить число одновременных запросов. Статья разбирает все три пункта, а затем показывает, как отличить настоящее ограничение частоты от антибот-блокировки под тем же кодом ответа, ведь реагировать на них надо противоположным образом.

Что понадобится

  • Python 3.10 или новее и библиотека requests для примеров. Логика переносится на любой HTTP-клиент без изменений.
  • Терминал, чтобы посмотреть на заголовки ответа до того, как писать код повторов.
  • CapSkip запущен, и нужен только для последнего раздела: либо в локальном режиме на адресе loopback, либо в серверном на машине, доступной вашим воркерам. Оба варианта описаны в разделе Настройки подключения, так что выберите один до начала.

Шаг 1: прочитайте ответ, прежде чем его повторять

Обработку 429 обычно пишут слепо, поэтому она и не работает. Сначала посмотрите на реальный ответ. Вместе с кодом приходят заголовки, которые сообщают, каков лимит и когда он сбросится, и у разных сервисов они разные.

# No install needed. Dump headers, throw the body away.
curl -sS -o /dev/null -D - "https://example.com/api/items?page=2"

# Look for these, in this order of usefulness:
#   Retry-After: 30           seconds, or an HTTP date
#   RateLimit-Reset: 1724500000
#   X-RateLimit-Remaining: 0
#   RateLimit-Limit: 100

Главный из них это Retry-After. Он определён ровно для такой ситуации и встречается в двух формах: число секунд ожидания либо абсолютная дата в формате HTTP. Обе допустимы, обе встречаются на практике, и код, рассчитанный только на число, сломается на сайтах, которые присылают дату. MDN описывает обе формы, а её справочная страница про код ответа 429 тоже стоит двух минут вашего времени.

Разбирайте его аккуратно, соблюдайте, когда он есть, и возвращайтесь к собственному расписанию, когда его нет:

# pip install requests
from email.utils import parsedate_to_datetime
from datetime import datetime, timezone

def retry_delay(response, fallback):
    """Seconds to wait, from Retry-After if the server sent one."""
    raw = response.headers.get("Retry-After")
    if not raw:
        return fallback
    try:
        return max(0.0, float(raw))          # the delay-seconds form
    except ValueError:
        pass
    try:
        when = parsedate_to_datetime(raw)    # the HTTP-date form
        return max(0.0, (when - datetime.now(timezone.utc)).total_seconds())
    except (TypeError, ValueError):
        return fallback

Ограничивайте то, что пришло. Сервер, который просит 3600 секунд, говорит вам остановиться на час, и воркер, послушно уснувший на такой срок внутри обработчика запроса, будет выглядеть зависшим для всего, что находится выше. Берите меньшее из значения заголовка и вашего собственного потолка, а потом отдельно решайте, не отложить ли задачу.

Шаг 2: отступайте экспоненциально и со случайным разбросом

Когда следовать нечему, потому что Retry-After нет, удваивайте ожидание на каждом шаге и добавляйте случайность. Удвоение не даёт вам добивать сервис, которому и без вас тяжело. Случайность не даёт двум десяткам ваших воркеров, упёршихся в лимит в одну и ту же секунду, вечно повторять запросы в одну и ту же секунду.

# pip install requests
import random, time, requests

def get_with_backoff(url, attempts=5, base=1.0, ceiling=60.0):
    for attempt in range(attempts):
        response = requests.get(url, timeout=30)
        if response.status_code != 429:
            return response
        # Full jitter: sleep somewhere in [0, base * 2 ** attempt].
        window = min(ceiling, base * (2 ** attempt))
        delay = retry_delay(response, random.uniform(0, window))
        time.sleep(min(delay, ceiling))
    raise RuntimeError(f"still rate limited after {attempts} attempts")

# Waits land near 0-1s, 0-2s, 0-4s, 0-8s, 0-16s unless the
# server named a delay, in which case that wins.

Полный разброс, то есть случайное значение от нуля до окна, а не окно плюс небольшое дрожание, быстрее всего разводит парк по времени. Если писать это самому не хочется, в urllib3 такое уже есть, но чтобы оно приносило пользу, два аргумента надо задать явно:

# pip install requests
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

retry = Retry(
    total=5,
    # status_forcelist defaults to none, so 429 is NOT retried
    # unless you list it here yourself. This is the usual bug.
    status_forcelist=[429, 500, 502, 503, 504],
    backoff_factor=1,        # 1 * 2 ** previous_retries seconds
    backoff_jitter=1.0,      # urllib3 2.x only
    allowed_methods=["GET", "HEAD"],
)

session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry))

Про этот класс стоит знать две детали. Retry-After он соблюдает сам, потому что 429 входит в набор RETRY_AFTER_STATUS_CODES наряду с 413 и 503, а respect_retry_after_header по умолчанию равен true. Но status_forcelist по умолчанию пуст, поэтому свежий объект Retry не повторяет 429, пока вы его туда не впишете. Обычно предполагают обратное, а потом удивляются, почему адаптер ничего не сделал.

Шаг 3: ограничьте параллельность вместо более настойчивых повторов

Логика повторов лечит симптом. Если 429 идут ровным потоком, а не всплесками, вы просто просите больше, чем вам разрешено, и лечится это уменьшением нагрузки. Семафор плюс минимальный интервал между запросами закрывают больше случаев ограничения частоты, чем любая кривая отступа.

# Standard library only.
import asyncio

# Six in flight is a sane starting point for an unknown API.
gate = asyncio.Semaphore(6)
MIN_GAP = 0.2          # seconds between starts, per worker

async def fetch(client, url):
    async with gate:
        response = await client.get(url)
        await asyncio.sleep(MIN_GAP)
        return response

# Tune down on the first 429, and stay there for a while.
# Tuning back up too eagerly just rediscovers the limit.

После 429 первым делом хочется размазать ту же нагрузку по большему числу IP. С частью целей это работает, но это отдельное решение со своими компромиссами, которое мы разбирали в материале ротация прокси при распознавании капчи. Делайте это как выбор про пропускную способность, а не как способ не читать заголовок.

429 это не 403, и проверка тоже не они

Вот где обработка 429 обходится дороже всего. Ограничение частоты и детектирование ботов это разные системы, которые иногда делят один код ответа, и хотят они от вас противоположного. HTTP 429 Too Many Requests от ограничителя частоты это указание по расписанию, а тот же код от антибот-периметра это отказ. Вежливо отступать перед антибот-блокировкой значит потерять час. Настойчиво повторять при настоящем лимите значит получить бан по IP.

Что вы получилиЧто это обычно значитЧто действительно помогает
429 с заголовком Retry-AfterНастоящий описанный лимит частотыПодождать ровно столько и снизить темп
429 без заголовков и с телом в HTMLПериметр или антибот-слой, а не сам APIСчитать это блокировкой, а не лимитом
403 приходит мгновенноОтпечаток, TLS или репутация IPПравить клиента, ведь ожидание ничего не изменит
503 с заголовком Retry-AfterПерегрузка или обслуживаниеТот же путь с отступом, что и для 429
200 с проверкой в телеВас оценили и прервалиРешить проверку и продолжить

Последняя строка сбивает с толку, потому что формально ничего не упало. Запрос вернул 200, а в теле вместо ваших данных лежит страница проверки, поэтому цикл повторов, который смотрит только на код ответа, будет с удовольствием долбить её вечно. Проверяйте, есть ли в теле ожидаемый маркер, а не только строку статуса. Если вы нашли проверку, отступ тут не ответ, а решение проверки да.

Не наступите тем же циклом на те же грабли

Цикл опроса, который ждёт ответа по капче, сам является циклом повторов, и самодельные варианты допускают ровно те же ошибки. Две особенности CapSkip делают это проще, чем с сервисом по счётчику. Он работает на вашем железе, поэтому нет ни квоты на решения, ни собственного лимита частоты, в который можно упереться. И SDK уже отступают за вас: опрос начинается с четверти секунды и растёт до pollingInterval, который задаёт потолок, а не фиксированный интервал.

# pip install capskip
from capskip import CapSkip, NetworkException, TimeoutException

# Local mode. In Server mode, host is the solver box's address.
solver = CapSkip(host="127.0.0.1", port=8080, pollingInterval=2)

try:
    result = solver.recaptcha(
        sitekey="YOUR_SITEKEY",
        url="https://example.com/page-with-recaptcha",
    )
    print(result["code"][:24])   # token, submit it with the form
except TimeoutException:
    # Polling ran past recaptchaTimeout, 300 seconds by default.
    print("gave up waiting, try again or lower the timeout")
except NetworkException:
    # The solver is not reachable on that host and port.
    print("check CapSkip is running and the mode you set")

Уменьшить pollingInterval значит получить ответ быстрее и ничего за это не заплатить, а на платном API такого выбора у вас, по сути, нет. Если вы опрашиваете сырые HTTP-эндпоинты вместо SDK, рекомендованные задержки по каждому типу капчи собраны здесь: Документация по API. Там же указан ответ, который приходит, пока решение ещё готовится.

Как запустить решатель на сервере

Ограничение частоты обычно проблема парка, а не одного скрипта, а парк не делит один адрес loopback. Настройки подключения закрывают оба случая:

РежимПрослушиваетКогда использовать
Локально127.0.0.1, только это устройствоВаш скрейпер и решатель работают на одной машине
СерверВаш сетевой адрес или публичный IPВоркеры, контейнеры, VPS или хостинговая платформа обращаются по API

Укажите SDK адрес машины с решателем, и больше в коде ничего не меняется, поэтому десять воркеров могут пользоваться одним экземпляром. Статический публичный IP рекомендуется, когда вызовы идут из-за пределов вашей сети. Подробности собраны здесь: Настройки подключения. Серверный режим это по-прежнему ваше железо и по-прежнему без счётчика: меняется место запуска решателя, а не его владелец.

FAQ

Всегда ли надо подчиняться Retry-After?

Соблюдайте его, но с потолком. Это самый надёжный сигнал о том, когда окно откроется снова, поэтому игнорировать его значит угадывать хуже, чем вам уже сказали. Значение в час это уже другое решение: отложите задачу и вернитесь позже, а не держите воркера спящим, потому что всё выше по стеку прочитает это как зависание.

Как отличить лимит частоты от антибот-блокировки?

Посмотрите, что пришло вместе с кодом ответа. Настоящий лимит читается машиной: заголовок Retry-After или RateLimit, небольшое тело в JSON и предсказуемое поведение, когда вы подождали. Антибот-блокировка присылает страницу HTML, никаких сведений о времени и часто один и тот же ответ, сколько её ни выжидай. Второму нужен другой клиент, а не более долгий сон.

Возвращает ли решатель 429?

Нет. CapSkip работает на железе, которое вы контролируете, без квоты на решения, поэтому нет ни расчётного окна, которое можно исчерпать, ни вышестоящего лимита, в который можно упереться. Если вызов решения не удался, вы получите сетевую ошибку, потому что демон недоступен, либо таймаут, потому что опрос вышел за свой потолок. И то и другое означает что-то локальное, так что проверьте хост, порт и выбранный режим.

Мои воркеры работают на хостинговой платформе. Где тогда решатель?

На вашей машине, до которой платформа может дотянуться, с решателем в серверном режиме. Хостинговые раннеры и управляемые платформы автоматизации не видят ваш адрес loopback, поэтому привяжите API к сетевому или публичному IP и направьте туда каждый воркер. Один экземпляр обслуживает весь парк, и туннель не нужен.

Самая короткая версия

HTTP 429 Too Many Requests это задача про расписание, так к ней и относитесь. Прочитайте Retry-After и соблюдайте его до выбранного вами потолка. Когда заголовка нет, переходите к экспоненциальному отступу с полным разбросом. Затем снижайте параллельность, потому что ровный поток 429 это вопрос пропускной способности, который не решает никакая кривая повторов. И смотрите в тело, прежде чем вообще повторять, ведь страница проверки приходит с абсолютно здоровым кодом ответа и требует решения, а не ожидания. Для этой части подходит безлимитный сервис распознавания капчи , запущенный локально, а как он встраивается в пул воркеров, поясняет материал распознавание капчи для веб-скрейпинга про запуск такого решателя.