- Split single file into 8 focused modules (399 lines total): config.py — constants fetch.py — HTTP GET with retries paginate.py — page_count + HEAD verification parse.py — HTML parsing (topics, posts, tags) state.py — per-section state.json output.py — JSONL with file rotation run.py — main orchestration loop __main__.py — CLI entry point - Run: python -m vwts_scraper <URL> - Page count method fully tested (13 sections, 100+ topics, 0 errors) - Resume support (Ctrl+C → state saved) - Per-section state files (no conflicts between sections)
82 lines
2.8 KiB
Python
82 lines
2.8 KiB
Python
"""Определение количества страниц в пагинации XenForo.
|
|
|
|
Работает ТОЛЬКО внутри div.pageNav — не ловит числа из аватаров,
|
|
текста постов, путей к файлам (например /avatars/m/32/32165.jpg).
|
|
"""
|
|
|
|
import re, logging
|
|
from bs4 import BeautifulSoup
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def count(soup: BeautifulSoup) -> int:
|
|
"""Сколько страниц. Если пагинации нет — 1.
|
|
|
|
Принцип:
|
|
<div class="pageNav">
|
|
<a>1</a> ← текущая (без page-N в href)
|
|
<a href="page-2">2</a> ← соседняя
|
|
<a>…</a> ← эллипсис → ValueError → пропускаем
|
|
<a href="page-218">218</a> ← последняя
|
|
</div>
|
|
|
|
ВСЕ номера страниц видны на любой странице.
|
|
Кнопки <button>Выполнить</button> не трогаем — ищем только <a>.
|
|
"""
|
|
nav = soup.select_one("div.pageNav")
|
|
if not nav:
|
|
return 1
|
|
|
|
nums = set()
|
|
for a in nav.select("a"):
|
|
href = a.get("href", "")
|
|
text = a.text.strip()
|
|
|
|
# page-N из href
|
|
m = re.search(r"page-(\d+)", href)
|
|
if m:
|
|
nums.add(int(m.group(1)))
|
|
|
|
# текущая страница: число без page-N в href
|
|
try:
|
|
nums.add(int(text))
|
|
except ValueError:
|
|
pass # "…", "Назад", "Вперёд"
|
|
|
|
return max(nums) if nums else 1
|
|
|
|
|
|
def verify_last(section_url: str, claimed: int, session) -> int:
|
|
"""HEAD-запросами проверяет реальную последнюю страницу.
|
|
|
|
Если сайт показывает 218, но page-218 → 404 (админ потёр часть тем,
|
|
пагинацию не пересчитали) — бинарным поиском находит реальную границу.
|
|
"""
|
|
if claimed <= 1:
|
|
return claimed
|
|
|
|
try:
|
|
import requests
|
|
r = session.head(f"{section_url}page-{claimed}",
|
|
timeout=(10, 30), allow_redirects=True)
|
|
if r.status_code < 400:
|
|
return claimed
|
|
|
|
# Бинарный поиск
|
|
lo, hi = 1, claimed
|
|
while lo < hi:
|
|
mid = (lo + hi + 1) // 2
|
|
hr = session.head(f"{section_url}page-{mid}",
|
|
timeout=(10, 30), allow_redirects=True)
|
|
if hr.status_code >= 400:
|
|
hi = mid - 1
|
|
else:
|
|
lo = mid
|
|
|
|
log.warning(f" Пагинация врала: {claimed} → реально {lo}")
|
|
return lo
|
|
|
|
except Exception:
|
|
return claimed # не смогли — верим сайту
|