Анализ целевой аудитории сообщества VK: различия между версиями

Материал из Поле цифровой дидактики
Нет описания правки
Нет описания правки
 
(не показаны 23 промежуточные версии этого же участника)
Строка 1: Строка 1:
= Анализ аудитории сообществ ВКонтакте =
        <p><b>Автор:</b> Сабитова Алина </p>
        <p><b>Группа:</b> АДЭУ-221</p>
        <p><b>Дисциплина:</b> Работа с API социальных сетей и облачных сервисов</p>
        <p><b>Статус проекта:</b> Выполнен</p>
{| class="wikitable"
{| class="wikitable"
|-
|-
Строка 46: Строка 52:
Структура проекта в VS Code:
Структура проекта в VS Code:


[[Файл:Vk api1.png|600px]]
vk_analytics/
├── app.py
├── templates/
│  └── index.html
└── static/
    └── style.css 
 
; 📄 app.py
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.
 
; 📁 templates/index.html
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.
 
; 📁 static/style.css
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.


'''app.py''' — основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask:
== Код приложения ==


<pre>
=== app.py ===
{| class="wikitable mw-collapsible mw-collapsed"
! Показать код
|-
|<syntaxhighlight lang="python">
from flask import Flask, render_template, request, jsonify
from flask import Flask, render_template, request, jsonify
import vk_api
import vk_api
Строка 56: Строка 80:
matplotlib.use('Agg')   
matplotlib.use('Agg')   
import matplotlib.pyplot as plt
import matplotlib.pyplot as plt
import seaborn as sns
from collections import Counter
from collections import Counter
from datetime import datetime
from datetime import datetime
Строка 61: Строка 86:
import base64
import base64
import io
import io
import pandas as pd
import time


app = Flask(__name__)
app = Flask(__name__)
Строка 67: Строка 94:
os.makedirs('static', exist_ok=True)
os.makedirs('static', exist_ok=True)


TOKEN = "Ваш токен"
# Настройка стиля seaborn
sns.set_style("whitegrid")
sns.set_palette("husl")
 
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА
TOKEN = "ваш_токен_сюда"


# Собирает данные о сообществе
def get_community_data(group_id):
def get_community_data(group_id):
    """Собирает данные о сообществе"""
     try:
     try:
         vk_session = vk_api.VkApi(token=TOKEN)
         vk_session = vk_api.VkApi(token=TOKEN)
Строка 88: Строка 120:
             members_count = group['members_count']
             members_count = group['members_count']
         except:
         except:
            # Если не получилось, пробуем как числовой ID
             group = vk.groups.getById(group_id=str(group_id_for_api), fields='members_count,description')[0]
             group = vk.groups.getById(group_id=str(group_id_for_api), fields='members_count,description')[0]
             community_name = group['name']
             community_name = group['name']
             members_count = group['members_count']
             members_count = group['members_count']
          
          
         # Собираем подписчиков
         # Собираем подписчиков с пагинацией (до 2000)
         members_data = []
         members_data = []
         try:
         try:
             members = vk.groups.getMembers(group_id=group_id_for_api, fields='sex,bdate,city', count=500)
             total_to_collect = min(2000, members_count)
            for user in members['items']:
            offset = 0
                 user_info = {}
            batch_size = 1000
           
            while offset < total_to_collect:
                members = vk.groups.getMembers(
                    group_id=group_id_for_api,  
                    fields='sex,bdate,city',  
                    count=batch_size,
                    offset=offset
                 )
                  
                  
                 # Пол
                 for user in members['items']:
                if user.get('sex') == 1:
                    user_info = {}
                    user_info['sex'] = 'Женщины'
                   
                elif user.get('sex') == 2:
                    if user.get('sex') == 1:
                    user_info['sex'] = 'Мужчины'
                        user_info['sex'] = 'Женщины'
                else:
                    elif user.get('sex') == 2:
                    user_info['sex'] = 'Не указан'
                        user_info['sex'] = 'Мужчины'
                    else:
                        user_info['sex'] = 'Не указан'
                   
                    if user.get('bdate'):
                        bdate = user['bdate']
                        if len(bdate.split('.')) == 3:
                            try:
                                year = int(bdate.split('.')[2])
                                age = datetime.now().year - year
                                if 0 < age < 100:
                                    user_info['age'] = age
                            except:
                                pass
                   
                    if user.get('city') and user['city'].get('title'):
                        user_info['city'] = user['city']['title']
                   
                    members_data.append(user_info)
                  
                  
                 # Возраст
                 offset += len(members['items'])
                if user.get('bdate'):
                time.sleep(0.34)
                    bdate = user['bdate']
                    if len(bdate.split('.')) == 3:
                        try:
                            year = int(bdate.split('.')[2])
                            age = datetime.now().year - year
                            if 0 < age < 100:
                                user_info['age'] = age
                        except:
                            pass
                  
                  
                # Город
                if user.get('city') and user['city'].get('title'):
                    user_info['city'] = user['city']['title']
               
                members_data.append(user_info)
         except Exception as e:
         except Exception as e:
             print(f"Ошибка при сборе подписчиков: {e}")
             print(f"Ошибка при сборе подписчиков: {e}")
Строка 141: Строка 184:
                 }
                 }
                  
                  
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)
                 if post.get('attachments'):
                 if post.get('attachments'):
                     attach_type = post['attachments'][0]['type']
                     attach_type = post['attachments'][0]['type']
Строка 155: Строка 197:
                     post_info['type'] = 'Текст'
                     post_info['type'] = 'Текст'
                  
                  
                # ER
                 if post_info['views'] > 0:
                 if post_info['views'] > 0:
                     post_info['er'] = (post_info['likes'] + post_info['comments'] + post_info['reposts']) / post_info['views'] * 1000
                     post_info['er'] = (post_info['likes'] + post_info['comments'] + post_info['reposts']) / post_info['views'] * 1000
