"""Координация задержек между потоками + автоопределение бана. Throttle: Thread-safe координатор: максимум 1 запрос в DELAY секунд. При отключённой координации — просто time.sleep(DELAY). BanDetector: Проверяет, банит ли сервер за быстрые запросы. Шлёт COUNT запросов подряд без задержки, ищет 403/429. """ import time import threading import logging import requests log = logging.getLogger(__name__) class Throttle: """Координатор задержек между потоками. Позволяет запускать N потоков, но реальные HTTP-запросы идут не чаще 1 в delay секунд. Потоки ждут в очереди через threading.Lock. Атрибуты: _delay: Минимальный интервал между запросами (сек). _enabled: True — координация включена, False — только sleep. _lock: Мьютекс для потокобезопасности. _last: Монотонное время последнего запроса. """ def __init__(self, delay: float = 1.5, enabled: bool = True): """Инициализация. Args: delay: Минимальный интервал между запросами (сек). enabled: Если False — вместо координации просто sleep(delay). """ self._delay = delay self._enabled = enabled self._lock = threading.Lock() self._last = 0.0 # time.monotonic() последнего запроса # ── Свойства ──────────────────────────────────────────────────────── @property def delay(self) -> float: """Текущая задержка.""" return self._delay @property def enabled(self) -> bool: """Включена ли координация.""" return self._enabled # ── Основной метод ────────────────────────────────────────────────── def wait(self): """Подождать своей очереди на запрос. Если координация ВЫКЛЮЧЕНА: Просто спим delay секунд. Если координация ВКЛЮЧЕНА: Захватываем мьютекс, считаем сколько прошло с _last, если меньше delay — спим разницу, обновляем _last. """ if not self._enabled: # Без координации: каждый поток спит свою задержку time.sleep(self._delay) return with self._lock: elapsed = time.monotonic() - self._last if elapsed < self._delay: time.sleep(self._delay - elapsed) self._last = time.monotonic() # ── Управление ────────────────────────────────────────────────────── def disable(self): """Отключить координацию.""" self._enabled = False def enable(self): """Включить координацию.""" self._enabled = True class BanDetector: """Определяет, банит ли сервер за быстрые запросы. Статический метод test() шлёт count запросов подряд (без задержек) и смотрит ответы. Если хоть один 403/429 — сервер банит. """ @staticmethod def test(base_url: str, count: int = 5) -> bool: """Проверить бан быстрыми запросами. Args: base_url: Любой URL того же хоста (главная раздела). count: Сколько запросов сделать. Returns: True — сервер банит (есть 403/429), False — бан не обнаружен. """ log.info("🔍 Проверка бана: %d запросов подряд...", count) session = requests.Session() session.headers.update({ "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36" ), }) banned = False for i in range(1, count + 1): try: response = session.get(base_url, timeout=(10, 30)) log.info(" [%d/%d] HTTP %d", i, count, response.status_code) # Сервер явно банит if response.status_code in (403, 429): banned = True break except Exception as exc: # Сетевая ошибка — вероятно, бан на уровне соединения log.warning(" [%d/%d] ошибка: %s", i, count, exc) banned = True break session.close() if banned: log.warning("⚠️ Сервер банит быстрые запросы — включаю координацию") else: log.info("✅ Бан не обнаружен — без координации") return banned