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,111 @@
|
||||
"""Основной цикл: обход страниц раздела → темы → посты → сохранение."""
|
||||
|
||||
import sys, logging
|
||||
from .config import HEADERS
|
||||
from .fetch import get, session
|
||||
from .paginate import count, verify_last
|
||||
from .parse import parse_topics, parse_posts, parse_tags
|
||||
from .state import load, save
|
||||
from .output import save_topic
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def run(section_url: str):
|
||||
"""Главный цикл скрейпера.
|
||||
|
||||
Args:
|
||||
section_url: полный URL раздела, напр.
|
||||
https://vwts.ru/forum/vag/benzinovye-dvigateli/
|
||||
"""
|
||||
section_slug = section_url.rstrip("/").split("/")[-1]
|
||||
|
||||
# загружаем состояние (если было прервано — продолжим)
|
||||
st = load(section_slug)
|
||||
if st["topics_done"]:
|
||||
log.info(f"🔄 Продолжаем: {len(st['topics_done'])} тем, "
|
||||
f"part_{st['file_index']:04d}.jsonl")
|
||||
|
||||
# ── Страница 1: название раздела + количество страниц ────────────────
|
||||
soup = get(section_url)
|
||||
if not soup:
|
||||
log.error("❌ Сайт недоступен")
|
||||
sys.exit(1)
|
||||
|
||||
section_name = soup.select_one("h1")
|
||||
section_name = section_name.text.strip() if section_name else section_slug
|
||||
|
||||
total_pages = count(soup)
|
||||
# Проверяем: последняя страница реально существует?
|
||||
total_pages = verify_last(section_url, total_pages, session)
|
||||
log.info(f"📋 {section_name} | страниц: {total_pages}")
|
||||
|
||||
# ── Обход страниц раздела ───────────────────────────────────────────
|
||||
consecutive_404 = 0
|
||||
|
||||
for pg in range(1, total_pages + 1):
|
||||
if pg in st["pages_done"]:
|
||||
consecutive_404 = 0
|
||||
continue
|
||||
|
||||
url = f"{section_url}page-{pg}"
|
||||
log.info(f"📄 [{pg}/{total_pages}]")
|
||||
|
||||
page_soup = get(url)
|
||||
if not page_soup:
|
||||
consecutive_404 += 1
|
||||
# 3 пустых страницы подряд = раздел закончился раньше
|
||||
if consecutive_404 >= 3:
|
||||
log.info(f" ⏹️ {consecutive_404} пустых подряд — завершаем")
|
||||
break
|
||||
log.warning(f" ⚠️ Пропущена стр.{pg} "
|
||||
f"(пустых подряд: {consecutive_404})")
|
||||
continue
|
||||
consecutive_404 = 0
|
||||
|
||||
topics = parse_topics(page_soup)
|
||||
log.info(f" Тем: {len(topics)}")
|
||||
|
||||
# ── Обход тем на странице ──────────────────────────────────────
|
||||
for i, tp in enumerate(topics, 1):
|
||||
tid = tp["id"]
|
||||
if str(tid) in st["topics_done"]:
|
||||
continue
|
||||
|
||||
log.info(f" [{i}/{len(topics)}] #{tid}: {tp['title'][:80]}")
|
||||
topic_soup = get(tp["url"])
|
||||
if not topic_soup:
|
||||
# не удалось — помечаем чтобы не ретраить бесконечно
|
||||
st["topics_done"][str(tid)] = tp["title"]
|
||||
save(section_slug, st)
|
||||
continue
|
||||
|
||||
topic_pages = count(topic_soup)
|
||||
posts, tags = [], parse_tags(topic_soup)
|
||||
|
||||
# ── Обход страниц темы ────────────────────────────────────
|
||||
for tpp in range(1, topic_pages + 1):
|
||||
if tpp == 1:
|
||||
posts += parse_posts(topic_soup)
|
||||
else:
|
||||
tp2 = get(f"https://vwts.ru/forum/topic/{tid}/page-{tpp}")
|
||||
if tp2:
|
||||
posts += parse_posts(tp2)
|
||||
|
||||
log.info(f" {len(posts)} постов ({topic_pages} стр.), "
|
||||
f"теги: {tags}")
|
||||
save_topic(section_slug, section_name, tp, posts, tags, st)
|
||||
st["topics_done"][str(tid)] = tp["title"]
|
||||
save(section_slug, st)
|
||||
|
||||
st["pages_done"].append(pg)
|
||||
save(section_slug, st)
|
||||
|
||||
# ── Итог ────────────────────────────────────────────────────────────
|
||||
log.info("=" * 60)
|
||||
log.info(f"✅ '{section_name}' — {st['topic_count']} тем "
|
||||
f"в {st['file_index']+1} частях")
|
||||
for f in sorted((__import__('pathlib').Path('vwts_data') /
|
||||
section_slug).glob("part_*.jsonl")):
|
||||
log.info(f" 📄 {f.name} ({f.stat().st_size/1024/1024:.1f} MB)")
|
||||
log.info("=" * 60)
|
||||
Reference in New Issue
Block a user