Files
vesti/openspec/changes/rss-crawler/proposal.md
T

6.6 KiB
Raw Blame History

Proposal: rss-crawler

Why

В VESTI сейчас есть только telegram_crawler (Telethon, MTProto). RSS-источники — самый массовый и дешёвый класс источников (по оценке PRD — ~80% объёма новостей): веб-СМИ, блоги, IT-порталы публикуют RSS/Atom-ленты. Без RSS-краулера они недоступны.

Проблемы, которые решает модуль:

  • нет спроса на RSS вовсе: в источники (sources.yaml) нельзя добавить ни одного RSS-канала — краулер просто не умеет их обрабатывать (get_enabled_sources(crawler="rss") вернёт пусто);
  • негражданный режим опроса: без If-None-Match/If-Modified-Since каждый запуск тянет полные ленты — это лишний трафик для издателей и для нас;
  • мусорные ссылки: URL из лент часто содержат UTM-метки, якоря, трекеры — дедупликация по сырому URL даёт дубли одной статьи;
  • обрывки вместо текста: многие ленты отдают summary вместо полного текста — без дотягивания полного текста карточки кандидатов бедные;
  • нет состояния/здоровья: нет etag/modified/last_error/status для RSS-источников.

Для пользователя это значит: можно добавить в VESTI новостные сайты/блоги и получать их посты в конвейере (классификация → кандидаты → публикация) наравне с Telegram-каналами.

What Changes

  • Новый модуль краулера crawler/rss_crawler.py (feedparser + httpx):
    • читает sources.yaml (crawler: rss), группирует по priority;
    • HTTP GET c If-None-Match/If-Modified-Since из таблицы rss_state → 304 = пропуск;
    • нормализует URL (UTM-параметры, якоря) → canonical_url, дедуп по sha256;
    • извлекает полный текст через trafilatura, если запись — обрывок;
    • пишет посты в posts через существующий store_posts-механизм (дедуп по sha256 текста);
    • пишет метаданные ленты в rss_state (etag/modified/last_error/status, last_build_date);
    • ведёт runs (source_id, task, trigger) — как telegram_crawler;
    • CLI: python -m crawler.rss_crawler --all | --direction | --source <slug>.
  • Новая таблица rss_state (etag, modified, last_error, status, last_build_date) — аналог tg_state для RSS-лент.
  • requirements.txt: + trafilatura (извлечение полного текста).
  • sources.yaml: первый реальный RSS-источник для проверки — LWN (lwn.net) — и один закомментированный пример (opennet.ru).
  • .env.example: + RSS_USER_AGENT (информативный User-Agent).
  • Cron (Hermes): vesti-rss-crawler (каждые 15 мин, P0) — тихий, no_agent, как telegram.

Не меняется

  • Дедуп и схема posts — как у telegram_crawler (sha256 текста; url — дополнительный ключ).
  • Классификатор, веб, publisher — не затрагиваются (RSS-посты идут в тот же posts).
  • tg_state — остаётся для Telegram; RSS использует rss_state.
  • Никаких автопубликаций: посты попадают в кандидаты, подтверждение — человеком.

Capabilities

New Capabilities

  • rss-crawler: Синхронный краулер RSS/Atom-лент: реестр из sources.yaml, условные GET (etag/modified), нормализация URL, извлечение полного текста (trafilatura), состояние и здоровье ленты в БД, интеграция с runs/posts.

Modified Capabilities

  • news-store (таблица posts): без изменений схемы; RSS-посты используют её как есть.
  • sources.yaml: новые источники с crawler: rss.
  • vesti-web: без изменений (RSS-посты автоматически видны как кандидаты).

Impact

  • Файлы:
    • новый: crawler/rss_crawler.py (модуль-краулер);
    • изменён: db/schema.sql (+CREATE TABLE rss_state), requirements.txt (+ trafilatura), sources/sources.yaml (+lwn, пример opennet), .env.example (+RSS_USER_AGENT), config.py (+RSS_USER_AGENT, +RSS_*);
    • доки: STATUS.md, TODO.md, WALKTHROUGH.md, AGENT.MD (команда запуска).
  • Данные: миграция — CREATE TABLE IF NOT EXISTS; существующие данные не трогаются.
  • Секреты: не требует (RSS публичные). User-Agent — из .env.
  • Сеть: исходящие HTTP-запросы к сайтам издателей (только к лентам, учтиво: условные GET, рейт-лимит 1–2 с между источниками).
  • Rollback: модуль не вызывается cron — просто не добавлять источники/не запускать.

Risks

  • Издатели режут по User-Agent / отдают капчу — лечится RSS_USER_AGENT (честный, с контактом) и enabled: false для проблемных.
  • Ленты с кривой датой (published_parsed None) — ставим fetched_at, не падаем.
  • summary-only ленты — trafilatura может не найти полный текст; тогда сохраняем summary, пост всё равно попадает в конвейер (пометка в content_type/text).
  • Огромные ленты (мега-фиды): ограничение — первые N записей, остальные догонятся следующими запусками (инкрементально), как в telegram_crawler.
  • Троттлинг издателя: проставляем last_error и status='dead' после N=5 ошибок подряд (правило фид-здоровья), watchdog молчит до перехода alive↔dead.