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-тем при многопоточном парсинге
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"""Координация задержек между потоками + автоопределение бана.
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user