<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="ru">
	<id>http://digida.mgpu.ru/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Sabitova+Alina</id>
	<title>Поле цифровой дидактики - Вклад [ru]</title>
	<link rel="self" type="application/atom+xml" href="http://digida.mgpu.ru/api.php?action=feedcontributions&amp;feedformat=atom&amp;user=Sabitova+Alina"/>
	<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php/%D0%A1%D0%BB%D1%83%D0%B6%D0%B5%D0%B1%D0%BD%D0%B0%D1%8F:%D0%92%D0%BA%D0%BB%D0%B0%D0%B4/Sabitova_Alina"/>
	<updated>2026-05-27T08:14:19Z</updated>
	<subtitle>Вклад</subtitle>
	<generator>MediaWiki 1.44.0</generator>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%83%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA%D0%B0:Patarakin&amp;diff=45635</id>
		<title>Обсуждение участника:Patarakin</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%9E%D0%B1%D1%81%D1%83%D0%B6%D0%B4%D0%B5%D0%BD%D0%B8%D0%B5_%D1%83%D1%87%D0%B0%D1%81%D1%82%D0%BD%D0%B8%D0%BA%D0%B0:Patarakin&amp;diff=45635"/>
		<updated>2026-03-27T17:15:01Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* Исправление АДЭУ-221 */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;== teachers_flow_web ==&lt;br /&gt;
