Карта друзей: различия между версиями

Материал из Поле цифровой дидактики
Нет описания правки
Нет описания правки
 
(не показано 19 промежуточных версий этого же участника)
Строка 1: Строка 1:
{{DigitalTool
|Description=Карта друзей на основе API.
Интерактивная карта, отображающая геолокацию публичных постов друзей из ВКонтакте. Проект демонстрирует работу с API социальных сетей и визуализацию геоданных.
|Affordances=Сбор публичных постов с геометками через VK API. Сохранение координат в JSON. Визуализация точек на интерактивной карте. Отображение информации о месте и авторе поста при клике на маркер.
|Difficult=Требуется токен доступа VK (с ограниченным сроком жизни). Работает только с публичными постами (не со всеми друзьями). Зависит от стабильности API ВКонтакте. Не предназначен для слежки в реальном времени, только для анализа исторических открытых данных.
|Field_of_knowledge=Информатика, Образование, Большие данные, Картография, Статистика, Моделирование
|Область применения=Учебный проект
|End users=Учащиеся, Разработчики
|Developer=Муханова Анна АДЭУ-221
|launch year=2026
|distant_collab=Нет
|Language_Ru_Eng=Русский, Python/JavaScript
|AI=Нет
}}
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px;">
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto; padding: 20px;">


     <!-- Карточка проекта -->
     <!-- Карточка проекта -->
    <div style="background-color: #f0f8ff; padding: 15px; border-radius: 10px; margin-bottom: 25px;">
<div style="padding: 15px; margin-bottom: 25px;">
        <h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 8px;">🗺️ Карта друзей</h2>
    <h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 8px;">🗺️ Карта друзей</h2>
        <p><b>Автор:</b> Анна Муханова</p>
    <p><b>Автор:</b> Анна Муханова</p>
        <p><b>Дисциплина:</b> Работа с API социальных сетей и облачных сервисов</p>
    <p><b>Группа:</b> АДЭУ-221</p>
        <p><b>Статус проекта:</b> В разработке</p>
    <p><b>Дисциплина:</b> Работа с API социальных сетей и облачных сервисов</p>
    </div>
    <p><b>Статус проекта:</b> Выполнен</p>
</div>


     <!-- Цель проекта -->
     <!-- Цель проекта -->
     <h3 style="color: #34495e;">🎯 Цель работы</h3>
     <h3 style="color: #34495e;">Цель работы</h3>
     <p>Разработать инструмент для сбора и визуализации геоданных из открытых источников (VK) с целью изучения принципов работы API и создания интерактивных карт.</p>
     <p>Разработать инструмент для сбора и визуализации геоданных из открытых источников (VK) с целью изучения принципов работы [[API]] и создания интерактивных карт.</p>


     <!-- Задачи -->
     <!-- Задачи -->
     <h3 style="color: #34495e;">📋 Задачи</h3>
     <h3 style="color: #34495e;">Задачи</h3>
     <ol>
     <ol>
         <li>Изучить документацию VK API.</li>
         <li>Изучить документацию VK API.</li>
Строка 39: Строка 26:


     <!-- Планируемый стек технологий -->
     <!-- Планируемый стек технологий -->
     <h3 style="color: #34495e;">⚙️ Технологии</h3>
     <h3 style="color: #34495e;">Технологии</h3>
     <ul>
     <ul>
         <li><b>Языки:</b> Python, JavaScript, HTML/CSS</li>
         <li><b>Языки:</b> [[Python]], [[JavaScript]], [[HTML]]/[[CSS]]</li>
         <li><b>Библиотеки Python:</b> requests, vk_api</li>
         <li><b>Библиотеки [[Python]]:</b> requests, vk_api</li>
         <li><b>API:</b> VK API, OpenStreetMap (через Leaflet)</li>
         <li><b>API:</b> [[VK API]], OpenStreetMap (через Leaflet)</li>
         <li><b>Хостинг:</b> GitHub Pages (для карты)</li>
         <li><b>Хостинг:</b> [[GitHub]] Pages (для карты)</li>
     </ul>
     </ul>


     <!-- Ход работы (прогресс) -->
<h3 style="color: #34495e;">Диаграмма работы приложения «Карта друзей»</h3>
     <h3 style="color: #34495e;">🚀 Ход выполнения</h3>
 
     <table style="width: 100%; border-collapse: collapse;">
{{#mermaid: flowchart TB
         <tr style="background-color: #3498db; color: white;">
    A[Начало] --> B(Получение токена VK API)
            <th style="padding: 8px; border: 1px solid #ddd;">Этап</th>
    B --> C(Запрос списка друзей)
            <th style="padding: 8px; border: 1px solid #ddd;">Статус</th>
    C --> D{Есть друзья?}
             <th style="padding: 8px; border: 1px solid #ddd;">Комментарий</th>
    D -->|Нет| E[Завершить]
         </tr>
    D -->|Да| F[Цикл по каждому другу]
         <tr>
    F --> G(Запрос последних 30 постов)
             <td style="padding: 8px; border: 1px solid #ddd;">Создание страницы проекта в вики</td>
    G --> H{Пост содержит geo?}
            <td style="padding: 8px; border: 1px solid #ddd; background-color: #90ee90;">✅ Выполнено</td>
    H -->|Нет| I[Следующий пост]
            <td style="padding: 8px; border: 1px solid #ddd;">Страница зарегистрирована в форме DigitalTool</td>
    I --> G
         </tr>
    H -->|Да| J(Сохранить координаты, имя, место, ссылку)
         <tr>
    J --> K{Все посты проверены?}
            <td style="padding: 8px; border: 1px solid #ddd;">Получение токена VK API</td>
    K -->|Нет| I
            <td style="padding: 8px; border: 1px solid #ddd; background-color: #90ee90;">✅ Выполнено</td>
    K -->|Да| L{Все друзья обработаны?}
            <td style="padding: 8px; border: 1px solid #ddd;">-</td>
    L -->|Нет| F
        </tr>
    L -->|Да| M(Сохранить данные в JSON-файл)
        <tr>
    M --> N(Открыть HTML-страницу с картой)
            <td style="padding: 8px; border: 1px solid #ddd;">Написание скрипта сбора данных</td>
    N --> O(Загрузить JSON в JavaScript)
            <td style="padding: 8px; border: 1px solid #ddd; background-color: #ffb6c1;">⏳ В процессе</td>
    O --> P(Отрисовать маркеры на карте)
            <td style="padding: 8px; border: 1px solid #ddd;">-</td>
    P --> Q(Клик по маркеру → всплывающее окно)
        </tr>
    Q --> R[Готово]
    </table>
}}
 
     <!-- Структура проекта -->
     <h3 style="color: #34495e;">Структура проекта</h3>
     <div style="text-align: center; margin: 15px 0;">
         [[Файл:Project_folder.png|500px|center|thumb|Папка проекта в VS Code]]
        <p><i>Файлы проекта: config.py (токен), main.py (скрипт), friends_geo.json (результат), map.html (карта)</i></p>
    </div>
 
 
