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:
@@ -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
|
||||
Reference in New Issue
Block a user