- 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-тем при многопоточном парсинге
148 lines
5.7 KiB
Python
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
|