Карта друзей: различия между версиями
Нет описания правки |
Нет описания правки |
||
| (не показаны 23 промежуточные версии этого же участника) | |||
| Строка 1: | Строка 1: | ||
<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="padding: 15px; margin-bottom: 25px;"> | |||
<h2 style="color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 8px;">🗺️ Карта друзей</h2> | |||
<p><b>Автор:</b> Анна Муханова</p> | |||
<p><b>Группа:</b> АДЭУ-221</p> | |||
<p><b>Дисциплина:</b> Работа с API социальных сетей и облачных сервисов</p> | |||
<p><b>Статус проекта:</b> Выполнен</p> | |||
</div> | |||
<!-- Цель проекта --> | <!-- Цель проекта --> | ||
<h3 style="color: #34495e;"> | <h3 style="color: #34495e;">Цель работы</h3> | ||
<p>Разработать инструмент для сбора и визуализации геоданных из открытых источников (VK) с целью изучения принципов работы API и создания интерактивных карт.</p> | <p>Разработать инструмент для сбора и визуализации геоданных из открытых источников (VK) с целью изучения принципов работы [[API]] и создания интерактивных карт.</p> | ||
<!-- Задачи --> | <!-- Задачи --> | ||
<h3 style="color: #34495e;"> | <h3 style="color: #34495e;">Задачи</h3> | ||
<ol> | <ol> | ||
<li>Изучить документацию VK API.</li> | <li>Изучить документацию VK API.</li> | ||
| Строка 39: | Строка 26: | ||
<!-- Планируемый стек технологий --> | <!-- Планируемый стек технологий --> | ||
<h3 style="color: #34495e;"> | <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;"> | |||
< | {{#mermaid: flowchart TB | ||
< | A[Начало] --> B(Получение токена VK API) | ||
B --> C(Запрос списка друзей) | |||
C --> D{Есть друзья?} | |||
D -->|Нет| E[Завершить] | |||
D -->|Да| F[Цикл по каждому другу] | |||
F --> G(Запрос последних 30 постов) | |||
G --> H{Пост содержит geo?} | |||
H -->|Нет| I[Следующий пост] | |||
I --> G | |||
H -->|Да| J(Сохранить координаты, имя, место, ссылку) | |||
< | J --> K{Все посты проверены?} | ||
K -->|Нет| I | |||
K -->|Да| L{Все друзья обработаны?} | |||
L -->|Нет| F | |||
L -->|Да| M(Сохранить данные в JSON-файл) | |||
M --> N(Открыть HTML-страницу с картой) | |||
N --> O(Загрузить JSON в JavaScript) | |||
O --> P(Отрисовать маркеры на карте) | |||
P --> Q(Клик по маркеру → всплывающее окно) | |||
Q --> R[Готово] | |||
}} | |||
<!-- Структура проекта --> | |||
<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"> | |||
<!DOCTYPE html> | |||
<html lang="ru"> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<title>Дашборд: Карта друзей</title> | |||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" /> | |||
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script> | |||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |||
<style> | |||
* { 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; } | |||
</style> | |||
</head> | |||
<body> | |||
<div class="dashboard"> | |||
<h1>Карта друзей — аналитический дашборд</h1> | |||
<div class="stats"> | |||
<div class="stat-box"><div class="stat-number" id="totalFriends">0</div><div class="stat-label">Друзей обработано</div></div> | |||
<div class="stat-box"><div class="stat-number" id="totalMarkers">0</div><div class="stat-label">Геометок найдено</div></div> | |||
<div class="stat-box"><div class="stat-number" id="totalCities">0</div><div class="stat-label">Городов</div></div> | |||
<div class="stat-box"><div class="stat-number" id="topPlace">—</div><div class="stat-label">Самое популярное место</div></div> | |||
</div> | |||
<div class="row"> | |||
<div class="card map-container"> | |||
<h3>Карта с отметками</h3> | |||
<div id="map"></div> | |||
<hr> | |||
<label for="friendFilter">Фильтр по другу:</label> | |||
<select id="friendFilter" class="filter-select"><option value="all">Все друзья</option></select> | |||
</div> | |||
<div class="card"> | |||
<h3>Города по количеству отметок</h3> | |||
<canvas id="cityChart"></canvas> | |||
</div> | |||
</div> | |||
<div class="row"> | |||
<div class="card"> | |||
<h3>Активность по местам</h3> | |||
<canvas id="placesChart"></canvas> | |||
</div> | |||
<div class="card"> | |||
<h3>Список найденных мест</h3> | |||
<div id="placesList" style="max-height: 300px; overflow-y: auto;"></div> | |||
</div> | |||
</div> | |||
</div> | |||
<script> | |||
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(); | |||
</script> | |||
</body> | |||
</html> | |||
</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 | |||
# Авторизация | |||
< | 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() | |||
</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 | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<title>Карта друзей</title> | |||
<meta charset="utf-8" /> | |||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" /> | |||
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script> | |||
<style> | |||
body { margin: 0; padding: 0; } | |||
#map { width: 100%; height: 100vh; } | |||
</style> | |||
</head> | |||
<body> | |||
<div id="map"></div> | |||
<script> | |||
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(` | |||
<b>${item.friend_name}</b><br> | |||
<i>${item.place}</i><br> | |||
${item.post_text}<br> | |||
<a href="${item.post_url}" target="_blank">Открыть пост</a> | |||
`); | |||
}); | |||
</script> | |||
</body> | |||
</html> | |||
</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 | |||
}} | |||
Текущая версия от 10:39, 27 марта 2026
🗺️ Карта друзей
Автор: Анна Муханова
Группа: АДЭУ-221
Дисциплина: Работа с API социальных сетей и облачных сервисов
Статус проекта: Выполнен
Цель работы
Разработать инструмент для сбора и визуализации геоданных из открытых источников (VK) с целью изучения принципов работы API и создания интерактивных карт.
Задачи
- Изучить документацию VK API.
- Получить токен доступа для работы с данными.
- Написать скрипт на Python для сбора ID друзей и их публичных постов с геометками.
- Сохранить полученные координаты в формате JSON.
- Создать веб-страницу с картой (Leaflet) и нанести на нее точки.
- Опубликовать результат.
Технологии
- Языки: Python, JavaScript, HTML/CSS
- Библиотеки Python: requests, vk_api
- API: VK API, OpenStreetMap (через Leaflet)
- Хостинг: GitHub Pages (для карты)
Диаграмма работы приложения «Карта друзей»
Структура проекта

