Files
LLM-UI/scraper/core/throttle.py
naeel 5c142096de scraper: новая архитектура — core/ + forums/ (BaseAdapter, XenForo, phpBB)
- core/ — общий слой: fetch, throttle, output, state, runner
- forums/base.py — абстрактный BaseAdapter
- forums/xenforo/ — адаптер для XenForo (vwts.ru)
- forums/phpbb/ — адаптер для phpBB (нива-лада.рф)
- __main__.py — точка входа CLI
- history/001-initial-structure.md — журнал изменений

Также на ВМ (не в этом коммите):
- lada_scraper/parse.py — пропуск sticky-тем при многопоточном парсинге
2026-06-06 11:46:57 +04:00

148 lines
5.7 KiB
Python

"""Координация задержек между потоками + автоопределение бана.
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