=== Ход работы над проектом ===
 
==== Этап 1. Создание страницы в вики и выбор темы ====
Создала страницу проекта на Digida MGPU через форму DigitalTool. Тема: «Карта друзей» — сбор и визуализация геолокаций из постов ВКонтакте. Получила комментарий от преподавателя, исправила категорию на «Работа с API».
 
==== Этап 2. Изучение VK API и получение токена ====
Изучила документацию VK API. Пыталась создать приложение, но столкнулась с трудностями. Использовала сервис vkhost.github.io для получения временного токена. Позже разобралась с созданием собственного приложения, получила стабильный токен через standalone-приложение.
 
==== Этап 3. Установка библиотеки и написание Python-скрипта ====
Установила библиотеку vk_api через терминал:
 
<syntaxhighlight lang="bash">
pip install vk_api
</syntaxhighlight>
 
Написала скрипт, который:
* Авторизуется по токену
* Получает список друзей
* Для каждого друга запрашивает последние 30 постов
* Проверяет наличие поля <code>geo</code> в каждом посте
* Сохраняет координаты, имя друга, текст поста и ссылку в JSON-файл
 
[[Файл:Terminal_result1.png|400px|center|thumb|Процесс поиска данных в терминале]]
[[Файл:Terminal_result2.png|400px|center|thumb|Результат работы скрипта]]
 
'''Результат:''' собрано 18 постов с геометками у 23 обработанных друзей.
 
==== Этап 4. Сохранение данных в JSON ====
Скрипт сохранил все найденные геометки в файл <code>friends_geo.json</code>.
 
[[Файл:Json_data.png|500px|center|thumb|Файл friends_geo.json с координатами]]
 
''Структура данных: имя друга, координаты, место, текст поста, ссылка''
 
==== Этап 5. Создание интерактивной карты ====
Создала HTML-страницу <code>map.html</code> с картой на базе библиотеки Leaflet (OpenStreetMap). Написала JavaScript-код, который:
* Загружает данные из JSON-файла
* Разбирает координаты (формат "широта долгота")
* Добавляет маркеры на карту
* При клике на маркер показывает всплывающее окно с именем друга, местом и ссылкой на пост
 
[[Файл:Map_code1.png|400px|center|thumb|Код JavaScript для отображения маркеров]]
[[Файл:Map_code2.png|400px|center|thumb|Массив данных для карты]]
[[Файл:Map_result.png|400px|center|thumb|Готовая карта с точками]]
 
'''Результат:''' готовая интерактивная карта с точками всех найденных геометок.
 
==== Этап 6. Оформление страницы в вики ====
Добавила на страницу проекта:
* Блок с результатами
* Скриншоты всех этапов
* Структуру проекта
* Пример кода на Python
* Выводы и планы по развитию
 
=== Итог проекта ===
 
Создан работающий инструмент для сбора и визуализации геоданных из ВКонтакте
 
23 друга обработано | 18 геометок собрано | 14 городов на карте
 
Проект выполнен в рамках дисциплины «Работа с API социальных сетей и облачных сервисов»
 
 
===Аналитический дашборд===
 
[[Файл:Dashboard preview.png|700px|center|thumb|Аналитический дашборд «Карта друзей»]]
 
На основе собранных данных был разработан интерактивный дашборд, который включает:
 
* Интерактивную карту с маркерами всех найденных мест
* Блок статистики (количество друзей, геометок, городов, самое популярное место)
* Гистограмму распределения отметок по городам
* Круговую диаграмму топ-10 мест по популярности
* Фильтр по друзьям для детального анализа
* Список всех мест со ссылками на посты
 
<div class="mw-collapsible mw-collapsed">
 
'''▸ Показать код дашборда (dashboard.html)'''
 
<div class="mw-collapsible-content">
 
