feat: многопоточный парсинг + forum survey

- Throttle: Thread-safe координация задержек
- BanDetector: автоопределение бана
- fetch: thread-local сессии вместо глобальной
- run: ThreadPoolExecutor + диапазоны страниц + мерж
- output/state: поддержка worker-папок и merge_states()
- __main__: --workers, --no-delay, --no-ban-test
- VWTS/forums-survey.md: обзор 20+ автофорумов
This commit is contained in:
2026-06-04 10:48:57 +03:00
parent 22ca83adf8
commit c3211f0602
8 changed files with 361 additions and 93 deletions
+78
View File
@@ -0,0 +1,78 @@
"""Общая координация задержек и автоопределение бана."""
import time, threading, logging
import requests
from .config import DELAY, TIMEOUT, HEADERS
log = logging.getLogger(__name__)
class Throttle:
"""Thread-safe координатор задержек.
Все потоки ждут своей очереди — максимум 1 запрос в DELAY секунд.
"""
def __init__(self, delay: float = DELAY, enabled: bool = True):
self._delay = delay
self._enabled = enabled
self._lock = threading.Lock()
self._last = 0.0 # монотонное время последнего запроса
@property
def enabled(self) -> bool:
return self._enabled
def wait(self):
"""Ждать своей очереди. Без координации — просто sleep(delay)."""
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
class BanDetector:
"""Проверяет, банит ли сервер за быстрые запросы."""
@staticmethod
def test(base_url: str, count: int = 5) -> bool:
"""Шлёт count запросов без задержки. Возвращает True если банят.
Args:
base_url: любой URL того же хоста (напр. главная раздела)
count: сколько запросов подряд
Returns:
True — банит (есть 403/429), False — не банит
"""
log.info(f"🔍 Проверка бана: {count} запросов подряд...")
s = requests.Session()
s.headers.update(HEADERS)
banned = False
for i in range(count):
try:
r = s.get(base_url, timeout=TIMEOUT)
log.info(f" [{i+1}/{count}] HTTP {r.status_code}")
if r.status_code in (403, 429):
banned = True
break
except Exception as e:
log.warning(f" [{i+1}/{count}] ошибка: {e}")
banned = True
break
if banned:
log.warning("⚠️ Сервер банит быстрые запросы — включаю координацию")
else:
log.info("✅ Бан не обнаружен — без координации")
s.close()
return banned