Строка 176: Строка 217:
         return {'success': False, 'error': str(e)}
         return {'success': False, 'error': str(e)}


# Генерирует графики
def generate_charts(members_data, posts_data):
def generate_charts(members_data, posts_data):
    """Генерирует графики с помощью seaborn"""
     charts = {}
     charts = {}
      
      
    # Настройка шрифтов для русских букв
     plt.rcParams['font.family'] = 'sans-serif'
     plt.rcParams['font.family'] = 'sans-serif'
     plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans']
     plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans']
Строка 186: Строка 226:
     # 1. Распределение по полу
     # 1. Распределение по полу
     if members_data:
     if members_data:
         fig, ax = plt.subplots(figsize=(6, 4))
         fig, ax = plt.subplots(figsize=(7, 5))
         sex_counts = Counter([m.get('sex', 'Не указан') for m in members_data])
         sex_counts = Counter([m.get('sex', 'Не указан') for m in members_data])
         ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct='%1.1f%%')
        colors = ['#ff6b6b', '#4ecdc4', '#95a5a6']
         ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct='%1.1f%%',
              colors=colors[:len(sex_counts)], startangle=90,
              explode=[0.02] * len(sex_counts), shadow=True)
          
          
         buf = io.BytesIO()
         buf = io.BytesIO()
         plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
         plt.savefig(buf, format='png', bbox_inches='tight', dpi=100, facecolor='white')
         buf.seek(0)
         buf.seek(0)
         charts['sex'] = base64.b64encode(buf.getvalue()).decode()
         charts['sex'] = base64.b64encode(buf.getvalue()).decode()
Строка 199: Строка 242:
     ages = [m['age'] for m in members_data if 'age' in m]
     ages = [m['age'] for m in members_data if 'age' in m]
     if ages:
     if ages:
         fig, ax = plt.subplots(figsize=(6, 4))
         fig, ax = plt.subplots(figsize=(8, 5))
         ax.hist(ages, bins=15, color='skyblue', edgecolor='black')
         sns.histplot(ages, bins=15, kde=True, color='#3498db', edgecolor='white', linewidth=1.5, alpha=0.7)
         ax.set_xlabel('Возраст')
         ax.set_xlabel('Возраст')
         ax.set_ylabel('Количество')
         ax.set_ylabel('Количество подписчиков')
        avg_age = sum(ages) / len(ages)
        ax.axvline(avg_age, color='#e74c3c', linestyle='--', linewidth=2, label=f'Средний: {avg_age:.1f} лет')
        ax.legend()
          
          
         buf = io.BytesIO()
         buf = io.BytesIO()
         plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
         plt.savefig(buf, format='png', bbox_inches='tight', dpi=100, facecolor='white')
         buf.seek(0)
         buf.seek(0)
         charts['age'] = base64.b64encode(buf.getvalue()).decode()
         charts['age'] = base64.b64encode(buf.getvalue()).decode()
Строка 213: Строка 259:
     cities = [m['city'] for m in members_data if 'city' in m and m['city'] != 'Не указан']
     cities = [m['city'] for m in members_data if 'city' in m and m['city'] != 'Не указан']
     if cities:
     if cities:
         fig, ax = plt.subplots(figsize=(8, 5))
         fig, ax = plt.subplots(figsize=(9, 6))
         city_counts = Counter(cities).most_common(5)
         city_counts = Counter(cities).most_common(5)
         city_names, city_values = zip(*city_counts)
         city_names, city_values = zip(*city_counts)
   
        # Создаём горизонтальную столбчатую диаграмму
        bars = ax.barh(city_names, city_values, color='lightcoral', edgecolor='darkred')
        ax.set_title('Топ-5 городов аудитории', fontsize=14, fontweight='bold')
        ax.set_xlabel('Количество подписчиков', fontsize=11)
        ax.set_ylabel('Город', fontsize=11)
          
          
        df = pd.DataFrame({'Город': city_names, 'Количество': city_values})
        df = df.sort_values('Количество', ascending=True)
       
        sns.barplot(data=df, y='Город', x='Количество', hue='Город', palette='rocket', legend=False, ax=ax)
        ax.set_xlabel('Количество подписчиков')
        ax.set_ylabel('Город')
         plt.tight_layout()
         plt.tight_layout()
   
       
         buf = io.BytesIO()
         buf = io.BytesIO()
         plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
         plt.savefig(buf, format='png', bbox_inches='tight', dpi=100, facecolor='white')
         buf.seek(0)
         buf.seek(0)
         charts['cities'] = base64.b64encode(buf.getvalue()).decode()
         charts['cities'] = base64.b64encode(buf.getvalue()).decode()
Строка 233: Строка 279:
     # 4. Вовлечённость по типам контента
     # 4. Вовлечённость по типам контента
     if posts_data:
     if posts_data:
         fig, ax = plt.subplots(figsize=(8, 5))
         fig, ax = plt.subplots(figsize=(10, 6))
         type_er = {}
         type_er = {}
         for post in posts_data:
         for post in posts_data:
             post_type = post['type']
             if post['type'] not in type_er:
            if post_type not in type_er:
                 type_er[post['type']] = []
                 type_er[post_type] = []
             type_er[post['type']].append(post['er'])
             type_er[post_type].append(post['er'])
          
          
         avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}
         avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}
         bars = ax.bar(avg_er.keys(), avg_er.values(), color='lightgreen', edgecolor='darkgreen')
         df_er = pd.DataFrame({'Тип контента': list(avg_er.keys()), 'ER': list(avg_er.values())})
         ax.set_ylabel('ER (на 1000 просмотров)', fontsize=11)
        df_er = df_er.sort_values('ER', ascending=False)
         plt.xticks(rotation=45)
       
        bars = sns.barplot(data=df_er, x='Тип контента', y='ER', hue='Тип контента', palette='viridis', legend=False, ax=ax)
         ax.set_ylabel('ER (активность на 1000 просмотров)')
         ax.set_xlabel('Тип контента')
          
          
        # Добавляем значения на столбцы
         for bar, val in zip(bars.patches, df_er['ER'].values):
         for bar, val in zip(bars, avg_er.values()):
             ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1,  
             ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,  
                   f'{val:.1f}', ha='center', va='bottom', fontsize=10, fontweight='bold')
                   f'{val:.1f}', ha='center', va='bottom', fontsize=9)
          
          
        plt.xticks(rotation=45)
         plt.tight_layout()
         plt.tight_layout()
          
          
         buf = io.BytesIO()
         buf = io.BytesIO()
         plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
         plt.savefig(buf, format='png', bbox_inches='tight', dpi=100, facecolor='white')
         buf.seek(0)
         buf.seek(0)
         charts['engagement'] = base64.b64encode(buf.getvalue()).decode()
         charts['engagement'] = base64.b64encode(buf.getvalue()).decode()