<syntaxhighlight lang="html">
&lt;!DOCTYPE html&gt;
&lt;html lang="ru"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Дашборд: Карта друзей&lt;/title&gt;
    &lt;link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" /&gt;
    &lt;script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"&gt;&lt;/script&gt;
    &lt;script src="https://cdn.jsdelivr.net/npm/chart.js"&gt;&lt;/script&gt;
    &lt;style&gt;
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f7fa; padding: 20px; }
        .dashboard { max-width: 1400px; margin: 0 auto; }
        h1 { color: #2c3e50; margin-bottom: 20px; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
        .row { display: flex; gap: 20px; margin-bottom: 20px; flex-wrap: wrap; }
        .card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 20px; flex: 1; min-width: 250px; }
        .map-container { flex: 2; min-width: 500px; }
        .stats { display: flex; gap: 15px; flex-wrap: wrap; margin-bottom: 20px; }
        .stat-box { background: white; border-radius: 12px; padding: 20px; text-align: center; flex: 1; min-width: 150px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
        .stat-number { font-size: 32px; font-weight: bold; color: #3498db; }
        .stat-label { color: #7f8c8d; margin-top: 8px; }
        #map { height: 400px; width: 100%; border-radius: 8px; }
        canvas { max-height: 300px; }
        .filter-select { padding: 8px 12px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 15px; width: 100%; }
        hr { margin: 15px 0; border: none; border-top: 1px solid #eee; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class="dashboard"&gt;
    &lt;h1&gt;Карта друзей — аналитический дашборд&lt;/h1&gt;
   
    &lt;div class="stats"&gt;
        &lt;div class="stat-box"&gt;&lt;div class="stat-number" id="totalFriends"&gt;0&lt;/div&gt;&lt;div class="stat-label"&gt;Друзей обработано&lt;/div&gt;&lt;/div&gt;
        &lt;div class="stat-box"&gt;&lt;div class="stat-number" id="totalMarkers"&gt;0&lt;/div&gt;&lt;div class="stat-label"&gt;Геометок найдено&lt;/div&gt;&lt;/div&gt;
        &lt;div class="stat-box"&gt;&lt;div class="stat-number" id="totalCities"&gt;0&lt;/div&gt;&lt;div class="stat-label"&gt;Городов&lt;/div&gt;&lt;/div&gt;
        &lt;div class="stat-box"&gt;&lt;div class="stat-number" id="topPlace"&gt;—&lt;/div&gt;&lt;div class="stat-label"&gt;Самое популярное место&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
   
    &lt;div class="row"&gt;
        &lt;div class="card map-container"&gt;
            &lt;h3&gt;Карта с отметками&lt;/h3&gt;
            &lt;div id="map"&gt;&lt;/div&gt;
            &lt;hr&gt;
             &lt;label for="friendFilter"&gt;Фильтр по другу:&lt;/label&gt;
            &lt;select id="friendFilter" class="filter-select"&gt;&lt;option value="all"&gt;Все друзья&lt;/option&gt;&lt;/select&gt;
        &lt;/div&gt;
        &lt;div class="card"&gt;
            &lt;h3&gt;Города по количеству отметок&lt;/h3&gt;
            &lt;canvas id="cityChart"&gt;&lt;/canvas&gt;
        &lt;/div&gt;
    &lt;/div&gt;
   
    &lt;div class="row"&gt;
        &lt;div class="card"&gt;
            &lt;h3&gt;Активность по местам&lt;/h3&gt;
            &lt;canvas id="placesChart"&gt;&lt;/canvas&gt;
         &lt;/div&gt;
         &lt;div class="card"&gt;
            &lt;h3&gt;Список найденных мест&lt;/h3&gt;
             &lt;div id="placesList" style="max-height: 300px; overflow-y: auto;"&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
 
&lt;script&gt;
const rawData = [
    {
        "friend_name": "Наталья Шевковская",
        "friend_id": 4671765,
        "coordinates": "54.960033758443 20.47501174861",
        "place": "Зеленоградск",
        "post_text": "Балтика замерзла",
        "post_url": "https://vk.com/wall4671765_1030"
    },
......
    {
        "friend_name": "Татьяна Бондаренко",
        "friend_id": 131800785,
        "coordinates": "52.227369601217 21.003966668331",
        "place": "Warszawa Poland",
        "post_text": "довольные и счастливые #BGON ❤️",
        "post_url": "https://vk.com/wall131800785_3508"
    }
];
 
let currentFilter = "all";
 
const uniqueFriends = [...new Set(rawData.map(d => d.friend_id))];
const totalFriends = uniqueFriends.length;
const totalMarkers = rawData.length;
 
const cityCount = {};
const placeCount = {};
rawData.forEach(d => {
    const city = d.place.split(',')[0] || d.place;
    cityCount[city] = (cityCount[city] || 0) + 1;
    placeCount[d.place] = (placeCount[d.place] || 0) + 1;
});
 
const totalCities = Object.keys(cityCount).length;
 
let topPlaceName = "—";
let topPlaceCount = 0;
for (const [place, count] of Object.entries(placeCount)) {
    if (count > topPlaceCount) {
        topPlaceCount = count;
        topPlaceName = place;
    }
}
 
document.getElementById('totalFriends').innerText = totalFriends;
document.getElementById('totalMarkers').innerText = totalMarkers;
document.getElementById('totalCities').innerText = totalCities;
document.getElementById('topPlace').innerText = topPlaceName.length > 25 ? topPlaceName.slice(0, 25) + '…' : topPlaceName;
 
const friendSelect = document.getElementById('friendFilter');
const uniqueNames = [...new Map(rawData.map(d => [d.friend_id, d.friend_name])).entries()];
uniqueNames.forEach(([id, name]) => {
    const option = document.createElement('option');
    option.value = id;
    option.textContent = name;
    friendSelect.appendChild(option);
});
 
function getFilteredData() {
    if (currentFilter === "all") return rawData;
    return rawData.filter(d => d.friend_id == currentFilter);
}
 
const cityLabels = Object.keys(cityCount);
const cityValues = Object.values(cityCount);
const cityCtx = document.getElementById('cityChart').getContext('2d');
let cityChart = new Chart(cityCtx, {
    type: 'bar',
    data: { labels: cityLabels, datasets: [{ label: 'Количество отметок', data: cityValues, backgroundColor: '#3498db', borderRadius: 8 }] },
    options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'top' } } }
});
 
const sortedPlaces = Object.entries(placeCount).sort((a,b) => b[1] - a[1]).slice(0, 10);
const placesLabels = sortedPlaces.map(p => p[0].length > 20 ? p[0].slice(0, 20) + '…' : p[0]);
const placesValues = sortedPlaces.map(p => p[1]);
const placesCtx = document.getElementById('placesChart').getContext('2d');
let placesChart = new Chart(placesCtx, {
    type: 'pie',
    data: { labels: placesLabels, datasets: [{ data: placesValues, backgroundColor: ['#3498db', '#e74c3c', '#2ecc71', '#f1c40f', '#9b59b6', '#1abc9c', '#e67e22', '#34495e', '#16a085', '#27ae60'] }] },
    options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'right', labels: { boxWidth: 12, font: { size: 10 } } } } }
});
 
