Анализ целевой аудитории сообщества 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_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 === | ||
{| 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: | ||
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= | 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: | 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'] = 'Текст' | ||
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=( | 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=( | 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_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=( | 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) | ||
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=( | fig, ax = plt.subplots(figsize=(10, 6)) | ||
type_er = {} | type_er = {} | ||
for post in posts_data: | for post in posts_data: | ||
if post['type'] not in type_er: | |||
type_er[post['type']] = [] | |||
type_er[ | type_er[post['type']].append(post['er']) | ||
type_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()} | ||
df_er = pd.DataFrame({'Тип контента': list(avg_er.keys()), 'ER': list(avg_er.values())}) | |||
ax.set_ylabel('ER (на 1000 просмотров)' | 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): | |||
for bar, val in zip(bars, | 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() + | f'{val:.1f}', ha='center', va='bottom', fontsize=10, fontweight='bold') | ||
f'{val:.1f}', ha='center', va='bottom', fontsize= | |||
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': 'Введите ссылку на сообщество'}) | ||
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) | ||
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 лет) → используйте короткие, трендовые форматы, мемы | 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] | ||
recommendations.append(f"📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия") | |||
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) | ||
</ | </syntaxhighlight> | ||
|} | |||
=== templates/index.html === | |||
< | {| 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 | <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> | <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 сообщества | <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; | ||
// Графики | // Графики | ||
let chartsHtml = ''; | let chartsHtml = ''; | ||
if (data.charts.sex) { | if (data.charts.sex) { | ||
| Строка 516: | Строка 551: | ||
</body> | </body> | ||
</html> | </html> | ||
</ | </syntaxhighlight> | ||
|} | |||
=== static/style.css === | |||
< | {| 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: | max-width: 1400px; | ||
margin: 0 auto; | margin: 0 auto; | ||
} | } | ||
.card { | .card { | ||
background: | background: rgba(255, 255, 255, 0.98); | ||
backdrop-filter: blur(10px); | |||
border-radius: 32px; | |||
padding: 40px; | |||
box-shadow: 0 | box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); | ||
} | } | ||
h1 { | h1 { | ||
color: | font-size: 2.5rem; | ||
margin-bottom: | 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: # | font-size: 1.1rem; | ||
margin-bottom: | color: #6b7280; | ||
margin-bottom: 35px; | |||
border-left: 4px solid #667eea; | |||
padding-left: 16px; | |||
} | } | ||
.input-group { | .input-group { | ||
display: flex; | display: flex; | ||
gap: | gap: 12px; | ||
margin-bottom: | margin-bottom: 30px; | ||
flex-wrap: wrap; | |||
} | } | ||
input { | input { | ||
flex: 1; | flex: 1; | ||
padding: | padding: 16px 20px; | ||
border: 2px solid # | border: 2px solid #e5e7eb; | ||
border-radius: | border-radius: 20px; | ||
font-size: 16px; | font-size: 16px; | ||
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: | 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: | border-radius: 20px; | ||
font-size: 16px; | font-size: 16px; | ||
font-weight: 600; | |||
cursor: pointer; | cursor: pointer; | ||
transition: | 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); | |||
} | } | ||
| Строка 599: | Строка 645: | ||
display: none; | display: none; | ||
text-align: center; | text-align: center; | ||
padding: | padding: 50px; | ||
} | } | ||
.loader-spinner { | .loader-spinner { | ||
border: | width: 50px; | ||
border-top: | height: 50px; | ||
border: 4px solid #e5e7eb; | |||
border-top-color: #667eea; | |||
border-radius: 50%; | border-radius: 50%; | ||
animation: spin 0.8s linear infinite; | |||
margin: 0 auto 20px; | |||
animation: spin | |||
margin: 0 auto | |||
} | } | ||
@keyframes spin { | @keyframes spin { | ||
to { 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); | |||
} | |||
} | } | ||
. | .stats { | ||
display: grid; | display: grid; | ||
grid-template-columns: repeat(auto-fit, minmax( | grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); | ||
gap: | gap: 20px; | ||
margin: | margin-bottom: 40px; | ||
} | } | ||
. | .stat { | ||
background: #f8f9fa; | background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); | ||
padding: 24px 20px; | |||
border-radius: 24px; | |||
text-align: center; | 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: # | color: #6b7280; | ||
font-size: 14px; | |||
font- | font-weight: 500; | ||
} | } | ||
.chart- | .chart-grid { | ||
display: grid; | display: grid; | ||
grid-template-columns: repeat(auto-fit, minmax( | grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); | ||
gap: | gap: 30px; | ||
margin: | margin: 40px 0; | ||
} | } | ||
. | .chart-card { | ||
background: # | background: #ffffff; | ||
border-radius: 24px; | |||
padding: 24px; | |||
text-align: center; | 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 { | .recommendations { | ||
background: # | background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%); | ||
border-left: | border-left: 5px solid #22c55e; | ||
padding: | padding: 28px; | ||
border-radius: | border-radius: 24px; | ||
margin-top: | margin-top: 30px; | ||
} | } | ||
.recommendations h3 { | .recommendations h3 { | ||
color: # | color: #15803d; | ||
margin-bottom: | margin-bottom: 20px; | ||
font-size: 1.3rem; | |||
font-weight: 700; | |||
} | } | ||
| Строка 703: | Строка 757: | ||
.recommendations li { | .recommendations li { | ||
padding: | padding: 12px 0; | ||
color: # | color: #166534; | ||
border-bottom: 1px solid #bbf7d0; | |||
} | |||
.recommendations li:last-child { | |||
border-bottom: none; | |||
} | } | ||
.error { | .error { | ||
background: # | background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%); | ||
color: # | color: #dc2626; | ||
padding: | padding: 20px; | ||
border-radius: | border-radius: 20px; | ||
text-align: center; | |||
} | } | ||
@media (max-width: 768px) { | @media (max-width: 768px) { | ||
.input-group { | body { padding: 20px 15px; } | ||
.card { padding: 24px; } | |||
h1 { font-size: 1.8rem; } | |||
.input-group { flex-direction: column; } | |||
.chart-grid { | .chart-grid { grid-template-columns: 1fr; } | ||
.stats { grid-template-columns: 1fr; } | |||
} | |||
} | } | ||
</ | </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 |
Цель проекта
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.
Задачи
- Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.
- Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.
- Визуализация — построить 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 городов с наибольшим количеством подписчиков

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

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

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