Строка 272: Строка 320:
         return jsonify({'success': False, 'error': 'Введите ссылку на сообщество'})
         return jsonify({'success': False, 'error': 'Введите ссылку на сообщество'})
      
      
    # Извлекаем ID сообщества из ссылки
     if 'vk.com/' in group_url:
     if 'vk.com/' in group_url:
         group_id = group_url.split('vk.com/')[-1].split('/')[0]
         group_id = group_url.split('vk.com/')[-1].split('/')[0]
Строка 278: Строка 325:
         group_id = group_url
         group_id = group_url
      
      
    # Собираем данные
     data = get_community_data(group_id)
     data = get_community_data(group_id)
      
      
Строка 284: Строка 330:
         return jsonify({'success': False, 'error': data.get('error', 'Ошибка при получении данных')})
         return jsonify({'success': False, 'error': data.get('error', 'Ошибка при получении данных')})
      
      
    # Генерируем графики
     charts = generate_charts(data['members'], data['posts'])
     charts = generate_charts(data['members'], data['posts'])
      
      
    # Считаем статистику
     stats = {}
     stats = {}
      
      
    # Пол
     if data['members']:
     if data['members']:
         sex_counts = Counter([m.get('sex', 'Не указан') for m in data['members']])
         sex_counts = Counter([m.get('sex', 'Не указан') for m in data['members']])
         stats['sex'] = dict(sex_counts)
         stats['sex'] = dict(sex_counts)
      
      
    # Возраст
     ages = [m['age'] for m in data['members'] if 'age' in m]
     ages = [m['age'] for m in data['members'] if 'age' in m]
     if ages:
     if ages:
         stats['avg_age'] = sum(ages) / len(ages)
         stats['avg_age'] = sum(ages) / len(ages)
        stats['min_age'] = min(ages)
        stats['max_age'] = max(ages)
      
      
    # Города
     cities = [m['city'] for m in data['members'] if 'city' in m and m['city'] != 'Не указан']
     cities = [m['city'] for m in data['members'] if 'city' in m and m['city'] != 'Не указан']
     if cities:
     if cities:
         stats['top_cities'] = Counter(cities).most_common(5)
         stats['top_cities'] = Counter(cities).most_common(5)
      
      
    # Посты
     if data['posts']:
     if data['posts']:
         avg_er = sum(p['er'] for p in data['posts']) / len(data['posts'])
         avg_er = sum(p['er'] for p in data['posts']) / len(data['posts'])
         stats['avg_er'] = avg_er
         stats['avg_er'] = avg_er
        # Лучший тип контента
         type_er = {}
         type_er = {}
         for post in data['posts']:
         for post in data['posts']:
Строка 320: Строка 357:
         stats['best_post_type'] = best_type
         stats['best_post_type'] = best_type
      
      
    # Рекомендации
     recommendations = []
     recommendations = []
     if 'avg_age' in stats:
     if 'avg_age' in stats:
         if stats['avg_age'] < 20:
         if stats['avg_age'] < 20:
             recommendations.append("🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход")
             recommendations.append("🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы")
         elif stats['avg_age'] < 30:
         elif stats['avg_age'] < 30:
             recommendations.append("🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото")
             recommendations.append("🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент")
         else:
         else:
             recommendations.append("🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты")
             recommendations.append("🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент")
      
      
     if stats.get('best_post_type'):
     if stats.get('best_post_type'):
