Сегодня у многих есть свой Telegram-канал: личный блог, канал продукта, проектная рассылка или просто канал "для своих". Посты публикуются, идут реакции, подписчики иногда растут, иногда падают. Но до статистики большинство добирается редко: нужно отдельно открывать статистику от телеграм, которая не у всех то и доступна, смотреть графики, считать охваты и пытаться понять, какие посты зашли, а какие нет.

Очевидный путь - автоматизировать все самому: поднять VPS, поставить туда Python, написать скрипт, настроить crontab, следить за работой и молиться, что все будет работать без ошибок. Ради одного короткого задания раз в день это выглядит избыточно, приходится платить за целую виртуалку и тратить время на настройку.

В этой статье я покажу, как сделать проще: собрать небольшой Python-скрипт, который раз в день отправляет вам в Telegram краткий отчет по каналу: количество постов, просмотры, репосты, реакции, самые популярные реакции и топ-посты за указанный вами период. Без своего сервера и без настройки crontab: запуском по расписанию будет заниматься сервис Cron Jobs в Amvera Cloud, а вы будете платить только за фактическое время работы контейнера и буквально копейки за ожидание запуска.

В этой статье будет показан конкретный вариант реализации привязанный к нашему сервису. Но вы можете сделать по аналогии у любого другого провайдера, внеся минимальные изменения. Вы же можете дополнять код, улучшать и делать с ним все, к чему душа лежит, т.к. в конце будет доступна ссылка на исходники бота.

В статье разберем:

  • как с помощью клиента в Pyrogram получить статистику по постам канала (просмотры, репосты, реакции, прочее);

  • как сформировать простой отчет, который удобно читать прямо в Telegram;

  • как настроить и запусить это все в Amvera буквально за считанные минуты.

В итоге у вас получится простенький проект, который раз в определенный период будет отсылать настоящую статистику.

Пример статистики
Пример статистики

Что понадобится

Для повторения примера из статьи вам понадобится:

  • Аккаунт в Telegram и свой канал, в котором вы либо владелец, либо админ;

  • Аккаунт в Amvera Cloud - создадим чуть позже;

  • Созданный файл сессии Telegram - также создадим позже;

  • 20 минут свободного времени.

Теоретическая часть

Прежде чем начать разработку самого бота, давайте разберемся, как вообще все будет работать логически.

По сути у нас один разовый рабочий процесс:

  1. Скрипт стартует по расписанию;

  2. Забирает данные о постах в канале за нужный период;

  3. Собирает из этих данных сообщение-отчет;

  4. Отправляет сообщение в указанный чат.

Все это выполняет userbot - бот, запущенный с аккаунта пользователя Telegram. Т.е. все действия выполняются именно от имени вашего аккаунта. Через него мы можем получить всю необходимую информацию о канале и счетчики просмотров, репостов, реакций и т.д.

Задача занимает буквально 5-10 секунд в зависимости от числа постов, что очень выгодно для нас.

Алгоритм работы бота

Кратко логика такая:

  1. Прочитать переменные окружения (канал для статистики, чат для отчета, период в днях и т.д.).

  2. Подключиться к Telegram через Pyrogram-сессию пользователя, файл которой мы создадим чуть позже.

  3. Определить временной диапазон (сейчас - N дней) и пройтись по постам канала за этот период.

  4. Для каждого поста собрать все доступные счетчики: просмотры, репосты, реакции, популярные реакции.

  5. Посчитать суммарные и средние значения, выделить несколько топ-постов, которые позже добавим в сообщение отчета.

  6. Прочитать прошлое количество подписчиков из stats.json, посчитать прирост и обновить файл.

  7. Сформировать текстовый отчет и отправить его в указанный чат.

Подготовка, разработка и деплой

Теперь, когда у нас есть ясная картина работы бота, мы можем начать подготовку.

Создание файла сессии