function updatePlacesList() {
    const filtered = getFilteredData();
    const listContainer = document.getElementById('placesList');
    if (filtered.length === 0) { listContainer.innerHTML = '<p>Нет данных</p>'; return; }
    let html = '<ul style="list-style: none; padding: 0;">';
    filtered.forEach(d => {
         const text = d.post_text ? d.post_text.substring(0, 80) : '';
         html += `<li style="padding: 8px 0; border-bottom: 1px solid #eee;"><b>${d.place}</b><br><small>${d.friend_name} · ${text ? text + '…' : ''}<br><a href="${d.post_url}" target="_blank" style="color: #3498db;">открыть пост</a></small></li>`;
    });
    html += '</ul>';
    listContainer.innerHTML = html;
}
 
const map = L.map('map').setView([55.76, 37.64], 4);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map);
let markersLayer = L.layerGroup().addTo(map);
 
function updateMap() {
    markersLayer.clearLayers();
    const filtered = getFilteredData();
    const bounds = [];
    filtered.forEach(item => {
        const [lat, lng] = item.coordinates.split(' ');
        const marker = L.marker([parseFloat(lat), parseFloat(lng)]).addTo(markersLayer);
        const postText = item.post_text ? item.post_text.substring(0, 100) : '';
        marker.bindPopup(`<b>${item.friend_name}</b><br><i>${item.place}</i><br>${postText}<br><a href="${item.post_url}" target="_blank">Открыть пост</a>`);
        bounds.push([parseFloat(lat), parseFloat(lng)]);
    });
    if (bounds.length > 0) map.fitBounds(bounds);
}
 
function updateCharts() {
    const filtered = getFilteredData();
    const filteredCityCount = {};
    const filteredPlaceCount = {};
    filtered.forEach(d => {
        const city = d.place.split(',')[0] || d.place;
        filteredCityCount[city] = (filteredCityCount[city] || 0) + 1;
        filteredPlaceCount[d.place] = (filteredPlaceCount[d.place] || 0) + 1;
    });
    cityChart.data.labels = Object.keys(filteredCityCount);
    cityChart.data.datasets[0].data = Object.values(filteredCityCount);
    cityChart.update();
    const newSortedPlaces = Object.entries(filteredPlaceCount).sort((a,b) => b[1] - a[1]).slice(0, 10);
    placesChart.data.labels = newSortedPlaces.map(p => p[0].length > 20 ? p[0].slice(0,20)+'…' : p[0]);
    placesChart.data.datasets[0].data = newSortedPlaces.map(p => p[1]);
    placesChart.update();
}
 
friendSelect.addEventListener('change', (e) => {
    currentFilter = e.target.value;
    updateMap();
    updatePlacesList();
    updateCharts();
});
 
updateMap();
updatePlacesList();
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</syntaxhighlight>
 
</div>
</div>
 
=== Выводы ===
 
В ходе работы над проектом я:
* Научилась работать с VK API — получать токен, делать запросы, обрабатывать ответы.
* Освоила парсинг JSON-данных и сохранение результатов в файл.
* Создала интерактивную карту на Leaflet и нанесла на нее реальные геоданные.
* Поняла, как устроены современные веб-сервисы, которые собирают данные из разных источников.
* Научилась оформлять проекты в вики и структурировать отчёт.
 
Проект можно развивать дальше: добавить тепловую карту, сделать анализ популярных мест, подключить Яндекс.Карты с более детальными данными.
 
===Полный исходный код приложения===
 
<div class="mw-collapsible mw-collapsed">
 
'''▸ Показать полный код приложения'''
 
<div class="mw-collapsible-content">
 
<syntaxhighlight lang="python">
# config.py
# Токен доступа VK API (получен через standalone-приложение)
 
TOKEN = "vk1.a.мой_токен_здесь"
</syntaxhighlight>
 
<syntaxhighlight lang="python">
# main.py
import vk_api
from config import TOKEN
import json
import time


     <!-- Ссылки (пока пусто) -->
# Авторизация
     <h3 style="color: #34495e;">🔗 Ссылки</h3>
vk_session = vk_api.VkApi(token=TOKEN)
     <ul>
vk = vk_session.get_api()
         <li><i>Ссылка на репозиторий появится после создания GitHub</i></li>
 
         <li><i>Ссылка на демо-карту появится после публикации</i></li>
