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:
2026-06-06 11:46:57 +04:00
parent 673bfe5e2a
commit 5c142096de
18 changed files with 2118 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
"""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