- 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-тем при многопоточном парсинге
119 lines
4.7 KiB
Python
119 lines
4.7 KiB
Python
"""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
|