Строка 335: Строка 371:
     if data['posts'] and stats.get('avg_er'):
     if data['posts'] and stats.get('avg_er'):
         if stats['avg_er'] < 50:
         if stats['avg_er'] < 50:
             recommendations.append("⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив")
             recommendations.append("⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив")
         elif stats['avg_er'] > 150:
         elif stats['avg_er'] > 150:
             recommendations.append("🔥 Отличная вовлечённость! Продолжайте в том же духе")
             recommendations.append("🔥 Отличная вовлечённость! Продолжайте в том же духе")
      
      
    # Добавляем рекомендацию по городам
     if stats.get('top_cities'):
     if stats.get('top_cities'):
         main_city = stats['top_cities'][0][0]
         main_city = stats['top_cities'][0][0]
        main_city_count = stats['top_cities'][0][1]
         recommendations.append(f"📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия")
        total_with_city = sum(count for _, count in stats['top_cities'])
        if len(stats['top_cities']) > 1:
            recommendations.append(f"🏙️ Топ-5 городов: {', '.join([f'{city} ({cnt})' for city, cnt in stats['top_cities'][:3]])}")
         recommendations.append(f"📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу")
      
      
     return jsonify({
     return jsonify({
Строка 359: Строка 390:
if __name__ == '__main__':
if __name__ == '__main__':
     app.run(debug=True)
     app.run(debug=True)
</pre>
</syntaxhighlight>
|}


'''templates/index.html''' — HTML-шаблон с формой ввода, JavaScript-логикой и динамическим отображением графиков и рекомендаций:
=== templates/index.html ===
<pre>
{| class="wikitable mw-collapsible mw-collapsed"
! Показать код
|-
|<syntaxhighlight lang="html">
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="ru">
<html lang="ru">
Строка 368: Строка 403:
     <meta charset="UTF-8">
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>VK Community Analytics</title>
     <title>Анализ аудитории сообществ VK</title>
     <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
     <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
</head>
Строка 374: Строка 409:
     <div class="container">
     <div class="container">
         <div class="card">
         <div class="card">
             <h1>📊 VK Community Analytics</h1>
             <h1> Анализ аудитории сообществ VK</h1>
             <p class="subtitle">Анализ аудитории сообщества и рекомендации по контенту</p>
             <p class="subtitle">Анализ аудитории сообщества и рекомендации по контенту</p>
              
              
             <div class="input-group">
             <div class="input-group">
                 <input type="text" id="groupUrl" placeholder="Введите ссылку или ID сообщества (например: durov или vk.com/durov)">
                 <input type="text" id="groupUrl" placeholder="Введите ссылку или ID сообщества">
                 <button onclick="analyze()" id="analyzeBtn">🔍 Анализировать</button>
                 <button onclick="analyze()" id="analyzeBtn">🔍 Анализировать</button>
             </div>
             </div>
Строка 461: Строка 496:
             document.getElementById('stats').innerHTML = statsHtml;
             document.getElementById('stats').innerHTML = statsHtml;
              
              
             // Графики (4 штуки)
             // Графики
             let chartsHtml = '';
             let chartsHtml = '';
             if (data.charts.sex) {
             if (data.charts.sex) {
Строка 516: Строка 551:
</body>
</body>
</html>
</html>
</pre>
</syntaxhighlight>
|}


'''static/style.css''' — файл стилей для веб-интерфейса:
=== static/style.css ===
<pre>
{| class="wikitable mw-collapsible mw-collapsed"
! Показать код
|-
|<syntaxhighlight lang="css">
* {
* {
     margin: 0;
     margin: 0;
Строка 527: Строка 566:


body {
body {
     font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
     font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
     background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
     background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
     min-height: 100vh;
     min-height: 100vh;
     padding: 20px;
     padding: 40px 20px;
}
}


.container {
.container {
     max-width: 1200px;
     max-width: 1400px;
     margin: 0 auto;
     margin: 0 auto;
}
}


.card {
.card {
     background: white;
     background: rgba(255, 255, 255, 0.98);
     border-radius: 20px;
     backdrop-filter: blur(10px);
     padding: 30px;
     border-radius: 32px;
     margin-bottom: 20px;
     padding: 40px;
     box-shadow: 0 10px 40px rgba(0,0,0,0.1);
     box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}
}


h1 {
h1 {
     color: #333;
    font-size: 2.5rem;
     margin-bottom: 10px;
    font-weight: 700;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    -webkit-background-clip: text;
    background-clip: text;
     color: transparent;
     margin-bottom: 12px;
}
}


.subtitle {
.subtitle {
     color: #666;
    font-size: 1.1rem;
     margin-bottom: 30px;
     color: #6b7280;
     margin-bottom: 35px;
    border-left: 4px solid #667eea;
    padding-left: 16px;
}
}


.input-group {
.input-group {
     display: flex;
     display: flex;
     gap: 10px;
     gap: 12px;
     margin-bottom: 20px;
     margin-bottom: 30px;
    flex-wrap: wrap;
}
}


input {
input {
     flex: 1;
     flex: 1;
     padding: 15px;
     padding: 16px 20px;
     border: 2px solid #e0e0e0;
     border: 2px solid #e5e7eb;
     border-radius: 10px;
     border-radius: 20px;
     font-size: 16px;
     font-size: 16px;
     transition: border-color 0.3s;
     background: #f9fafb;
}
}


Строка 574: Строка 622:
     outline: none;
     outline: none;
     border-color: #667eea;
     border-color: #667eea;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
}
}


button {
button {
     padding: 15px 30px;
     padding: 16px 32px;
     background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
     background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
     color: white;
     color: white;
     border: none;
     border: none;
     border-radius: 10px;
     border-radius: 20px;
     font-size: 16px;
     font-size: 16px;
    font-weight: 600;
     cursor: pointer;
     cursor: pointer;
     transition: transform 0.2s;
     transition: all 0.3s ease;
}
}


button:hover {
button:hover {
     transform: translateY(-2px);
     transform: translateY(-2px);
}
     box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
 
button:disabled {
     opacity: 0.6;
    cursor: not-allowed;
}
}


Строка 599: Строка 645:
     display: none;
     display: none;
     text-align: center;
     text-align: center;
     padding: 40px;
     padding: 50px;
}
}


.loader-spinner {
.loader-spinner {
     border: 3px solid #f3f3f3;
    width: 50px;
     border-top: 3px solid #667eea;
    height: 50px;
     border: 4px solid #e5e7eb;
     border-top-color: #667eea;
     border-radius: 50%;
     border-radius: 50%;
    width: 40px;
     animation: spin 0.8s linear infinite;
    height: 40px;
     margin: 0 auto 20px;
     animation: spin 1s linear infinite;
     margin: 0 auto 15px;
}
}


@keyframes spin {
@keyframes spin {
     0% { transform: rotate(0deg); }
     to { transform: rotate(360deg); }
    100% { transform: rotate(360deg); }
}
}


.results {
.results {
     display: none;
     display: none;
    animation: fadeInUp 0.5s ease;
}
@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translateY(20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}
}


.chart-grid {
.stats {
     display: grid;
     display: grid;
     grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
     grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
     gap: 25px;
     gap: 20px;
     margin: 30px 0;
     margin-bottom: 40px;
}
}


.chart-card {
.stat {
     background: #f8f9fa;
     background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
     border-radius: 15px;
     padding: 24px 20px;
     padding: 20px;
     border-radius: 24px;
     text-align: center;
     text-align: center;
     transition: transform 0.2s, box-shadow 0.2s;
     box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
}


.chart-card:hover {
.stat-value {
     transform: translateY(-5px);
     font-size: 32px;
     box-shadow: 0 5px 20px rgba(0,0,0,0.1);
    font-weight: 800;
     background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}
}


.chart-card h3 {
.stat-label {
     color: #333;
     color: #6b7280;
     margin-bottom: 15px;
     font-size: 14px;
     font-size: 18px;
     font-weight: 500;
}
}


.chart-card img {
.chart-grid {
    max-width: 100%;
    height: auto;
    border-radius: 10px;
}
 
.stats {
     display: grid;
     display: grid;
     grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
     grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
     gap: 15px;
     gap: 30px;
     margin: 20px 0;
     margin: 40px 0;
}
}


.stat {
.chart-card {
     background: #f8f9fa;
     background: #ffffff;
     padding: 15px;
     border-radius: 24px;
     border-radius: 10px;
     padding: 24px;
     text-align: center;
     text-align: center;
     transition: transform 0.2s;
     box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
}


.stat:hover {
.chart-card h3 {
     transform: translateY(-3px);
     font-size: 1.3rem;
    font-weight: 600;
    margin-bottom: 20px;
    color: #1f2937;
}
}


.stat-value {
.chart-card img {
     font-size: 28px;
     max-width: 100%;
     font-weight: bold;
     height: auto;
    color: #667eea;
     border-radius: 16px;
}
 
.stat-label {
    color: #666;
    font-size: 14px;
     margin-top: 5px;
}
}


.recommendations {
.recommendations {
     background: #e8f5e9;
     background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
     border-left: 4px solid #4caf50;
     border-left: 5px solid #22c55e;
     padding: 20px;
     padding: 28px;
     border-radius: 10px;
     border-radius: 24px;
     margin-top: 20px;
     margin-top: 30px;
}
}


.recommendations h3 {
.recommendations h3 {
     color: #2e7d32;
     color: #15803d;
     margin-bottom: 15px;
     margin-bottom: 20px;
    font-size: 1.3rem;
    font-weight: 700;
}
}


Строка 703: Строка 757:


.recommendations li {
.recommendations li {
     padding: 8px 0;
     padding: 12px 0;
     color: #1b5e20;
     color: #166534;
    border-bottom: 1px solid #bbf7d0;
}
 
.recommendations li:last-child {
    border-bottom: none;
}
}


.error {
.error {
     background: #ffebee;
     background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
     color: #c62828;
     color: #dc2626;
     padding: 15px;
     padding: 20px;
     border-radius: 10px;
     border-radius: 20px;
     margin-top: 20px;
     text-align: center;
}
}


@media (max-width: 768px) {
@media (max-width: 768px) {
     .input-group {
    body { padding: 20px 15px; }
        flex-direction: column;
    .card { padding: 24px; }
    }
    h1 { font-size: 1.8rem; }
   
     .input-group { flex-direction: column; }
     .chart-grid {
     .chart-grid { grid-template-columns: 1fr; }
        grid-template-columns: 1fr;
     .stats { grid-template-columns: 1fr; }
     }
}
}
</pre>
</syntaxhighlight>
|}


== Ход выполнения ==
== Ход выполнения ==
Строка 748: Строка 807:
[[Файл:Vk api5.png|1200px|слева]]
[[Файл:Vk api5.png|1200px|слева]]


'''В приложении выводятся:'''
=== '''В приложении выводятся:''' ===


=== Ключевые метрики ===
=== Ключевые метрики ===
Строка 781: Строка 840:
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.
[[Категория: Работа с API]]

Текущая версия от 20:12, 27 марта 2026

Анализ аудитории сообществ ВКонтакте

Автор: Сабитова Алина

Группа: АДЭУ-221

Дисциплина: Работа с API социальных сетей и облачных сервисов

Статус проекта: Выполнен

Параметр Описание
Описание Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.
Область знаний Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.
Близкие понятия SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.
Среда разработки Python 3.8+, Flask, vk_api, matplotlib, pandas

Цель проекта

Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.

Задачи

  1. Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.
  2. Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.
  3. Визуализация — построить 4 графика:
    • Распределение по полу (круговая диаграмма)
    • Возрастное распределение (гистограмма)
    • Топ-5 городов (горизонтальная столбчатая диаграмма)
    • Вовлечённость по типам контента (столбчатая диаграмма)
  4. Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.
  5. Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.

Диаграмма работы приложения

Структура проекта

Структура проекта в VS Code:

vk_analytics/
├── app.py
├── templates/
│   └── index.html
└── static/
    └── style.css   
📄 app.py
основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.
📁 templates/index.html
HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.
📁 static/style.css
файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.

Код приложения

app.py

Показать код
from flask import Flask, render_template, request, jsonify
import vk_api
import matplotlib
matplotlib.use('Agg')  
import matplotlib.pyplot as plt
import seaborn as sns
from collections import Counter
from datetime import datetime
import os
import base64
import io
import pandas as pd
import time

app = Flask(__name__)

# Папка для статики
os.makedirs('static', exist_ok=True)

# Настройка стиля seaborn
sns.set_style("whitegrid")
sns.set_palette("husl")

# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА
TOKEN = "ваш_токен_сюда"

def get_community_data(group_id):
    """Собирает данные о сообществе"""
    try:
        vk_session = vk_api.VkApi(token=TOKEN)
        vk = vk_session.get_api()
        
        # Определяем ID сообщества
        if str(group_id).lstrip('-').isdigit():
            owner_id = int(group_id)
            group_id_for_api = owner_id
        else:
            group_id_for_api = group_id
        
        # Получаем информацию о сообществе
        try:
            group = vk.groups.getById(group_id=group_id_for_api, fields='members_count,description')[0]
            community_name = group['name']
            members_count = group['members_count']
        except:
            group = vk.groups.getById(group_id=str(group_id_for_api), fields='members_count,description')[0]
            community_name = group['name']
            members_count = group['members_count']
        
        # Собираем подписчиков с пагинацией (до 2000)
        members_data = []
        try:
            total_to_collect = min(2000, members_count)
            offset = 0
            batch_size = 1000
            
            while offset < total_to_collect:
                members = vk.groups.getMembers(
                    group_id=group_id_for_api, 
                    fields='sex,bdate,city', 
                    count=batch_size,
                    offset=offset
                )
                
                for user in members['items']:
                    user_info = {}
                    
                    if user.get('sex') == 1:
                        user_info['sex'] = 'Женщины'
                    elif user.get('sex') == 2:
                        user_info['sex'] = 'Мужчины'
                    else:
                        user_info['sex'] = 'Не указан'
                    
                    if user.get('bdate'):
                        bdate = user['bdate']
                        if len(bdate.split('.')) == 3:
                            try:
                                year = int(bdate.split('.')[2])
                                age = datetime.now().year - year
                                if 0 < age < 100:
                                    user_info['age'] = age
                            except:
                                pass
                    
                    if user.get('city') and user['city'].get('title'):
                        user_info['city'] = user['city']['title']
                    
                    members_data.append(user_info)
                
                offset += len(members['items'])
                time.sleep(0.34)
                
        except Exception as e:
            print(f"Ошибка при сборе подписчиков: {e}")
        
        # Собираем посты
        posts_data = []
        try:
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip('-').isdigit() else group_id, count=20, filter='owner')
            for post in wall['items']:
                post_info = {
                    'text': post.get('text', '')[:100],
                    'likes': post['likes']['count'],
                    'comments': post['comments']['count'],
                    'reposts': post['reposts']['count'],
                    'views': post.get('views', {}).get('count', 0)
                }
                
                if post.get('attachments'):
                    attach_type = post['attachments'][0]['type']
                    if attach_type == 'photo':
                        post_info['type'] = 'Фото'
                    elif attach_type == 'video':
                        post_info['type'] = 'Видео'
                    elif attach_type == 'link':
                        post_info['type'] = 'Ссылка'
                    else:
                        post_info['type'] = 'Текст'
                else:
                    post_info['type'] = 'Текст'
                
                if post_info['views'] > 0:
                    post_info['er'] = (post_info['likes'] + post_info['comments'] + post_info['reposts']) / post_info['views'] * 1000
                else:
                    post_info['er'] = 0
                
                posts_data.append(post_info)
        except Exception as e:
            print(f"Ошибка при сборе постов: {e}")
        
        return {
            'success': True,
            'community_name': community_name,
            'members_count': members_count,
            'members': members_data,
            'posts': posts_data
        }
        
    except Exception as e:
        return {'success': False, 'error': str(e)}

def generate_charts(members_data, posts_data):
    """Генерирует графики с помощью seaborn"""
    charts = {}
    
    plt.rcParams['font.family'] = 'sans-serif'
    plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans']
    
    # 1. Распределение по полу
    if members_data:
        fig, ax = plt.subplots(figsize=(7, 5))
        sex_counts = Counter([m.get('sex', 'Не указан') for m in members_data])
        colors = ['#ff6b6b', '#4ecdc4', '#95a5a6']
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct='%1.1f%%',
               colors=colors[:len(sex_counts)], startangle=90,
               explode=[0.02] * len(sex_counts), shadow=True)
        
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', dpi=100, facecolor='white')
        buf.seek(0)
        charts['sex'] = base64.b64encode(buf.getvalue()).decode()
        plt.close(fig)
    
    # 2. Возрастное распределение
    ages = [m['age'] for m in members_data if 'age' in m]
    if ages:
        fig, ax = plt.subplots(figsize=(8, 5))
        sns.histplot(ages, bins=15, kde=True, color='#3498db', edgecolor='white', linewidth=1.5, alpha=0.7)
        ax.set_xlabel('Возраст')
        ax.set_ylabel('Количество подписчиков')
        avg_age = sum(ages) / len(ages)
        ax.axvline(avg_age, color='#e74c3c', linestyle='--', linewidth=2, label=f'Средний: {avg_age:.1f} лет')
        ax.legend()
        
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', dpi=100, facecolor='white')
        buf.seek(0)
        charts['age'] = base64.b64encode(buf.getvalue()).decode()
        plt.close(fig)
    
    # 3. Топ-5 городов
    cities = [m['city'] for m in members_data if 'city' in m and m['city'] != 'Не указан']
    if cities:
        fig, ax = plt.subplots(figsize=(9, 6))
        city_counts = Counter(cities).most_common(5)
        city_names, city_values = zip(*city_counts)
        
        df = pd.DataFrame({'Город': city_names, 'Количество': city_values})
        df = df.sort_values('Количество', ascending=True)
        
        sns.barplot(data=df, y='Город', x='Количество', hue='Город', palette='rocket', legend=False, ax=ax)
        ax.set_xlabel('Количество подписчиков')
        ax.set_ylabel('Город')
        plt.tight_layout()
        
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', dpi=100, facecolor='white')
        buf.seek(0)
        charts['cities'] = base64.b64encode(buf.getvalue()).decode()
        plt.close(fig)
    
    # 4. Вовлечённость по типам контента
    if posts_data:
        fig, ax = plt.subplots(figsize=(10, 6))
        type_er = {}
        for post in posts_data:
            if post['type'] not in type_er:
                type_er[post['type']] = []
            type_er[post['type']].append(post['er'])
        
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}
        df_er = pd.DataFrame({'Тип контента': list(avg_er.keys()), 'ER': list(avg_er.values())})
        df_er = df_er.sort_values('ER', ascending=False)
        
        bars = sns.barplot(data=df_er, x='Тип контента', y='ER', hue='Тип контента', palette='viridis', legend=False, ax=ax)
        ax.set_ylabel('ER (активность на 1000 просмотров)')
        ax.set_xlabel('Тип контента')
        
        for bar, val in zip(bars.patches, df_er['ER'].values):
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, 
                   f'{val:.1f}', ha='center', va='bottom', fontsize=10, fontweight='bold')
        
        plt.xticks(rotation=45)
        plt.tight_layout()
        
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', dpi=100, facecolor='white')
        buf.seek(0)
        charts['engagement'] = base64.b64encode(buf.getvalue()).decode()
        plt.close(fig)
    
    return charts

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/analyze', methods=['POST'])
def analyze():
    group_url = request.json.get('group_url', '').strip()
    
    if not group_url:
        return jsonify({'success': False, 'error': 'Введите ссылку на сообщество'})
    
    if 'vk.com/' in group_url:
        group_id = group_url.split('vk.com/')[-1].split('/')[0]
    else:
        group_id = group_url
    
    data = get_community_data(group_id)
    
    if not data['success']:
        return jsonify({'success': False, 'error': data.get('error', 'Ошибка при получении данных')})
    
    charts = generate_charts(data['members'], data['posts'])
    
    stats = {}
    
    if data['members']:
        sex_counts = Counter([m.get('sex', 'Не указан') for m in data['members']])
        stats['sex'] = dict(sex_counts)
    
    ages = [m['age'] for m in data['members'] if 'age' in m]
    if ages:
        stats['avg_age'] = sum(ages) / len(ages)
    
    cities = [m['city'] for m in data['members'] if 'city' in m and m['city'] != 'Не указан']
    if cities:
        stats['top_cities'] = Counter(cities).most_common(5)
    
    if data['posts']:
        avg_er = sum(p['er'] for p in data['posts']) / len(data['posts'])
        stats['avg_er'] = avg_er
        type_er = {}
        for post in data['posts']:
            if post['type'] not in type_er:
                type_er[post['type']] = []
            type_er[post['type']].append(post['er'])
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))
        stats['best_post_type'] = best_type
    
    recommendations = []
    if 'avg_age' in stats:
        if stats['avg_age'] < 20:
            recommendations.append("🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы")
        elif stats['avg_age'] < 30:
            recommendations.append("🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент")
        else:
            recommendations.append("🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент")
    
    if stats.get('best_post_type'):
        recommendations.append(f"📈 Лучший формат контента: {stats['best_post_type']} → публикуйте чаще именно его")
    
    if data['posts'] and stats.get('avg_er'):
        if stats['avg_er'] < 50:
            recommendations.append("⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив")
        elif stats['avg_er'] > 150:
            recommendations.append("🔥 Отличная вовлечённость! Продолжайте в том же духе")
    
    if stats.get('top_cities'):
        main_city = stats['top_cities'][0][0]
        recommendations.append(f"📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия")
    
    return jsonify({
        'success': True,
        'community_name': data['community_name'],
        'members_count': data['members_count'],
        'stats': stats,
        'charts': charts,
        'recommendations': recommendations
    })