https://digida.mgpu.ru/index.php/Обсуждение:Заглавная_страница#Как_я_взаимодействию_с_ассистентом_искусственного_интеллекта_Перплексити&lt;br /&gt;
&lt;br /&gt;
==Тезаурус 4== &lt;br /&gt;
&lt;br /&gt;
Да, хорошо. Положите пожалуйста. Потому что моя основная проблема, что у меня VOSviewer не работает же и я не могла эти данные получить в таком виде, как вы их прислали. &lt;br /&gt;
Но у меня почти получилось что-то похожее! Обходными путями странными) &lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
==Тезаурус 3==&lt;br /&gt;
&lt;br /&gt;
Хорошо, я остановлюсь, во избежание апокалипсиса) &lt;br /&gt;
Но мне казалось, что все не так плохо, за исключением того, что в ручную просматривать фамилии в текстовом файле сложно. Попробовала поговорить с ЖПТ, он написал код для R. Но дальше уже я не сильна, какая-то каша началась.&lt;br /&gt;
&lt;br /&gt;
Я научилась в Экселе сделать функцию &amp;quot;Найти&amp;quot; и он выдает все ячейки с таким ФИО, там есть кнопка &amp;quot;Заменить&amp;quot;. С ней еще не поняла как работать. И есть ли смысл вообще так делать.&lt;br /&gt;
&lt;br /&gt;
== Тезаурус 2== &lt;br /&gt;
Я сохранила из экселя только столбец с авторами, он нормально открылся на маке потом, с этим разобралась. Сейчас основная проблема это понять какой логикой заменять? &lt;br /&gt;
Потому что я вижу автора одного, например. Он есть как самостоятельный автор, есть в соавторстве. И непонятно как с этим соавторством быть. Объединять ли или нет, потому что с одним автором есть 2-3 статьи в соавторстве с разными людьми. &lt;br /&gt;
Это еще полпроблемы. Проблема если в одном соавторстве на русском, а в другом на английском?&lt;br /&gt;
На 59 строк обработала, приступаю на 270. &lt;br /&gt;
Но 900 не факт, что осилю физически и эмоционально. &lt;br /&gt;
&lt;br /&gt;
Я сейчас делаю текстовый документ по каждому универу, в котором отдельных &amp;quot;Нормальных&amp;quot; авторов удаляю, а повторки оставляю и группирую, чтобы вам завтра показать. &lt;br /&gt;
&lt;br /&gt;
== Тезаурус== &lt;br /&gt;
&lt;br /&gt;
Здравствуйте, Евгений Дмитриевич! &lt;br /&gt;
&lt;br /&gt;
Начала разбираться с тезаурусом, возник ряд проблем: &lt;br /&gt;
Если в датасете есть один автор с точками, другой без точек - мы их меняем. &lt;br /&gt;
1. А если есть один автор, но он без точек в разных местах по одному - как быть? &lt;br /&gt;
2. Если автор в соавторстве пишет, и в одном месте он с точками, а в другом - без точек. Что делаем в этом случае? &lt;br /&gt;
&lt;br /&gt;
Помогите пожалуйста) Айжан --[[Участник:Айжан Ужинкина]] ([[Обсуждение участника:Айжан Ужинкина|обсуждение]])&lt;br /&gt;
&lt;br /&gt;
== Просьба от Антюховой Эльзы ==&lt;br /&gt;
&lt;br /&gt;
Евгений Дмитриевич, посмотрите, пожалуйста, почему я не могу добавить приложения, разработанные на языке программирования Snap?&lt;br /&gt;
&lt;br /&gt;
== Если нужно задать ==&lt;br /&gt;
&lt;br /&gt;
Примерный вопрос ... --[[Участник:Patarakin|Patarakin]] ([[Обсуждение участника:Patarakin|обсуждение]]) 17:16, 25 ноября 2025 (MSK)&lt;br /&gt;
&lt;br /&gt;
Получилось добавить Dataset Lens как таблицу, сделала заголовки. Посмотрите, пожалуйста, и отметите в ЛМС это задание, если всё нормально. (Татьяна Астафьева) --[[Участник:Татьяна Астафьева|Татьяна Астафьева]] ([[Обсуждение участника:Татьяна Астафьева|обсуждение]]) 20:30, 25 ноября 2025 (MSK)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Добрый вечер, Евгений Дмитриевич, посмотрите, пожалуйста, страничку и отметьте в LMS сделанные мной задания --[[Участник:Дементьева Мария Владимировна|Дементьева Мария]] ([[Обсуждение участника:Дементьева Мария Владимировна|обсуждение]]) 16:08, 5 декабря 2025 (MSK)&lt;br /&gt;
&lt;br /&gt;
Евгений Дмитриевич, проверьте, пожалуйста, выполненные задания на моей странице и проставьте баллы в LMS --[[Участник:Ширкина Д|Ширкина Д]] ([[Обсуждение участника:Ширкина Д|обсуждение]]) 10:57, 6 декабря 2025 (MSK)&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
Здравствуйте, Евгений Дмитриевич, посмотрите, пожалуйста, задания и отметьте, если все хорошо, в LMS, прошу прощения за скриншоты с телефона, не могу зайти на сайты через браузер ПК [[Участник: Tatiana Khaidarova]] ([[Обсуждение участника: Tatiana Khaidarova|обсуждение]]) 23:43, 6 декабря 2025 (MSK)&lt;br /&gt;
&lt;br /&gt;
Добрый вечер, посмотрите, пожалуйста, страницу и отметьте в LMS (Тюрикова Арина), если всё хорошо --[[Участник:Tyurikovaaa155|Tyurikovaaa155]] ([[Обсуждение участника:Tyurikovaaa155|обсуждение]])&lt;br /&gt;
&lt;br /&gt;
Здравствуйте, Евгений Дмитриевич! Посмотрите, пожалуйста, страницу (Семина Мария)&lt;br /&gt;
[[Участник:Мария Семина|Мария Семина]] ([[Участник:Мария Семина|обсуждение]])&lt;br /&gt;
&lt;br /&gt;
Здравствуйте, Евгений Дмитриевич, ссылки в LMS проставила, посмотрите, пожалуйста [[Участник: Tatiana Khaidarova]] ([[Обсуждение участника: Tatiana Khaidarova|обсуждение]]) 15:39, 10 декабря 2025 (MSK)&lt;br /&gt;
&lt;br /&gt;
Добрый вечер, Евгений Дмитриевич, посмотрите, пожалуйста, страничку и отметьте в LMS сделанные мной задания (Волынчикова Анна)&lt;br /&gt;
&lt;br /&gt;
Добрый вечер, посмотрите, пожалуйста, страницу и отметьте в LMS (Дмитренко Надежда), если всё хорошо и нужно ли что-то подправить --[[Участник:DmitrenkoNP|DmitrenkoNP]] ([[Обсуждение участника:DmitrenkoNP|обсуждение]])&lt;br /&gt;
&lt;br /&gt;
Добрый вечер. Выполнил задания. Просьба проверить и отметить в LMS. [[Участник:InasovAA]] (Инасов Артем)&lt;br /&gt;
&lt;br /&gt;
Добрый вечер. Выполнила задания Евгений Дмитриевич. посмотрите, пожалуйста, страничку, проверить и отметить в LMS. [[Участник:Карина]] (Мергалиева Карина)&lt;br /&gt;
&lt;br /&gt;
Сделал NetsBlox. [[Участник:Вольдемар]] (Шкабара Владимир)&lt;br /&gt;
&lt;br /&gt;
Здравствуйте, Евгений Дмитриевич. Сделал NetsBlox, прошу проверить. [[Участник:DzhamalkhanovRV]]&lt;br /&gt;
&lt;br /&gt;
Здравствуйте, Евгений Дмитриевич, посмотрите, пожалуйста, страничку и отметьте в LMS сделанные мной задания. (Беляева Мария) [[Участник:БеляеваМД]]&lt;br /&gt;
&lt;br /&gt;
== Основы моделирования - результаты к зачету ==&lt;br /&gt;
&lt;br /&gt;
И здесь - пришел, указал, что сделал, оставил подпись --[[Участник:Patarakin|Patarakin]] ([[Обсуждение участника:Patarakin|обсуждение]]) 09:20, 26 декабря 2025 (MSK)&lt;br /&gt;
&lt;br /&gt;
== Проверка работ в LMS ==&lt;br /&gt;
&lt;br /&gt;
Здравствуйте, Евгений Дмитриевич!&lt;br /&gt;
Добавил ссылки на выполненные работы в LMS.&lt;br /&gt;
Просьба проверить. и проставить оценку.&lt;br /&gt;
&lt;br /&gt;
== Информация от Антюховой Эльзы ==&lt;br /&gt;
&lt;br /&gt;
Здравствуйте, Евгений Дмитриевич! &lt;br /&gt;
Хочу сообщить о том, что я внесла необходимые правки в свою выпускную квалификационную работу. Буду очень признательна, если Вы сможете уделить время и посмотреть ее. Заранее спасибо!&lt;br /&gt;
&lt;br /&gt;
== Проверка статьи &amp;quot;Репрезентация мотивов самостоятельной учебной работы в личностных нарративах студентов&amp;quot; ==&lt;br /&gt;
&lt;br /&gt;
Евгений Дмитриевич, добрый день! Могли бы Вы проверить мою работу? Что нужно скорректировать/добавить?&lt;br /&gt;
&lt;br /&gt;
== Репрезентация мотивов самостоятельной учебной работы в личностных нарративах студентов ==&lt;br /&gt;
&lt;br /&gt;
Евгений Дмитриевич, я добавила категорию [[:Категория:Психо-лингвистическое исследование]] в конце текста статьи, но на странице моя работа не появилась&lt;br /&gt;
Подскажите, что нужно поправить сейчас?&lt;br /&gt;
&lt;br /&gt;
== Исправление АДЭУ-221 ==&lt;br /&gt;
&lt;br /&gt;
 Арлинская поправила [[Аналитика_профиля_ВК#🗂_Структура_проекта|Работа_Арлинская]]&lt;br /&gt;
 Муханова Анна (добавила интерактивную карту на страничку) [[Карта друзей|Муханова_карта_друзей]]&lt;br /&gt;
 Сабитова Алина (поменяла оформление кодов) [[Анализ целевой аудитории сообщества VK|Сабитова_проект]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45634</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45634"/>
		<updated>2026-03-27T17:12:07Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
 vk_analytics/&lt;br /&gt;
 ├── app.py&lt;br /&gt;
 ├── templates/&lt;br /&gt;
 │   └── index.html&lt;br /&gt;
 └── static/&lt;br /&gt;
     └── style.css   &lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Анализ аудитории сообществ VK&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt; Анализ аудитории сообществ VK&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;css&amp;quot;&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45633</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45633"/>
		<updated>2026-03-27T17:08:25Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* Структура проекта */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre style=&amp;quot;background: #1e1e2f; color: #f8f8f2; padding: 15px; border-radius: 10px; font-family: &#039;Courier New&#039;, monospace;&amp;quot;&amp;gt;&lt;br /&gt;
vk_analytics/&lt;br /&gt;
│&lt;br /&gt;
├── app.py                 &lt;br /&gt;
│&lt;br /&gt;
├── templates/&lt;br /&gt;
│   └── index.html         &lt;br /&gt;
│&lt;br /&gt;
└── static/&lt;br /&gt;
    └── style.css           &lt;br /&gt;
&amp;lt;/pre&amp;gt;    &lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Анализ аудитории сообществ VK&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt; Анализ аудитории сообществ VK&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;css&amp;quot;&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45632</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45632"/>
		<updated>2026-03-27T17:07:01Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* Структура проекта */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
vk_analytics/&lt;br /&gt;
├── app.py              &lt;br /&gt;
├── templates/&lt;br /&gt;
│   └── index.html      &lt;br /&gt;
└── static/&lt;br /&gt;
    └── style.css      &lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Анализ аудитории сообществ VK&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt; Анализ аудитории сообществ VK&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;css&amp;quot;&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45631</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45631"/>
		<updated>2026-03-27T17:05:35Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* templates/index.html */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;html&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Анализ аудитории сообществ VK&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt; Анализ аудитории сообществ VK&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;css&amp;quot;&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45630</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45630"/>
		<updated>2026-03-27T17:04:56Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* static/style.css */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Анализ аудитории сообществ VK&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt; Анализ аудитории сообществ VK&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;css&amp;quot;&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45629</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45629"/>
		<updated>2026-03-27T17:03:42Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* static/style.css */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Анализ аудитории сообществ VK&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt; Анализ аудитории сообществ VK&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
&amp;lt;/syntaxhighligt&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45628</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45628"/>
		<updated>2026-03-27T17:02:41Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* static/style.css */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Анализ аудитории сообществ VK&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt; Анализ аудитории сообществ VK&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighligt&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45627</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45627"/>
		<updated>2026-03-27T17:01:49Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* templates/index.html */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;Анализ аудитории сообществ VK&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt; Анализ аудитории сообществ VK&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45626</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45626"/>
		<updated>2026-03-27T17:01:04Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* templates/index.html */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45625</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45625"/>
		<updated>2026-03-27T17:00:38Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* templates/index.html */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45624</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45624"/>
		<updated>2026-03-27T17:00:07Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* templates/index.html */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45623</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45623"/>
		<updated>2026-03-27T16:59:34Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* app.py */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45622</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45622"/>
		<updated>2026-03-27T16:59:09Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight lang&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45621</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45621"/>
		<updated>2026-03-27T16:57:24Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* app.py */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45620</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45620"/>
		<updated>2026-03-27T16:57:00Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* app.py */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
|&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45619</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45619"/>
		<updated>2026-03-27T16:56:41Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: /* app.py */&lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45618</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45618"/>
		<updated>2026-03-27T16:55:50Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/syntaxhighlight lang=&amp;quot;python&amp;quot;&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45526</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45526"/>
		<updated>2026-03-27T06:11:28Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
[[Категория: Работа с API]]&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45446</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45446"/>
		<updated>2026-03-26T20:48:46Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Сабитова Алина &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45444</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45444"/>
		<updated>2026-03-26T20:48:03Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Автор:&amp;lt;/b&amp;gt; Светлана Селиверстова &amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Группа:&amp;lt;/b&amp;gt; АДЭУ-221&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Дисциплина:&amp;lt;/b&amp;gt; Работа с API социальных сетей и облачных сервисов&amp;lt;/p&amp;gt;&lt;br /&gt;
        &amp;lt;p&amp;gt;&amp;lt;b&amp;gt;Статус проекта:&amp;lt;/b&amp;gt; Выполнен&amp;lt;/p&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45443</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45443"/>
		<updated>2026-03-26T20:45:00Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45439</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45439"/>
		<updated>2026-03-26T20:39:20Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
&lt;br /&gt;
== Описание проекта ==&lt;br /&gt;
Веб-приложение для автоматического анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width: 100%;&amp;quot;&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Область знаний&#039;&#039;&#039;&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Стек технологий&#039;&#039;&#039;&lt;br /&gt;
| Python 3.8+, Flask, VK API, Matplotlib, Seaborn, Pandas, HTML/CSS/JavaScript&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Среда разработки&#039;&#039;&#039;&lt;br /&gt;
| VS Code / PyCharm, локальный сервер (localhost)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: graph LR&lt;br /&gt;
    A[Пользователь] --&amp;gt; B[Flask]&lt;br /&gt;
    B --&amp;gt; C[VK API]&lt;br /&gt;
    C --&amp;gt; D[Анализ]&lt;br /&gt;
    D --&amp;gt; E[Графики]&lt;br /&gt;
    D --&amp;gt; F[Рекомендации]&lt;br /&gt;
    E --&amp;gt; G[Результат]&lt;br /&gt;
    F --&amp;gt; G&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Результаты работы ==&lt;br /&gt;
&#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api10.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api11.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45435</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45435"/>
		<updated>2026-03-26T20:35:05Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;= Анализ аудитории сообществ ВКонтакте =&lt;br /&gt;
&lt;br /&gt;
== Описание проекта ==&lt;br /&gt;
Веб-приложение для автоматического анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot; style=&amp;quot;width: 100%;&amp;quot;&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Область знаний&#039;&#039;&#039;&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Стек технологий&#039;&#039;&#039;&lt;br /&gt;
| Python 3.8+, Flask, VK API, Matplotlib, Seaborn, Pandas, HTML/CSS/JavaScript&lt;br /&gt;
|-&lt;br /&gt;
| &#039;&#039;&#039;Среда разработки&#039;&#039;&#039;&lt;br /&gt;
| VS Code / PyCharm, локальный сервер (localhost)&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: graph LR&lt;br /&gt;
    A[Пользователь] --&amp;gt; B[Flask]&lt;br /&gt;
    B --&amp;gt; C[VK API]&lt;br /&gt;
    C --&amp;gt; D[Анализ]&lt;br /&gt;
    D --&amp;gt; E[Графики]&lt;br /&gt;
    D --&amp;gt; F[Рекомендации]&lt;br /&gt;
    E --&amp;gt; G[Результат]&lt;br /&gt;
    F --&amp;gt; G&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
; 📄 app.py&lt;br /&gt;
: основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
; 📁 templates/index.html&lt;br /&gt;
: HTML-шаблон главной страницы. Содержит форму для ввода ссылки, область для отображения результатов и JavaScript-логику.&lt;br /&gt;
&lt;br /&gt;
; 📁 static/style.css&lt;br /&gt;
: файл стилей для веб-интерфейса. Адаптивный дизайн, анимации, градиентный фон, сетка графиков.&lt;br /&gt;
&lt;br /&gt;
== Код приложения ==&lt;br /&gt;
&lt;br /&gt;
=== app.py ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
import seaborn as sns&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
import pandas as pd&lt;br /&gt;
import time&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
# Настройка стиля seaborn&lt;br /&gt;
sns.set_style(&amp;quot;whitegrid&amp;quot;)&lt;br /&gt;
sns.set_palette(&amp;quot;husl&amp;quot;)&lt;br /&gt;
&lt;br /&gt;
# ВСТАВЬТЕ ВАШ ТОКЕН СЮДА&lt;br /&gt;
TOKEN = &amp;quot;ваш_токен_сюда&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков с пагинацией (до 2000)&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            total_to_collect = min(2000, members_count)&lt;br /&gt;
            offset = 0&lt;br /&gt;
            batch_size = 1000&lt;br /&gt;
            &lt;br /&gt;
            while offset &amp;lt; total_to_collect:&lt;br /&gt;
                members = vk.groups.getMembers(&lt;br /&gt;
                    group_id=group_id_for_api, &lt;br /&gt;
                    fields=&#039;sex,bdate,city&#039;, &lt;br /&gt;
                    count=batch_size,&lt;br /&gt;
                    offset=offset&lt;br /&gt;
                )&lt;br /&gt;
                &lt;br /&gt;
                for user in members[&#039;items&#039;]:&lt;br /&gt;
                    user_info = {}&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                    elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;bdate&#039;):&lt;br /&gt;
                        bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                        if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                            try:&lt;br /&gt;
                                year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                                age = datetime.now().year - year&lt;br /&gt;
                                if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                    user_info[&#039;age&#039;] = age&lt;br /&gt;
                            except:&lt;br /&gt;
                                pass&lt;br /&gt;
                    &lt;br /&gt;
                    if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                        user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                    &lt;br /&gt;
                    members_data.append(user_info)&lt;br /&gt;
                &lt;br /&gt;
                offset += len(members[&#039;items&#039;])&lt;br /&gt;
                time.sleep(0.34)&lt;br /&gt;
                &lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики с помощью seaborn&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(7, 5))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        colors = [&#039;#ff6b6b&#039;, &#039;#4ecdc4&#039;, &#039;#95a5a6&#039;]&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;,&lt;br /&gt;
               colors=colors[:len(sex_counts)], startangle=90,&lt;br /&gt;
               explode=[0.02] * len(sex_counts), shadow=True)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        sns.histplot(ages, bins=15, kde=True, color=&#039;#3498db&#039;, edgecolor=&#039;white&#039;, linewidth=1.5, alpha=0.7)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        avg_age = sum(ages) / len(ages)&lt;br /&gt;
        ax.axvline(avg_age, color=&#039;#e74c3c&#039;, linestyle=&#039;--&#039;, linewidth=2, label=f&#039;Средний: {avg_age:.1f} лет&#039;)&lt;br /&gt;
        ax.legend()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(9, 6))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
        &lt;br /&gt;
        df = pd.DataFrame({&#039;Город&#039;: city_names, &#039;Количество&#039;: city_values})&lt;br /&gt;
        df = df.sort_values(&#039;Количество&#039;, ascending=True)&lt;br /&gt;
        &lt;br /&gt;
        sns.barplot(data=df, y=&#039;Город&#039;, x=&#039;Количество&#039;, hue=&#039;Город&#039;, palette=&#039;rocket&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(10, 6))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        df_er = pd.DataFrame({&#039;Тип контента&#039;: list(avg_er.keys()), &#039;ER&#039;: list(avg_er.values())})&lt;br /&gt;
        df_er = df_er.sort_values(&#039;ER&#039;, ascending=False)&lt;br /&gt;
        &lt;br /&gt;
        bars = sns.barplot(data=df_er, x=&#039;Тип контента&#039;, y=&#039;ER&#039;, hue=&#039;Тип контента&#039;, palette=&#039;viridis&#039;, legend=False, ax=ax)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (активность на 1000 просмотров)&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Тип контента&#039;)&lt;br /&gt;
        &lt;br /&gt;
        for bar, val in zip(bars.patches, df_er[&#039;ER&#039;].values):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=10, fontweight=&#039;bold&#039;)&lt;br /&gt;
        &lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100, facecolor=&#039;white&#039;)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
    &lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → добавьте опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== templates/index.html ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;amp;lt;!DOCTYPE html&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;head&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;title&amp;amp;gt;Анализ аудитории сообществ VK&amp;amp;lt;/title&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/head&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;body&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;div class=&amp;quot;container&amp;quot;&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;div class=&amp;quot;card&amp;quot;&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;h1&amp;amp;gt;📱 Анализ аудитории сообществ VK&amp;amp;lt;/h1&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;amp;gt;🔍 Анализировать&amp;amp;lt;/button&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;p&amp;amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;amp;lt;/p&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;amp;gt;&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
            &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
        &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;amp;lt;script&amp;amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: { &#039;Content-Type&#039;: &#039;application/json&#039; },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.community_name}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Название сообщества&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.members_count.toLocaleString()}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Подписчиков&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средний возраст&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;amp;lt;div class=&amp;quot;stat&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;amp;gt;${data.stats.avg_er.toFixed(2)}&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;amp;gt;Средняя вовлечённость (ER)&amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;👥 Распределение по полу&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🎂 Возрастное распределение&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;🏙️ Топ-5 городов&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;h3&amp;amp;gt;📈 Вовлечённость по типам контента&amp;amp;lt;/h3&amp;amp;gt;&lt;br /&gt;
                        &amp;amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;amp;gt;&lt;br /&gt;
                    &amp;amp;lt;/div&amp;amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            let recHtml = &#039;&amp;amp;lt;h3&amp;amp;gt;💡 Рекомендации по контент-стратегии&amp;amp;lt;/h3&amp;amp;gt;&amp;amp;lt;ul&amp;amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;amp;lt;li&amp;amp;gt;${rec}&amp;amp;lt;/li&amp;amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;amp;lt;/ul&amp;amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;amp;lt;div class=&amp;quot;error&amp;quot;&amp;amp;gt;❌ Ошибка: ${message}&amp;amp;lt;/div&amp;amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;amp;lt;/script&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/body&amp;amp;gt;&lt;br /&gt;
&amp;amp;lt;/html&amp;amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== static/style.css ===&lt;br /&gt;
{| class=&amp;quot;wikitable mw-collapsible mw-collapsed&amp;quot;&lt;br /&gt;
! Показать код&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: &#039;Inter&#039;, -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 40px 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1400px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: rgba(255, 255, 255, 0.98);&lt;br /&gt;
    backdrop-filter: blur(10px);&lt;br /&gt;
    border-radius: 32px;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    font-size: 2.5rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
    margin-bottom: 12px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    font-size: 1.1rem;&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    margin-bottom: 35px;&lt;br /&gt;
    border-left: 4px solid #667eea;&lt;br /&gt;
    padding-left: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 12px;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
    flex-wrap: wrap;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 16px 20px;&lt;br /&gt;
    border: 2px solid #e5e7eb;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    background: #f9fafb;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
    box-shadow: 0 0 0 4px rgba(102, 126, 234, 0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 16px 32px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: all 0.3s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
    box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 50px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    width: 50px;&lt;br /&gt;
    height: 50px;&lt;br /&gt;
    border: 4px solid #e5e7eb;&lt;br /&gt;
    border-top-color: #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    animation: spin 0.8s linear infinite;&lt;br /&gt;
    margin: 0 auto 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    to { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
    animation: fadeInUp 0.5s ease;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes fadeInUp {&lt;br /&gt;
    from {&lt;br /&gt;
        opacity: 0;&lt;br /&gt;
        transform: translateY(20px);&lt;br /&gt;
    }&lt;br /&gt;
    to {&lt;br /&gt;
        opacity: 1;&lt;br /&gt;
        transform: translateY(0);&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));&lt;br /&gt;
    gap: 20px;&lt;br /&gt;
    margin-bottom: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);&lt;br /&gt;
    padding: 24px 20px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 32px;&lt;br /&gt;
    font-weight: 800;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    -webkit-background-clip: text;&lt;br /&gt;
    background-clip: text;&lt;br /&gt;
    color: transparent;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #6b7280;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    font-weight: 500;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));&lt;br /&gt;
    gap: 30px;&lt;br /&gt;
    margin: 40px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #ffffff;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    padding: 24px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 600;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    color: #1f2937;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 16px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: linear-gradient(135deg, #f0fdf4 0%, #dcfce7 100%);&lt;br /&gt;
    border-left: 5px solid #22c55e;&lt;br /&gt;
    padding: 28px;&lt;br /&gt;
    border-radius: 24px;&lt;br /&gt;
    margin-top: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #15803d;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    font-size: 1.3rem;&lt;br /&gt;
    font-weight: 700;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 12px 0;&lt;br /&gt;
    color: #166534;&lt;br /&gt;
    border-bottom: 1px solid #bbf7d0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li:last-child {&lt;br /&gt;
    border-bottom: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);&lt;br /&gt;
    color: #dc2626;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    body { padding: 20px 15px; }&lt;br /&gt;
    .card { padding: 24px; }&lt;br /&gt;
    h1 { font-size: 1.8rem; }&lt;br /&gt;
    .input-group { flex-direction: column; }&lt;br /&gt;
    .chart-grid { grid-template-columns: 1fr; }&lt;br /&gt;
    .stats { grid-template-columns: 1fr; }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Результаты работы ==&lt;br /&gt;
&#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api10.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api11.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&#039;&#039;&#039;Дата выполнения:&#039;&#039;&#039; март 2026 г.&amp;lt;br&amp;gt;&lt;br /&gt;
&#039;&#039;&#039;Стек технологий:&#039;&#039;&#039; Python, Flask, VK API, Matplotlib, Seaborn, Pandas, HTML/CSS/JavaScript&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45431</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45431"/>
		<updated>2026-03-26T20:30:20Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;app.py&#039;&#039;&#039; — основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
TOKEN = &amp;quot;Ваш токен&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Собирает данные о сообществе&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            # Если не получилось, пробуем как числовой ID&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            members = vk.groups.getMembers(group_id=group_id_for_api, fields=&#039;sex,bdate,city&#039;, count=500)&lt;br /&gt;
            for user in members[&#039;items&#039;]:&lt;br /&gt;
                user_info = {}&lt;br /&gt;
                &lt;br /&gt;
                # Пол&lt;br /&gt;
                if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                &lt;br /&gt;
                # Возраст&lt;br /&gt;
                if user.get(&#039;bdate&#039;):&lt;br /&gt;
                    bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                    if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                        try:&lt;br /&gt;
                            year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                            age = datetime.now().year - year&lt;br /&gt;
                            if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                user_info[&#039;age&#039;] = age&lt;br /&gt;
                        except:&lt;br /&gt;
                            pass&lt;br /&gt;
                &lt;br /&gt;
                # Город&lt;br /&gt;
                if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                    user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                &lt;br /&gt;
                members_data.append(user_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)&lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                # ER&lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
# Генерирует графики&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    # Настройка шрифтов для русских букв&lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        ax.hist(ages, bins=15, color=&#039;skyblue&#039;, edgecolor=&#039;black&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
    &lt;br /&gt;
        # Создаём горизонтальную столбчатую диаграмму&lt;br /&gt;
        bars = ax.barh(city_names, city_values, color=&#039;lightcoral&#039;, edgecolor=&#039;darkred&#039;)&lt;br /&gt;
        ax.set_title(&#039;Топ-5 городов аудитории&#039;, fontsize=14, fontweight=&#039;bold&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;, fontsize=11)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;, fontsize=11)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
    &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            post_type = post[&#039;type&#039;]&lt;br /&gt;
            if post_type not in type_er:&lt;br /&gt;
                type_er[post_type] = []&lt;br /&gt;
            type_er[post_type].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        bars = ax.bar(avg_er.keys(), avg_er.values(), color=&#039;lightgreen&#039;, edgecolor=&#039;darkgreen&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (на 1000 просмотров)&#039;, fontsize=11)&lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        &lt;br /&gt;
        # Добавляем значения на столбцы&lt;br /&gt;
        for bar, val in zip(bars, avg_er.values()):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=9)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    # Извлекаем ID сообщества из ссылки&lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    # Собираем данные&lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    # Генерируем графики&lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    # Считаем статистику&lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    # Пол&lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    # Возраст&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
        stats[&#039;min_age&#039;] = min(ages)&lt;br /&gt;
        stats[&#039;max_age&#039;] = max(ages)&lt;br /&gt;
    &lt;br /&gt;
    # Города&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    # Посты&lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        # Лучший тип контента&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    # Рекомендации&lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Добавляем рекомендацию по городам&lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        main_city_count = stats[&#039;top_cities&#039;][0][1]&lt;br /&gt;
        total_with_city = sum(count for _, count in stats[&#039;top_cities&#039;])&lt;br /&gt;
        if len(stats[&#039;top_cities&#039;]) &amp;gt; 1:&lt;br /&gt;
            recommendations.append(f&amp;quot;🏙️ Топ-5 городов: {&#039;, &#039;.join([f&#039;{city} ({cnt})&#039; for city, cnt in stats[&#039;top_cities&#039;][:3]])}&amp;quot;)&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;templates/index.html&#039;&#039;&#039; — HTML-шаблон с формой ввода, JavaScript-логикой и динамическим отображением графиков и рекомендаций:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;VK Community Analytics&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt;📊 VK Community Analytics&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества (например: durov или vk.com/durov)&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики (4 штуки)&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;static/style.css&#039;&#039;&#039; — файл стилей для веб-интерфейса:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: white;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    padding: 30px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    box-shadow: 0 10px 40px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 10px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border: 2px solid #e0e0e0;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    transition: border-color 0.3s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 15px 30px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:disabled {&lt;br /&gt;
    opacity: 0.6;&lt;br /&gt;
    cursor: not-allowed;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    border: 3px solid #f3f3f3;&lt;br /&gt;
    border-top: 3px solid #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    width: 40px;&lt;br /&gt;
    height: 40px;&lt;br /&gt;
    animation: spin 1s linear infinite;&lt;br /&gt;
    margin: 0 auto 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    0% { transform: rotate(0deg); }&lt;br /&gt;
    100% { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));&lt;br /&gt;
    gap: 25px;&lt;br /&gt;
    margin: 30px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    border-radius: 15px;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s, box-shadow 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card:hover {&lt;br /&gt;
    transform: translateY(-5px);&lt;br /&gt;
    box-shadow: 0 5px 20px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
    font-size: 18px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));&lt;br /&gt;
    gap: 15px;&lt;br /&gt;
    margin: 20px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat:hover {&lt;br /&gt;
    transform: translateY(-3px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 28px;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    margin-top: 5px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: #e8f5e9;&lt;br /&gt;
    border-left: 4px solid #4caf50;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #2e7d32;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 8px 0;&lt;br /&gt;
    color: #1b5e20;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: #ffebee;&lt;br /&gt;
    color: #c62828;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    .input-group {&lt;br /&gt;
        flex-direction: column;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    .chart-grid {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
=== &#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039; ===&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45430</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45430"/>
		<updated>2026-03-26T20:29:09Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;app.py&#039;&#039;&#039; — основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
TOKEN = &amp;quot;Ваш токен&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Собирает данные о сообществе&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            # Если не получилось, пробуем как числовой ID&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            members = vk.groups.getMembers(group_id=group_id_for_api, fields=&#039;sex,bdate,city&#039;, count=500)&lt;br /&gt;
            for user in members[&#039;items&#039;]:&lt;br /&gt;
                user_info = {}&lt;br /&gt;
                &lt;br /&gt;
                # Пол&lt;br /&gt;
                if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                &lt;br /&gt;
                # Возраст&lt;br /&gt;
                if user.get(&#039;bdate&#039;):&lt;br /&gt;
                    bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                    if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                        try:&lt;br /&gt;
                            year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                            age = datetime.now().year - year&lt;br /&gt;
                            if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                user_info[&#039;age&#039;] = age&lt;br /&gt;
                        except:&lt;br /&gt;
                            pass&lt;br /&gt;
                &lt;br /&gt;
                # Город&lt;br /&gt;
                if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                    user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                &lt;br /&gt;
                members_data.append(user_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)&lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                # ER&lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
# Генерирует графики&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    # Настройка шрифтов для русских букв&lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        ax.hist(ages, bins=15, color=&#039;skyblue&#039;, edgecolor=&#039;black&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
    &lt;br /&gt;
        # Создаём горизонтальную столбчатую диаграмму&lt;br /&gt;
        bars = ax.barh(city_names, city_values, color=&#039;lightcoral&#039;, edgecolor=&#039;darkred&#039;)&lt;br /&gt;
        ax.set_title(&#039;Топ-5 городов аудитории&#039;, fontsize=14, fontweight=&#039;bold&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;, fontsize=11)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;, fontsize=11)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
    &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            post_type = post[&#039;type&#039;]&lt;br /&gt;
            if post_type not in type_er:&lt;br /&gt;
                type_er[post_type] = []&lt;br /&gt;
            type_er[post_type].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        bars = ax.bar(avg_er.keys(), avg_er.values(), color=&#039;lightgreen&#039;, edgecolor=&#039;darkgreen&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (на 1000 просмотров)&#039;, fontsize=11)&lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        &lt;br /&gt;
        # Добавляем значения на столбцы&lt;br /&gt;
        for bar, val in zip(bars, avg_er.values()):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=9)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    # Извлекаем ID сообщества из ссылки&lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    # Собираем данные&lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    # Генерируем графики&lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    # Считаем статистику&lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    # Пол&lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    # Возраст&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
        stats[&#039;min_age&#039;] = min(ages)&lt;br /&gt;
        stats[&#039;max_age&#039;] = max(ages)&lt;br /&gt;
    &lt;br /&gt;
    # Города&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    # Посты&lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        # Лучший тип контента&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    # Рекомендации&lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Добавляем рекомендацию по городам&lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        main_city_count = stats[&#039;top_cities&#039;][0][1]&lt;br /&gt;
        total_with_city = sum(count for _, count in stats[&#039;top_cities&#039;])&lt;br /&gt;
        if len(stats[&#039;top_cities&#039;]) &amp;gt; 1:&lt;br /&gt;
            recommendations.append(f&amp;quot;🏙️ Топ-5 городов: {&#039;, &#039;.join([f&#039;{city} ({cnt})&#039; for city, cnt in stats[&#039;top_cities&#039;][:3]])}&amp;quot;)&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;templates/index.html&#039;&#039;&#039; — HTML-шаблон с формой ввода, JavaScript-логикой и динамическим отображением графиков и рекомендаций:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;VK Community Analytics&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt;📊 VK Community Analytics&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества (например: durov или vk.com/durov)&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики (4 штуки)&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;static/style.css&#039;&#039;&#039; — файл стилей для веб-интерфейса:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: white;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    padding: 30px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    box-shadow: 0 10px 40px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 10px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border: 2px solid #e0e0e0;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    transition: border-color 0.3s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 15px 30px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:disabled {&lt;br /&gt;
    opacity: 0.6;&lt;br /&gt;
    cursor: not-allowed;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    border: 3px solid #f3f3f3;&lt;br /&gt;
    border-top: 3px solid #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    width: 40px;&lt;br /&gt;
    height: 40px;&lt;br /&gt;
    animation: spin 1s linear infinite;&lt;br /&gt;
    margin: 0 auto 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    0% { transform: rotate(0deg); }&lt;br /&gt;
    100% { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));&lt;br /&gt;
    gap: 25px;&lt;br /&gt;
    margin: 30px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    border-radius: 15px;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s, box-shadow 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card:hover {&lt;br /&gt;
    transform: translateY(-5px);&lt;br /&gt;
    box-shadow: 0 5px 20px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
    font-size: 18px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));&lt;br /&gt;
    gap: 15px;&lt;br /&gt;
    margin: 20px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat:hover {&lt;br /&gt;
    transform: translateY(-3px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 28px;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    margin-top: 5px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: #e8f5e9;&lt;br /&gt;
    border-left: 4px solid #4caf50;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #2e7d32;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 8px 0;&lt;br /&gt;
    color: #1b5e20;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: #ffebee;&lt;br /&gt;
    color: #c62828;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    .input-group {&lt;br /&gt;
        flex-direction: column;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    .chart-grid {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;В приложении выводятся:&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== График, отображающий распределение подписчиков сообщества по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== График, показывающий распределение подписчиков по возрасту с указанием среднего значения ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График, выводящий 5 городов с наибольшим количеством подписчиков ===&lt;br /&gt;
[[Файл:Vk api11.png|600px|thumb|center|Топ-5 городов по количеству подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== График сравнения вовлеченности подписчиков в зависимости от типа контента ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|Вовлечённость по типам контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api11.png&amp;diff=45429</id>
		<title>Файл:Vk api11.png</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api11.png&amp;diff=45429"/>
		<updated>2026-03-26T20:28:52Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45421</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45421"/>
		<updated>2026-03-26T20:23:50Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;app.py&#039;&#039;&#039; — основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
TOKEN = &amp;quot;Ваш токен&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Собирает данные о сообществе&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            # Если не получилось, пробуем как числовой ID&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            members = vk.groups.getMembers(group_id=group_id_for_api, fields=&#039;sex,bdate,city&#039;, count=500)&lt;br /&gt;
            for user in members[&#039;items&#039;]:&lt;br /&gt;
                user_info = {}&lt;br /&gt;
                &lt;br /&gt;
                # Пол&lt;br /&gt;
                if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                &lt;br /&gt;
                # Возраст&lt;br /&gt;
                if user.get(&#039;bdate&#039;):&lt;br /&gt;
                    bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                    if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                        try:&lt;br /&gt;
                            year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                            age = datetime.now().year - year&lt;br /&gt;
                            if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                user_info[&#039;age&#039;] = age&lt;br /&gt;
                        except:&lt;br /&gt;
                            pass&lt;br /&gt;
                &lt;br /&gt;
                # Город&lt;br /&gt;
                if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                    user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                &lt;br /&gt;
                members_data.append(user_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)&lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                # ER&lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
# Генерирует графики&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    # Настройка шрифтов для русских букв&lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        ax.hist(ages, bins=15, color=&#039;skyblue&#039;, edgecolor=&#039;black&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
    &lt;br /&gt;
        # Создаём горизонтальную столбчатую диаграмму&lt;br /&gt;
        bars = ax.barh(city_names, city_values, color=&#039;lightcoral&#039;, edgecolor=&#039;darkred&#039;)&lt;br /&gt;
        ax.set_title(&#039;Топ-5 городов аудитории&#039;, fontsize=14, fontweight=&#039;bold&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;, fontsize=11)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;, fontsize=11)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
    &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            post_type = post[&#039;type&#039;]&lt;br /&gt;
            if post_type not in type_er:&lt;br /&gt;
                type_er[post_type] = []&lt;br /&gt;
            type_er[post_type].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        bars = ax.bar(avg_er.keys(), avg_er.values(), color=&#039;lightgreen&#039;, edgecolor=&#039;darkgreen&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (на 1000 просмотров)&#039;, fontsize=11)&lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        &lt;br /&gt;
        # Добавляем значения на столбцы&lt;br /&gt;
        for bar, val in zip(bars, avg_er.values()):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=9)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    # Извлекаем ID сообщества из ссылки&lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    # Собираем данные&lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    # Генерируем графики&lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    # Считаем статистику&lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    # Пол&lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    # Возраст&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
        stats[&#039;min_age&#039;] = min(ages)&lt;br /&gt;
        stats[&#039;max_age&#039;] = max(ages)&lt;br /&gt;
    &lt;br /&gt;
    # Города&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    # Посты&lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        # Лучший тип контента&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    # Рекомендации&lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Добавляем рекомендацию по городам&lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        main_city_count = stats[&#039;top_cities&#039;][0][1]&lt;br /&gt;
        total_with_city = sum(count for _, count in stats[&#039;top_cities&#039;])&lt;br /&gt;
        if len(stats[&#039;top_cities&#039;]) &amp;gt; 1:&lt;br /&gt;
            recommendations.append(f&amp;quot;🏙️ Топ-5 городов: {&#039;, &#039;.join([f&#039;{city} ({cnt})&#039; for city, cnt in stats[&#039;top_cities&#039;][:3]])}&amp;quot;)&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;templates/index.html&#039;&#039;&#039; — HTML-шаблон с формой ввода, JavaScript-логикой и динамическим отображением графиков и рекомендаций:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;VK Community Analytics&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt;📊 VK Community Analytics&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества (например: durov или vk.com/durov)&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики (4 штуки)&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;static/style.css&#039;&#039;&#039; — файл стилей для веб-интерфейса:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: white;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    padding: 30px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    box-shadow: 0 10px 40px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 10px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border: 2px solid #e0e0e0;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    transition: border-color 0.3s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 15px 30px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:disabled {&lt;br /&gt;
    opacity: 0.6;&lt;br /&gt;
    cursor: not-allowed;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    border: 3px solid #f3f3f3;&lt;br /&gt;
    border-top: 3px solid #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    width: 40px;&lt;br /&gt;
    height: 40px;&lt;br /&gt;
    animation: spin 1s linear infinite;&lt;br /&gt;
    margin: 0 auto 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    0% { transform: rotate(0deg); }&lt;br /&gt;
    100% { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));&lt;br /&gt;
    gap: 25px;&lt;br /&gt;
    margin: 30px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    border-radius: 15px;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s, box-shadow 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card:hover {&lt;br /&gt;
    transform: translateY(-5px);&lt;br /&gt;
    box-shadow: 0 5px 20px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
    font-size: 18px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));&lt;br /&gt;
    gap: 15px;&lt;br /&gt;
    margin: 20px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat:hover {&lt;br /&gt;
    transform: translateY(-3px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 28px;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    margin-top: 5px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: #e8f5e9;&lt;br /&gt;
    border-left: 4px solid #4caf50;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #2e7d32;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 8px 0;&lt;br /&gt;
    color: #1b5e20;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: #ffebee;&lt;br /&gt;
    color: #c62828;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    .input-group {&lt;br /&gt;
        flex-direction: column;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    .chart-grid {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
В приложении выводятся:&lt;br /&gt;
&lt;br /&gt;
=== Ключевые метрики ===&lt;br /&gt;
[[Файл:Vk api6.png|1200px|thumb|center|Ключевые метрики сообщества]]&lt;br /&gt;
&lt;br /&gt;
=== Распределение по полу ===&lt;br /&gt;
[[Файл:Vk api7.png|600px|thumb|center|Распределение подписчиков по полу]]&lt;br /&gt;
&lt;br /&gt;
=== Возрастное распределение ===&lt;br /&gt;
[[Файл:Vk api8.png|600px|thumb|center|Возрастное распределение подписчиков со средним значением]]&lt;br /&gt;
&lt;br /&gt;
=== Топ-5 городов ===&lt;br /&gt;
[[Файл:Vk api9.png|600px|thumb|center|5 городов с наибольшим количеством подписчиков]]&lt;br /&gt;
&lt;br /&gt;
=== Вовлечённость по типам контента ===&lt;br /&gt;
[[Файл:Vk api10.png|600px|thumb|center|Сравнение вовлечённости подписчиков в зависимости от типа контента]]&lt;br /&gt;
&lt;br /&gt;
=== Рекомендации ===&lt;br /&gt;
По итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлечённости и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества.&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api11.png|1200px|thumb|center|Рекомендации по контент-стратегии]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45413</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45413"/>
		<updated>2026-03-26T20:20:14Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;app.py&#039;&#039;&#039; — основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
TOKEN = &amp;quot;Ваш токен&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Собирает данные о сообществе&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            # Если не получилось, пробуем как числовой ID&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            members = vk.groups.getMembers(group_id=group_id_for_api, fields=&#039;sex,bdate,city&#039;, count=500)&lt;br /&gt;
            for user in members[&#039;items&#039;]:&lt;br /&gt;
                user_info = {}&lt;br /&gt;
                &lt;br /&gt;
                # Пол&lt;br /&gt;
                if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                &lt;br /&gt;
                # Возраст&lt;br /&gt;
                if user.get(&#039;bdate&#039;):&lt;br /&gt;
                    bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                    if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                        try:&lt;br /&gt;
                            year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                            age = datetime.now().year - year&lt;br /&gt;
                            if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                user_info[&#039;age&#039;] = age&lt;br /&gt;
                        except:&lt;br /&gt;
                            pass&lt;br /&gt;
                &lt;br /&gt;
                # Город&lt;br /&gt;
                if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                    user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                &lt;br /&gt;
                members_data.append(user_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)&lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                # ER&lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
# Генерирует графики&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    # Настройка шрифтов для русских букв&lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        ax.hist(ages, bins=15, color=&#039;skyblue&#039;, edgecolor=&#039;black&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
    &lt;br /&gt;
        # Создаём горизонтальную столбчатую диаграмму&lt;br /&gt;
        bars = ax.barh(city_names, city_values, color=&#039;lightcoral&#039;, edgecolor=&#039;darkred&#039;)&lt;br /&gt;
        ax.set_title(&#039;Топ-5 городов аудитории&#039;, fontsize=14, fontweight=&#039;bold&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;, fontsize=11)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;, fontsize=11)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
    &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            post_type = post[&#039;type&#039;]&lt;br /&gt;
            if post_type not in type_er:&lt;br /&gt;
                type_er[post_type] = []&lt;br /&gt;
            type_er[post_type].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        bars = ax.bar(avg_er.keys(), avg_er.values(), color=&#039;lightgreen&#039;, edgecolor=&#039;darkgreen&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (на 1000 просмотров)&#039;, fontsize=11)&lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        &lt;br /&gt;
        # Добавляем значения на столбцы&lt;br /&gt;
        for bar, val in zip(bars, avg_er.values()):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=9)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    # Извлекаем ID сообщества из ссылки&lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    # Собираем данные&lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    # Генерируем графики&lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    # Считаем статистику&lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    # Пол&lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    # Возраст&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
        stats[&#039;min_age&#039;] = min(ages)&lt;br /&gt;
        stats[&#039;max_age&#039;] = max(ages)&lt;br /&gt;
    &lt;br /&gt;
    # Города&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    # Посты&lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        # Лучший тип контента&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    # Рекомендации&lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Добавляем рекомендацию по городам&lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        main_city_count = stats[&#039;top_cities&#039;][0][1]&lt;br /&gt;
        total_with_city = sum(count for _, count in stats[&#039;top_cities&#039;])&lt;br /&gt;
        if len(stats[&#039;top_cities&#039;]) &amp;gt; 1:&lt;br /&gt;
            recommendations.append(f&amp;quot;🏙️ Топ-5 городов: {&#039;, &#039;.join([f&#039;{city} ({cnt})&#039; for city, cnt in stats[&#039;top_cities&#039;][:3]])}&amp;quot;)&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;templates/index.html&#039;&#039;&#039; — HTML-шаблон с формой ввода, JavaScript-логикой и динамическим отображением графиков и рекомендаций:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;VK Community Analytics&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt;📊 VK Community Analytics&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества (например: durov или vk.com/durov)&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики (4 штуки)&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;static/style.css&#039;&#039;&#039; — файл стилей для веб-интерфейса:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: white;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    padding: 30px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    box-shadow: 0 10px 40px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 10px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border: 2px solid #e0e0e0;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    transition: border-color 0.3s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 15px 30px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:disabled {&lt;br /&gt;
    opacity: 0.6;&lt;br /&gt;
    cursor: not-allowed;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    border: 3px solid #f3f3f3;&lt;br /&gt;
    border-top: 3px solid #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    width: 40px;&lt;br /&gt;
    height: 40px;&lt;br /&gt;
    animation: spin 1s linear infinite;&lt;br /&gt;
    margin: 0 auto 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    0% { transform: rotate(0deg); }&lt;br /&gt;
    100% { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));&lt;br /&gt;
    gap: 25px;&lt;br /&gt;
    margin: 30px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    border-radius: 15px;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s, box-shadow 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card:hover {&lt;br /&gt;
    transform: translateY(-5px);&lt;br /&gt;
    box-shadow: 0 5px 20px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
    font-size: 18px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));&lt;br /&gt;
    gap: 15px;&lt;br /&gt;
    margin: 20px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat:hover {&lt;br /&gt;
    transform: translateY(-3px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 28px;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    margin-top: 5px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: #e8f5e9;&lt;br /&gt;
    border-left: 4px solid #4caf50;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #2e7d32;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 8px 0;&lt;br /&gt;
    color: #1b5e20;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: #ffebee;&lt;br /&gt;
    color: #c62828;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    .input-group {&lt;br /&gt;
        flex-direction: column;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    .chart-grid {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Результат==&lt;br /&gt;
&lt;br /&gt;
Перейдем в браузер и воспользуемся приложением. Главный экран выглядит следующим образом:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api4.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
Введем ссылку на сообщество &amp;quot;Афиша&amp;quot; и посмотрим результат:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api5.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
В приложении выводятся:&lt;br /&gt;
&lt;br /&gt;
* Ключевые метрики:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api6.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
* График, отображающий распределение подписчиков сообщества по полу:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api7.png|600px|слева]]&lt;br /&gt;
&lt;br /&gt;
* График, показывающий распределение подписчиков по возрасту с указанием среднего значения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api8.png|600px|слева]]&lt;br /&gt;
&lt;br /&gt;
* График, выводящий 5 городов с наибольшим количеством подписчиков:&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
* График сравнения вовлеченности подписчиков в зависимости от типа контента:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api9.png|600px|слева]]&lt;br /&gt;
&lt;br /&gt;
* Рекомендации - по итогам анализа определяется средний возраст аудитории, лучший формат контента, уровень вовлеченности и география подписчиков. В зависимости от этих показателей выводятся советы для дальнейшего развития сообщества:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api10.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api10.png&amp;diff=45412</id>
		<title>Файл:Vk api10.png</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api10.png&amp;diff=45412"/>
		<updated>2026-03-26T20:20:00Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api9.png&amp;diff=45408</id>
		<title>Файл:Vk api9.png</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api9.png&amp;diff=45408"/>
		<updated>2026-03-26T20:16:43Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api8.png&amp;diff=45404</id>
		<title>Файл:Vk api8.png</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api8.png&amp;diff=45404"/>
		<updated>2026-03-26T20:12:22Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api7.png&amp;diff=45401</id>
		<title>Файл:Vk api7.png</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api7.png&amp;diff=45401"/>
		<updated>2026-03-26T20:09:30Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api6.png&amp;diff=45400</id>
		<title>Файл:Vk api6.png</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api6.png&amp;diff=45400"/>
		<updated>2026-03-26T20:08:07Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api5.png&amp;diff=45399</id>
		<title>Файл:Vk api5.png</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api5.png&amp;diff=45399"/>
		<updated>2026-03-26T20:06:24Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api4.png&amp;diff=45398</id>
		<title>Файл:Vk api4.png</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api4.png&amp;diff=45398"/>
		<updated>2026-03-26T20:04:42Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45384</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45384"/>
		<updated>2026-03-26T19:24:55Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;app.py&#039;&#039;&#039; — основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
TOKEN = &amp;quot;Ваш токен&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Собирает данные о сообществе&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            # Если не получилось, пробуем как числовой ID&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            members = vk.groups.getMembers(group_id=group_id_for_api, fields=&#039;sex,bdate,city&#039;, count=500)&lt;br /&gt;
            for user in members[&#039;items&#039;]:&lt;br /&gt;
                user_info = {}&lt;br /&gt;
                &lt;br /&gt;
                # Пол&lt;br /&gt;
                if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                &lt;br /&gt;
                # Возраст&lt;br /&gt;
                if user.get(&#039;bdate&#039;):&lt;br /&gt;
                    bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                    if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                        try:&lt;br /&gt;
                            year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                            age = datetime.now().year - year&lt;br /&gt;
                            if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                user_info[&#039;age&#039;] = age&lt;br /&gt;
                        except:&lt;br /&gt;
                            pass&lt;br /&gt;
                &lt;br /&gt;
                # Город&lt;br /&gt;
                if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                    user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                &lt;br /&gt;
                members_data.append(user_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)&lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                # ER&lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
# Генерирует графики&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    # Настройка шрифтов для русских букв&lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        ax.hist(ages, bins=15, color=&#039;skyblue&#039;, edgecolor=&#039;black&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
    &lt;br /&gt;
        # Создаём горизонтальную столбчатую диаграмму&lt;br /&gt;
        bars = ax.barh(city_names, city_values, color=&#039;lightcoral&#039;, edgecolor=&#039;darkred&#039;)&lt;br /&gt;
        ax.set_title(&#039;Топ-5 городов аудитории&#039;, fontsize=14, fontweight=&#039;bold&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;, fontsize=11)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;, fontsize=11)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
    &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            post_type = post[&#039;type&#039;]&lt;br /&gt;
            if post_type not in type_er:&lt;br /&gt;
                type_er[post_type] = []&lt;br /&gt;
            type_er[post_type].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        bars = ax.bar(avg_er.keys(), avg_er.values(), color=&#039;lightgreen&#039;, edgecolor=&#039;darkgreen&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (на 1000 просмотров)&#039;, fontsize=11)&lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        &lt;br /&gt;
        # Добавляем значения на столбцы&lt;br /&gt;
        for bar, val in zip(bars, avg_er.values()):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=9)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    # Извлекаем ID сообщества из ссылки&lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    # Собираем данные&lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    # Генерируем графики&lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    # Считаем статистику&lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    # Пол&lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    # Возраст&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
        stats[&#039;min_age&#039;] = min(ages)&lt;br /&gt;
        stats[&#039;max_age&#039;] = max(ages)&lt;br /&gt;
    &lt;br /&gt;
    # Города&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    # Посты&lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        # Лучший тип контента&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    # Рекомендации&lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Добавляем рекомендацию по городам&lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        main_city_count = stats[&#039;top_cities&#039;][0][1]&lt;br /&gt;
        total_with_city = sum(count for _, count in stats[&#039;top_cities&#039;])&lt;br /&gt;
        if len(stats[&#039;top_cities&#039;]) &amp;gt; 1:&lt;br /&gt;
            recommendations.append(f&amp;quot;🏙️ Топ-5 городов: {&#039;, &#039;.join([f&#039;{city} ({cnt})&#039; for city, cnt in stats[&#039;top_cities&#039;][:3]])}&amp;quot;)&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;templates/index.html&#039;&#039;&#039; — HTML-шаблон с формой ввода, JavaScript-логикой и динамическим отображением графиков и рекомендаций:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;VK Community Analytics&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt;📊 VK Community Analytics&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества (например: durov или vk.com/durov)&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики (4 штуки)&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;static/style.css&#039;&#039;&#039; — файл стилей для веб-интерфейса:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: white;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    padding: 30px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    box-shadow: 0 10px 40px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 10px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border: 2px solid #e0e0e0;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    transition: border-color 0.3s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 15px 30px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:disabled {&lt;br /&gt;
    opacity: 0.6;&lt;br /&gt;
    cursor: not-allowed;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    border: 3px solid #f3f3f3;&lt;br /&gt;
    border-top: 3px solid #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    width: 40px;&lt;br /&gt;
    height: 40px;&lt;br /&gt;
    animation: spin 1s linear infinite;&lt;br /&gt;
    margin: 0 auto 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    0% { transform: rotate(0deg); }&lt;br /&gt;
    100% { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));&lt;br /&gt;
    gap: 25px;&lt;br /&gt;
    margin: 30px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    border-radius: 15px;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s, box-shadow 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card:hover {&lt;br /&gt;
    transform: translateY(-5px);&lt;br /&gt;
    box-shadow: 0 5px 20px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
    font-size: 18px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));&lt;br /&gt;
    gap: 15px;&lt;br /&gt;
    margin: 20px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat:hover {&lt;br /&gt;
    transform: translateY(-3px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 28px;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    margin-top: 5px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: #e8f5e9;&lt;br /&gt;
    border-left: 4px solid #4caf50;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #2e7d32;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 8px 0;&lt;br /&gt;
    color: #1b5e20;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: #ffebee;&lt;br /&gt;
    color: #c62828;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    .input-group {&lt;br /&gt;
        flex-direction: column;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    .chart-grid {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|слева]]&lt;br /&gt;
&lt;br /&gt;
Запускаем основной скрипт приложения:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api3.png|1200px|слева]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api3.png&amp;diff=45383</id>
		<title>Файл:Vk api3.png</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api3.png&amp;diff=45383"/>
		<updated>2026-03-26T19:24:31Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45379</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45379"/>
		<updated>2026-03-26T19:21:13Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;app.py&#039;&#039;&#039; — основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
TOKEN = &amp;quot;Ваш токен&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Собирает данные о сообществе&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            # Если не получилось, пробуем как числовой ID&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            members = vk.groups.getMembers(group_id=group_id_for_api, fields=&#039;sex,bdate,city&#039;, count=500)&lt;br /&gt;
            for user in members[&#039;items&#039;]:&lt;br /&gt;
                user_info = {}&lt;br /&gt;
                &lt;br /&gt;
                # Пол&lt;br /&gt;
                if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                &lt;br /&gt;
                # Возраст&lt;br /&gt;
                if user.get(&#039;bdate&#039;):&lt;br /&gt;
                    bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                    if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                        try:&lt;br /&gt;
                            year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                            age = datetime.now().year - year&lt;br /&gt;
                            if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                user_info[&#039;age&#039;] = age&lt;br /&gt;
                        except:&lt;br /&gt;
                            pass&lt;br /&gt;
                &lt;br /&gt;
                # Город&lt;br /&gt;
                if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                    user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                &lt;br /&gt;
                members_data.append(user_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)&lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                # ER&lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
# Генерирует графики&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    # Настройка шрифтов для русских букв&lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        ax.hist(ages, bins=15, color=&#039;skyblue&#039;, edgecolor=&#039;black&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
    &lt;br /&gt;
        # Создаём горизонтальную столбчатую диаграмму&lt;br /&gt;
        bars = ax.barh(city_names, city_values, color=&#039;lightcoral&#039;, edgecolor=&#039;darkred&#039;)&lt;br /&gt;
        ax.set_title(&#039;Топ-5 городов аудитории&#039;, fontsize=14, fontweight=&#039;bold&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;, fontsize=11)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;, fontsize=11)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
    &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            post_type = post[&#039;type&#039;]&lt;br /&gt;
            if post_type not in type_er:&lt;br /&gt;
                type_er[post_type] = []&lt;br /&gt;
            type_er[post_type].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        bars = ax.bar(avg_er.keys(), avg_er.values(), color=&#039;lightgreen&#039;, edgecolor=&#039;darkgreen&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (на 1000 просмотров)&#039;, fontsize=11)&lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        &lt;br /&gt;
        # Добавляем значения на столбцы&lt;br /&gt;
        for bar, val in zip(bars, avg_er.values()):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=9)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    # Извлекаем ID сообщества из ссылки&lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    # Собираем данные&lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    # Генерируем графики&lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    # Считаем статистику&lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    # Пол&lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    # Возраст&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
        stats[&#039;min_age&#039;] = min(ages)&lt;br /&gt;
        stats[&#039;max_age&#039;] = max(ages)&lt;br /&gt;
    &lt;br /&gt;
    # Города&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    # Посты&lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        # Лучший тип контента&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    # Рекомендации&lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Добавляем рекомендацию по городам&lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        main_city_count = stats[&#039;top_cities&#039;][0][1]&lt;br /&gt;
        total_with_city = sum(count for _, count in stats[&#039;top_cities&#039;])&lt;br /&gt;
        if len(stats[&#039;top_cities&#039;]) &amp;gt; 1:&lt;br /&gt;
            recommendations.append(f&amp;quot;🏙️ Топ-5 городов: {&#039;, &#039;.join([f&#039;{city} ({cnt})&#039; for city, cnt in stats[&#039;top_cities&#039;][:3]])}&amp;quot;)&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;templates/index.html&#039;&#039;&#039; — HTML-шаблон с формой ввода, JavaScript-логикой и динамическим отображением графиков и рекомендаций:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;VK Community Analytics&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt;📊 VK Community Analytics&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества (например: durov или vk.com/durov)&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики (4 штуки)&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;static/style.css&#039;&#039;&#039; — файл стилей для веб-интерфейса:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: white;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    padding: 30px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    box-shadow: 0 10px 40px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 10px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border: 2px solid #e0e0e0;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    transition: border-color 0.3s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 15px 30px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:disabled {&lt;br /&gt;
    opacity: 0.6;&lt;br /&gt;
    cursor: not-allowed;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    border: 3px solid #f3f3f3;&lt;br /&gt;
    border-top: 3px solid #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    width: 40px;&lt;br /&gt;
    height: 40px;&lt;br /&gt;
    animation: spin 1s linear infinite;&lt;br /&gt;
    margin: 0 auto 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    0% { transform: rotate(0deg); }&lt;br /&gt;
    100% { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));&lt;br /&gt;
    gap: 25px;&lt;br /&gt;
    margin: 30px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    border-radius: 15px;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s, box-shadow 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card:hover {&lt;br /&gt;
    transform: translateY(-5px);&lt;br /&gt;
    box-shadow: 0 5px 20px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
    font-size: 18px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));&lt;br /&gt;
    gap: 15px;&lt;br /&gt;
    margin: 20px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat:hover {&lt;br /&gt;
    transform: translateY(-3px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 28px;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    margin-top: 5px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: #e8f5e9;&lt;br /&gt;
    border-left: 4px solid #4caf50;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #2e7d32;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 8px 0;&lt;br /&gt;
    color: #1b5e20;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: #ffebee;&lt;br /&gt;
    color: #c62828;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    .input-group {&lt;br /&gt;
        flex-direction: column;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    .chart-grid {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
== Ход выполнения ==&lt;br /&gt;
&lt;br /&gt;
Устанавливаем необходимые библиотеки:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api2.png|1000px|мини|слева]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api2.png&amp;diff=45378</id>
		<title>Файл:Vk api2.png</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%A4%D0%B0%D0%B9%D0%BB:Vk_api2.png&amp;diff=45378"/>
		<updated>2026-03-26T19:20:37Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;-&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45377</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45377"/>
		<updated>2026-03-26T19:17:58Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;app.py&#039;&#039;&#039; — основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
TOKEN = &amp;quot;Ваш токен&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Собирает данные о сообществе&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            # Если не получилось, пробуем как числовой ID&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            members = vk.groups.getMembers(group_id=group_id_for_api, fields=&#039;sex,bdate,city&#039;, count=500)&lt;br /&gt;
            for user in members[&#039;items&#039;]:&lt;br /&gt;
                user_info = {}&lt;br /&gt;
                &lt;br /&gt;
                # Пол&lt;br /&gt;
                if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                &lt;br /&gt;
                # Возраст&lt;br /&gt;
                if user.get(&#039;bdate&#039;):&lt;br /&gt;
                    bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                    if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                        try:&lt;br /&gt;
                            year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                            age = datetime.now().year - year&lt;br /&gt;
                            if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                user_info[&#039;age&#039;] = age&lt;br /&gt;
                        except:&lt;br /&gt;
                            pass&lt;br /&gt;
                &lt;br /&gt;
                # Город&lt;br /&gt;
                if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                    user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                &lt;br /&gt;
                members_data.append(user_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)&lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                # ER&lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
# Генерирует графики&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    # Настройка шрифтов для русских букв&lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        ax.hist(ages, bins=15, color=&#039;skyblue&#039;, edgecolor=&#039;black&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
    &lt;br /&gt;
        # Создаём горизонтальную столбчатую диаграмму&lt;br /&gt;
        bars = ax.barh(city_names, city_values, color=&#039;lightcoral&#039;, edgecolor=&#039;darkred&#039;)&lt;br /&gt;
        ax.set_title(&#039;Топ-5 городов аудитории&#039;, fontsize=14, fontweight=&#039;bold&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;, fontsize=11)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;, fontsize=11)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
    &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            post_type = post[&#039;type&#039;]&lt;br /&gt;
            if post_type not in type_er:&lt;br /&gt;
                type_er[post_type] = []&lt;br /&gt;
            type_er[post_type].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        bars = ax.bar(avg_er.keys(), avg_er.values(), color=&#039;lightgreen&#039;, edgecolor=&#039;darkgreen&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (на 1000 просмотров)&#039;, fontsize=11)&lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        &lt;br /&gt;
        # Добавляем значения на столбцы&lt;br /&gt;
        for bar, val in zip(bars, avg_er.values()):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=9)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    # Извлекаем ID сообщества из ссылки&lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    # Собираем данные&lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    # Генерируем графики&lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    # Считаем статистику&lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    # Пол&lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    # Возраст&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
        stats[&#039;min_age&#039;] = min(ages)&lt;br /&gt;
        stats[&#039;max_age&#039;] = max(ages)&lt;br /&gt;
    &lt;br /&gt;
    # Города&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    # Посты&lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        # Лучший тип контента&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    # Рекомендации&lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Добавляем рекомендацию по городам&lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        main_city_count = stats[&#039;top_cities&#039;][0][1]&lt;br /&gt;
        total_with_city = sum(count for _, count in stats[&#039;top_cities&#039;])&lt;br /&gt;
        if len(stats[&#039;top_cities&#039;]) &amp;gt; 1:&lt;br /&gt;
            recommendations.append(f&amp;quot;🏙️ Топ-5 городов: {&#039;, &#039;.join([f&#039;{city} ({cnt})&#039; for city, cnt in stats[&#039;top_cities&#039;][:3]])}&amp;quot;)&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;templates/index.html&#039;&#039;&#039; — HTML-шаблон с формой ввода, JavaScript-логикой и динамическим отображением графиков и рекомендаций:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
&amp;lt;!DOCTYPE html&amp;gt;&lt;br /&gt;
&amp;lt;html lang=&amp;quot;ru&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;head&amp;gt;&lt;br /&gt;
    &amp;lt;meta charset=&amp;quot;UTF-8&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;meta name=&amp;quot;viewport&amp;quot; content=&amp;quot;width=device-width, initial-scale=1.0&amp;quot;&amp;gt;&lt;br /&gt;
    &amp;lt;title&amp;gt;VK Community Analytics&amp;lt;/title&amp;gt;&lt;br /&gt;
    &amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;{{ url_for(&#039;static&#039;, filename=&#039;style.css&#039;) }}&amp;quot;&amp;gt;&lt;br /&gt;
&amp;lt;/head&amp;gt;&lt;br /&gt;
&amp;lt;body&amp;gt;&lt;br /&gt;
    &amp;lt;div class=&amp;quot;container&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div class=&amp;quot;card&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;h1&amp;gt;📊 VK Community Analytics&amp;lt;/h1&amp;gt;&lt;br /&gt;
            &amp;lt;p class=&amp;quot;subtitle&amp;quot;&amp;gt;Анализ аудитории сообщества и рекомендации по контенту&amp;lt;/p&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;input-group&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input type=&amp;quot;text&amp;quot; id=&amp;quot;groupUrl&amp;quot; placeholder=&amp;quot;Введите ссылку или ID сообщества (например: durov или vk.com/durov)&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;button onclick=&amp;quot;analyze()&amp;quot; id=&amp;quot;analyzeBtn&amp;quot;&amp;gt;🔍 Анализировать&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;loader&amp;quot; id=&amp;quot;loader&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;loader-spinner&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;p&amp;gt;Анализируем сообщество... Это может занять до 30 секунд&amp;lt;/p&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
            &lt;br /&gt;
            &amp;lt;div class=&amp;quot;results&amp;quot; id=&amp;quot;results&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stats&amp;quot; id=&amp;quot;stats&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;chart-grid&amp;quot; id=&amp;quot;charts&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;recommendations&amp;quot; id=&amp;quot;recommendations&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
    &lt;br /&gt;
    &amp;lt;script&amp;gt;&lt;br /&gt;
        async function analyze() {&lt;br /&gt;
            const groupUrl = document.getElementById(&#039;groupUrl&#039;).value;&lt;br /&gt;
            if (!groupUrl) {&lt;br /&gt;
                alert(&#039;Введите ссылку или ID сообщества&#039;);&lt;br /&gt;
                return;&lt;br /&gt;
            }&lt;br /&gt;
            &lt;br /&gt;
            const btn = document.getElementById(&#039;analyzeBtn&#039;);&lt;br /&gt;
            const loader = document.getElementById(&#039;loader&#039;);&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            &lt;br /&gt;
            btn.disabled = true;&lt;br /&gt;
            loader.style.display = &#039;block&#039;;&lt;br /&gt;
            results.style.display = &#039;none&#039;;&lt;br /&gt;
            &lt;br /&gt;
            try {&lt;br /&gt;
                const response = await fetch(&#039;/analyze&#039;, {&lt;br /&gt;
                    method: &#039;POST&#039;,&lt;br /&gt;
                    headers: {&lt;br /&gt;
                        &#039;Content-Type&#039;: &#039;application/json&#039;&lt;br /&gt;
                    },&lt;br /&gt;
                    body: JSON.stringify({ group_url: groupUrl })&lt;br /&gt;
                });&lt;br /&gt;
                &lt;br /&gt;
                const data = await response.json();&lt;br /&gt;
                &lt;br /&gt;
                if (data.success) {&lt;br /&gt;
                    displayResults(data);&lt;br /&gt;
                } else {&lt;br /&gt;
                    showError(data.error);&lt;br /&gt;
                }&lt;br /&gt;
            } catch (error) {&lt;br /&gt;
                showError(&#039;Ошибка соединения с сервером&#039;);&lt;br /&gt;
            } finally {&lt;br /&gt;
                btn.disabled = false;&lt;br /&gt;
                loader.style.display = &#039;none&#039;;&lt;br /&gt;
            }&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function displayResults(data) {&lt;br /&gt;
            // Статистика&lt;br /&gt;
            const statsHtml = `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.community_name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Название сообщества&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.members_count.toLocaleString()}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Подписчиков&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ${data.stats.avg_age ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_age.toFixed(1)} лет&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средний возраст&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
                ${data.stats.avg_er ? `&lt;br /&gt;
                &amp;lt;div class=&amp;quot;stat&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-value&amp;quot;&amp;gt;${data.stats.avg_er.toFixed(2)}&amp;lt;/div&amp;gt;&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;stat-label&amp;quot;&amp;gt;Средняя вовлечённость (ER)&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;/div&amp;gt;&lt;br /&gt;
                ` : &#039;&#039;}&lt;br /&gt;
            `;&lt;br /&gt;
            document.getElementById(&#039;stats&#039;).innerHTML = statsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Графики (4 штуки)&lt;br /&gt;
            let chartsHtml = &#039;&#039;;&lt;br /&gt;
            if (data.charts.sex) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;👥 Распределение по полу&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.sex}&amp;quot; alt=&amp;quot;Распределение по полу&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.age) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🎂 Возрастное распределение&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.age}&amp;quot; alt=&amp;quot;Возраст&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.cities) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;🏙️ Топ-5 городов&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.cities}&amp;quot; alt=&amp;quot;Города&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            if (data.charts.engagement) {&lt;br /&gt;
                chartsHtml += `&lt;br /&gt;
                    &amp;lt;div class=&amp;quot;chart-card&amp;quot;&amp;gt;&lt;br /&gt;
                        &amp;lt;h3&amp;gt;📈 Вовлечённость по типам контента&amp;lt;/h3&amp;gt;&lt;br /&gt;
                        &amp;lt;img src=&amp;quot;data:image/png;base64,${data.charts.engagement}&amp;quot; alt=&amp;quot;ER&amp;quot;&amp;gt;&lt;br /&gt;
                    &amp;lt;/div&amp;gt;&lt;br /&gt;
                `;&lt;br /&gt;
            }&lt;br /&gt;
            document.getElementById(&#039;charts&#039;).innerHTML = chartsHtml;&lt;br /&gt;
            &lt;br /&gt;
            // Рекомендации&lt;br /&gt;
            let recHtml = &#039;&amp;lt;h3&amp;gt;💡 Рекомендации по контент-стратегии&amp;lt;/h3&amp;gt;&amp;lt;ul&amp;gt;&#039;;&lt;br /&gt;
            data.recommendations.forEach(rec =&amp;gt; {&lt;br /&gt;
                recHtml += `&amp;lt;li&amp;gt;${rec}&amp;lt;/li&amp;gt;`;&lt;br /&gt;
            });&lt;br /&gt;
            recHtml += &#039;&amp;lt;/ul&amp;gt;&#039;;&lt;br /&gt;
            document.getElementById(&#039;recommendations&#039;).innerHTML = recHtml;&lt;br /&gt;
            &lt;br /&gt;
            document.getElementById(&#039;results&#039;).style.display = &#039;block&#039;;&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
        function showError(message) {&lt;br /&gt;
            const results = document.getElementById(&#039;results&#039;);&lt;br /&gt;
            results.style.display = &#039;block&#039;;&lt;br /&gt;
            results.innerHTML = `&amp;lt;div class=&amp;quot;error&amp;quot;&amp;gt;❌ Ошибка: ${message}&amp;lt;/div&amp;gt;`;&lt;br /&gt;
        }&lt;br /&gt;
    &amp;lt;/script&amp;gt;&lt;br /&gt;
&amp;lt;/body&amp;gt;&lt;br /&gt;
&amp;lt;/html&amp;gt;&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;static/style.css&#039;&#039;&#039; — файл стилей для веб-интерфейса:&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
* {&lt;br /&gt;
    margin: 0;&lt;br /&gt;
    padding: 0;&lt;br /&gt;
    box-sizing: border-box;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
body {&lt;br /&gt;
    font-family: -apple-system, BlinkMacSystemFont, &#039;Segoe UI&#039;, Roboto, sans-serif;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    min-height: 100vh;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.container {&lt;br /&gt;
    max-width: 1200px;&lt;br /&gt;
    margin: 0 auto;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.card {&lt;br /&gt;
    background: white;&lt;br /&gt;
    border-radius: 20px;&lt;br /&gt;
    padding: 30px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
    box-shadow: 0 10px 40px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
h1 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.subtitle {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    margin-bottom: 30px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.input-group {&lt;br /&gt;
    display: flex;&lt;br /&gt;
    gap: 10px;&lt;br /&gt;
    margin-bottom: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input {&lt;br /&gt;
    flex: 1;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border: 2px solid #e0e0e0;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    transition: border-color 0.3s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
input:focus {&lt;br /&gt;
    outline: none;&lt;br /&gt;
    border-color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button {&lt;br /&gt;
    padding: 15px 30px;&lt;br /&gt;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);&lt;br /&gt;
    color: white;&lt;br /&gt;
    border: none;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    font-size: 16px;&lt;br /&gt;
    cursor: pointer;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:hover {&lt;br /&gt;
    transform: translateY(-2px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
button:disabled {&lt;br /&gt;
    opacity: 0.6;&lt;br /&gt;
    cursor: not-allowed;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader {&lt;br /&gt;
    display: none;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    padding: 40px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.loader-spinner {&lt;br /&gt;
    border: 3px solid #f3f3f3;&lt;br /&gt;
    border-top: 3px solid #667eea;&lt;br /&gt;
    border-radius: 50%;&lt;br /&gt;
    width: 40px;&lt;br /&gt;
    height: 40px;&lt;br /&gt;
    animation: spin 1s linear infinite;&lt;br /&gt;
    margin: 0 auto 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@keyframes spin {&lt;br /&gt;
    0% { transform: rotate(0deg); }&lt;br /&gt;
    100% { transform: rotate(360deg); }&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.results {&lt;br /&gt;
    display: none;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-grid {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));&lt;br /&gt;
    gap: 25px;&lt;br /&gt;
    margin: 30px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    border-radius: 15px;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s, box-shadow 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card:hover {&lt;br /&gt;
    transform: translateY(-5px);&lt;br /&gt;
    box-shadow: 0 5px 20px rgba(0,0,0,0.1);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card h3 {&lt;br /&gt;
    color: #333;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
    font-size: 18px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.chart-card img {&lt;br /&gt;
    max-width: 100%;&lt;br /&gt;
    height: auto;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stats {&lt;br /&gt;
    display: grid;&lt;br /&gt;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));&lt;br /&gt;
    gap: 15px;&lt;br /&gt;
    margin: 20px 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat {&lt;br /&gt;
    background: #f8f9fa;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    text-align: center;&lt;br /&gt;
    transition: transform 0.2s;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat:hover {&lt;br /&gt;
    transform: translateY(-3px);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-value {&lt;br /&gt;
    font-size: 28px;&lt;br /&gt;
    font-weight: bold;&lt;br /&gt;
    color: #667eea;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.stat-label {&lt;br /&gt;
    color: #666;&lt;br /&gt;
    font-size: 14px;&lt;br /&gt;
    margin-top: 5px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations {&lt;br /&gt;
    background: #e8f5e9;&lt;br /&gt;
    border-left: 4px solid #4caf50;&lt;br /&gt;
    padding: 20px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations h3 {&lt;br /&gt;
    color: #2e7d32;&lt;br /&gt;
    margin-bottom: 15px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations ul {&lt;br /&gt;
    list-style: none;&lt;br /&gt;
    padding-left: 0;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.recommendations li {&lt;br /&gt;
    padding: 8px 0;&lt;br /&gt;
    color: #1b5e20;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
.error {&lt;br /&gt;
    background: #ffebee;&lt;br /&gt;
    color: #c62828;&lt;br /&gt;
    padding: 15px;&lt;br /&gt;
    border-radius: 10px;&lt;br /&gt;
    margin-top: 20px;&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
@media (max-width: 768px) {&lt;br /&gt;
    .input-group {&lt;br /&gt;
        flex-direction: column;&lt;br /&gt;
    }&lt;br /&gt;
    &lt;br /&gt;
    .chart-grid {&lt;br /&gt;
        grid-template-columns: 1fr;&lt;br /&gt;
    }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45376</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45376"/>
		<updated>2026-03-26T19:13:40Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
&#039;&#039;&#039;app.py&#039;&#039;&#039; — основной Python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
TOKEN = &amp;quot;Ваш токен&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Собирает данные о сообществе&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            # Если не получилось, пробуем как числовой ID&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            members = vk.groups.getMembers(group_id=group_id_for_api, fields=&#039;sex,bdate,city&#039;, count=500)&lt;br /&gt;
            for user in members[&#039;items&#039;]:&lt;br /&gt;
                user_info = {}&lt;br /&gt;
                &lt;br /&gt;
                # Пол&lt;br /&gt;
                if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                &lt;br /&gt;
                # Возраст&lt;br /&gt;
                if user.get(&#039;bdate&#039;):&lt;br /&gt;
                    bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                    if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                        try:&lt;br /&gt;
                            year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                            age = datetime.now().year - year&lt;br /&gt;
                            if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                user_info[&#039;age&#039;] = age&lt;br /&gt;
                        except:&lt;br /&gt;
                            pass&lt;br /&gt;
                &lt;br /&gt;
                # Город&lt;br /&gt;
                if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                    user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                &lt;br /&gt;
                members_data.append(user_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)&lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                # ER&lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
# Генерирует графики&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    # Настройка шрифтов для русских букв&lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        ax.hist(ages, bins=15, color=&#039;skyblue&#039;, edgecolor=&#039;black&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
    &lt;br /&gt;
        # Создаём горизонтальную столбчатую диаграмму&lt;br /&gt;
        bars = ax.barh(city_names, city_values, color=&#039;lightcoral&#039;, edgecolor=&#039;darkred&#039;)&lt;br /&gt;
        ax.set_title(&#039;Топ-5 городов аудитории&#039;, fontsize=14, fontweight=&#039;bold&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;, fontsize=11)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;, fontsize=11)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
    &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            post_type = post[&#039;type&#039;]&lt;br /&gt;
            if post_type not in type_er:&lt;br /&gt;
                type_er[post_type] = []&lt;br /&gt;
            type_er[post_type].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        bars = ax.bar(avg_er.keys(), avg_er.values(), color=&#039;lightgreen&#039;, edgecolor=&#039;darkgreen&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (на 1000 просмотров)&#039;, fontsize=11)&lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        &lt;br /&gt;
        # Добавляем значения на столбцы&lt;br /&gt;
        for bar, val in zip(bars, avg_er.values()):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=9)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    # Извлекаем ID сообщества из ссылки&lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    # Собираем данные&lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    # Генерируем графики&lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    # Считаем статистику&lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    # Пол&lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    # Возраст&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
        stats[&#039;min_age&#039;] = min(ages)&lt;br /&gt;
        stats[&#039;max_age&#039;] = max(ages)&lt;br /&gt;
    &lt;br /&gt;
    # Города&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    # Посты&lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        # Лучший тип контента&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    # Рекомендации&lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Добавляем рекомендацию по городам&lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        main_city_count = stats[&#039;top_cities&#039;][0][1]&lt;br /&gt;
        total_with_city = sum(count for _, count in stats[&#039;top_cities&#039;])&lt;br /&gt;
        if len(stats[&#039;top_cities&#039;]) &amp;gt; 1:&lt;br /&gt;
            recommendations.append(f&amp;quot;🏙️ Топ-5 городов: {&#039;, &#039;.join([f&#039;{city} ({cnt})&#039; for city, cnt in stats[&#039;top_cities&#039;][:3]])}&amp;quot;)&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45375</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45375"/>
		<updated>2026-03-26T19:12:48Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
**app.py** - основной python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
TOKEN = &amp;quot;Ваш токен&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Собирает данные о сообществе&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            # Если не получилось, пробуем как числовой ID&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            members = vk.groups.getMembers(group_id=group_id_for_api, fields=&#039;sex,bdate,city&#039;, count=500)&lt;br /&gt;
            for user in members[&#039;items&#039;]:&lt;br /&gt;
                user_info = {}&lt;br /&gt;
                &lt;br /&gt;
                # Пол&lt;br /&gt;
                if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                &lt;br /&gt;
                # Возраст&lt;br /&gt;
                if user.get(&#039;bdate&#039;):&lt;br /&gt;
                    bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                    if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                        try:&lt;br /&gt;
                            year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                            age = datetime.now().year - year&lt;br /&gt;
                            if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                user_info[&#039;age&#039;] = age&lt;br /&gt;
                        except:&lt;br /&gt;
                            pass&lt;br /&gt;
                &lt;br /&gt;
                # Город&lt;br /&gt;
                if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                    user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                &lt;br /&gt;
                members_data.append(user_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)&lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                # ER&lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
# Генерирует графики&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    # Настройка шрифтов для русских букв&lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        ax.hist(ages, bins=15, color=&#039;skyblue&#039;, edgecolor=&#039;black&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
    &lt;br /&gt;
        # Создаём горизонтальную столбчатую диаграмму&lt;br /&gt;
        bars = ax.barh(city_names, city_values, color=&#039;lightcoral&#039;, edgecolor=&#039;darkred&#039;)&lt;br /&gt;
        ax.set_title(&#039;Топ-5 городов аудитории&#039;, fontsize=14, fontweight=&#039;bold&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;, fontsize=11)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;, fontsize=11)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
    &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            post_type = post[&#039;type&#039;]&lt;br /&gt;
            if post_type not in type_er:&lt;br /&gt;
                type_er[post_type] = []&lt;br /&gt;
            type_er[post_type].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        bars = ax.bar(avg_er.keys(), avg_er.values(), color=&#039;lightgreen&#039;, edgecolor=&#039;darkgreen&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (на 1000 просмотров)&#039;, fontsize=11)&lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        &lt;br /&gt;
        # Добавляем значения на столбцы&lt;br /&gt;
        for bar, val in zip(bars, avg_er.values()):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=9)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    # Извлекаем ID сообщества из ссылки&lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    # Собираем данные&lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    # Генерируем графики&lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    # Считаем статистику&lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    # Пол&lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    # Возраст&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
        stats[&#039;min_age&#039;] = min(ages)&lt;br /&gt;
        stats[&#039;max_age&#039;] = max(ages)&lt;br /&gt;
    &lt;br /&gt;
    # Города&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    # Посты&lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        # Лучший тип контента&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    # Рекомендации&lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Добавляем рекомендацию по городам&lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        main_city_count = stats[&#039;top_cities&#039;][0][1]&lt;br /&gt;
        total_with_city = sum(count for _, count in stats[&#039;top_cities&#039;])&lt;br /&gt;
        if len(stats[&#039;top_cities&#039;]) &amp;gt; 1:&lt;br /&gt;
            recommendations.append(f&amp;quot;🏙️ Топ-5 городов: {&#039;, &#039;.join([f&#039;{city} ({cnt})&#039; for city, cnt in stats[&#039;top_cities&#039;][:3]])}&amp;quot;)&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45372</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45372"/>
		<updated>2026-03-26T19:11:05Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
Основной python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;pre&amp;gt;&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
TOKEN = &amp;quot;Ваш токен&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Собирает данные о сообществе&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            # Если не получилось, пробуем как числовой ID&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            members = vk.groups.getMembers(group_id=group_id_for_api, fields=&#039;sex,bdate,city&#039;, count=500)&lt;br /&gt;
            for user in members[&#039;items&#039;]:&lt;br /&gt;
                user_info = {}&lt;br /&gt;
                &lt;br /&gt;
                # Пол&lt;br /&gt;
                if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                &lt;br /&gt;
                # Возраст&lt;br /&gt;
                if user.get(&#039;bdate&#039;):&lt;br /&gt;
                    bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                    if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                        try:&lt;br /&gt;
                            year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                            age = datetime.now().year - year&lt;br /&gt;
                            if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                user_info[&#039;age&#039;] = age&lt;br /&gt;
                        except:&lt;br /&gt;
                            pass&lt;br /&gt;
                &lt;br /&gt;
                # Город&lt;br /&gt;
                if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                    user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                &lt;br /&gt;
                members_data.append(user_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)&lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                # ER&lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
# Генерирует графики&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    # Настройка шрифтов для русских букв&lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        ax.hist(ages, bins=15, color=&#039;skyblue&#039;, edgecolor=&#039;black&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
    &lt;br /&gt;
        # Создаём горизонтальную столбчатую диаграмму&lt;br /&gt;
        bars = ax.barh(city_names, city_values, color=&#039;lightcoral&#039;, edgecolor=&#039;darkred&#039;)&lt;br /&gt;
        ax.set_title(&#039;Топ-5 городов аудитории&#039;, fontsize=14, fontweight=&#039;bold&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;, fontsize=11)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;, fontsize=11)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
    &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            post_type = post[&#039;type&#039;]&lt;br /&gt;
            if post_type not in type_er:&lt;br /&gt;
                type_er[post_type] = []&lt;br /&gt;
            type_er[post_type].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        bars = ax.bar(avg_er.keys(), avg_er.values(), color=&#039;lightgreen&#039;, edgecolor=&#039;darkgreen&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (на 1000 просмотров)&#039;, fontsize=11)&lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        &lt;br /&gt;
        # Добавляем значения на столбцы&lt;br /&gt;
        for bar, val in zip(bars, avg_er.values()):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=9)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    # Извлекаем ID сообщества из ссылки&lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    # Собираем данные&lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    # Генерируем графики&lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    # Считаем статистику&lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    # Пол&lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    # Возраст&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
        stats[&#039;min_age&#039;] = min(ages)&lt;br /&gt;
        stats[&#039;max_age&#039;] = max(ages)&lt;br /&gt;
    &lt;br /&gt;
    # Города&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    # Посты&lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        # Лучший тип контента&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    # Рекомендации&lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Добавляем рекомендацию по городам&lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        main_city_count = stats[&#039;top_cities&#039;][0][1]&lt;br /&gt;
        total_with_city = sum(count for _, count in stats[&#039;top_cities&#039;])&lt;br /&gt;
        if len(stats[&#039;top_cities&#039;]) &amp;gt; 1:&lt;br /&gt;
            recommendations.append(f&amp;quot;🏙️ Топ-5 городов: {&#039;, &#039;.join([f&#039;{city} ({cnt})&#039; for city, cnt in stats[&#039;top_cities&#039;][:3]])}&amp;quot;)&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
&amp;lt;/pre&amp;gt;&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45369</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45369"/>
		<updated>2026-03-26T19:09:12Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
Основной python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask:&lt;br /&gt;
```&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
TOKEN = &amp;quot;Ваш токен&amp;quot;&lt;br /&gt;
&lt;br /&gt;
# Собирает данные о сообществе&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            # Если не получилось, пробуем как числовой ID&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            members = vk.groups.getMembers(group_id=group_id_for_api, fields=&#039;sex,bdate,city&#039;, count=500)&lt;br /&gt;
            for user in members[&#039;items&#039;]:&lt;br /&gt;
                user_info = {}&lt;br /&gt;
                &lt;br /&gt;
                # Пол&lt;br /&gt;
                if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                &lt;br /&gt;
                # Возраст&lt;br /&gt;
                if user.get(&#039;bdate&#039;):&lt;br /&gt;
                    bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                    if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                        try:&lt;br /&gt;
                            year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                            age = datetime.now().year - year&lt;br /&gt;
                            if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                user_info[&#039;age&#039;] = age&lt;br /&gt;
                        except:&lt;br /&gt;
                            pass&lt;br /&gt;
                &lt;br /&gt;
                # Город&lt;br /&gt;
                if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                    user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                &lt;br /&gt;
                members_data.append(user_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)&lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                # ER&lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
# Генерирует графики&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    # Настройка шрифтов для русских букв&lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        ax.hist(ages, bins=15, color=&#039;skyblue&#039;, edgecolor=&#039;black&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
    &lt;br /&gt;
        # Создаём горизонтальную столбчатую диаграмму&lt;br /&gt;
        bars = ax.barh(city_names, city_values, color=&#039;lightcoral&#039;, edgecolor=&#039;darkred&#039;)&lt;br /&gt;
        ax.set_title(&#039;Топ-5 городов аудитории&#039;, fontsize=14, fontweight=&#039;bold&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;, fontsize=11)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;, fontsize=11)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
    &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            post_type = post[&#039;type&#039;]&lt;br /&gt;
            if post_type not in type_er:&lt;br /&gt;
                type_er[post_type] = []&lt;br /&gt;
            type_er[post_type].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        bars = ax.bar(avg_er.keys(), avg_er.values(), color=&#039;lightgreen&#039;, edgecolor=&#039;darkgreen&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (на 1000 просмотров)&#039;, fontsize=11)&lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        &lt;br /&gt;
        # Добавляем значения на столбцы&lt;br /&gt;
        for bar, val in zip(bars, avg_er.values()):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=9)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    # Извлекаем ID сообщества из ссылки&lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    # Собираем данные&lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    # Генерируем графики&lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    # Считаем статистику&lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    # Пол&lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    # Возраст&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
        stats[&#039;min_age&#039;] = min(ages)&lt;br /&gt;
        stats[&#039;max_age&#039;] = max(ages)&lt;br /&gt;
    &lt;br /&gt;
    # Города&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    # Посты&lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        # Лучший тип контента&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    # Рекомендации&lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Добавляем рекомендацию по городам&lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        main_city_count = stats[&#039;top_cities&#039;][0][1]&lt;br /&gt;
        total_with_city = sum(count for _, count in stats[&#039;top_cities&#039;])&lt;br /&gt;
        if len(stats[&#039;top_cities&#039;]) &amp;gt; 1:&lt;br /&gt;
            recommendations.append(f&amp;quot;🏙️ Топ-5 городов: {&#039;, &#039;.join([f&#039;{city} ({cnt})&#039; for city, cnt in stats[&#039;top_cities&#039;][:3]])}&amp;quot;)&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45367</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45367"/>
		<updated>2026-03-26T19:07:32Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
Основной python-скрипт приложения. Обеспечивает интеграцию с VK API, сбор данных о подписчиках и постах, генерацию 4 графиков, формирование рекомендаций и работу веб-сервера на Flask:&lt;br /&gt;
```&lt;br /&gt;
from flask import Flask, render_template, request, jsonify&lt;br /&gt;
import vk_api&lt;br /&gt;
import matplotlib&lt;br /&gt;
matplotlib.use(&#039;Agg&#039;)  &lt;br /&gt;
import matplotlib.pyplot as plt&lt;br /&gt;
from collections import Counter&lt;br /&gt;
from datetime import datetime&lt;br /&gt;
import os&lt;br /&gt;
import base64&lt;br /&gt;
import io&lt;br /&gt;
&lt;br /&gt;
app = Flask(__name__)&lt;br /&gt;
&lt;br /&gt;
# Папка для статики&lt;br /&gt;
os.makedirs(&#039;static&#039;, exist_ok=True)&lt;br /&gt;
&lt;br /&gt;
TOKEN = &amp;quot;Ваш токен&amp;quot;&lt;br /&gt;
&lt;br /&gt;
def get_community_data(group_id):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Собирает данные о сообществе&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    try:&lt;br /&gt;
        vk_session = vk_api.VkApi(token=TOKEN)&lt;br /&gt;
        vk = vk_session.get_api()&lt;br /&gt;
        &lt;br /&gt;
        # Определяем ID сообщества&lt;br /&gt;
        if str(group_id).lstrip(&#039;-&#039;).isdigit():&lt;br /&gt;
            owner_id = int(group_id)&lt;br /&gt;
            group_id_for_api = owner_id&lt;br /&gt;
        else:&lt;br /&gt;
            group_id_for_api = group_id&lt;br /&gt;
        &lt;br /&gt;
        # Получаем информацию о сообществе&lt;br /&gt;
        try:&lt;br /&gt;
            group = vk.groups.getById(group_id=group_id_for_api, fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        except:&lt;br /&gt;
            # Если не получилось, пробуем как числовой ID&lt;br /&gt;
            group = vk.groups.getById(group_id=str(group_id_for_api), fields=&#039;members_count,description&#039;)[0]&lt;br /&gt;
            community_name = group[&#039;name&#039;]&lt;br /&gt;
            members_count = group[&#039;members_count&#039;]&lt;br /&gt;
        &lt;br /&gt;
        # Собираем подписчиков&lt;br /&gt;
        members_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            members = vk.groups.getMembers(group_id=group_id_for_api, fields=&#039;sex,bdate,city&#039;, count=500)&lt;br /&gt;
            for user in members[&#039;items&#039;]:&lt;br /&gt;
                user_info = {}&lt;br /&gt;
                &lt;br /&gt;
                # Пол&lt;br /&gt;
                if user.get(&#039;sex&#039;) == 1:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Женщины&#039;&lt;br /&gt;
                elif user.get(&#039;sex&#039;) == 2:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Мужчины&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    user_info[&#039;sex&#039;] = &#039;Не указан&#039;&lt;br /&gt;
                &lt;br /&gt;
                # Возраст&lt;br /&gt;
                if user.get(&#039;bdate&#039;):&lt;br /&gt;
                    bdate = user[&#039;bdate&#039;]&lt;br /&gt;
                    if len(bdate.split(&#039;.&#039;)) == 3:&lt;br /&gt;
                        try:&lt;br /&gt;
                            year = int(bdate.split(&#039;.&#039;)[2])&lt;br /&gt;
                            age = datetime.now().year - year&lt;br /&gt;
                            if 0 &amp;lt; age &amp;lt; 100:&lt;br /&gt;
                                user_info[&#039;age&#039;] = age&lt;br /&gt;
                        except:&lt;br /&gt;
                            pass&lt;br /&gt;
                &lt;br /&gt;
                # Город&lt;br /&gt;
                if user.get(&#039;city&#039;) and user[&#039;city&#039;].get(&#039;title&#039;):&lt;br /&gt;
                    user_info[&#039;city&#039;] = user[&#039;city&#039;][&#039;title&#039;]&lt;br /&gt;
                &lt;br /&gt;
                members_data.append(user_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе подписчиков: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        # Собираем посты&lt;br /&gt;
        posts_data = []&lt;br /&gt;
        try:&lt;br /&gt;
            wall = vk.wall.get(owner_id=owner_id if str(group_id).lstrip(&#039;-&#039;).isdigit() else group_id, count=20, filter=&#039;owner&#039;)&lt;br /&gt;
            for post in wall[&#039;items&#039;]:&lt;br /&gt;
                post_info = {&lt;br /&gt;
                    &#039;text&#039;: post.get(&#039;text&#039;, &#039;&#039;)[:100],&lt;br /&gt;
                    &#039;likes&#039;: post[&#039;likes&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;comments&#039;: post[&#039;comments&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;reposts&#039;: post[&#039;reposts&#039;][&#039;count&#039;],&lt;br /&gt;
                    &#039;views&#039;: post.get(&#039;views&#039;, {}).get(&#039;count&#039;, 0)&lt;br /&gt;
                }&lt;br /&gt;
                &lt;br /&gt;
                # Тип контента (без эмодзи, чтобы не было проблем с шрифтами)&lt;br /&gt;
                if post.get(&#039;attachments&#039;):&lt;br /&gt;
                    attach_type = post[&#039;attachments&#039;][0][&#039;type&#039;]&lt;br /&gt;
                    if attach_type == &#039;photo&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Фото&#039;&lt;br /&gt;
                    elif attach_type == &#039;video&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Видео&#039;&lt;br /&gt;
                    elif attach_type == &#039;link&#039;:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Ссылка&#039;&lt;br /&gt;
                    else:&lt;br /&gt;
                        post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;type&#039;] = &#039;Текст&#039;&lt;br /&gt;
                &lt;br /&gt;
                # ER&lt;br /&gt;
                if post_info[&#039;views&#039;] &amp;gt; 0:&lt;br /&gt;
                    post_info[&#039;er&#039;] = (post_info[&#039;likes&#039;] + post_info[&#039;comments&#039;] + post_info[&#039;reposts&#039;]) / post_info[&#039;views&#039;] * 1000&lt;br /&gt;
                else:&lt;br /&gt;
                    post_info[&#039;er&#039;] = 0&lt;br /&gt;
                &lt;br /&gt;
                posts_data.append(post_info)&lt;br /&gt;
        except Exception as e:&lt;br /&gt;
            print(f&amp;quot;Ошибка при сборе постов: {e}&amp;quot;)&lt;br /&gt;
        &lt;br /&gt;
        return {&lt;br /&gt;
            &#039;success&#039;: True,&lt;br /&gt;
            &#039;community_name&#039;: community_name,&lt;br /&gt;
            &#039;members_count&#039;: members_count,&lt;br /&gt;
            &#039;members&#039;: members_data,&lt;br /&gt;
            &#039;posts&#039;: posts_data&lt;br /&gt;
        }&lt;br /&gt;
        &lt;br /&gt;
    except Exception as e:&lt;br /&gt;
        return {&#039;success&#039;: False, &#039;error&#039;: str(e)}&lt;br /&gt;
&lt;br /&gt;
def generate_charts(members_data, posts_data):&lt;br /&gt;
    &amp;quot;&amp;quot;&amp;quot;Генерирует графики и возвращает их в base64&amp;quot;&amp;quot;&amp;quot;&lt;br /&gt;
    charts = {}&lt;br /&gt;
    &lt;br /&gt;
    # Настройка шрифтов для русских букв&lt;br /&gt;
    plt.rcParams[&#039;font.family&#039;] = &#039;sans-serif&#039;&lt;br /&gt;
    plt.rcParams[&#039;font.sans-serif&#039;] = [&#039;Arial&#039;, &#039;DejaVu Sans&#039;]&lt;br /&gt;
    &lt;br /&gt;
    # 1. Распределение по полу&lt;br /&gt;
    if members_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in members_data])&lt;br /&gt;
        ax.pie(sex_counts.values(), labels=sex_counts.keys(), autopct=&#039;%1.1f%%&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;sex&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 2. Возрастное распределение&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in members_data if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(6, 4))&lt;br /&gt;
        ax.hist(ages, bins=15, color=&#039;skyblue&#039;, edgecolor=&#039;black&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Возраст&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;Количество&#039;)&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;age&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 3. Топ-5 городов&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in members_data if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        city_counts = Counter(cities).most_common(5)&lt;br /&gt;
        city_names, city_values = zip(*city_counts)&lt;br /&gt;
    &lt;br /&gt;
        # Создаём горизонтальную столбчатую диаграмму&lt;br /&gt;
        bars = ax.barh(city_names, city_values, color=&#039;lightcoral&#039;, edgecolor=&#039;darkred&#039;)&lt;br /&gt;
        ax.set_title(&#039;Топ-5 городов аудитории&#039;, fontsize=14, fontweight=&#039;bold&#039;)&lt;br /&gt;
        ax.set_xlabel(&#039;Количество подписчиков&#039;, fontsize=11)&lt;br /&gt;
        ax.set_ylabel(&#039;Город&#039;, fontsize=11)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
    &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;cities&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    # 4. Вовлечённость по типам контента&lt;br /&gt;
    if posts_data:&lt;br /&gt;
        fig, ax = plt.subplots(figsize=(8, 5))&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in posts_data:&lt;br /&gt;
            post_type = post[&#039;type&#039;]&lt;br /&gt;
            if post_type not in type_er:&lt;br /&gt;
                type_er[post_type] = []&lt;br /&gt;
            type_er[post_type].append(post[&#039;er&#039;])&lt;br /&gt;
        &lt;br /&gt;
        avg_er = {t: sum(ers)/len(ers) for t, ers in type_er.items()}&lt;br /&gt;
        bars = ax.bar(avg_er.keys(), avg_er.values(), color=&#039;lightgreen&#039;, edgecolor=&#039;darkgreen&#039;)&lt;br /&gt;
        ax.set_ylabel(&#039;ER (на 1000 просмотров)&#039;, fontsize=11)&lt;br /&gt;
        plt.xticks(rotation=45)&lt;br /&gt;
        &lt;br /&gt;
        # Добавляем значения на столбцы&lt;br /&gt;
        for bar, val in zip(bars, avg_er.values()):&lt;br /&gt;
            ax.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, &lt;br /&gt;
                   f&#039;{val:.1f}&#039;, ha=&#039;center&#039;, va=&#039;bottom&#039;, fontsize=9)&lt;br /&gt;
        &lt;br /&gt;
        plt.tight_layout()&lt;br /&gt;
        &lt;br /&gt;
        buf = io.BytesIO()&lt;br /&gt;
        plt.savefig(buf, format=&#039;png&#039;, bbox_inches=&#039;tight&#039;, dpi=100)&lt;br /&gt;
        buf.seek(0)&lt;br /&gt;
        charts[&#039;engagement&#039;] = base64.b64encode(buf.getvalue()).decode()&lt;br /&gt;
        plt.close(fig)&lt;br /&gt;
    &lt;br /&gt;
    return charts&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/&#039;)&lt;br /&gt;
def index():&lt;br /&gt;
    return render_template(&#039;index.html&#039;)&lt;br /&gt;
&lt;br /&gt;
@app.route(&#039;/analyze&#039;, methods=[&#039;POST&#039;])&lt;br /&gt;
def analyze():&lt;br /&gt;
    group_url = request.json.get(&#039;group_url&#039;, &#039;&#039;).strip()&lt;br /&gt;
    &lt;br /&gt;
    if not group_url:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: &#039;Введите ссылку на сообщество&#039;})&lt;br /&gt;
    &lt;br /&gt;
    # Извлекаем ID сообщества из ссылки&lt;br /&gt;
    if &#039;vk.com/&#039; in group_url:&lt;br /&gt;
        group_id = group_url.split(&#039;vk.com/&#039;)[-1].split(&#039;/&#039;)[0]&lt;br /&gt;
    else:&lt;br /&gt;
        group_id = group_url&lt;br /&gt;
    &lt;br /&gt;
    # Собираем данные&lt;br /&gt;
    data = get_community_data(group_id)&lt;br /&gt;
    &lt;br /&gt;
    if not data[&#039;success&#039;]:&lt;br /&gt;
        return jsonify({&#039;success&#039;: False, &#039;error&#039;: data.get(&#039;error&#039;, &#039;Ошибка при получении данных&#039;)})&lt;br /&gt;
    &lt;br /&gt;
    # Генерируем графики&lt;br /&gt;
    charts = generate_charts(data[&#039;members&#039;], data[&#039;posts&#039;])&lt;br /&gt;
    &lt;br /&gt;
    # Считаем статистику&lt;br /&gt;
    stats = {}&lt;br /&gt;
    &lt;br /&gt;
    # Пол&lt;br /&gt;
    if data[&#039;members&#039;]:&lt;br /&gt;
        sex_counts = Counter([m.get(&#039;sex&#039;, &#039;Не указан&#039;) for m in data[&#039;members&#039;]])&lt;br /&gt;
        stats[&#039;sex&#039;] = dict(sex_counts)&lt;br /&gt;
    &lt;br /&gt;
    # Возраст&lt;br /&gt;
    ages = [m[&#039;age&#039;] for m in data[&#039;members&#039;] if &#039;age&#039; in m]&lt;br /&gt;
    if ages:&lt;br /&gt;
        stats[&#039;avg_age&#039;] = sum(ages) / len(ages)&lt;br /&gt;
        stats[&#039;min_age&#039;] = min(ages)&lt;br /&gt;
        stats[&#039;max_age&#039;] = max(ages)&lt;br /&gt;
    &lt;br /&gt;
    # Города&lt;br /&gt;
    cities = [m[&#039;city&#039;] for m in data[&#039;members&#039;] if &#039;city&#039; in m and m[&#039;city&#039;] != &#039;Не указан&#039;]&lt;br /&gt;
    if cities:&lt;br /&gt;
        stats[&#039;top_cities&#039;] = Counter(cities).most_common(5)&lt;br /&gt;
    &lt;br /&gt;
    # Посты&lt;br /&gt;
    if data[&#039;posts&#039;]:&lt;br /&gt;
        avg_er = sum(p[&#039;er&#039;] for p in data[&#039;posts&#039;]) / len(data[&#039;posts&#039;])&lt;br /&gt;
        stats[&#039;avg_er&#039;] = avg_er&lt;br /&gt;
        # Лучший тип контента&lt;br /&gt;
        type_er = {}&lt;br /&gt;
        for post in data[&#039;posts&#039;]:&lt;br /&gt;
            if post[&#039;type&#039;] not in type_er:&lt;br /&gt;
                type_er[post[&#039;type&#039;]] = []&lt;br /&gt;
            type_er[post[&#039;type&#039;]].append(post[&#039;er&#039;])&lt;br /&gt;
        best_type = max(type_er.keys(), key=lambda t: sum(type_er[t])/len(type_er[t]))&lt;br /&gt;
        stats[&#039;best_post_type&#039;] = best_type&lt;br /&gt;
    &lt;br /&gt;
    # Рекомендации&lt;br /&gt;
    recommendations = []&lt;br /&gt;
    if &#039;avg_age&#039; in stats:&lt;br /&gt;
        if stats[&#039;avg_age&#039;] &amp;lt; 20:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория молодая (до 20 лет) → используйте короткие, трендовые форматы, мемы, TikTok-подход&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_age&#039;] &amp;lt; 30:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 20-30 лет → чередуйте развлекательный и полезный контент, используйте качественные фото&amp;quot;)&lt;br /&gt;
        else:&lt;br /&gt;
            recommendations.append(&amp;quot;🎯 Аудитория 30+ лет → делайте акцент на полезный, экспертный контент, длинные посты&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if stats.get(&#039;best_post_type&#039;):&lt;br /&gt;
        recommendations.append(f&amp;quot;📈 Лучший формат контента: {stats[&#039;best_post_type&#039;]} → публикуйте чаще именно его&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    if data[&#039;posts&#039;] and stats.get(&#039;avg_er&#039;):&lt;br /&gt;
        if stats[&#039;avg_er&#039;] &amp;lt; 50:&lt;br /&gt;
            recommendations.append(&amp;quot;⚠️ Вовлечённость ниже среднего → попробуйте добавить опросы, конкурсы и интерактив&amp;quot;)&lt;br /&gt;
        elif stats[&#039;avg_er&#039;] &amp;gt; 150:&lt;br /&gt;
            recommendations.append(&amp;quot;🔥 Отличная вовлечённость! Продолжайте в том же духе&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    # Добавляем рекомендацию по городам&lt;br /&gt;
    if stats.get(&#039;top_cities&#039;):&lt;br /&gt;
        main_city = stats[&#039;top_cities&#039;][0][0]&lt;br /&gt;
        main_city_count = stats[&#039;top_cities&#039;][0][1]&lt;br /&gt;
        total_with_city = sum(count for _, count in stats[&#039;top_cities&#039;])&lt;br /&gt;
        if len(stats[&#039;top_cities&#039;]) &amp;gt; 1:&lt;br /&gt;
            recommendations.append(f&amp;quot;🏙️ Топ-5 городов: {&#039;, &#039;.join([f&#039;{city} ({cnt})&#039; for city, cnt in stats[&#039;top_cities&#039;][:3]])}&amp;quot;)&lt;br /&gt;
        recommendations.append(f&amp;quot;📍 Основная аудитория из {main_city} → можно проводить локальные мероприятия или делать привязку к городу&amp;quot;)&lt;br /&gt;
    &lt;br /&gt;
    return jsonify({&lt;br /&gt;
        &#039;success&#039;: True,&lt;br /&gt;
        &#039;community_name&#039;: data[&#039;community_name&#039;],&lt;br /&gt;
        &#039;members_count&#039;: data[&#039;members_count&#039;],&lt;br /&gt;
        &#039;stats&#039;: stats,&lt;br /&gt;
        &#039;charts&#039;: charts,&lt;br /&gt;
        &#039;recommendations&#039;: recommendations&lt;br /&gt;
    })&lt;br /&gt;
&lt;br /&gt;
if __name__ == &#039;__main__&#039;:&lt;br /&gt;
    app.run(debug=True)&lt;br /&gt;
```&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45363</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45363"/>
		<updated>2026-03-26T19:01:09Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
Структура проекта в VS Code:&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45362</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45362"/>
		<updated>2026-03-26T19:00:18Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|600px]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45361</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45361"/>
		<updated>2026-03-26T19:00:03Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
[[Файл:Vk api1.png|800px]]&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
	<entry>
		<id>http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45360</id>
		<title>Анализ целевой аудитории сообщества VK</title>
		<link rel="alternate" type="text/html" href="http://digida.mgpu.ru/index.php?title=%D0%90%D0%BD%D0%B0%D0%BB%D0%B8%D0%B7_%D1%86%D0%B5%D0%BB%D0%B5%D0%B2%D0%BE%D0%B9_%D0%B0%D1%83%D0%B4%D0%B8%D1%82%D0%BE%D1%80%D0%B8%D0%B8_%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D1%81%D1%82%D0%B2%D0%B0_VK&amp;diff=45360"/>
		<updated>2026-03-26T18:59:42Z</updated>

		<summary type="html">&lt;p&gt;Sabitova Alina: &lt;/p&gt;
&lt;hr /&gt;
&lt;div&gt;{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
|-&lt;br /&gt;
! Параметр&lt;br /&gt;
! Описание&lt;br /&gt;
|-&lt;br /&gt;
| Описание&lt;br /&gt;
| Веб-приложение для анализа аудитории сообществ ВКонтакте. Инструмент собирает данные о подписчиках (пол, возраст, география) и постах (типы контента, вовлечённость), визуализирует статистику в виде графиков и формирует рекомендации по оптимизации контент-стратегии. Проект решает задачу автоматизации SMM-аналитики и помогает администраторам сообществ понимать свою целевую аудиторию.&lt;br /&gt;
|-&lt;br /&gt;
| Область знаний&lt;br /&gt;
| Веб-разработка, анализ данных, работа с API социальных сетей, визуализация данных, SMM-аналитика, Python-программирование.&lt;br /&gt;
|-&lt;br /&gt;
| Близкие понятия&lt;br /&gt;
| SMM-аналитика, парсинг VK API, дашборд для сообществ, анализ целевой аудитории (ЦА), вовлечённость (ER), демографический портрет аудитории, контент-стратегия, репрезентативная выборка, Flask-приложение.&lt;br /&gt;
|-&lt;br /&gt;
| Среда разработки&lt;br /&gt;
| Python 3.8+, Flask, vk_api, matplotlib, pandas&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
== Цель проекта ==&lt;br /&gt;
Разработать веб-приложение для автоматического анализа аудитории сообществ ВКонтакте с целью получения демографической статистики и рекомендаций по оптимизации контент-стратегии. Приложение должно предоставлять наглядную визуализацию данных в виде графиков и формировать практические советы для администраторов сообществ.&lt;br /&gt;
&lt;br /&gt;
== Задачи ==&lt;br /&gt;
# Интеграция с VK API — реализовать сбор данных о подписчиках и постах сообщества.&lt;br /&gt;
# Обработка данных — агрегировать информацию о поле, возрасте, географии и вовлечённости.&lt;br /&gt;
# Визуализация — построить 4 графика:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Формирование рекомендаций — на основе полученных данных выдать текстовые советы по контент-стратегии.&lt;br /&gt;
# Создание веб-интерфейса — разработать удобную форму для ввода ссылки на сообщество и отображения результатов.&lt;br /&gt;
&lt;br /&gt;
== Диаграмма работы приложения ==&lt;br /&gt;
{{#mermaid: flowchart TD&lt;br /&gt;
    A[Ввод ссылки на сообщество] --&amp;gt; B[Запрос к VK API]&lt;br /&gt;
    B --&amp;gt; C[Анализ подписчиков]&lt;br /&gt;
    B --&amp;gt; D[Анализ постов]&lt;br /&gt;
    C --&amp;gt; E[Отображение графиков]&lt;br /&gt;
    D --&amp;gt; F[Вывод ключевых показателей вовлечённости]&lt;br /&gt;
    F --&amp;gt; Q[Формирование рекомендаций]&lt;br /&gt;
    E --&amp;gt; Q&lt;br /&gt;
}}&lt;br /&gt;
&lt;br /&gt;
== Структура проекта ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;gallery&amp;gt;&lt;br /&gt;
[[Файл:Vk api1.png|800px]]&lt;br /&gt;
&amp;lt;/gallery&amp;gt;&lt;br /&gt;
&lt;br /&gt;
После запуска приложение доступно по адресу: &#039;&#039;&#039;http://127.0.0.1:5000&#039;&#039;&#039;&lt;br /&gt;
&lt;br /&gt;
== Выводы ==&lt;br /&gt;
&lt;br /&gt;
# Разработано полноценное веб-приложение для анализа сообществ ВКонтакте, работающее на localhost.&lt;br /&gt;
# Реализована интеграция с VK API — приложение успешно получает данные о подписчиках (пол, возраст, город) и постах (типы контента, вовлечённость).&lt;br /&gt;
# Создана система визуализации — 4 информативных графика, отображающих ключевые метрики аудитории:&lt;br /&gt;
#* Распределение по полу (круговая диаграмма)&lt;br /&gt;
#* Возрастное распределение (гистограмма)&lt;br /&gt;
#* Топ-5 городов (горизонтальная столбчатая диаграмма)&lt;br /&gt;
#* Вовлечённость по типам контента (столбчатая диаграмма)&lt;br /&gt;
# Автоматическое формирование рекомендаций — на основе анализа данных приложение выдаёт практические советы по оптимизации контент-стратегии.&lt;br /&gt;
# Удобный пользовательский интерфейс — адаптивный дизайн, анимации загрузки, понятная навигация.&lt;/div&gt;</summary>
		<author><name>Sabitova Alina</name></author>
	</entry>
</feed>