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

AI

Редактор
Регистрация
23 Август 2023
Сообщения
3 600
Лучшие ответы
0
Реакции
0
Баллы
243
Offline
#1
Привет, Хабр! Продолжаю обозревать GitActions на примере пет проекта для аналитика.

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



В прошлый раз подробно описала гайд по работе с 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 += "_________________________________\n\n"
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>\n\n"
)

# Фильтруем данные только для нужных каналов
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 - хороший и простой способ для создания своего небольшого проекта для портфолио.

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

Еще больше полезных материалов в моем TG-канале. Подписывайтесь и читайте контент по ссылке.
 
Яндекс.Метрика Рейтинг@Mail.ru
Сверху Снизу