if __name__ == '__main__':
    app.run(debug=True)

templates/index.html

Показать код
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Анализ аудитории сообществ VK</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
    <div class="container">
        <div class="card">
            <h1> Анализ аудитории сообществ VK</h1>
            <p class="subtitle">Анализ аудитории сообщества и рекомендации по контенту</p>
            
            <div class="input-group">
                <input type="text" id="groupUrl" placeholder="Введите ссылку или ID сообщества">
                <button onclick="analyze()" id="analyzeBtn">🔍 Анализировать</button>
            </div>
            
            <div class="loader" id="loader">
                <div class="loader-spinner"></div>
                <p>Анализируем сообщество... Это может занять до 30 секунд</p>
            </div>
            
            <div class="results" id="results">
                <div class="stats" id="stats"></div>
                <div class="chart-grid" id="charts"></div>
                <div class="recommendations" id="recommendations"></div>
            </div>
        </div>
    </div>
    
    <script>
        async function analyze() {
            const groupUrl = document.getElementById('groupUrl').value;
            if (!groupUrl) {
                alert('Введите ссылку или ID сообщества');
                return;
            }
            
            const btn = document.getElementById('analyzeBtn');
            const loader = document.getElementById('loader');
            const results = document.getElementById('results');
            
            btn.disabled = true;
            loader.style.display = 'block';
            results.style.display = 'none';
            
            try {
                const response = await fetch('/analyze', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({ group_url: groupUrl })
                });
                
                const data = await response.json();
                
                if (data.success) {
                    displayResults(data);
                } else {
                    showError(data.error);
                }
            } catch (error) {
                showError('Ошибка соединения с сервером');
            } finally {
                btn.disabled = false;
                loader.style.display = 'none';
            }
        }
        
        function displayResults(data) {
            // Статистика
            const statsHtml = `
                <div class="stat">
                    <div class="stat-value">${data.community_name}</div>
                    <div class="stat-label">Название сообщества</div>
                </div>
                <div class="stat">
                    <div class="stat-value">${data.members_count.toLocaleString()}</div>
                    <div class="stat-label">Подписчиков</div>
                </div>
                ${data.stats.avg_age ? `
                <div class="stat">
                    <div class="stat-value">${data.stats.avg_age.toFixed(1)} лет</div>
                    <div class="stat-label">Средний возраст</div>
                </div>
                ` : ''}
                ${data.stats.avg_er ? `
                <div class="stat">
                    <div class="stat-value">${data.stats.avg_er.toFixed(2)}</div>
                    <div class="stat-label">Средняя вовлечённость (ER)</div>
                </div>
                ` : ''}
            `;
            document.getElementById('stats').innerHTML = statsHtml;
            
            // Графики
            let chartsHtml = '';
            if (data.charts.sex) {
                chartsHtml += `
                    <div class="chart-card">
                        <h3>👥 Распределение по полу</h3>
                        <img src="data:image/png;base64,${data.charts.sex}" alt="Распределение по полу">
                    </div>
                `;
            }
            if (data.charts.age) {
                chartsHtml += `
                    <div class="chart-card">
                        <h3>🎂 Возрастное распределение</h3>
                        <img src="data:image/png;base64,${data.charts.age}" alt="Возраст">
                    </div>
                `;
            }
            if (data.charts.cities) {
                chartsHtml += `
                    <div class="chart-card">
                        <h3>🏙️ Топ-5 городов</h3>
                        <img src="data:image/png;base64,${data.charts.cities}" alt="Города">
                    </div>
                `;
            }
            if (data.charts.engagement) {
                chartsHtml += `
                    <div class="chart-card">
                        <h3>📈 Вовлечённость по типам контента</h3>
                        <img src="data:image/png;base64,${data.charts.engagement}" alt="ER">
                    </div>
                `;
            }
            document.getElementById('charts').innerHTML = chartsHtml;
            
            // Рекомендации
            let recHtml = '<h3>💡 Рекомендации по контент-стратегии</h3><ul>';
            data.recommendations.forEach(rec => {
                recHtml += `<li>${rec}</li>`;
            });
            recHtml += '</ul>';
            document.getElementById('recommendations').innerHTML = recHtml;
            
            document.getElementById('results').style.display = 'block';
        }
        
        function showError(message) {
            const results = document.getElementById('results');
            results.style.display = 'block';
            results.innerHTML = `<div class="error">❌ Ошибка: ${message}</div>`;
        }
    </script>
