- 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-тем при многопоточном парсинге
115 lines
4.5 KiB
Python
115 lines
4.5 KiB
Python
"""Управление состоянием: state.json в папке раздела.
|
|
|
|
Позволяет:
|
|
- Сохранять прогресс (какие темы обработаны, какие страницы пройдены).
|
|
- Продолжать парсинг с места остановки (той же командой).
|
|
- Мержить состояния воркеров в единый state.json (после завершения).
|
|
|
|
Файл state.json (раздел):
|
|
{
|
|
"topics_done": {"123": "Название темы", "456": "..."},
|
|
"pages_done": [1, 2, 3],
|
|
"file_index": 5,
|
|
"topic_count": 432
|
|
}
|
|
|
|
Файл worker_N/state.json:
|
|
Аналогичная структура, но только для одного воркера.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def _state_file(data_dir: Path, section_slug: str) -> Path:
|
|
"""Полный путь к state.json для раздела.
|
|
|
|
Args:
|
|
data_dir: Корневая папка с данными (напр. ./vwts_data).
|
|
section_slug: Короткое имя раздела.
|
|
|
|
Returns:
|
|
Path к state.json.
|
|
"""
|
|
section_dir = data_dir / section_slug
|
|
section_dir.mkdir(parents=True, exist_ok=True)
|
|
return section_dir / "state.json"
|
|
|
|
|
|
def load(data_dir: Path, section_slug: str) -> dict:
|
|
"""Загрузить state.json раздела.
|
|
|
|
Args:
|
|
data_dir: Корневая папка с данными.
|
|
section_slug: Короткое имя раздела.
|
|
|
|
Returns:
|
|
Словарь состояния. Если файла нет — пустой state.
|
|
"""
|
|
path = _state_file(data_dir, section_slug)
|
|
|
|
if path.exists():
|
|
try:
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
except (json.JSONDecodeError, OSError) as exc:
|
|
log.warning("⚠️ Ошибка чтения %s: %s", path, exc)
|
|
|
|
# Пустой state по умолчанию
|
|
return {"topics_done": {}, "pages_done": [], "file_index": 0, "topic_count": 0}
|
|
|
|
|
|
def save(data_dir: Path, section_slug: str, state: dict):
|
|
"""Сохранить state.json раздела.
|
|
|
|
Args:
|
|
data_dir: Корневая папка с данными.
|
|
section_slug: Короткое имя раздела.
|
|
state: Словарь состояния для сохранения.
|
|
"""
|
|
path = _state_file(data_dir, section_slug)
|
|
path.write_text(
|
|
json.dumps(state, ensure_ascii=False, indent=2),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
def merge_workers(data_dir: Path, section_slug: str):
|
|
"""Собрать состояния всех воркеров в единый state.json раздела.
|
|
|
|
Проходит по worker_*/state.json, сливает topics_done, pages_done,
|
|
суммирует topic_count.
|
|
|
|
Args:
|
|
data_dir: Корневая папка с данными.
|
|
section_slug: Короткое имя раздела.
|
|
"""
|
|
root = data_dir / section_slug
|
|
|
|
# ── Собираем все worker_*/state.json ────────────────────────────────
|
|
merged = {"topics_done": {}, "pages_done": [], "file_index": 0, "topic_count": 0}
|
|
|
|
for worker_state_path in sorted(root.glob("worker_*/state.json")):
|
|
try:
|
|
worker_state = json.loads(
|
|
worker_state_path.read_text(encoding="utf-8"),
|
|
)
|
|
merged["topics_done"].update(worker_state.get("topics_done", {}))
|
|
merged["pages_done"].extend(worker_state.get("pages_done", []))
|
|
merged["topic_count"] += worker_state.get("topic_count", 0)
|
|
except (json.JSONDecodeError, OSError) as exc:
|
|
log.warning("⚠️ Ошибка чтения %s: %s", worker_state_path, exc)
|
|
|
|
# Чистим дубликаты и сортируем
|
|
merged["pages_done"] = sorted(set(merged["pages_done"]))
|
|
|
|
# Определяем следующий file_index по количеству part_*.jsonl
|
|
existing_parts = list(root.glob("part_*.jsonl"))
|
|
merged["file_index"] = max(0, len(existing_parts) - 1)
|
|
|
|
# ── Пишем ───────────────────────────────────────────────────────────
|
|
save(data_dir, section_slug, merged)
|
|
log.info("🔗 Мерж состояний: %d воркеров → state.json", len(list(root.glob("worker_*"))))
|