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

Материал из Поле цифровой дидактики
Нет описания правки
Нет описания правки
Строка 43: Строка 43:


== Структура проекта ==
== Структура проекта ==
<pre>
vk_analytics/
├── app.py                # Основной файл приложения (Flask)
├── templates/
│  └── index.html        # HTML-шаблон главной страницы
└── static/
    └── style.css          # Стили для веб-интерфейса
</pre>


=== Описание файлов ===
{| class="wikitable"
! Файл
! Назначение
|-
| app.py
| Содержит логику приложения: маршруты Flask, функции для работы с VK API, обработку данных, генерацию графиков и формирование рекомендаций.
|-
| templates/index.html
| HTML-страница с формой ввода и областью для отображения результатов. Включает JavaScript для отправки запросов к серверу.
|-
| static/style.css
| CSS-стили для оформления интерфейса: адаптивная сетка, анимации, цветовая схема.
|}
== Ход работы над проектом ==
=== 1. Настройка окружения ===
Установка необходимых библиотек:
<pre>
pip install flask vk_api matplotlib pandas
</pre>
=== 2. Получение токена доступа VK API ===
Для работы с VK API используется токен доступа, полученный через сервис vkhost.github.io:
<pre>
TOKEN = "ваш_токен_сюда"
</pre>
=== 3. Функция сбора данных о сообществе ===
<pre>
def get_community_data(group_id):
    """Собирает данные о сообществе"""
    try:
        vk_session = vk_api.VkApi(token=TOKEN)
        vk = vk_session.get_api()
       
        # Получаем информацию о сообществе
        group = vk.groups.getById(group_id=group_id, fields='members_count,description')[0]
        community_name = group['name']
        members_count = group['members_count']
       
        # Собираем подписчиков (500 записей)
        members = vk.groups.getMembers(group_id=group_id, fields='sex,bdate,city', count=500)
        members_data = []
       
        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') and len(user['bdate'].split('.')) == 3:
                year = int(user['bdate'].split('.')[2])
                age = datetime.now().year - year
                if 0 < age < 100:
                    user_info['age'] = age
           
            # Город
            if user.get('city') and user['city'].get('title'):
                user_info['city'] = user['city']['title']
           
            members_data.append(user_info)
       
        # Собираем посты
        wall = vk.wall.get(owner_id=group_id, count=20, filter='owner')
        posts_data = []
       
        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'] = 'Текст'
           
            # Расчёт вовлечённости (ER)
            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)
       
        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)}
</pre>
=== 4. Функция генерации графиков ===
<pre>
def generate_charts(members_data, posts_data):
    """Генерирует графики и возвращает их в base64"""
    charts = {}
   
    # Настройка шрифтов для русских букв
    plt.rcParams['font.family'] = 'sans-serif'
    plt.rcParams['font.sans-serif'] = ['Arial', 'DejaVu Sans']
   
    # 1. Распределение по полу
    if members_data:
        fig, ax = plt.subplots(figsize=(6, 4))
        sex_counts = Counter([m.get('sex', 'Не указан') for m in members_data])
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct='%1.1f%%')
        ax.set_title('Распределение по полу')
       
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
        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=(6, 4))
        ax.hist(ages, bins=15, color='skyblue', edgecolor='black')
        ax.set_title('Возрастное распределение')
        ax.set_xlabel('Возраст')
        ax.set_ylabel('Количество')
       
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
        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]
    if cities:
        fig, ax = plt.subplots(figsize=(8, 5))
        city_counts = Counter(cities).most_common(5)
        city_names, city_values = zip(*city_counts)
       
        ax.barh(city_names, city_values, color='lightcoral', edgecolor='darkred')
        ax.set_title('Топ-5 городов аудитории', fontsize=14, fontweight='bold')
        ax.set_xlabel('Количество подписчиков')
        ax.set_ylabel('Город')
       
        plt.tight_layout()
       
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
        buf.seek(0)
        charts['cities'] = base64.b64encode(buf.getvalue()).decode()
        plt.close(fig)
   
    # 4. Вовлечённость по типам контента
    if posts_data:
        fig, ax = plt.subplots(figsize=(8, 5))
        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()}
        ax.bar(avg_er.keys(), avg_er.values(), color='lightgreen', edgecolor='darkgreen')
        ax.set_title('Вовлечённость по типам контента', fontsize=14, fontweight='bold')
        ax.set_ylabel('ER (на 1000 просмотров)')
        plt.xticks(rotation=45)
       
        plt.tight_layout()
       
        buf = io.BytesIO()
        plt.savefig(buf, format='png', bbox_inches='tight', dpi=100)
        buf.seek(0)
        charts['engagement'] = base64.b64encode(buf.getvalue()).decode()
        plt.close(fig)
   
    return charts