</body>
</html>

static/style.css

Показать код
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
    min-height: 100vh;
    padding: 40px 20px;
}

.container {
    max-width: 1400px;
    margin: 0 auto;
}

.card {
    background: rgba(255, 255, 255, 0.98);
    backdrop-filter: blur(10px);
    border-radius: 32px;
    padding: 40px;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
}

h1 {
    font-size: 2.5rem;
    font-weight: 700;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    margin-bottom: 12px;
}

.subtitle {
    font-size: 1.1rem;
    color: #6b7280;
    margin-bottom: 35px;
    border-left: 4px solid #667eea;
    padding-left: 16px;
}

.input-group {
    display: flex;
    gap: 12px;
    margin-bottom: 30px;
    flex-wrap: wrap;
}

input {
    flex: 1;
    padding: 16px 20px;
    border: 2px solid #e5e7eb;
    border-radius: 20px;
    font-size: 16px;
    background: #f9fafb;
}

input:focus {
    outline: none;
    border-color: #667eea;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);
}

button {
    padding: 16px 32px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 20px;
    font-size: 16px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s ease;
}

button:hover {
    transform: translateY(-2px);
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
}

.loader {
    display: none;
    text-align: center;
    padding: 50px;
}

.loader-spinner {
    width: 50px;
    height: 50px;
    border: 4px solid #e5e7eb;
    border-top-color: #667eea;
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
    margin: 0 auto 20px;
}