Файлы проекта: 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.

Структура данных: имя друга, координаты, место, текст поста, ссылка
Этап 5. Создание интерактивной карты
Создала HTML-страницу map.html с картой на базе библиотеки Leaflet (OpenStreetMap). Написала JavaScript-код, который:
- Загружает данные из JSON-файла
- Разбирает координаты (формат "широта долгота")
- Добавляет маркеры на карту
- При клике на маркер показывает всплывающее окно с именем друга, местом и ссылкой на пост



Результат: готовая интерактивная карта с точками всех найденных геометок.
Этап 6. Оформление страницы в вики
Добавила на страницу проекта:
- Блок с результатами
- Скриншоты всех этапов
- Структуру проекта
- Пример кода на Python
- Выводы и планы по развитию
Итог проекта
Создан работающий инструмент для сбора и визуализации геоданных из ВКонтакте
23 друга обработано | 18 геометок собрано | 14 городов на карте
Проект выполнен в рамках дисциплины «Работа с API социальных сетей и облачных сервисов»
Аналитический дашборд

На основе собранных данных был разработан интерактивный дашборд, который включает:
- Интерактивную карту с маркерами всех найденных мест
- Блок статистики (количество друзей, геометок, городов, самое популярное место)
- Гистограмму распределения отметок по городам
- Круговую диаграмму топ-10 мест по популярности
- Фильтр по друзьям для детального анализа
- Список всех мест со ссылками на посты
▸ Показать код дашборда (dashboard.html)
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<title>Дашборд: Карта друзей</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
* { 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; }
</style>
</head>
<body>
<div class="dashboard">
<h1>Карта друзей — аналитический дашборд</h1>
<div class="stats">
<div class="stat-box"><div class="stat-number" id="totalFriends">0</div><div class="stat-label">Друзей обработано</div></div>
<div class="stat-box"><div class="stat-number" id="totalMarkers">0</div><div class="stat-label">Геометок найдено</div></div>
<div class="stat-box"><div class="stat-number" id="totalCities">0</div><div class="stat-label">Городов</div></div>
<div class="stat-box"><div class="stat-number" id="topPlace">—</div><div class="stat-label">Самое популярное место</div></div>
</div>
<div class="row">
<div class="card map-container">
<h3>Карта с отметками</h3>
<div id="map"></div>
<hr>
<label for="friendFilter">Фильтр по другу:</label>
<select id="friendFilter" class="filter-select"><option value="all">Все друзья</option></select>
</div>
<div class="card">
<h3>Города по количеству отметок</h3>
<canvas id="cityChart"></canvas>
</div>
</div>
<div class="row">
<div class="card">
<h3>Активность по местам</h3>
<canvas id="placesChart"></canvas>
</div>
<div class="card">
<h3>Список найденных мест</h3>
<div id="placesList" style="max-height: 300px; overflow-y: auto;"></div>
</div>
</div>
</div>
<script>
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();
</script>
</body>
</html>
Выводы
В ходе работы над проектом я:
- Научилась работать с 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
<!DOCTYPE html>
<html>
<head>
<title>Карта друзей</title>
<meta charset="utf-8" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"></script>
<style>
body { margin: 0; padding: 0; }
#map { width: 100%; height: 100vh; }
</style>
</head>
<body>
<div id="map"></div>
<script>
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(`
<b>${item.friend_name}</b><br>
<i>${item.place}</i><br>
${item.post_text}<br>
<a href="${item.post_url}" target="_blank">Открыть пост</a>
`);
});
</script>
</body>
</html>
# requirements.txt
vk-api==5.107
requests==2.31.0
Географическая карта с интерактивными метками (создано с помощью семантических страниц)