def get_friends_with_geo():
    </ul>
     """
    Получает список друзей и собирает их посты с геометками
    """
    print("Получаю список друзей...")
    friends = vk.friends.get(fields=['first_name', 'last_name'])
   
    friends_data = []
    total_friends = friends['count']
    print(f"Найдено друзей: {total_friends}")
   
    # Обрабатываем первых 30 друзей
    for i, friend in enumerate(friends['items'][:30]):
        print(f"Обрабатываю друга {i+1}/30: {friend['first_name']} {friend['last_name']}")
       
        try:
            # Получаем последние 50 постов друга
            posts = vk.wall.get(owner_id=friend['id'], count=50)
           
            for post in posts['items']:
                if 'geo' in post and post['geo']:
                    geo_data = {
                        'friend_name': f"{friend['first_name']} {friend['last_name']}",
                        'friend_id': friend['id'],
                        'coordinates': post['geo']['coordinates'],
                        'place': post['geo'].get('place', {}).get('title', 'Без названия'),
                        'post_text': post.get('text', '')[:100],
                        'post_url': f"https://vk.com/wall{friend['id']}_{post['id']}"
                    }
                    friends_data.append(geo_data)
                    print(f"  Найдена геометка: {geo_data['coordinates']} - {geo_data['place']}")
                   
        except Exception as e:
            print(f"  Ошибка при обработке: {e}")
       
        time.sleep(0.5)  # пауза, чтобы не спамить API
   
    # Сохраняем результат
    with open('friends_geo.json', 'w', encoding='utf-8') as f:
        json.dump(friends_data, f, ensure_ascii=False, indent=4)
   
    print(f"\nГотово! Найдено {len(friends_data)} постов с геометками.")
    return friends_data
 
if __name__ == "__main__":
    get_friends_with_geo()
</syntaxhighlight>
 
<syntaxhighlight lang="json">
# friends_geo.json (пример данных)
[
    {
        "friend_name": "Анна Смирнова",
        "friend_id": 123456789,
        "coordinates": "55.751244 37.618423",
        "place": "Красная площадь",
        "post_text": "Красота!",
        "post_url": "https://vk.com/wall123456789_123"
    },
    {
        "friend_name": "Иван Петров",
        "friend_id": 987654321,
        "coordinates": "55.755814 37.617635",
        "place": "Парк Горького",
        "post_text": "Гуляем с друзьями",
        "post_url": "https://vk.com/wall987654321_456"
     }
]
</syntaxhighlight>
 
<syntaxhighlight lang="html">
# map.html
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;title&gt;Карта друзей&lt;/title&gt;
    &lt;meta charset="utf-8" /&gt;
    &lt;link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" /&gt;
    &lt;script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"&gt;&lt;/script&gt;
    &lt;style&gt;
        body { margin: 0; padding: 0; }
        #map { width: 100%; height: 100vh; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div id="map"&gt;&lt;/div&gt;
     &lt;script&gt;
        const friendsData = [
            {
                "friend_name": "Анна Смирнова",
                "coordinates": "55.751244 37.618423",
                "place": "Красная площадь",
                "post_text": "Красота!",
                "post_url": "https://vk.com/wall123456789_123"
            },
            {
                "friend_name": "Иван Петров",
                "coordinates": "55.755814 37.617635",
                "place": "Парк Горького",
                "post_text": "Гуляем с друзьями",
                "post_url": "https://vk.com/wall987654321_456"
            }
        ];
 
        const map = L.map('map').setView([55.76, 37.64], 10);
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '© OpenStreetMap contributors'
        }).addTo(map);
 
         friendsData.forEach(item => {
            const [lat, lng] = item.coordinates.split(' ');
            const marker = L.marker([parseFloat(lat), parseFloat(lng)]).addTo(map);
            marker.bindPopup(`
                &lt;b&gt;${item.friend_name}&lt;/b&gt;&lt;br&gt;
                &lt;i&gt;${item.place}&lt;/i&gt;&lt;br&gt;
                ${item.post_text}&lt;br&gt;
                &lt;a href="${item.post_url}" target="_blank"&gt;Открыть пост&lt;/a&gt;
            `);
         });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</syntaxhighlight>
 
<syntaxhighlight lang="text">
# requirements.txt
vk-api==5.107
requests==2.31.0
</syntaxhighlight>
 
</div>
</div>
 
=== Географическая карта с интерактивными метками (создано с помощью семантических страниц) ===
 
{{#ask:
[[Category:Карта друзей]]
|?Coordinate
|?Friend
|?PostText
|format=map
|height=400
|width=700
}}


    <!-- Раздел для заметок -->
    <h3 style="color: #34495e;">📝 Заметки по разработке</h3>
    [[Файл:Карта_друзей.png|мини]]
    <p><i>Здесь будет появляться полезная информация, ход работы и примеры кода. Возможно, что-то еще интересное 🗺️</i></p>





Текущая версия от 10:39, 27 марта 2026

🗺️ Карта друзей

Автор: Анна Муханова

Группа: АДЭУ-221

Дисциплина: Работа с API социальных сетей и облачных сервисов

Статус проекта: Выполнен

Цель работы

Разработать инструмент для сбора и визуализации геоданных из открытых источников (VK) с целью изучения принципов работы API и создания интерактивных карт.

Задачи

  1. Изучить документацию VK API.
  2. Получить токен доступа для работы с данными.
  3. Написать скрипт на Python для сбора ID друзей и их публичных постов с геометками.
  4. Сохранить полученные координаты в формате JSON.
  5. Создать веб-страницу с картой (Leaflet) и нанести на нее точки.
  6. Опубликовать результат.

Технологии

Диаграмма работы приложения «Карта друзей»

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

Папка проекта в VS Code

Файлы проекта: config.py (токен), main.py (скрипт), friends_geo.json (результат), map.html (карта)


Ход работы над проектом

Этап 1. Создание страницы в вики и выбор темы

Создала страницу проекта на Digida MGPU через форму DigitalTool. Тема: «Карта друзей» — сбор и визуализация геолокаций из постов ВКонтакте. Получила комментарий от преподавателя, исправила категорию на «Работа с API».

Этап 2. Изучение VK API и получение токена

Изучила документацию VK API. Пыталась создать приложение, но столкнулась с трудностями. Использовала сервис vkhost.github.io для получения временного токена. Позже разобралась с созданием собственного приложения, получила стабильный токен через standalone-приложение.

Этап 3. Установка библиотеки и написание Python-скрипта

Установила библиотеку vk_api через терминал:

pip install vk_api

