В прошлой статье я показывал простой способ автоматизировать сбор постов из Telegram‑каналов с помощью Python и Telethon.
Если не читали — вот она: «Как автоматизировать сбор постов из Telegram‑каналов».
Тогда задача была довольно простой: взять несколько каналов, пройтись по сообщениям и сохранить нужные данные.
После публикации я продолжил развивать этот проект и довольно быстро упёрся в интересную проблему: сами посты — это только небольшая часть информации, которая есть в Telegram.
Пост существует не сам по себе.
У него есть автор, источник, дата, просмотры, реакции, комментарии, медиа, ссылки и другие связанные сущности.
А если одновременно собирать информацию из нескольких каналов или чатов, возникает ещё одна задача: как всё это нормально хранить и не превратить базу в набор CSV‑файлов, которые потом невозможно использовать.
В этой статье разберём, как я подошёл к этой задаче.
Почему просто собрать текст постов недостаточно
Представим обычную задачу.
Есть 30 Telegram‑каналов по одной тематике. Нужно понять:
о чём они пишут;
какие публикации получают больше внимания;
какие темы повторяются;
кто участвует в обсуждениях;
какие посты похожи друг на друга.
Если мы сохраним только:
{ "date": "...", "text": "..." }
то большую часть полезной информации потеряем.
Поэтому модель данных начинает выглядеть примерно так:
Channel │ ├── Post │ ├── Media │ ├── Reactions │ └── Comments │ └── Users └── Activity
Причём один и тот же пользователь может встретиться в нескольких источниках.
И здесь начинается самое интересное.
Один пользователь — несколько источников
Допустим, мы парсим три чата:
@crypto_chat @invest_chat @trading_chat
Пользователь с Telegram ID 123456789 может присутствовать во всех трёх.
Хранить его как три разных записи — плохая идея.
Поэтому логичнее разделить сущности.
Условно:
telegram_user = { "telegram_id": 123456789, "username": "example", "first_name": "Alex" }
А связь с источником хранить отдельно:
source_member = { "user_id": 123456789, "source_id": 42 }
Получается довольно простая модель:
telegram_users │ ├──── source_members ──── channel A │ ├──── source_members ──── channel B │ └──── source_members ──── chat C
Теперь можно ответить не только на вопрос «кто этот пользователь?», но и на вопрос:
В каких источниках он встречался?
И это уже гораздо полезнее обычного списка участников.
А что с постами?
С постами примерно такая же история.
Сам пост можно представить как отдельную сущность:
post = { "telegram_message_id": 1842, "source_id": 17, "text": "...", "date": "...", "views": 15200 }
Но дальше у него появляются связанные данные.
Например, реакции:
reaction = { "post_id": 1842, "reaction": "👍", "count": 327 }
Комментарии:
comment = { "post_id": 1842, "user_id": 123456789, "text": "Интересный материал", "date": "..." }
Медиа:
media = { "post_id": 1842, "type": "photo", "file_id": "..." }
В итоге один пост превращается в небольшое дерево связанных данных.
И именно это, на мой взгляд, гораздо интереснее обычного «Telegram scraper».
Telethon здесь по‑прежнему остаётся хорошей отправной точкой
В предыдущей статье я уже показывал базовую работу с Telethon, поэтому повторяться с авторизацией не буду.
Сам принцип получения сообщений практически не меняется:
from telethon import TelegramClient client = TelegramClient( "session", api_id, api_hash ) async def parse_channel(channel): async for message in client.iter_messages(channel): print( message.id, message.date, message.text )
Но дальше возникает вопрос: что делать с полученным объектом?
Можно сразу писать его в CSV.
А можно сначала нормализовать.
Я выбрал второй вариант.
Нормализация данных
На практике удобно сначала превратить Telegram‑объект в собственную модель.
Например:
def normalize_message(message, source_id): return { "telegram_message_id": message.id, "source_id": source_id, "text": message.text or "", "date": message.date, "views": message.views or 0, "forwards": message.forwards or 0, }
Теперь остальная система вообще не обязана знать, что данные пришли из Telethon.
Это небольшая деталь, но она сильно упрощает дальнейшее развитие проекта.
Если завтра изменится способ получения данных, слой хранения при этом можно оставить прежним.
Самая неприятная часть — дубликаты
Когда начинаешь собирать данные из нескольких источников, очень быстро обнаруживаешь дубликаты.
Например, один и тот же пользователь встретился:
канал A → user 123 чат B → user 123 чат C → user 123
Если просто делать:
users.append(user)
получим три записи.
Поэтому ключом пользователя становится Telegram ID:
users_by_id[user.id] = user
В базе это уже выражается уникальным ограничением:
UNIQUE(telegram_id)
И получается:
123456789 → один пользователь
а информация о том, где он встретился, хранится отдельно.
Это один из тех моментов, которые на маленьком скрипте вообще не заметны, а на реальном объёме данных начинают иметь огромное значение.
Parsing Job — ещё одна важная сущность
Есть ещё одна проблема.
Допустим, сегодня мы спарсили:
20 каналов 100 000 постов
А завтра пользователь снова запускает парсинг.
Как понять:
Какие данные были получены именно в результате этого запуска?
Поэтому я считаю полезным хранить отдельную сущность — задачу парсинга.
Например:
parsing_job = { "id": 1024, "type": "posts", "status": "completed", "created_at": "...", }
А дальше данные связываются с этой задачей.
Получается:
Parsing Job │ ├── Source A ├── Source B ├── Source C │ └── Parsed data
Это даёт довольно приятную возможность.
Можно посмотреть не только:
«Все посты в моей базе»
но и:
«Покажи мне результат конкретного запуска парсинга».
Почему я не стал делать отдельную базу для каждого парсинга
На первый взгляд кажется логичным:
Парсинг №1 → таблица №1 Парсинг №2 → таблица №2 Парсинг №3 → таблица №3
Но довольно быстро это становится неудобно.
Гораздо интереснее иметь единую нормализованную базу.
Например:
telegram_users sources posts comments reactions media parsing_jobs source_members
А связи между сущностями позволяют построить практически любое представление.
Например:
Все пользователи
или:
Пользователи из @channel
или:
Пользователи, встречавшиеся одновременно в @channel1 и @channel2
или:
Посты конкретного запуска парсинга
То есть физически данные лежат вместе, а логически пользователь может работать с ними как с разными коллекциями.
Откуда брать пользователей
Здесь Telegram становится особенно интересным.
В зависимости от типа источника и доступных данных можно работать не только со списком участников.
Например, можно собирать пользователей, которые:
писали сообщения;
оставляли комментарии;
взаимодействовали с публикациями;
встречались в нескольких источниках.
При этом я бы не пытался сделать из этого «магический сбор всех пользователей Telegram».
Доступность конкретных данных зависит от типа источника, прав аккаунта и возможностей Telegram API.
Это важно учитывать ещё на этапе проектирования.
Фильтрация данных
Когда база уже нормализована, появляется возможность фильтровать результаты.
Например, при сборе пользователей можно использовать такие условия:
def include_user(user): if user.bot: return False if not user.username and not user.phone: return False return True
Это всего лишь упрощённый пример.
В реальном парсере условия становятся параметрами задачи:
Не добавлять ботов Только пользователи с телефоном Только активные пользователи
И это уже намного удобнее, чем потом чистить огромный CSV руками.
«Старые» аккаунты — интересный фильтр
Ещё одна полезная характеристика — активность пользователя.
Например, можно задать условие:
Не включать пользователей, которые давно не заходили в Telegram.
На уровне данных это выглядит примерно так:
from datetime import datetime, timedelta limit = datetime.now() - timedelta(days=180) if user.last_seen and user.last_seen < limit: skip_user(user)
Но здесь тоже есть важный нюанс.
Информация о последнем посещении Telegram доступна не всегда и может зависеть от настроек приватности пользователя и типа получаемых данных.
Поэтому корректнее считать такой фильтр условным:
есть информация об активности → применяем фильтр нет информации → отдельное состояние
а не автоматически считать пользователя неактивным.
Что в итоге получается
После всех этих изменений парсер начинает выглядеть уже немного иначе.
Это не просто:
Telegram → CSV
а скорее:
Telegram │ ▼ Telethon │ ▼ Normalization │ ▼ Parsing Job │ ▼ ┌────────────────┐ │ Database │ ├────────────────┤ │ Users │ │ Sources │ │ Posts │ │ Comments │ │ Reactions │ │ Media │ │ Relationships │ └────────────────┘ │ ┌────────┴────────┐ ▼ ▼ Filters Collections │ │ └────────┬────────┘ ▼ Export
И вот здесь появляется следующий интересный вопрос:
а зачем вообще держать такую базу только ради экспорта?
Если мы уже сохранили посты, реакции, комментарии и метаданные, то поверх этих данных можно строить аналитику.
Например, искать публикации, которые получили необычно высокий отклик.
Или сравнивать несколько каналов между собой.
Или искать похожие публикации.
Или автоматически классифицировать контент.
Именно на этом этапе я решил добавить в проект ещё один слой — AI.
Что можно сделать с собранными постами с помощью AI
Допустим, после парсинга у нас получилось:
12 000 постов 37 каналов 8 тематик
Можно отправить выбранную коллекцию на обработку и получить, например:
Исходный пост ↓ AI ↓ краткая выжимка
Или:
Исходный пост ↓ AI ↓ уникализированная версия
Или вообще:
12 000 постов ↓ AI-анализ ↓ темы ключевые слова похожие публикации лучшие посты
Причём исходные данные при этом не должны изменяться.
Логика получается такой:
Исходная коллекция │ ▼ AI Job │ ▼ Новая коллекция
Так можно сохранить историю обработки и всегда сравнить результат с оригиналом.
Почему мне нравится именно такая архитектура
В начале проекта казалось, что я делаю обычный телеграм парсер.
Но по мере развития стало понятно, что сам парсинг — только первый этап.
Сначала мы получаем данные.
Потом нормализуем их.
Потом связываем между собой.
Потом фильтруем.
Потом анализируем.
И только после этого появляется настоящая ценность данных.
Получается небольшая цепочка:
Telegram ↓ ТГ парсинг ↓ Сырые данные ↓ Нормализация ↓ Единая база ↓ Фильтрация ↓ Аналитика ↓ AI
Именно поэтому я постепенно отошёл от идеи «написать ещё один Telegram scraper» и начал делать интерфейс, в котором с собранными данными можно нормально работать.
А можно ли всё это сделать без интерфейса?
Конечно.
Для первого прототипа вообще достаточно нескольких десятков строк Python и Telethon.
Но как только источников становится больше, появляется необходимость:
хранить результаты разных запусков;
не создавать дубликаты;
фильтровать данные;
повторно использовать уже собранную информацию;
экспортировать разные выборки;
понимать происхождение каждой записи.
И тогда CLI‑скрипт постепенно превращается в маленькую информационную систему.
В моём случае интерфейс в итоге оказался даже важнее самого парсера.
Что получилось в итоге
Я продолжил тот самый эксперимент из предыдущей статьи и собрал поверх Telegram API полноценный интерфейс для работы с собранными данными.
Сейчас в нём можно работать не только с постами, но и с пользователями, источниками и результатами отдельных запусков.
Для постов сохраняются не только текст и дата, но и доступные метаданные, медиа, реакции и комментарии.
Данные собираются в единую нормализованную базу, поэтому один и тот же пользователь или источник не превращается в десятки независимых записей.
А поверх коллекций можно запускать дальнейшую обработку, в том числе AI‑анализ.
Проект пока находится в активной разработке, поэтому я оставил его доступным для бесплатного тестирования.
Если кому‑то интересно покрутить такой подход на своих задачах — можно попробовать.
Если материал зайдёт, в следующей статье могу отдельно разобрать архитектуру фонового парсинга: как организовать несколько Telegram‑аккаунтов, прокси, очереди задач и параллельную обработку так, чтобы система не развалилась, когда одновременно запускают парсинг десятки пользователей.

