refactor: vwts_scraper — proper Python package structure
- 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)
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
"""Определение количества страниц в пагинации 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 # не смогли — верим сайту
|
||||
Reference in New Issue
Block a user