@keyframes spin {
    to { transform: rotate(360deg); }
}

.results {
    display: none;
    animation: fadeInUp 0.5s ease;
}

@keyframes fadeInUp {
    from {
        opacity: 0;
        transform: translateY(20px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
    gap: 20px;
    margin-bottom: 40px;
}

.stat {
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
    padding: 24px 20px;
    border-radius: 24px;
    text-align: center;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}

.stat-value {
    font-size: 32px;
    font-weight: 800;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
}

.stat-label {
    color: #6b7280;
    font-size: 14px;
    font-weight: 500;
}

.chart-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
    gap: 30px;
    margin: 40px 0;
}

.chart-card {
    background: #ffffff;
    border-radius: 24px;
    padding: 24px;
    text-align: center;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}

.chart-card h3 {
    font-size: 1.3rem;
    font-weight: 600;
    margin-bottom: 20px;
    color: #1f2937;
}

.chart-card img {
    max-width: 100%;
    height: auto;
    border-radius: 16px;
}

.recommendations {
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);
    border-left: 5px solid #22c55e;
    padding: 28px;
    border-radius: 24px;
    margin-top: 30px;
}

.recommendations h3 {
    color: #15803d;
    margin-bottom: 20px;
    font-size: 1.3rem;
    font-weight: 700;
}

