"""HTTP-запросы: GET с ретраями. Единая точка вызова HTTP для всех адаптеров. Не зависит от движка форума. """ import time import logging import requests from bs4 import BeautifulSoup # ─── Модульный логгер ─────────────────────────────────────────────────── log = logging.getLogger(__name__) # ─── Глобальный throttle (устанавливается в runner.py) ────────────────── # fetch.get() вызывает throttle.wait() перед каждым запросом. throttle = None # type: ignore # ─── Дефолтные значения ───────────────────────────────────────────────── TIMEOUT = (10, 30) # (connect, read) — requests-таймаут HEADERS = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/125.0.0.0 Safari/537.36" ), } RETRIES = 3 # максимум HTTP-попыток на один URL def make_session() -> requests.Session: """Создать HTTP-сессию с дефолтными заголовками. Returns: requests.Session с предустановленным User-Agent. """ session = requests.Session() session.headers.update(HEADERS) return session def get(url: str, session=None) -> BeautifulSoup | None: """GET-запрос с ретраями, парсинг в BeautifulSoup (lxml). Логика повторных попыток: 1. 404 → сразу None (бесполезно ретраить). 2. 403/429/503 → ждём 10*N сек, потом ретрай. 3. Сеть/Таймаут → ждём 5 сек, потом ретрай. 4. Прочее → ждём 5 сек, потом ретрай. 5. Все RETRIES исчерпаны → None. Args: url: Полный URL для загрузки. session: Сессия (если None — временная). Returns: BeautifulSoup или None при недоступности. """ # ── Сессия ────────────────────────────────────────────────────────── if session is None: session = make_session() # ── Throttle ──────────────────────────────────────────────────────── if throttle is not None: throttle.wait() # ── Цикл ретраев ──────────────────────────────────────────────────── for attempt in range(1, RETRIES + 1): try: response = session.get(url, timeout=TIMEOUT) # 404 — не найдено if response.status_code == 404: log.warning(" ⏭️ 404: %s", url[:80]) return None # 403/429/503 — сервер банит/перегружен if response.status_code in (403, 429, 503): wait = 10 * attempt log.warning( "⚠️ HTTP %d — попытка %d/%d, жду %d с", response.status_code, attempt, RETRIES, wait, ) time.sleep(wait) continue # Любая другая HTTP-ошибка response.raise_for_status() # Успех return BeautifulSoup(response.text, "lxml") except (requests.ConnectionError, requests.Timeout) as exc: log.warning( "⚠️ Сеть: %s — попытка %d/%d, жду 5 с", exc, attempt, RETRIES, ) time.sleep(5) except requests.HTTPError as exc: log.warning( "⚠️ HTTP %s — попытка %d/%d, жду 5 с", exc, attempt, RETRIES, ) time.sleep(5) except Exception as exc: log.warning( "⚠️ Ошибка: %s — попытка %d/%d, жду 5 с", exc, attempt, RETRIES, ) time.sleep(5) # Все попытки исчерпаны log.error("❌ Не загружено (%d попыток): %s", RETRIES, url[:80]) return None