Парсинг Яндекс Карт на Python: API, Selenium, сбор организаций

В прошлом году мне нужно было собрать список всех автосервисов в Петербурге для анализа конкурентов: название, адрес, телефон, рейтинг, часы работы. Вручную это 2000+ карточек. Через API Яндекс Карт я получил структурированные данные за вечер — 50 запросов покрыли весь город. Яндекс Карты это крупнейший справочник организаций в России: более 100 миллионов точек интереса с контактами, рейтингами и отзывами.
Существуют три способа извлечь данные из Яндекс Карт: официальный API (Search API), прямой парсинг через Selenium и парсинг через HTTP-запросы. В этой статье разбираю каждый способ с кодом, объясняю ограничения и показываю, как сохранить результат в CSV.
Какие данные можно собрать
Карточка организации на Яндекс Картах содержит набор полей, полезных для маркетинга, аналитики и лидогенерации.
- Название организации и категория (рубрика)
- Адрес, координаты (широта/долгота)
- Телефон, email, сайт
- Рейтинг и количество отзывов
- Часы работы
- Фотографии
- Текст отзывов (доступен при парсинге, ограничен в API)
Способ 1: Search API (официальный)
Search API (API Поиска по организациям) это официальный HTTP-интерфейс Яндекс Карт для поиска компаний по запросу и местоположению. Он возвращает структурированный JSON с данными организаций.
Получение API-ключа
- Зайдите в Кабинет разработчика: developer.tech.yandex.ru.
- Нажмите "Подключить API".
- Выберите "API Поиска по организациям" (Search API).
- Заполните форму и получите ключ.
API платный. Бесплатный тариф даёт ограниченное количество запросов в сутки. Точные лимиты зависят от тарифного плана.
Минимальный запрос
import requests
API_KEY = "ваш-ключ"
params = {
"text": "автосервис",
"lang": "ru_RU",
"apikey": API_KEY,
"results": 50,
"ll": "30.3351,59.9343", # центр Петербурга (долгота, широта)
"spn": "0.5,0.5", # область поиска
"rspn": 1, # строго внутри области
}
response = requests.get("https://search-maps.yandex.ru/v1/", params=params)
response.raise_for_status()
data = response.json()
for feature in data["features"]:
props = feature["properties"]
meta = props["CompanyMetaData"]
print(f"Название: {props['name']}")
print(f"Адрес: {meta['address']}")
if "Phones" in meta:
phones = [p["formatted"] for p in meta["Phones"]]
print(f"Телефоны: {', '.join(phones)}")
print("-" * 40)
Параметр ll задаёт центр поиска (долгота, широта), spn задаёт размер области, results определяет количество результатов (максимум 500 за запрос). Параметр rspn=1 ограничивает результаты строго указанной областью.
Вывод:
Название: АвтоМастер
Адрес: Россия, Санкт-Петербург, Невский проспект, 28
Телефоны: +7 (812) 555-12-34
----------------------------------------
Название: СТО Профи
Адрес: Россия, Санкт-Петербург, ул. Марата, 15
Телефоны: +7 (812) 333-45-67
----------------------------------------
Пагинация: сбор всех результатов
Один запрос возвращает до 500 организаций. Если в городе больше, используйте параметр skip для пагинации.
import requests
import time
API_KEY = "ваш-ключ"
all_orgs = []
skip = 0
while True:
params = {
"text": "автосервис",
"lang": "ru_RU",
"apikey": API_KEY,
"results": 500,
"ll": "30.3351,59.9343",
"spn": "0.5,0.5",
"rspn": 1,
"skip": skip,
}
response = requests.get("https://search-maps.yandex.ru/v1/", params=params)
response.raise_for_status()
data = response.json()
features = data.get("features", [])
if not features:
break
all_orgs.extend(features)
skip += len(features)
print(f"Получено: {len(all_orgs)} организаций")
time.sleep(1) # пауза между запросами
print(f"Итого: {len(all_orgs)} организаций")
Параметр skip сдвигает выборку: skip=500 вернёт результаты 501–1000. Цикл останавливается, когда сервер возвращает пустой список.
Сохранение в CSV
import csv
with open("organizations.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["name", "address", "phone", "url", "rating", "lat", "lon"])
for feature in all_orgs:
props = feature["properties"]
meta = props.get("CompanyMetaData", {})
coords = feature["geometry"]["coordinates"]
phones = ""
if "Phones" in meta:
phones = "; ".join(p["formatted"] for p in meta["Phones"])
writer.writerow(
[
props.get("name", ""),
meta.get("address", ""),
phones,
meta.get("url", ""),
meta.get("rating", ""),
coords[1], # широта
coords[0], # долгота
]
)
print(f"Сохранено {len(all_orgs)} организаций в organizations.csv")
Координаты в API приходят в формате [долгота, широта]. Для CSV удобнее записывать [широта, долгота], потому что так принято в географии.
Способ 2: Selenium (парсинг HTML)
Selenium нужен, когда API не покрывает задачу: нужны отзывы, фотографии или данные, которых нет в Search API. Selenium управляет настоящим браузером и видит всё, что видит пользователь.
Установка
pip install selenium webdriver-manager
Сбор карточек из поисковой выдачи
import csv
import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
options = Options()
options.add_argument("--no-sandbox")
options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument(
"user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36"
)
driver = webdriver.Chrome(
service=Service(ChromeDriverManager().install()), options=options
)
driver.get("https://yandex.ru/maps/2/saint-petersburg/search/автосервис/")
time.sleep(5)
# Прокрутка списка результатов для подгрузки
sidebar = driver.find_element(By.CLASS_NAME, "search-list-view")
for _ in range(10):
driver.execute_script("arguments[0].scrollTop = arguments[0].scrollHeight", sidebar)
time.sleep(2)
# Извлечение карточек
cards = driver.find_elements(By.CSS_SELECTOR, "li.search-snippet-view")
results = []
for card in cards:
try:
name = card.find_element(
By.CSS_SELECTOR, ".search-business-snippet-view__title"
).text
except Exception:
name = ""
try:
address = card.find_element(
By.CSS_SELECTOR, ".search-business-snippet-view__address"
).text
except Exception:
address = ""
try:
rating = card.find_element(
By.CSS_SELECTOR, ".business-rating-badge-view__rating-text"
).text
except Exception:
rating = ""
results.append({"name": name, "address": address, "rating": rating})
driver.quit()
# Сохранение
with open("yandex_maps_selenium.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["name", "address", "rating"])
writer.writeheader()
writer.writerows(results)
print(f"Собрано {len(results)} организаций")
CSS-селекторы в коде выше приведены как пример. Яндекс регулярно меняет классы элементов, поэтому перед запуском нужно проверить актуальные селекторы через DevTools браузера.
Парсинг детальной карточки (телефон, часы, отзывы)
def parse_card(driver, url):
driver.get(url)
time.sleep(3)
data = {}
try:
data["name"] = driver.find_element(
By.CSS_SELECTOR, "h1.orgpage-header-view__header"
).text
except Exception:
data["name"] = ""
try:
data["phone"] = driver.find_element(
By.CSS_SELECTOR, ".orgpage-phones-view__phone-number"
).text
except Exception:
data["phone"] = ""
try:
data["hours"] = driver.find_element(
By.CSS_SELECTOR, ".business-working-status-view__text"
).text
except Exception:
data["hours"] = ""
# Сбор отзывов
reviews = []
try:
review_elements = driver.find_elements(
By.CSS_SELECTOR, ".business-review-view__body-text"
)
for el in review_elements[:10]:
reviews.append(el.text)
except Exception:
pass
data["reviews"] = reviews
return data
Функция открывает карточку организации и извлекает телефон, часы работы и текст первых 10 отзывов.
Способ 3: HTTP-запросы без браузера
Между API и Selenium есть промежуточный вариант: отправлять запросы напрямую к внутренним эндпоинтам Яндекс Карт через requests. Этот способ быстрее Selenium, но менее стабилен: эндпоинты могут меняться без предупреждения.
import requests
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36",
"Accept-Language": "ru,en;q=0.9",
"Referer": "https://yandex.ru/maps/",
}
url = "https://yandex.ru/maps/api/search"
params = {
"text": "стоматология",
"ll": "37.6173,55.7558",
"spn": "0.3,0.3",
"lang": "ru",
"results": 30,
"origin": "maps-search-form",
}
response = requests.get(url, headers=headers, params=params)
if response.status_code == 200:
data = response.json()
for item in data.get("data", {}).get("items", []):
print(item.get("name"), "|", item.get("address"))
else:
print(f"Ошибка: {response.status_code}")
Этот подход требует инспекции сетевых запросов в DevTools (вкладка Network) для определения актуальных URL и параметров. Структура ответа не документирована и может измениться в любой момент.
Сравнение трёх способов
Обход блокировок
Яндекс активно блокирует автоматизированные запросы. При парсинге через Selenium или requests вы столкнётесь с капчей и блокировкой IP.
Ротация User-Agent
import random
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Safari/605.1.15",
"Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0",
]
headers = {"User-Agent": random.choice(USER_AGENTS)}
Ротация прокси
import itertools
PROXIES = [
"http://user:pass@proxy1.example.com:8080",
"http://user:pass@proxy2.example.com:8080",
"http://user:pass@proxy3.example.com:8080",
]
proxy_pool = itertools.cycle(PROXIES)
def get_with_proxy(url, params):
proxy = next(proxy_pool)
return requests.get(
url, params=params, proxies={"http": proxy, "https": proxy}, timeout=10
)
Ротация прокси распределяет запросы между разными IP-адресами, снижая вероятность блокировки. Для серьёзного парсинга нужны резидентные прокси — они имитируют трафик обычных пользователей.
Задержки между запросами
import time
import random
def polite_sleep():
delay = random.uniform(2.0, 5.0)
time.sleep(delay)
Случайные задержки от 2 до 5 секунд имитируют поведение человека. Фиксированные задержки (ровно 3 секунды каждый раз) легко обнаруживаются системами защиты.
Парсинг по нескольким городам
Для покрытия всей России разбейте поиск по городам. Каждый город определяется своими координатами и областью.
CITIES = {
"Москва": {"ll": "37.6173,55.7558", "spn": "0.6,0.4"},
"Санкт-Петербург": {"ll": "30.3351,59.9343", "spn": "0.5,0.3"},
"Новосибирск": {"ll": "82.9346,55.0084", "spn": "0.4,0.3"},
"Екатеринбург": {"ll": "60.6122,56.8519", "spn": "0.4,0.3"},
"Казань": {"ll": "49.1082,55.7963", "spn": "0.4,0.3"},
}
all_results = []
for city_name, coords in CITIES.items():
print(f"Парсим: {city_name}")
skip = 0
while True:
params = {
"text": "стоматология",
"lang": "ru_RU",
"apikey": API_KEY,
"results": 500,
"ll": coords["ll"],
"spn": coords["spn"],
"rspn": 1,
"skip": skip,
}
response = requests.get("https://search-maps.yandex.ru/v1/", params=params)
data = response.json()
features = data.get("features", [])
if not features:
break
for f in features:
f["properties"]["_city"] = city_name
all_results.extend(features)
skip += len(features)
time.sleep(1)
print(f" {city_name}: {skip} организаций")
print(f"Всего: {len(all_results)} организаций")
Добавление _city в свойства каждой записи позволяет потом фильтровать результаты по городам в CSV или базе данных.
Неочевидные детали
Первый факт: координаты в API Яндекс Карт записываются в порядке "долгота, широта" (ll=37.6,55.7), а не "широта, долгота", как принято в географии. Перепутаете — получите результаты из океана.
Второй факт: параметр results=500 это максимум для одного запроса в Search API, но общее количество доступных результатов по запросу может быть ограничено 1000–1500. Для плотных категорий (кафе в Москве) придётся разбивать город на районы и парсить каждый отдельно.
Третий факт: Selenium в headless-режиме (без отображения окна) детектируется Яндексом чаще, чем обычный режим. Если парсер получает страницу "Доступ к нашему сервису временно запрещён", попробуйте отключить headless.
Четвёртый факт: Search API возвращает не все поля для каждой организации. Если владелец не указал телефон или сайт, соответствующего поля в JSON не будет. Всегда используйте.get() с значением по умолчанию вместо прямого обращения по ключу.
Пятый факт: CSS-классы на Яндекс Картах обфусцированы и меняются при каждом обновлении фронтенда. Парсер на Selenium, написанный сегодня, может сломаться через неделю. API не имеет этой проблемы.
Юридические аспекты
Парсинг Яндекс Карт находится в правовой серой зоне. Основные риски :
- Условия использования: Terms of Service Яндекс Карт запрещают автоматизированный сбор данных без разрешения.
- Авторское право: Яндекс Карты как база данных защищены ст. 1260 и 1334 ГК РФ. Систематическое извлечение существенной части данных может считаться нарушением.
- Персональные данные: Сбор телефонов и имён владельцев подпадает под ФЗ №152 "О персональных данных".
- Безопасный путь: Использование официального API в рамках лимитов считается легальным, потому что API предоставлен для этих целей.
FAQ
Что лучше: API или Selenium?
Для сбора основных данных (название, адрес, телефон, рейтинг) используйте Search API. Для отзывов и данных, недоступных через API, используйте Selenium.
Сколько стоит Search API?
Яндекс предоставляет бесплатный лимит запросов. После превышения оплата зависит от тарифа. Актуальные цены смотрите в Кабинете разработчика.
Как парсить отзывы?
Search API не возвращает текст отзывов. Для отзывов нужен Selenium: откройте карточку организации, перейдите на вкладку "Отзывы" и извлеките текст каждого элемента.
Можно ли собрать все организации России?
Теоретически да, но потребуется разбить всю территорию на мелкие квадраты и парсить каждый отдельно. Это тысячи запросов и несколько дней работы. Через API это реалистичнее и безопаснее, чем через Selenium.
Чем Яндекс Карты отличаются от Google Maps для парсинга?
Яндекс Карты доминируют в России и СНГ: более детальный справочник организаций, лучше покрытие городов. Google Maps сильнее глобально. Для российского рынка парсите Яндекс, для международного — Google.
Мой совет: начните с Search API. Он стабилен, документирован, не ломается при обновлениях фронтенда и не грозит блокировкой. Переходите на Selenium только для задач, которые API не покрывает: отзывы, фотографии, специфические фильтры. Для крупных проектов (десятки тысяч организаций) заложите бюджет на API-тариф — это дешевле, чем отлаживать парсер после каждого обновления Яндекса.
