Анализ целевой аудитории сообщества VK: различия между версиями
| Строка 69: | Строка 69: | ||
! Показать код | ! Показать код | ||
|- | |- | ||
| | |||
<syntaxhighlight lang="python"> | <syntaxhighlight lang="python"> | ||
from flask import Flask, render_template, request, jsonify | from flask import Flask, render_template, request, jsonify | ||
Версия от 19:57, 27 марта 2026
Анализ аудитории сообществ ВКонтакте
Автор: Сабитова Алина
Группа: АДЭУ-221
Дисциплина: Работа с API социальных сетей и облачных сервисов
Статус проекта: Выполнен
| Параметр | Описание |
|---|---|
| Описание | Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию. |
| Область знаний | Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование. |
| Близкие понятия | SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение. |
| Среда разработки | Python 3.8+, Flask, vk_api, matplotlib, pandas |
Цель проекта
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.
Задачи
- Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.
- Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.
- Визуализация — построить 4 графика:
- Распределение по полу (круговая диаграмма)
- Возрастное распределение (гистограмма)
- Топ-5 городов (горизонтальная столбчатая диаграмма)
- Вовлечённость по типам контента (столбчатая диаграмма)
- Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.
- Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.
Диаграмма работы приложения
Структура проекта
Структура проекта в VS Code:
- 📄 app.py
- основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.
- 📁 templates/index.html
- HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.
- 📁 static/style.css
- файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.
Код приложения
app.py
| Показать код |
|---|
|
<syntaxhighlight lang="python"> 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)
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) </syntaxhighlight lang="python"> |
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 городов с наибольшим количеством подписчиков

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

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

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