</pre>
=== 5. Веб-интерфейс ===
==== HTML-шаблон (templates/index.html) ====
<pre>
&lt;!DOCTYPE html&gt;
&lt;html lang="ru"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;title&gt;VK Community Analytics&lt;/title&gt;
    &lt;link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div class="container"&gt;
        &lt;div class="card"&gt;
            &lt;h1&gt;📊 VK Community Analytics&lt;/h1&gt;
            &lt;p class="subtitle"&gt;Анализ аудитории сообщества и рекомендации по контенту&lt;/p&gt;
           
            &lt;div class="input-group"&gt;
                &lt;input type="text" id="groupUrl" placeholder="Введите ссылку или ID сообщества (например: durov или vk.com/durov)"&gt;
                &lt;button onclick="analyze()" id="analyzeBtn"&gt;🔍 Анализировать&lt;/button&gt;
            &lt;/div&gt;
           
            &lt;div class="loader" id="loader"&gt;
                &lt;div class="loader-spinner"&gt;&lt;/div&gt;
                &lt;p&gt;Анализируем сообщество... Это может занять до 30 секунд&lt;/p&gt;
            &lt;/div&gt;
           
            &lt;div class="results" id="results"&gt;
                &lt;div class="stats" id="stats"&gt;&lt;/div&gt;
                &lt;div class="chart-grid" id="charts"&gt;&lt;/div&gt;
                &lt;div class="recommendations" id="recommendations"&gt;&lt;/div&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
   
    &lt;script&gt;
        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 = `
                &lt;div class="stat"&gt;
                    &lt;div class="stat-value"&gt;${data.community_name}&lt;/div&gt;
                    &lt;div class="stat-label"&gt;Название сообщества&lt;/div&gt;
                &lt;/div&gt;
                &lt;div class="stat"&gt;
                    &lt;div class="stat-value"&gt;${data.members_count.toLocaleString()}&lt;/div&gt;
                    &lt;div class="stat-label"&gt;Подписчиков&lt;/div&gt;
                &lt;/div&gt;
                ${data.stats.avg_age ? `
                &lt;div class="stat"&gt;
                    &lt;div class="stat-value"&gt;${data.stats.avg_age.toFixed(1)} лет&lt;/div&gt;
                    &lt;div class="stat-label"&gt;Средний возраст&lt;/div&gt;
                &lt;/div&gt;
                ` : ''}
                ${data.stats.avg_er ? `
                &lt;div class="stat"&gt;
                    &lt;div class="stat-value"&gt;${data.stats.avg_er.toFixed(2)}&lt;/div&gt;
                    &lt;div class="stat-label"&gt;Средняя вовлечённость (ER)&lt;/div&gt;
                &lt;/div&gt;
                ` : ''}
            `;
            document.getElementById('stats').innerHTML = statsHtml;
           
            // Графики
            let chartsHtml = '';
            if (data.charts.sex) {
                chartsHtml += `
                    &lt;div class="chart-card"&gt;
                        &lt;h3&gt;👥 Распределение по полу&lt;/h3&gt;
                        &lt;img src="data:image/png;base64,${data.charts.sex}" alt="Распределение по полу"&gt;
                    &lt;/div&gt;
                `;
            }
            if (data.charts.age) {
                chartsHtml += `
                    &lt;div class="chart-card"&gt;
                        &lt;h3&gt;🎂 Возрастное распределение&lt;/h3&gt;
                        &lt;img src="data:image/png;base64,${data.charts.age}" alt="Возраст"&gt;
                    &lt;/div&gt;
                `;
            }
            if (data.charts.cities) {
                chartsHtml += `
                    &lt;div class="chart-card"&gt;
                        &lt;h3&gt;🏙️ Топ-5 городов&lt;/h3&gt;
                        &lt;img src="data:image/png;base64,${data.charts.cities}" alt="Города"&gt;
                    &lt;/div&gt;
                `;
            }
            if (data.charts.engagement) {
                chartsHtml += `
                    &lt;div class="chart-card"&gt;
                        &lt;h3&gt;📈 Вовлечённость по типам контента&lt;/h3&gt;
                        &lt;img src="data:image/png;base64,${data.charts.engagement}" alt="ER"&gt;
                    &lt;/div&gt;
                `;
            }
            document.getElementById('charts').innerHTML = chartsHtml;
           
            // Рекомендации
            let recHtml = '&lt;h3&gt;💡 Рекомендации по контент-стратегии&lt;/h3&gt;&lt;ul&gt;';
            data.recommendations.forEach(rec => {
                recHtml += `&lt;li&gt;${rec}&lt;/li&gt;`;
            });
            recHtml += '&lt;/ul&gt;';
            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 = `&lt;div class="error"&gt;❌ Ошибка: ${message}&lt;/div&gt;`;
        }
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</pre>
==== CSS-стили (static/style.css) ====
<pre>
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}
body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    min-height: 100vh;
    padding: 20px;
}
.container {
    max-width: 1200px;
    margin: 0 auto;
}
.card {
    background: white;
    border-radius: 20px;
    padding: 30px;
    margin-bottom: 20px;
    box-shadow: 0 10px 40px rgba(0,0,0,0.1);
}
h1 {
    color: #333;
    margin-bottom: 10px;
}
.subtitle {
    color: #666;
    margin-bottom: 30px;
}
.input-group {
    display: flex;
    gap: 10px;
    margin-bottom: 20px;
}
input {
    flex: 1;
    padding: 15px;
    border: 2px solid #e0e0e0;
    border-radius: 10px;
    font-size: 16px;
    transition: border-color 0.3s;
}
input:focus {
    outline: none;
    border-color: #667eea;
}
button {
    padding: 15px 30px;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    border: none;
    border-radius: 10px;
    font-size: 16px;
    cursor: pointer;
    transition: transform 0.2s;
}
button:hover {
    transform: translateY(-2px);
}
button:disabled {
    opacity: 0.6;
    cursor: not-allowed;
}
.loader {
    display: none;
    text-align: center;
    padding: 40px;
}
.loader-spinner {
    border: 3px solid #f3f3f3;
    border-top: 3px solid #667eea;
    border-radius: 50%;
    width: 40px;
    height: 40px;
    animation: spin 1s linear infinite;
    margin: 0 auto 15px;
}
@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}
.results {
    display: none;
}
.chart-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
    gap: 25px;
    margin: 30px 0;
}
.chart-card {
    background: #f8f9fa;
    border-radius: 15px;
    padding: 20px;
    text-align: center;
    transition: transform 0.2s, box-shadow 0.2s;
}
.chart-card:hover {
    transform: translateY(-5px);
    box-shadow: 0 5px 20px rgba(0,0,0,0.1);
}
.chart-card h3 {
    color: #333;
    margin-bottom: 15px;
    font-size: 18px;
}
.chart-card img {
    max-width: 100%;
    height: auto;
    border-radius: 10px;
}
.stats {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: 15px;
    margin: 20px 0;
}
.stat {
    background: #f8f9fa;
    padding: 15px;
    border-radius: 10px;
    text-align: center;
    transition: transform 0.2s;
}
.stat:hover {
    transform: translateY(-3px);
}
.stat-value {
    font-size: 28px;
    font-weight: bold;
    color: #667eea;
}
.stat-label {
    color: #666;
    font-size: 14px;
    margin-top: 5px;
}
.recommendations {
    background: #e8f5e9;
    border-left: 4px solid #4caf50;
    padding: 20px;
    border-radius: 10px;
    margin-top: 20px;
}
.recommendations h3 {
    color: #2e7d32;
    margin-bottom: 15px;
}
.recommendations ul {
    list-style: none;
    padding-left: 0;
}
.recommendations li {
    padding: 8px 0;
    color: #1b5e20;
}
.error {
    background: #ffebee;
    color: #c62828;
    padding: 15px;
    border-radius: 10px;
    margin-top: 20px;
}
@media (max-width: 768px) {
    .input-group {
        flex-direction: column;
    }
   
    .chart-grid {
        grid-template-columns: 1fr;
    }
}
</pre>
=== 6. Формирование рекомендаций ===
<pre>
# Рекомендации
recommendations = []
# Рекомендации по возрасту
if 'avg_age' in stats:
    if stats['avg_age'] < 20:
        recommendations.append("🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход")
    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} → можно проводить локальные мероприятия или делать привязку к городу")
</pre>
=== 7. Запуск приложения ===
<pre>
python app.py
</pre>


После запуска приложение доступно по адресу: '''http://127.0.0.1:5000'''
После запуска приложение доступно по адресу: '''http://127.0.0.1:5000'''
Строка 661: Строка 49:
== Выводы ==
== Выводы ==


=== Достигнутые результаты ===
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).

Версия от 22:46, 24 марта 2026

Параметр Описание
Описание Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации 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. Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.

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

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

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

Выводы

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