Написала скрипт, который:

  • Авторизуется по токену
  • Получает список друзей
  • Для каждого друга запрашивает последние 30 постов
  • Проверяет наличие поля geo в каждом посте
  • Сохраняет координаты, имя друга, текст поста и ссылку в JSON-файл
Процесс поиска данных в терминале
Результат работы скрипта

Результат: собрано 18 постов с геометками у 23 обработанных друзей.

Этап 4. Сохранение данных в JSON

Скрипт сохранил все найденные геометки в файл friends_geo.json.

Файл friends_geo.json с координатами

Структура данных: имя друга, координаты, место, текст поста, ссылка

Этап 5. Создание интерактивной карты

Создала HTML-страницу map.html с картой на базе библиотеки Leaflet (OpenStreetMap). Написала JavaScript-код, который:

  • Загружает данные из JSON-файла
  • Разбирает координаты (формат "широта долгота")
  • Добавляет маркеры на карту
  • При клике на маркер показывает всплывающее окно с именем друга, местом и ссылкой на пост
Код JavaScript для отображения маркеров
Массив данных для карты
Готовая карта с точками

Результат: готовая интерактивная карта с точками всех найденных геометок.

Этап 6. Оформление страницы в вики

Добавила на страницу проекта:

  • Блок с результатами
  • Скриншоты всех этапов
  • Структуру проекта
  • Пример кода на Python
  • Выводы и планы по развитию

Итог проекта

Создан работающий инструмент для сбора и визуализации геоданных из ВКонтакте

23 друга обработано | 18 геометок собрано | 14 городов на карте

Проект выполнен в рамках дисциплины «Работа с API социальных сетей и облачных сервисов»


Аналитический дашборд

Аналитический дашборд «Карта друзей»

На основе собранных данных был разработан интерактивный дашборд, который включает:

  • Интерактивную карту с маркерами всех найденных мест
  • Блок статистики (количество друзей, геометок, городов, самое популярное место)
  • Гистограмму распределения отметок по городам
  • Круговую диаграмму топ-10 мест по популярности
  • Фильтр по друзьям для детального анализа
  • Список всех мест со ссылками на посты

▸ Показать код дашборда (dashboard.html)