Как я говорил ранее, нам понадобится файл сессии .session. Его мы создадим с помощью специального скрипта init_session.py:

# pip install pyrogram tgcrypto
from pyrogram import Client

api_id = int(input("api_id: "))
api_hash = input("api_hash: ")
session_name = input("session name: ")

with Client(session_name, api_id=api_id, api_hash=api_hash) as app:
    print("Успешная авторизация как", app.me.id)

Фактически скрипт авторизовывается с помощью app_id и app_hash, который вы можете создать по ссылке, и создает файл сессии с введенным session name названием. После ввода session_name, начнется авторизация через номер телефона - просто следуйте инструкциям.

Если вы все сделали правильно, в директории запускаемого скрипта появится файл <session_name>.session - именно он нам понадобится вдальнейшем.

Знакомство с Amvera и регистрация

Что за сервис Amvera и зачем он нужен? Amvera Cloud - облачный сервис для быстрого и удобного деплоя IT-приложений различной сложности. Его особенность в простоте деплоя и обновлений любого проекта.

Из бонусов:

  • Можно сделать git push amvera master и сервис сам все соберет и настроит.

  • Встроенный сервис запуска Cron Jobs, что позволяет платить только за те минуты, пока выполняется наша крон-задача.

  • Стартовые 111 рублей после регистрации для тестов, которые вы получите без дополнительных условий;

А что интересно конкретно нам: вы платите лишь за работу самого приложения, запущенного с помощью Cron и копейки за время ожидания запуска.

На примере тарифа "Начальный Плюс CPU", вы платите 4 р./час за работающее приложение (а работать у нас проект будет не более 30 секунд в день).

Код main.py и файл зависимостей

Для деплоя приложения нам необходимо иметь 2 файла: main.py - сам код бота и requirements.txt - файл зависимостей в PIP.

Для нашего кода requirements.txt будет выглядеть так:

Pyrogram==2.0.106
TgCrypto==1.2.5
python-dotenv==1.2.1

Ранее мы уже описывали алгоритм работы бота, поэтому не вижу смысла расписывать каждую строчку кода.

main.py:

import os
import json
import asyncio

from datetime import datetime, timedelta, timezone

from dotenv import load_dotenv

from pyrogram import Client

STATE_FILE = os.getenv("TG_STATE_FILE", "stats.json")

def parse_chat_ref(text: str):
    if not text:
        return text
    text = text.strip()

    if text.startswith("http://") or text.startswith("https://"):
        if "t.me/" in text:
            text = text.split("t.me/", 1)[1]
        text = text.split("?", 1)[0]

    if text.startswith("@"):
        text = text[1:]

    if text.lstrip("-").isdigit():
        return int(text)

    return text


def format_int(n):
    return f"{int(n):,}".replace(",", " ")


