Источники и модель подборки
Оглавление серии «Агрегатор лент» · Урок 1 из 10.
Редактору технического сайта приходится следить за выпусками библиотек, изменениями документации и новыми руководствами. Открывать десятки вкладок каждый день неудобно. В этой серии создадим «Сигнал»: небольшое приложение, которое собирает записи из заранее выбранных RSS и Atom-лент, запоминает изменения и помогает подготовить собственную подборку ссылок. Начнём с договора об источниках и учебных данных, а не с бесконечного обхода интернета.
Результат первого урока — структура проекта, два собственных примера лент и явный список источников. Пока приложение только показывает этот список. Такое начало позволяет отдельно понять, какие сведения получаем от издателя и что хотим хранить у себя. Сборщик ещё не скачивает страницы, не создаёт базу и не публикует материалы в ProfessorWeb.
Из сообщения ленты получается кандидат
Представим, что редакция готовит еженедельный обзор инструментов веб-разработки. Ей достаточно увидеть название материала, дату, источник и ссылку, затем открыть первоисточник и решить, заслуживает ли тема собственного объяснения. Полный текст чужой статьи не нужен для этого первого решения. Поэтому «Сигнал» хранит метаданные и происхождение записи, а редакционный текст появляется только после самостоятельной работы автора.
RSS описывает канал и элементы item. Atom использует feed и entry. Эти форматы решают близкую задачу доставки списка обновлений, но различаются именами полей и некоторыми правилами идентификации. В следующем уроке библиотека приведёт их к одной внутренней модели. Сейчас важно не спутать три сущности: источник, запись источника и нашу будущую публикацию.
Источник имеет собственный постоянный код, например demo-rss. Запись получает внешний идентификатор издателя. Публикация ProfessorWeb, если редактор решит написать её, будет иметь собственный Markdown-файл и URL. Эти связи не означают, что удаление записи из очередного ответа ленты должно удалять нашу статью. Лента обычно показывает лишь последние сообщения, поэтому отсутствие старого элемента ещё ничего не говорит о его существовании.
Фиксируем источники в конфигурации
Добавим полный файл config.py:
from pathlib import Path
BASE = Path(__file__).resolve().parent
SOURCES = {
"demo-rss": {"name": "Учебная редакция", "fixture": "rss.xml"},
"demo-atom": {"name": "Учебная лаборатория", "fixture": "atom.xml"},
"github-blog": {"name": "GitHub Blog", "url": "https://github.blog/feed/"},
}
DB = BASE / "signal.db"
MAX_BYTES = 1024 * 1024
INTERVAL = 1800
Два источника читаются из папки fixtures. Третий описывает внешний адрес GitHub Blog и пригодится при обсуждении HTTP. Сетевая работа будет включаться отдельным параметром. Пока сам факт наличия URL в конфигурации не запускает загрузку. Пользователь не сможет передать сборщику произвольный адрес через браузер или запрос API: список изменяет разработчик в исходном файле.
Код источника нельзя выбирать заново при каждом запуске. Он входит в будущую идентичность записей. Если заменить github-blog другим кодом, приложение получит другую область идентификаторов и может воспринять прежние сообщения как новые. Смена отображаемого имени, напротив, не должна разрушать историю. Поэтому короткий технический код и название для человека существуют отдельно.
Путь BASE вычисляется относительно файла проекта. Учебные данные и будущая база не зависят от текущей папки терминала. Это полезно, когда человек запускает команду из другого каталога: относительный путь fixtures/rss.xml без опорной директории часто приводит к поиску файла совсем в другом месте. Значения лимита и интервала тоже размещаем здесь, чтобы поздние шаги использовали единый договор.
Создаём собственную RSS-ленту
Полный файл fixtures/rss.xml:
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"><channel><title>Учебная редакция</title>
<link>https://example.org/</link><description>Собственные вымышленные данные курса</description>
<item><guid isPermaLink="false">release-1</guid><title>Заметки о выпуске</title>
<link>https://example.org/lab/release-1</link><pubDate>Sun, 11 Oct 2026 10:00:00 GMT</pubDate>
<description><p>Полное описание не сохраняется в подборке.</p></description></item>
<item><guid isPermaLink="false">release-2</guid><title>Заметки о выпуске</title>
<link>https://example.org/lab/release-2</link></item>
</channel></rss>
Оба элемента имеют одинаковый заголовок, но разные guid и ссылки. Эта небольшая особенность станет важной в третьем уроке: название не является уникальным ключом. Второй элемент намеренно не содержит даты. Мы не будем подменять неизвестное время моментом загрузки, иначе старая публикация выглядела бы свежей после каждого обновления сборщика.
Домен example.org используется как адрес вымышленных материалов. Проект не должен пытаться доказать существование этих страниц. Пример описывает форму сообщения, а не готовую внешнюю редакцию. Содержимое description также наше; позже сборщик его не сохранит. Это помогает увидеть, что наличие поля в ленте ещё не требует его включения в собственную модель.
Отдельный fixtures/atom.xml содержит одну запись с идентификатором urn:signal:layout-1 и временем обновления в часовом поясе +03:00. Это не альтернативный проект, а второй вход того же сборщика. Мы будем сравнивать результаты двух форматов, чтобы нормализация не оказалась случайно привязанной только к item из RSS.
Подготовка Python-проекта
Для самостоятельного прохождения используйте Python 3.13. Начиная с расписания понадобится Linux или macOS: блокировку единственного рабочего процесса реализуем через fcntl. В Windows такой модуль отсутствует; подходящий вариант для этого конкретного примера — среда WSL. Изменять механизм блокировки, не объяснив его поведение после аварии, было бы отдельной задачей.
В requirements.txt фиксируем feedparser==6.0.14. Версия проверена по карточке проекта PyPI. Данная серия разбирает именно её интерфейс; команды установки приведены для читателя в архиве исходников. При подготовке курса зависимости не устанавливались. Файл базы и виртуальное окружение не входят в архив, поэтому новый читатель начинает с данных примеров, а не с чужого рабочего состояния.
Выполнив самостоятельно python collect.py из первой папки урока, читатель должен получить перечень трёх источников и отметку об отключённой HTTP-загрузке. Команда ничего не разбирает и не сохраняет. Если вместо перечня возникает ошибка пути, сначала следует проверить выбранную папку состояния и наличие config.py, а не добавлять сетевые библиотеки наугад.
Мы подготовили границы редакционного агрегатора и входные примеры. Следующий шаг даст каждому сообщению одинаковые поля, безопасную ссылку и понятное представление даты. После этого можно будет обсуждать историю, обновления и экспорт, сохраняя связь между техническим событием в ленте и решением человека о публикации.
Полезно заранее записать критерий успешного прохождения серии: редактор получает понятную очередь кандидатов, может повторно собрать тот же источник без появления дублей и переносит только явно выбранные версии ссылок. Количество скачанных байтов само по себе не является результатом. Если приложение обходит множество страниц, но не объясняет происхождение карточки и повторно добавляет один материал, редакционная задача остаётся нерешённой. Такой критерий поможет оценивать каждый следующий шаг независимо от числа подключённых источников.
Полное состояние шага — папка lesson-01 архива «Сигнал». Код, сеть, база данных и учебный сервер при подготовке не запускались; описанное поведение ожидается из разбора исходников.