Анализ целевой аудитории сообщества VK: различия между версиями
Материал из Поле цифровой дидактики
Нет описания правки |
Нет описания правки |
||
| Строка 17: | Строка 17: | ||
|} | |} | ||
|} | |||
## | == Цель проекта == | ||
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ. | |||
== Задачи == | |||
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества. | |||
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости. | |||
# Визуализация — построить 4 графика: | |||
#* Распределение по полу (круговая диаграмма) | |||
#* Возрастное распределение (гистограмма) | |||
#* Топ-5 городов (горизонтальная столбчатая диаграмма) | |||
#* Вовлечённость по типам контента (столбчатая диаграмма) | |||
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии. | |||
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов. | |||
== Диаграмма работы приложения == | |||
<pre> | |||
┌─────────────────┐ | |||
│ Пользователь │ | |||
│ вводит ссылку │ | |||
│ на сообщество │ | |||
└────────┬────────┘ | |||
▼ | |||
┌─────────────────┐ | |||
│ Flask-сервер │ | |||
│ (localhost) │ | |||
└────────┬────────┘ | |||
▼ | |||
┌─────────────────┐ | |||
│ VK API │ | |||
│ groups.getById │ | |||
│ groups.getMembers│ | |||
│ wall.get │ | |||
└────────┬────────┘ | |||
▼ | |||
┌─────────────────┐ | |||
│ Обработка │ | |||
│ данных │ | |||
│ (Python) │ | |||
└────────┬────────┘ | |||
▼ | |||
┌─────────────────┐ | |||
│ Визуализация │ | |||
│ (matplotlib) │ | |||
└────────┬────────┘ | |||
▼ | |||
┌─────────────────┐ | |||
│ Формирование │ | |||
│ рекомендаций │ | |||
└────────┬────────┘ | |||
▼ | |||
┌─────────────────┐ | |||
│ Отображение │ | |||
│ результатов │ | |||
│ в браузере │ | |||
└─────────────────┘ | |||
</pre> | |||
== Структура проекта == | |||
<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> | |||
<!DOCTYPE html> | |||
<html lang="ru"> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||
<title>VK Community Analytics</title> | |||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> | |||
</head> | |||
<body> | |||
<div class="container"> | |||
<div class="card"> | |||
<h1>📊 VK Community Analytics</h1> | |||
<p class="subtitle">Анализ аудитории сообщества и рекомендации по контенту</p> | |||
<div class="input-group"> | |||
<input type="text" id="groupUrl" placeholder="Введите ссылку или ID сообщества (например: durov или vk.com/durov)"> | |||
<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> | |||
</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''' | |||
== Выводы == | |||
## | === Достигнутые результаты === | ||
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost. | |||
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость). | |||
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории: | |||
#* Распределение по полу (круговая диаграмма) | |||
#* Возрастное распределение (гистограмма) | |||
#* Топ-5 городов (горизонтальная столбчатая диаграмма) | |||
#* Вовлечённость по типам контента (столбчатая диаграмма) | |||
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии. | |||
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация. | |||
Версия от 21:40, 24 марта 2026
| Параметр | Описание |
|---|---|
| Описание | Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию. |
| Область знаний | Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование. |
| Близкие понятия | SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение. |
| Среда разработки | Python 3.8+, Flask, vk_api, matplotlib, pandas |
|}
Цель проекта
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.
Задачи
- Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.
- Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.
- Визуализация — построить 4 графика:
- Распределение по полу (круговая диаграмма)
- Возрастное распределение (гистограмма)
- Топ-5 городов (горизонтальная столбчатая диаграмма)
- Вовлечённость по типам контента (столбчатая диаграмма)
- Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.
- Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.
Диаграмма работы приложения
┌─────────────────┐
│ Пользователь │
│ вводит ссылку │
│ на сообщество │
└────────┬────────┘
▼
┌─────────────────┐
│ Flask-сервер │
│ (localhost) │
└────────┬────────┘
▼
┌─────────────────┐
│ VK API │
│ groups.getById │
│ groups.getMembers│
│ wall.get │
└────────┬────────┘
▼
┌─────────────────┐
│ Обработка │
│ данных │
│ (Python) │
└────────┬────────┘
▼
┌─────────────────┐
│ Визуализация │
│ (matplotlib) │
└────────┬────────┘
▼
┌─────────────────┐
│ Формирование │
│ рекомендаций │
└────────┬────────┘
▼
┌─────────────────┐
│ Отображение │
│ результатов │
│ в браузере │
└─────────────────┘
Структура проекта
vk_analytics/
│
├── app.py # Основной файл приложения (Flask)
│
├── templates/
│ └── index.html # HTML-шаблон главной страницы
│
└── static/
└── style.css # Стили для веб-интерфейса
Описание файлов
| Файл | Назначение |
|---|---|
| app.py | Содержит логику приложения: маршруты Flask, функции для работы с VK API, обработку данных, генерацию графиков и формирование рекомендаций. |
| templates/index.html | HTML-страница с формой ввода и областью для отображения результатов. Включает JavaScript для отправки запросов к серверу. |
| static/style.css | CSS-стили для оформления интерфейса: адаптивная сетка, анимации, цветовая схема. |
Ход работы над проектом
1. Настройка окружения
Установка необходимых библиотек:
pip install flask vk_api matplotlib pandas
2. Получение токена доступа VK API
Для работы с VK API используется токен доступа, полученный через сервис vkhost.github.io:
TOKEN = "ваш_токен_сюда"
3. Функция сбора данных о сообществе
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)}
4. Функция генерации графиков
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
5. Веб-интерфейс
HTML-шаблон (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 Community Analytics</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<div class="container">
<div class="card">
<h1>📊 VK Community Analytics</h1>
<p class="subtitle">Анализ аудитории сообщества и рекомендации по контенту</p>
<div class="input-group">
<input type="text" id="groupUrl" placeholder="Введите ссылку или ID сообщества (например: durov или vk.com/durov)">
<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>
CSS-стили (static/style.css)
* {
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;
}
}
6. Формирование рекомендаций
# Рекомендации
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} → можно проводить локальные мероприятия или делать привязку к городу")
7. Запуск приложения
python app.py
После запуска приложение доступно по адресу: http://127.0.0.1:5000
Выводы
Достигнутые результаты
- Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.
- Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).
- Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:
- Распределение по полу (круговая диаграмма)
- Возрастное распределение (гистограмма)
- Топ-5 городов (горизонтальная столбчатая диаграмма)
- Вовлечённость по типам контента (столбчатая диаграмма)
- Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.
- Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.
