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

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 это вопрос пропускной способности, который не решает никакая кривая повторов. И смотрите в тело, прежде чем вообще повторять, ведь страница проверки приходит с абсолютно здоровым кодом ответа и требует решения, а не ожидания. Для этой части подходит безлимитный сервис распознавания капчи , запущенный локально, а как он встраивается в пул воркеров, поясняет материал распознавание капчи для веб-скрейпинга про запуск такого решателя.