async def main():
    load_dotenv()

    api_id = os.getenv("TG_API_ID")
    api_hash = os.getenv("TG_API_HASH")
    session_name = os.getenv("TG_SESSION_NAME")

    if not api_id or not api_hash or not session_name:
        raise RuntimeError("Нужно прописать TG_API_ID, TG_API_HASH и TG_SESSION_NAME в переменные окружения")

    api_id = int(api_id)

    stats_chat_raw = os.getenv("TG_STATS_CHANNEL")
    report_chat_raw = os.getenv("TG_REPORT_CHAT")

    if not stats_chat_raw or not report_chat_raw:
        raise RuntimeError("Нужно прописать TG_STATS_CHANNEL и TG_REPORT_CHAT в переменные окружения")

    period_days = int(os.getenv("TG_PERIOD_DAYS", "1"))
    top_posts = int(os.getenv("TG_TOP_POSTS", "3"))

    stats_chat_ref = parse_chat_ref(stats_chat_raw)
    report_chat_ref = parse_chat_ref(report_chat_raw)

    async with Client(
        name=session_name,
        api_id=api_id,
        api_hash=api_hash,
        workdir=".",
    ) as app:
        chat = await app.get_chat(stats_chat_ref)

        now = datetime.now(timezone.utc)
        since = now - timedelta(days=period_days)

        posts = 0
        views_sum = 0
        forwards_sum = 0
        reactions_sum = 0
        emoji_stats = {}
        top_list = []

        async for msg in app.get_chat_history(chat.id, limit=0):
            msg_date = msg.date
            if msg_date.tzinfo is None:
                msg_date = msg_date.replace(tzinfo=timezone.utc)

            if msg_date < since:
                break

            if getattr(msg, "service", False):
                continue

            posts += 1

			# Основные счетчики
            views = msg.views or 0
            forwards = msg.forwards or 0
            views_sum += views
            forwards_sum += forwards

            if msg.reactions and getattr(msg.reactions, "reactions", None):
                for r in msg.reactions.reactions:
                    count = r.count or 0
                    if count <= 0:
                        continue
                    reactions_sum += count
                    emoji = r.emoji or "⭐"
                    emoji_stats[emoji] = emoji_stats.get(emoji, 0) + count

            score = views * 10 + forwards
            top_list.append((score, views, forwards, msg.id))

        top_list.sort(reverse=True)
        if top_posts > 0:
            top_list = top_list[:top_posts]

        subs_now = chat.members_count or 0

        state = {}
        if os.path.exists(STATE_FILE):
            try:
                with open(STATE_FILE, "r", encoding="utf-8") as f:
                    data = json.load(f)
                    if isinstance(data, dict):
                        state = data
            except Exception:
                state = {}

        key = str(chat.id)
        old_info = state.get(key, {})
        old_subs = old_info.get("subscribers")
        subs_diff = None
        if isinstance(old_subs, int):
            subs_diff = subs_now - old_subs

        lines = []

        title = chat.title or chat.username or str(chat.id)
        start_str = since.strftime("%d.%m.%Y")
        end_str = now.strftime("%d.%m.%Y")

        lines.append(f"? Статистика канала «{title}»")
        if period_days == 1:
            lines.append(f"За последние 24 часа (с {start_str} по {end_str})")
        else:
            lines.append(f"За последние {period_days} дн. (с {start_str} по {end_str})")

        lines.append("")
        lines.append(f"Подписчики: {format_int(subs_now)}")
        if subs_diff is None:
            if subs_now:
                lines.append("• Новые за период: первый запуск, пока не с чем сравнить")
        else:
            sign = "+" if subs_diff >= 0 else ""
            lines.append(f"• Новые за период: {sign}{format_int(subs_diff)}")

        lines.append("")
        lines.append("Посты и охваты")
        lines.append(f"• Постов: {posts}")

        if posts > 0:
            lines.append(f"• Средний охват на пост: {format_int(views_sum // posts)}")
        else:
            lines.append("• Средний охват на пост: 0")

        lines.append(f"• Суммарные просмотры: {format_int(views_sum)}")

        if forwards_sum > 0 and posts > 0:
            lines.append(f"• Суммарные репосты: {format_int(forwards_sum)}")
            lines.append(f"• Средние репосты на пост: {format_int(forwards_sum // posts)}")
        else:
            lines.append("• Репостов пока не было")

        if reactions_sum > 0 and posts > 0:
            lines.append(f"• Суммарные реакции: {format_int(reactions_sum)}")
            lines.append(f"• Средние реакции на пост: {format_int(reactions_sum // posts)}")

            top_emoji = sorted(
                emoji_stats.items(),
                key=lambda x: x[1],
                reverse=True
            )[:3]

            if top_emoji:
                emoji_text = ", ".join(
                    f"{emoji} - {format_int(count)}" for emoji, count in top_emoji
                )
                lines.append(f"• Чаще всего ставят: {emoji_text}")
        else:
            lines.append("• Реакций пока не было")

        if top_list:
            lines.append("")
            lines.append("Топ постов за период:")
            username = chat.username

            for idx, (score, views, forwards, msg_id) in enumerate(top_list, start=1):
                if username:
                    link = f"https://t.me/{username}/{msg\\_id}"
                else:
                    link = f"ID сообщения: {msg_id}"

                line = f"{idx}) {format_int(views)} просмотров"
                if forwards > 0:
                    line += f", {format_int(forwards)} репостов"
                line += f" - {link}"
                lines.append(line)

        text = "\n".join(lines)

        await app.send_message(report_chat_ref, text)

        state[key] = {
            "subscribers": subs_now,
            "last_run": now.isoformat()
        }

        tmp_file = STATE_FILE + ".tmp"
        with open(tmp_file, "w", encoding="utf-8") as f:
            json.dump(state, f, ensure_ascii=False, indent=2)
        os.replace(tmp_file, STATE_FILE)


if __name__ == "__main__":
    asyncio.run(main())

Основную массу кода занимает формирование строки отчета.

Создание проекта и переменных окружения

Когда мы имеем все для запуска, мы можем начать создание проекта. Для этого откройте главную страницу проектов и жмите на плитку Cron Jobs -> Создайте первый Cron Job.

В открывшемся окне выбираем:

  • Название проекта: совершенно произвольное, это не играет роли;

  • Выберите тариф: для нашего проекта будет достаточно Начального Плюс CPU;

  • Подключить постоянное хранилище: жмем галочку;

  • Обьем: нам хватит и 1ГБ.

Создание проекта
Создание проекта

Жмём далее.

Теперь нужно настроить сам Cron Job.

  • Cron выражение: у вас тут будет свое значение - его можно взять из примеров в документации. Для теста я выберу * * * * *, что будет запускать код каждую минуту.

  • Максимальное время работы в секундах: можно поставить 600

  • Политика запуска: рекомендуется ставить REPLACE

Задание параметров запуска по крону
Задание параметров запуска по крону

Все остальные этапы создания проекта просто пропускаем.

После создания проекта, в разделе Cron Jobs появится ваш проект - откройте его и перейдите во вкладку "Переменные".

Здесь вам нужно создать следующие переменные окружения:

  • TG_API_ID: ваш API_ID, используемый при создании файла сессии

  • TG_API_HASH: ваш API_HASH, используемый при создании файла сессии

  • TG_STATS_CHANNEL: канал, чью статистику вы будете собирать. Рекомендуется писать в формате @username.

  • TG_REPORT_CHAT: айди чата, в который будут присылаться отчеты

  • TG_TOP_POSTS: топ из скольких постов будет писаться в отчете

  • TG_SESSION_NAME: название файла сессии без расширения .session

  • TG_STATE_FILE: обязательно /data/stats.json

  • TG_PERIOD_DAYS: период сбора статистики из постов в днях (посты за сколько дней будут просмотрены)

Задание переменных
Задание переменных

Загрузим файлы. Переходим во вкладку "Репозиторий" и загружаем в Code файлы main.py, requirements.txt, файл сессии.

Остался последний шаг - настройка конфигурации приложения. Тут перейдите во вкладку "Конфигурация" и просто повторите настройки за мной:

После применения конфигурации нажимаем кнопку "Собрать" - и все, в течение пары минут приложение соберется и начнет работу!

Исходники проекта уже доступны на GitHub!

Итог

Давайте коротко подведем итог, что мы сделали.

  1. Создали простого юзербота, которые от имени вашего аккаунта собирает статистики по постам и отправляет отчет в определенный чат

  2. Упаковали это все в проект на Amvera.

  3. Сэкономили на VPS.

Далее можно развивать идею как угодно: добавлять выгрузку в GoogleSheets, строить свои метрики или отправлять разные отчеты в разные чаты - тут работает только ваша фантазия!

Комментарии (0)