&lt;!DOCTYPE html&gt;
&lt;html lang="ru"&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;title&gt;Дашборд: Карта друзей&lt;/title&gt;
    &lt;link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" /&gt;
    &lt;script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"&gt;&lt;/script&gt;
    &lt;script src="https://cdn.jsdelivr.net/npm/chart.js"&gt;&lt;/script&gt;
    &lt;style&gt;
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background: #f5f7fa; padding: 20px; }
        .dashboard { max-width: 1400px; margin: 0 auto; }
        h1 { color: #2c3e50; margin-bottom: 20px; border-bottom: 3px solid #3498db; padding-bottom: 10px; }
        .row { display: flex; gap: 20px; margin-bottom: 20px; flex-wrap: wrap; }
        .card { background: white; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 20px; flex: 1; min-width: 250px; }
        .map-container { flex: 2; min-width: 500px; }
        .stats { display: flex; gap: 15px; flex-wrap: wrap; margin-bottom: 20px; }
        .stat-box { background: white; border-radius: 12px; padding: 20px; text-align: center; flex: 1; min-width: 150px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
        .stat-number { font-size: 32px; font-weight: bold; color: #3498db; }
        .stat-label { color: #7f8c8d; margin-top: 8px; }
        #map { height: 400px; width: 100%; border-radius: 8px; }
        canvas { max-height: 300px; }
        .filter-select { padding: 8px 12px; border-radius: 6px; border: 1px solid #ddd; margin-bottom: 15px; width: 100%; }
        hr { margin: 15px 0; border: none; border-top: 1px solid #eee; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;div class="dashboard"&gt;
    &lt;h1&gt;Карта друзей — аналитический дашборд&lt;/h1&gt;
    
    &lt;div class="stats"&gt;
        &lt;div class="stat-box"&gt;&lt;div class="stat-number" id="totalFriends"&gt;0&lt;/div&gt;&lt;div class="stat-label"&gt;Друзей обработано&lt;/div&gt;&lt;/div&gt;
        &lt;div class="stat-box"&gt;&lt;div class="stat-number" id="totalMarkers"&gt;0&lt;/div&gt;&lt;div class="stat-label"&gt;Геометок найдено&lt;/div&gt;&lt;/div&gt;
        &lt;div class="stat-box"&gt;&lt;div class="stat-number" id="totalCities"&gt;0&lt;/div&gt;&lt;div class="stat-label"&gt;Городов&lt;/div&gt;&lt;/div&gt;
        &lt;div class="stat-box"&gt;&lt;div class="stat-number" id="topPlace"&gt;&lt;/div&gt;&lt;div class="stat-label"&gt;Самое популярное место&lt;/div&gt;&lt;/div&gt;
    &lt;/div&gt;
    
    &lt;div class="row"&gt;
        &lt;div class="card map-container"&gt;
            &lt;h3&gt;Карта с отметками&lt;/h3&gt;
            &lt;div id="map"&gt;&lt;/div&gt;
            &lt;hr&gt;
            &lt;label for="friendFilter"&gt;Фильтр по другу:&lt;/label&gt;
            &lt;select id="friendFilter" class="filter-select"&gt;&lt;option value="all"&gt;Все друзья&lt;/option&gt;&lt;/select&gt;
        &lt;/div&gt;
        &lt;div class="card"&gt;
            &lt;h3&gt;Города по количеству отметок&lt;/h3&gt;
            &lt;canvas id="cityChart"&gt;&lt;/canvas&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    
    &lt;div class="row"&gt;
        &lt;div class="card"&gt;
            &lt;h3&gt;Активность по местам&lt;/h3&gt;
            &lt;canvas id="placesChart"&gt;&lt;/canvas&gt;
        &lt;/div&gt;
        &lt;div class="card"&gt;
            &lt;h3&gt;Список найденных мест&lt;/h3&gt;
            &lt;div id="placesList" style="max-height: 300px; overflow-y: auto;"&gt;&lt;/div&gt;
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;script&gt;
const rawData = [
    {
        "friend_name": "Наталья Шевковская",
        "friend_id": 4671765,
        "coordinates": "54.960033758443 20.47501174861",
        "place": "Зеленоградск",
        "post_text": "Балтика замерзла",
        "post_url": "https://vk.com/wall4671765_1030"
    },
......
    {
        "friend_name": "Татьяна Бондаренко",
        "friend_id": 131800785,
        "coordinates": "52.227369601217 21.003966668331",
        "place": "Warszawa Poland",
        "post_text": "довольные и счастливые #BGON ❤️",
        "post_url": "https://vk.com/wall131800785_3508"
    }
];

let currentFilter = "all";

const uniqueFriends = [...new Set(rawData.map(d => d.friend_id))];
const totalFriends = uniqueFriends.length;
const totalMarkers = rawData.length;

const cityCount = {};
const placeCount = {};
rawData.forEach(d => {
    const city = d.place.split(',')[0] || d.place;
    cityCount[city] = (cityCount[city] || 0) + 1;
    placeCount[d.place] = (placeCount[d.place] || 0) + 1;
});

const totalCities = Object.keys(cityCount).length;

let topPlaceName = "—";
let topPlaceCount = 0;
for (const [place, count] of Object.entries(placeCount)) {
    if (count > topPlaceCount) {
        topPlaceCount = count;
        topPlaceName = place;
    }
}

document.getElementById('totalFriends').innerText = totalFriends;
document.getElementById('totalMarkers').innerText = totalMarkers;
document.getElementById('totalCities').innerText = totalCities;
document.getElementById('topPlace').innerText = topPlaceName.length > 25 ? topPlaceName.slice(0, 25) + '…' : topPlaceName;

const friendSelect = document.getElementById('friendFilter');
const uniqueNames = [...new Map(rawData.map(d => [d.friend_id, d.friend_name])).entries()];
uniqueNames.forEach(([id, name]) => {
    const option = document.createElement('option');
    option.value = id;
    option.textContent = name;
    friendSelect.appendChild(option);
});

function getFilteredData() {
    if (currentFilter === "all") return rawData;
    return rawData.filter(d => d.friend_id == currentFilter);
}

const cityLabels = Object.keys(cityCount);
const cityValues = Object.values(cityCount);
const cityCtx = document.getElementById('cityChart').getContext('2d');
let cityChart = new Chart(cityCtx, {
    type: 'bar',
    data: { labels: cityLabels, datasets: [{ label: 'Количество отметок', data: cityValues, backgroundColor: '#3498db', borderRadius: 8 }] },
    options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'top' } } }
});

const sortedPlaces = Object.entries(placeCount).sort((a,b) => b[1] - a[1]).slice(0, 10);
const placesLabels = sortedPlaces.map(p => p[0].length > 20 ? p[0].slice(0, 20) + '…' : p[0]);
const placesValues = sortedPlaces.map(p => p[1]);
const placesCtx = document.getElementById('placesChart').getContext('2d');
let placesChart = new Chart(placesCtx, {
    type: 'pie',
    data: { labels: placesLabels, datasets: [{ data: placesValues, backgroundColor: ['#3498db', '#e74c3c', '#2ecc71', '#f1c40f', '#9b59b6', '#1abc9c', '#e67e22', '#34495e', '#16a085', '#27ae60'] }] },
    options: { responsive: true, maintainAspectRatio: true, plugins: { legend: { position: 'right', labels: { boxWidth: 12, font: { size: 10 } } } } }
});

function updatePlacesList() {
    const filtered = getFilteredData();
    const listContainer = document.getElementById('placesList');
    if (filtered.length === 0) { listContainer.innerHTML = '<p>Нет данных</p>'; return; }
    let html = '<ul style="list-style: none; padding: 0;">';
    filtered.forEach(d => {
        const text = d.post_text ? d.post_text.substring(0, 80) : '';
        html += `<li style="padding: 8px 0; border-bottom: 1px solid #eee;"><b>${d.place}</b><br><small>${d.friend_name} · ${text ? text + '…' : ''}<br><a href="${d.post_url}" target="_blank" style="color: #3498db;">открыть пост</a></small></li>`;
    });
    html += '</ul>';
    listContainer.innerHTML = html;
}

const map = L.map('map').setView([55.76, 37.64], 4);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map);
let markersLayer = L.layerGroup().addTo(map);

function updateMap() {
    markersLayer.clearLayers();
    const filtered = getFilteredData();
    const bounds = [];
    filtered.forEach(item => {
        const [lat, lng] = item.coordinates.split(' ');
        const marker = L.marker([parseFloat(lat), parseFloat(lng)]).addTo(markersLayer);
        const postText = item.post_text ? item.post_text.substring(0, 100) : '';
        marker.bindPopup(`<b>${item.friend_name}</b><br><i>${item.place}</i><br>${postText}<br><a href="${item.post_url}" target="_blank">Открыть пост</a>`);
        bounds.push([parseFloat(lat), parseFloat(lng)]);
    });
    if (bounds.length > 0) map.fitBounds(bounds);
}

function updateCharts() {
    const filtered = getFilteredData();
    const filteredCityCount = {};
    const filteredPlaceCount = {};
    filtered.forEach(d => {
        const city = d.place.split(',')[0] || d.place;
        filteredCityCount[city] = (filteredCityCount[city] || 0) + 1;
        filteredPlaceCount[d.place] = (filteredPlaceCount[d.place] || 0) + 1;
    });
    cityChart.data.labels = Object.keys(filteredCityCount);
    cityChart.data.datasets[0].data = Object.values(filteredCityCount);
    cityChart.update();
    const newSortedPlaces = Object.entries(filteredPlaceCount).sort((a,b) => b[1] - a[1]).slice(0, 10);
    placesChart.data.labels = newSortedPlaces.map(p => p[0].length > 20 ? p[0].slice(0,20)+'…' : p[0]);
    placesChart.data.datasets[0].data = newSortedPlaces.map(p => p[1]);
    placesChart.update();
}

friendSelect.addEventListener('change', (e) => {
    currentFilter = e.target.value;
    updateMap();
    updatePlacesList();
    updateCharts();
});

updateMap();
updatePlacesList();
&lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;

Выводы

В ходе работы над проектом я:

  • Научилась работать с VK API — получать токен, делать запросы, обрабатывать ответы.
  • Освоила парсинг JSON-данных и сохранение результатов в файл.
  • Создала интерактивную карту на Leaflet и нанесла на нее реальные геоданные.
  • Поняла, как устроены современные веб-сервисы, которые собирают данные из разных источников.
  • Научилась оформлять проекты в вики и структурировать отчёт.

Проект можно развивать дальше: добавить тепловую карту, сделать анализ популярных мест, подключить Яндекс.Карты с более детальными данными.

Полный исходный код приложения

▸ Показать полный код приложения

# config.py
# Токен доступа VK API (получен через standalone-приложение)

TOKEN = "vk1.a.мой_токен_здесь"
# main.py
import vk_api
from config import TOKEN
import json
import time

# Авторизация
vk_session = vk_api.VkApi(token=TOKEN)
vk = vk_session.get_api()

def get_friends_with_geo():
    """
    Получает список друзей и собирает их посты с геометками
    """
    print("Получаю список друзей...")
    friends = vk.friends.get(fields=['first_name', 'last_name'])
    
    friends_data = []
    total_friends = friends['count']
    print(f"Найдено друзей: {total_friends}")
    
    # Обрабатываем первых 30 друзей
    for i, friend in enumerate(friends['items'][:30]):
        print(f"Обрабатываю друга {i+1}/30: {friend['first_name']} {friend['last_name']}")
        
        try:
            # Получаем последние 50 постов друга
            posts = vk.wall.get(owner_id=friend['id'], count=50)
            
            for post in posts['items']:
                if 'geo' in post and post['geo']:
                    geo_data = {
                        'friend_name': f"{friend['first_name']} {friend['last_name']}",
                        'friend_id': friend['id'],
                        'coordinates': post['geo']['coordinates'],
                        'place': post['geo'].get('place', {}).get('title', 'Без названия'),
                        'post_text': post.get('text', '')[:100],
                        'post_url': f"https://vk.com/wall{friend['id']}_{post['id']}"
                    }
                    friends_data.append(geo_data)
                    print(f"  Найдена геометка: {geo_data['coordinates']} - {geo_data['place']}")
                    
        except Exception as e:
            print(f"  Ошибка при обработке: {e}")
        
        time.sleep(0.5)  # пауза, чтобы не спамить API
    
    # Сохраняем результат
    with open('friends_geo.json', 'w', encoding='utf-8') as f:
        json.dump(friends_data, f, ensure_ascii=False, indent=4)
    
    print(f"\nГотово! Найдено {len(friends_data)} постов с геометками.")
    return friends_data

if __name__ == "__main__":
    get_friends_with_geo()
# friends_geo.json (пример данных)
[
    {
        "friend_name": "Анна Смирнова",
        "friend_id": 123456789,
        "coordinates": "55.751244 37.618423",
        "place": "Красная площадь",
        "post_text": "Красота!",
        "post_url": "https://vk.com/wall123456789_123"
    },
    {
        "friend_name": "Иван Петров",
        "friend_id": 987654321,
        "coordinates": "55.755814 37.617635",
        "place": "Парк Горького",
        "post_text": "Гуляем с друзьями",
        "post_url": "https://vk.com/wall987654321_456"
    }
]
# map.html
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;title&gt;Карта друзей&lt;/title&gt;
    &lt;meta charset="utf-8" /&gt;
    &lt;link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" /&gt;
    &lt;script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"&gt;&lt;/script&gt;
    &lt;style&gt;
        body { margin: 0; padding: 0; }
        #map { width: 100%; height: 100vh; }
    &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div id="map"&gt;&lt;/div&gt;
    &lt;script&gt;
        const friendsData = [
            {
                "friend_name": "Анна Смирнова",
                "coordinates": "55.751244 37.618423",
                "place": "Красная площадь",
                "post_text": "Красота!",
                "post_url": "https://vk.com/wall123456789_123"
            },
            {
                "friend_name": "Иван Петров",
                "coordinates": "55.755814 37.617635",
                "place": "Парк Горького",
                "post_text": "Гуляем с друзьями",
                "post_url": "https://vk.com/wall987654321_456"
            }
        ];

        const map = L.map('map').setView([55.76, 37.64], 10);
        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '© OpenStreetMap contributors'
        }).addTo(map);

        friendsData.forEach(item => {
            const [lat, lng] = item.coordinates.split(' ');
            const marker = L.marker([parseFloat(lat), parseFloat(lng)]).addTo(map);
            marker.bindPopup(`
                &lt;b&gt;${item.friend_name}&lt;/b&gt;&lt;br&gt;
                &lt;i&gt;${item.place}&lt;/i&gt;&lt;br&gt;
                ${item.post_text}&lt;br&gt;
                &lt;a href="${item.post_url}" target="_blank"&gt;Открыть пост&lt;/a&gt;
            `);
        });
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
# requirements.txt
vk-api==5.107
requests==2.31.0

Географическая карта с интерактивными метками (создано с помощью семантических страниц)

Идёт загрузка карты…