381 lines
16 KiB
Python
381 lines
16 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
Сбор данных об учебных заведениях Челябинской области через бесплатные API
|
||
Использует: OpenStreetMap Nominatim API, Overpass API для сбора данных
|
||
"""
|
||
|
||
import requests
|
||
import json
|
||
import time
|
||
from typing import List, Dict, Optional
|
||
from urllib.parse import quote
|
||
|
||
|
||
class EducationalInstitutionsCollector:
|
||
"""Сборщик данных об учебных заведениях через бесплатные API"""
|
||
|
||
def __init__(self):
|
||
self.nominatim_url = "https://nominatim.openstreetmap.org"
|
||
self.overpass_url = "https://overpass-api.de/api/interpreter"
|
||
self.session = requests.Session()
|
||
self.session.headers.update({
|
||
'User-Agent': 'Educational Institutions Collector 1.0 (educational purposes)'
|
||
})
|
||
self.results = []
|
||
|
||
def search_nominatim(self, query: str, region: str = "Челябинская область") -> List[Dict]:
|
||
"""Поиск через Nominatim API"""
|
||
print(f"Поиск через Nominatim: {query}")
|
||
|
||
params = {
|
||
'q': f"{query}, {region}",
|
||
'format': 'json',
|
||
'limit': 50,
|
||
'addressdetails': 1,
|
||
'extratags': 1,
|
||
'countrycodes': 'ru'
|
||
}
|
||
|
||
try:
|
||
response = self.session.get(f"{self.nominatim_url}/search", params=params)
|
||
response.raise_for_status()
|
||
time.sleep(1) # Уважаем лимиты API
|
||
|
||
data = response.json()
|
||
print(f"Найдено {len(data)} результатов")
|
||
return data
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
print(f"Ошибка запроса к Nominatim: {e}")
|
||
return []
|
||
|
||
def search_overpass(self, amenity_type: str) -> List[Dict]:
|
||
"""Поиск через Overpass API (OpenStreetMap)"""
|
||
print(f"Поиск через Overpass API: {amenity_type}")
|
||
|
||
# Bounding box для Челябинской области (приблизительно)
|
||
bbox = "53.5,58.0,56.5,63.5" # south,west,north,east
|
||
|
||
query = f"""
|
||
[out:json][timeout:60];
|
||
(
|
||
node["amenity"="{amenity_type}"]({bbox});
|
||
way["amenity"="{amenity_type}"]({bbox});
|
||
relation["amenity"="{amenity_type}"]({bbox});
|
||
);
|
||
out center meta;
|
||
"""
|
||
|
||
try:
|
||
response = self.session.post(self.overpass_url, data=query)
|
||
response.raise_for_status()
|
||
time.sleep(2) # Уважаем лимиты API
|
||
|
||
data = response.json()
|
||
elements = data.get('elements', [])
|
||
print(f"Найдено {len(elements)} объектов")
|
||
return elements
|
||
|
||
except requests.exceptions.RequestException as e:
|
||
print(f"Ошибка запроса к Overpass API: {e}")
|
||
return []
|
||
|
||
def process_nominatim_result(self, item: Dict) -> Dict:
|
||
"""Обработка результата от Nominatim"""
|
||
address = item.get('address', {})
|
||
|
||
return {
|
||
'id': f"nominatim_{item.get('place_id')}",
|
||
'name': item.get('display_name', '').split(',')[0].strip(),
|
||
'full_name': item.get('display_name', ''),
|
||
'address': self._format_address(address),
|
||
'latitude': float(item.get('lat', 0)),
|
||
'longitude': float(item.get('lon', 0)),
|
||
'type': item.get('type', ''),
|
||
'category': item.get('category', ''),
|
||
'city': address.get('city') or address.get('town') or address.get('village', ''),
|
||
'region': address.get('state', ''),
|
||
'source': 'OpenStreetMap Nominatim',
|
||
'osm_id': item.get('osm_id'),
|
||
'osm_type': item.get('osm_type'),
|
||
'raw_data': item
|
||
}
|
||
|
||
def process_overpass_result(self, item: Dict) -> Dict:
|
||
"""Обработка результата от Overpass API"""
|
||
tags = item.get('tags', {})
|
||
|
||
# Определяем координаты
|
||
if 'center' in item:
|
||
lat = item['center']['lat']
|
||
lon = item['center']['lon']
|
||
else:
|
||
lat = item.get('lat', 0)
|
||
lon = item.get('lon', 0)
|
||
|
||
# Форматируем адрес
|
||
address_parts = []
|
||
if tags.get('addr:street'):
|
||
if tags.get('addr:housenumber'):
|
||
address_parts.append(f"{tags['addr:street']}, {tags['addr:housenumber']}")
|
||
else:
|
||
address_parts.append(tags['addr:street'])
|
||
if tags.get('addr:city'):
|
||
address_parts.append(tags['addr:city'])
|
||
|
||
address = ', '.join(address_parts) if address_parts else ''
|
||
|
||
return {
|
||
'id': f"osm_{item.get('id')}",
|
||
'name': tags.get('name', ''),
|
||
'official_name': tags.get('official_name', ''),
|
||
'address': address,
|
||
'latitude': float(lat),
|
||
'longitude': float(lon),
|
||
'amenity': tags.get('amenity', ''),
|
||
'operator': tags.get('operator', ''),
|
||
'website': tags.get('website', ''),
|
||
'phone': tags.get('phone', ''),
|
||
'email': tags.get('email', ''),
|
||
'city': tags.get('addr:city', ''),
|
||
'region': tags.get('addr:state', ''),
|
||
'opening_hours': tags.get('opening_hours', ''),
|
||
'source': 'OpenStreetMap Overpass',
|
||
'osm_id': item.get('id'),
|
||
'osm_type': item.get('type'),
|
||
'raw_data': item
|
||
}
|
||
|
||
def _format_address(self, address: Dict) -> str:
|
||
"""Форматирование адреса"""
|
||
parts = []
|
||
|
||
# Улица и номер дома
|
||
if address.get('road'):
|
||
if address.get('house_number'):
|
||
parts.append(f"{address['road']}, {address['house_number']}")
|
||
else:
|
||
parts.append(address['road'])
|
||
|
||
# Город
|
||
city = address.get('city') or address.get('town') or address.get('village')
|
||
if city:
|
||
parts.append(city)
|
||
|
||
return ', '.join(parts)
|
||
|
||
def collect_all_data(self) -> List[Dict]:
|
||
"""Собираем все данные об учебных заведениях"""
|
||
print("=" * 80)
|
||
print("СБОР ДАННЫХ ОБ УЧЕБНЫХ ЗАВЕДЕНИЯХ ЧЕЛЯБИНСКОЙ ОБЛАСТИ")
|
||
print("=" * 80)
|
||
|
||
all_institutions = []
|
||
seen_names = set()
|
||
|
||
# 1. Поиск через Nominatim
|
||
print("\n1. Поиск через Nominatim API...")
|
||
nominatim_queries = [
|
||
"университет",
|
||
"институт",
|
||
"колледж",
|
||
"техникум",
|
||
"училище",
|
||
"академия",
|
||
"ВУЗ",
|
||
"образовательное учреждение"
|
||
]
|
||
|
||
for query in nominatim_queries:
|
||
results = self.search_nominatim(query)
|
||
for item in results:
|
||
processed = self.process_nominatim_result(item)
|
||
if processed['name'] and processed['name'] not in seen_names:
|
||
all_institutions.append(processed)
|
||
seen_names.add(processed['name'])
|
||
|
||
# 2. Поиск через Overpass API
|
||
print("\n2. Поиск через Overpass API...")
|
||
overpass_amenities = [
|
||
"university",
|
||
"college",
|
||
"school" # может включать техникумы и училища
|
||
]
|
||
|
||
for amenity in overpass_amenities:
|
||
results = self.search_overpass(amenity)
|
||
for item in results:
|
||
processed = self.process_overpass_result(item)
|
||
if processed['name'] and processed['name'] not in seen_names:
|
||
# Фильтруем школы, оставляем только ВУЗы и колледжи
|
||
name = processed['name'].lower()
|
||
if any(word in name for word in ['университет', 'институт', 'колледж', 'техникум', 'училище', 'академия']):
|
||
all_institutions.append(processed)
|
||
seen_names.add(processed['name'])
|
||
|
||
# 3. Добавляем известные учебные заведения
|
||
print("\n3. Добавляем известные учебные заведения...")
|
||
known_institutions = self._get_known_institutions()
|
||
for institution in known_institutions:
|
||
if institution['name'] not in seen_names:
|
||
all_institutions.append(institution)
|
||
seen_names.add(institution['name'])
|
||
|
||
print(f"\nВсего найдено уникальных учебных заведений: {len(all_institutions)}")
|
||
return all_institutions
|
||
|
||
def _get_known_institutions(self) -> List[Dict]:
|
||
"""Список известных учебных заведений для дополнения"""
|
||
return [
|
||
{
|
||
'id': 'known_1',
|
||
'name': 'Южно-Уральский государственный университет',
|
||
'official_name': 'ФГАОУ ВО "Южно-Уральский государственный университет (национальный исследовательский университет)"',
|
||
'address': 'проспект Ленина, 76, Челябинск',
|
||
'latitude': 55.1549,
|
||
'longitude': 61.4289,
|
||
'phone': '+7 (351) 267-90-90',
|
||
'website': 'https://www.susu.ru',
|
||
'type': 'университет',
|
||
'city': 'Челябинск',
|
||
'region': 'Челябинская область',
|
||
'source': 'Дополнительные данные'
|
||
},
|
||
{
|
||
'id': 'known_2',
|
||
'name': 'Челябинский государственный университет',
|
||
'official_name': 'ФГБОУ ВО "Челябинский государственный университет"',
|
||
'address': 'ул. Братьев Кашириных, 129, Челябинск',
|
||
'latitude': 55.1605,
|
||
'longitude': 61.4025,
|
||
'phone': '+7 (351) 799-70-02',
|
||
'website': 'https://www.csu.ru',
|
||
'type': 'университет',
|
||
'city': 'Челябинск',
|
||
'region': 'Челябинская область',
|
||
'source': 'Дополнительные данные'
|
||
},
|
||
{
|
||
'id': 'known_3',
|
||
'name': 'Магнитогорский государственный технический университет',
|
||
'official_name': 'ФГБОУ ВО "Магнитогорский государственный технический университет им. Г.И. Носова"',
|
||
'address': 'проспект Ленина, 38, Магнитогорск',
|
||
'latitude': 53.4213,
|
||
'longitude': 59.0457,
|
||
'phone': '+7 (3519) 29-84-42',
|
||
'website': 'https://www.magtu.ru',
|
||
'type': 'университет',
|
||
'city': 'Магнитогорск',
|
||
'region': 'Челябинская область',
|
||
'source': 'Дополнительные данные'
|
||
},
|
||
{
|
||
'id': 'known_4',
|
||
'name': 'Южно-Уральский государственный медицинский университет',
|
||
'official_name': 'ФГБОУ ВО "Южно-Уральский государственный медицинский университет"',
|
||
'address': 'ул. Воровского, 64, Челябинск',
|
||
'latitude': 55.1634,
|
||
'longitude': 61.4012,
|
||
'phone': '+7 (351) 232-73-47',
|
||
'website': 'https://www.chelsma.ru',
|
||
'type': 'университет',
|
||
'city': 'Челябинск',
|
||
'region': 'Челябинская область',
|
||
'source': 'Дополнительные данные'
|
||
}
|
||
]
|
||
|
||
def save_to_json(self, data: List[Dict], filename: str = "chelyabinsk_educational_institutions.json"):
|
||
"""Сохранение данных в JSON"""
|
||
try:
|
||
output = {
|
||
'region': 'Челябинская область',
|
||
'collection_date': time.strftime('%Y-%m-%d %H:%M:%S'),
|
||
'total_count': len(data),
|
||
'sources': ['OpenStreetMap Nominatim', 'OpenStreetMap Overpass', 'Дополнительные данные'],
|
||
'data_types': {
|
||
'universities': len([x for x in data if 'университет' in x.get('name', '').lower() or x.get('type') == 'университет']),
|
||
'institutes': len([x for x in data if 'институт' in x.get('name', '').lower()]),
|
||
'colleges': len([x for x in data if any(word in x.get('name', '').lower() for word in ['колледж', 'техникум', 'училище'])]),
|
||
},
|
||
'institutions': data
|
||
}
|
||
|
||
with open(filename, 'w', encoding='utf-8') as f:
|
||
json.dump(output, f, ensure_ascii=False, indent=2)
|
||
|
||
print(f"\n✅ Данные сохранены в файл: {filename}")
|
||
return filename
|
||
|
||
except Exception as e:
|
||
print(f"❌ Ошибка сохранения: {e}")
|
||
return None
|
||
|
||
|
||
def main():
|
||
"""Основная функция"""
|
||
collector = EducationalInstitutionsCollector()
|
||
|
||
# Собираем данные
|
||
institutions = collector.collect_all_data()
|
||
|
||
if institutions:
|
||
# Сохраняем в JSON
|
||
filename = collector.save_to_json(institutions)
|
||
|
||
# Выводим статистику
|
||
print("\n" + "=" * 80)
|
||
print("СТАТИСТИКА СОБРАННЫХ ДАННЫХ")
|
||
print("=" * 80)
|
||
|
||
cities = {}
|
||
types = {}
|
||
|
||
for inst in institutions:
|
||
city = inst.get('city', 'Не указан')
|
||
cities[city] = cities.get(city, 0) + 1
|
||
|
||
name = inst.get('name', '').lower()
|
||
if 'университет' in name:
|
||
inst_type = 'Университеты'
|
||
elif 'институт' in name:
|
||
inst_type = 'Институты'
|
||
elif 'колледж' in name:
|
||
inst_type = 'Колледжи'
|
||
elif 'техникум' in name:
|
||
inst_type = 'Техникумы'
|
||
elif 'училище' in name:
|
||
inst_type = 'Училища'
|
||
else:
|
||
inst_type = 'Другие'
|
||
|
||
types[inst_type] = types.get(inst_type, 0) + 1
|
||
|
||
print(f"Всего учебных заведений: {len(institutions)}")
|
||
|
||
print(f"\nПо типам:")
|
||
for inst_type, count in sorted(types.items(), key=lambda x: x[1], reverse=True):
|
||
print(f" {inst_type}: {count}")
|
||
|
||
print(f"\nПо городам:")
|
||
for city, count in sorted(cities.items(), key=lambda x: x[1], reverse=True)[:10]:
|
||
print(f" {city}: {count}")
|
||
|
||
print(f"\nПримеры найденных учреждений:")
|
||
for i, inst in enumerate(institutions[:5], 1):
|
||
print(f"{i}. {inst['name']}")
|
||
if inst.get('address'):
|
||
print(f" 📍 {inst['address']}")
|
||
if inst.get('phone'):
|
||
print(f" 📞 {inst['phone']}")
|
||
if inst.get('website'):
|
||
print(f" 🌐 {inst['website']}")
|
||
print()
|
||
|
||
else:
|
||
print("❌ Не удалось собрать данные об учебных заведениях")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |