Создаем пет-проект по аналитике в связке с GitHub Actions. Часть 2

Привет, Хабр! Продолжаю обозревать GitActions на примере пет проекта для аналитика.

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

Создаем пет-проект по аналитике в связке с GitHub Actions. Часть 2 - 1

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

Выбираем проект и проблему

Лучшим вариантом будет выбрать не просто проект по анализу данных с kaggle, а проект, приближенный к реальности. То есть тот, который включает в себя:

  • Реальные данные — те, которые можно получить в настоящий момент времени

  • Использование нескольких инструментов (sql, python, git, excel — в любой комбинации)

  • Наличие проблемы и решения этой конкретной проблемы

  • Корректные и применимые выводы

То есть идеальный проект — это по факту исследовательская задача по рабочему проекту, для которой ты сам  выбираешь способ реализации.

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

Для этой статьи я подобрала удобный пример — парсинг данных с тг каналов. Во первых, для меня эта тема актуальна из-за ведения тг канала, во вторых это вполне доступная история:)

Особой проблемы у меня нет, поэтому накручу ее себе — к примеру, я хочу понимать статистику по tg-каналам конкурентов. Для выстраивания стратегии по ведению мне необходимо понимать:

  • Как растут чужие tg-каналы (количество подписчиков, ежедневный рост)

  • Охваты постов

  • Частота публикации контента

  • Темы публикации

  • Вовлеченность пользователей в контент (комментарии, лайки, просмотры)

  • Топовые темы/ ключевые слова для публикаций, которые выходят в топ по метрикам вовлеченности

  • Наличие реклам

Делаю вид, что tg-stat в моей реальности не существует или там недостаточно глубокий анализ для моего запроса -> потому у меня есть потребность сделать этот проект вручную без использования уже существующих инструментов.

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


Где брать данные?

Как уже упомянула ранее — данные нам можно доставать через API или парсинг сайтов/ страниц.
Вот несколько источников, которые можно использовать для своих проектов:

  • Apple store через официальный API

  • Google Play через библиотеку google-play-scraper

  • Telegram — через API или библиотеки (разберем ниже)

  • Alpha Vantage через API

  • World Bank по API

  • Faker по API

Так как я разбираю Tg, то данные буду брать из него.
Из телеграмма можно доставать данные через python библиотеки, а также через обходные пути (парсинг html страниц).
На что будем обращать внимание:

Интересующие нас параметры

Telethon

Парсинг

Дата и время поста

Просмотры

Полный текст

Реакции

Количество подписчиков

Официальный способ

Сбор через личный tg аккаунт

Сбор без логина в tg

Отсутствие лимитов по запросам и времени между запросами 

Наличие риска бана аккаунта

Простота реализации

Моя первая попытка была именно через Telethon и, после пары первых успешных попыток, я постоянно начала ловить разлогирование со своего аккаунта. И эта проблема не решилась даже после добавления задержек между запросами.

Еще одной причиной этой проблемы были повторяющиеся запросы — tg их легко отлавливает и стопорит.

Когда мой акк в tg заблочили на ~30 минут первый раз я решила, что такого я больше не переживу и надо искать способ попроще для моих нервов.

С учетом того, что я настраивала ежедневный сбор данных, то у меня не было задачи смотреть в глубину канала и проверять посты годовой давности. Для такой постановки вопроса парсинг звучит проще и реализуемей (и головной боли тоже становится меньше).

Поэтому для этого проекта выбор остановила именно на простом парсинге html страниц tg-каналов (те самые, которые открываются в браузере, когда ты переходишь по ссылке).


Реализация и скрипты

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

TEST_REPO_FOR_HABR
│
├── .github
│   └── workflows
│       └── run_daily.yml
│
├── .gitignore
├── requirements.txt
│
├── parser.py
├── post_analysing.py
├── delete_old_data.py
│
├── channels.csv
├── parsed_data.csv
│
└── README.md

Создание технических файлов (.gitignore и requirements.txt) / файлов из .github описала в прошлой статье. А остальные разберем ниже подробнее.

Для проекта создадим три python файла:

  • parser.py — для парсинга нужных данных

  • post_analysing.py — для анализа данных/ подсчета метрик/ построения визуализации

  • delete_old_data.py — для удаления данных старше N месяцев

И создадим 2 csv файла:

  • channels.csv — список каналов, которые будем использовать для анализа

  • parsed_data.csv — данные, которые собираем через parser.py

Пример рабочего скрипта parser.py 

Этот скрипт будет записывать количество подписчиков ежедневно по каждому каналу. Для того, чтобы пощупать этот способ и собрать свои первые данные первый раз — самое то.

import os
import csv
import re
import time
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options

# Настраиваем драйвер для чтения страницы
options = Options()

# Без открытия окна браузера
options.add_argument('--headless') 
options.add_argument('--disable-gpu')
options.add_argument('--no-sandbox')

 # Отключаем ненужные логи
options.add_argument('--log-level=3')

# Создаём сервис с подавлением логов
service = Service(ChromeDriverManager().install(), log_path='NULL')   

# Настраиваем драйвер для запуска браузера
driver = webdriver.Chrome(service=service, options=options)

# Достаем список каналов
channel_list = []

with open('channels.csv', 'r', encoding='utf-8') as ch_file:
    reader = csv.DictReader(ch_file)
    for row in reader:
        url = row['url'].strip().strip("'"")
        if url and url.startswith("https://t.me/"):
            channel_list.append(url)

# Указываем файл, куда будем сохранять данные по каналу
csv_file = 'parsed_data.csv'
csv_exists = os.path.exists(csv_file)

# Получаем сегодняшнюю дату
date_today = datetime.now().strftime("%Y-%m-%d")

# Проверяем, есть ли уже данные за сегодня
already_parsed_today = False
saved_entries = set()

# Читаем ранее сохранённые username
if csv_exists:
    with open(csv_file, 'r', encoding='utf-8', newline='') as f:
        reader = csv.DictReader(f)
        name_field = 'username' if 'username' in reader.fieldnames else 'channel_name'
        for row in reader:
            saved_entries.add((row['date'], row[name_field]))
            if row['date'] == date_today:
                already_parsed_today = True

if already_parsed_today:
    print(f"n🟡 Данные за {date_today} уже есть — парсинг не выполняется.")
    driver.quit()
    exit()

# Парсим и сохраняем данные

with open(csv_file, 'a', encoding='utf-8', newline='') as f:
    writer = csv.writer(f)
    if not csv_exists:
        writer.writerow(['date', 'channel_name', 'subscribers'])

    print("n🟢 Запуск парсинга и сохранение данных:n" + "-" * 60)

    for url in channel_list:
        if (date_today, url) in saved_entries:
            print(f"⏭️ Пропущено (уже есть): {url}")
            continue

        connect_status = "❌"
        data_status = "❌"
        clean_title = "—"
        subs = "—"

        try:
            driver.get(url)
            connect_status = "✅"
            time.sleep(5)
            text = driver.find_element("tag name", "body").text
            lines = text.splitlines()
            
            for i, line in enumerate(lines):
                if "subscribers" in line.lower():
                    raw_title = lines[i - 1] if i > 0 else "—"
                    subs = re.sub(r'[^d]', '', line).strip()
                    channel_name = re.sub(r'[^а-яА-Яa-zA-ZёЁs]', '', raw_title).strip()
                    data_status = "✅"
                    break

        except Exception as e:
            channel_name = f"⚠️ Ошибка: {str(e)}"

        print(f"{channel_name} — {date_today} — {subs} ({url})")
        
        if data_status == "✅":
            writer.writerow([date_today, channel_name, subs])

driver.quit()

Можем переходить к следующему шагу — анализ данных и отправка результатов. Для отправки нам понадобится:

  • Создать приватный tg канал

  • Создать бота в tg

  • Раздать админские права боту в созданном tg канале

  • Получить токен бота и id tg канала

  • Добавить полученные данные в secrets в Git

Описывать эти действия тут не буду дабы не плодить кучу повторяющейся информации в интернете.
Но вообще для этих действий пригодится только бот BotFather — в нем уже есть встроенная инструкция по созданию бота.


Перейдем к написанию скрипта для анализа данных, визуализации и отправки в созданный приватный канал.

Пример несложного скрипта можно подсмотреть ниже.

import os
import requests
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime, timedelta
from dotenv import load_dotenv
load_doten()

# Загружаем данные по каналам, которые мы уже собрали на предыдущем шаге
df = pd.read_csv('parsed_data.csv')
df['date'] = pd.to_datetime(df['date'])
df['subscribers'] = df['subscribers'].astype(int)

# Фильтруем данные за последние 30 дней
one_month_ago = datetime.now() - timedelta(days=30)
one_week_ago = datetime.now() - timedelta(weeks=1)
df = df[df['date'] >= one_month_ago]
df_week = df[df['date'] >= one_week_ago]

# Выбираем целевой канал, на который будем ориентироваться при построении визуализации. 
# Условно - это канал, который для нас наиболее интересен.

# Выбираю канал по слову в его наименовании
target_channel = 'ANY_CHANNEL_NAME'
target_matches = df[df['channel_name'].str.contains(target_channel, case=False, na=False)]
if target_matches.empty:
    raise ValueError(f"Не найден канал с ключом: {target_channel}")
main_channel_name = target_matches['channel_name'].iloc[0]

# Вычисляем прирост для всех каналов за 30 дней
growth = df.groupby('channel_name')['subscribers'].agg(['min', 'max'])
growth['delta'] = growth['max'] - growth['min']
growth = growth.reset_index()
main_subs = growth[growth['channel_name'] == main_channel_name]['max'].values[0]

# Отбираем ближайшие каналы к нашему целевому. Это сделано для того, чтобы снизить разброс между каналами на графике.

closest_above = growth[growth['max'] > main_subs].sort_values(by='max').head(10)
closest_below = growth[growth['max'] < main_subs].sort_values(by='max', ascending=False).head(3)
main_channel_row = growth[growth['channel_name'] == main_channel_name]
top_channels = pd.concat([closest_below, main_channel_row, closest_above])

# Формируем текст с метриками прироста
metrics_text = "U0001F4CA <b>Метрики прироста за месяц:</b>n"
metrics_text += "_________________________________nn"
for , row in topchannels.sort_values(by='max').iterrows():
    start = row['min']
    end = row['max']
    delta = row['delta']
    percent = (delta / start * 100) if start > 0 else 0
    channel_label = row['channel_name']
    icon = 'U0001F31D' if channel_label.strip().lower() == 'ANY_CHANNEL_NAME' else 'U0001F4AC'
    metrics_text += (
        f"{icon} <b>{channel_label}</b>n"
        f"  Подписчиков: {end}n"
        f"  Прирост за месяц: <b>{percent:.2f}%</b>nn"
    )

# Фильтруем данные только для нужных каналов
df_plot = df[df['channel_name'].isin(top_channels['channel_name'])]

# Сортируем каналы по числу подписчиков
channel_order = (
    top_channels.sort_values(by='max', ascending=False)['channel_name'].tolist()
)

# Строим график
def wrap_label(text, max_len=25):
    words = text.split()
    lines = []
    current = ""
    for word in words:
        if len(current + " " + word) <= max_len:
            current += " " + word if current else word
        else:
            lines.append(current)
            current = word
    if current:
        lines.append(current)
    return 'n'.join(lines)

plt.figure(figsize=(12, 6))

for name in channel_order:
    group = df_plot[df_plot['channel_name'] == name].sort_values('date')
    if name.strip().lower() == main_channel_name.strip().lower():
        plt.plot(
            group['date'],
            group['subscribers'],
            label=f'>> {name} <<',
            color='red',
            linewidth=2
        )

    else:
        plt.plot(
            group['date'],
            group['subscribers'],
            label=wrap_label(name),
            linestyle='--',
            linewidth=1.5,
            alpha=0.8
        )

plt.title('Динамика подписчиков за последние 30 дней')
plt.grid(True)
plt.legend(
    loc='center left',
    bbox_to_anchor=(1.0, 0.5),
    borderaxespad=0.5,
    fontsize=9,
    frameon=False
)

plt.subplots_adjust(right=0.75)
min_date = df['date'].min()
max_date = df['date'].max()
tick_dates = pd.date_range(start=min_date, end=max_date, freq='5D')
plt.xticks(tick_dates, rotation=0)
plot_path = 'participants_growth.png'
plt.savefig(plot_path)

# Отправка текста и графика в Telegram
bot_token = os.getenv('bot_token')
channel_id = os.getenv('channel_id')
send_text_url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
payload = {
    'chat_id': channel_id,
    'text': metrics_text,
    'parse_mode': 'HTML'
}

requests.post(send_text_url, data=payload)
send_photo_url = f"https://api.telegram.org/bot{bot_token}/sendPhoto"
with open(plot_path, 'rb') as photo:
    requests.post(
        send_photo_url,
        data={'chat_id': channel_id},
        files={'photo': photo}
    )

Результаты

Скрипт выдает довольно скромную визуализацию и текст, при наличии желания всё это можно подстроить под свои хотелки.

Создаем пет-проект по аналитике в связке с GitHub Actions. Часть 2 - 2
Создаем пет-проект по аналитике в связке с GitHub Actions. Часть 2 - 3

Такие полученные результаты полноценным проектом назвать нельзя, так как мы еще не закрыли остальные требования к хорошему проекту. Поэтому не рекомендую останавливаться на этом шаге. Как минимум тут нужны выводы, а только по росту каналов выводов особо не сделаешь :). Поэтому, в любом случае, нужно добавлять дополнительные метрики и погружаться в дальнейший анализ.

Анализ текста, просмотров, наличие реклам и прочего оставлю на будущие разборы, но к тому моменту возможно у вас на руках уже будет доработанный проект :)

Итоги

Парсинг данных в сочетании с GitHub Actions — хороший и простой способ для создания своего небольшого проекта для портфолио.

Это точно лучше заезженных историй с «датасетом по Титанику», поэтому 100 % рекомендую для развития на старте.

Еще больше полезных материалов в моем TG-канале. Подписывайтесь и читайте контент по ссылке.

Автор: Alena_Les

Источник

Оставить комментарий