.recommendations ul {
    list-style: none;
    padding-left: 0;
}

.recommendations li {
    padding: 12px 0;
    color: #166534;
    border-bottom: 1px solid #bbf7d0;
}

.recommendations li:last-child {
    border-bottom: none;
}

.error {
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
    color: #dc2626;
    padding: 20px;
    border-radius: 20px;
    text-align: center;
}

@media (max-width: 768px) {
    body { padding: 20px 15px; }
    .card { padding: 24px; }
    h1 { font-size: 1.8rem; }
    .input-group { flex-direction: column; }
    .chart-grid { grid-template-columns: 1fr; }
    .stats { grid-template-columns: 1fr; }
}

Ход выполнения

Устанавливаем необходимые библиотеки:

Запускаем основной скрипт приложения:

После запуска приложение доступно по адресу: http://127.0.0.1:5000

Результат

Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:

Введем ссылку на сообщество "Афиша" и посмотрим результат:

В приложении выводятся:

Ключевые метрики

Ключевые метрики сообщества

График, отображающий распределение подписчиков сообщества по полу

Распределение подписчиков по полу

График, показывающий распределение подписчиков по возрасту с указанием среднего значения

Возрастное распределение подписчиков

График, выводящий 5 городов с наибольшим количеством подписчиков

Топ-5 городов по количеству подписчиков

График сравнения вовлеченности подписчиков в зависимости от типа контента

Вовлечённость по типам контента

Рекомендации

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

Рекомендации по контент-стратегии

Выводы

  1. Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.
  2. Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).
  3. Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:
    • Распределение по полу (круговая диаграмма)
    • Возрастное распределение (гистограмма)
    • Топ-5 городов (горизонтальная столбчатая диаграмма)
    • Вовлечённость по типам контента (столбчатая диаграмма)
  4. Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.
  5. Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.