💻
Разработка в IT
Опубликовано:
16.04.2026
Обновлено:
16.04.2026

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

Иван Смирнов

В прошлом году мне нужно было собрать список всех автосервисов в Петербурге для анализа конкурентов: название, адрес, телефон, рейтинг, часы работы. Вручную это 2000+ карточек. Через API Яндекс Карт я получил структурированные данные за вечер — 50 запросов покрыли весь город. Яндекс Карты это крупнейший справочник организаций в России: более 100 миллионов точек интереса с контактами, рейтингами и отзывами.

Существуют три способа извлечь данные из Яндекс Карт: официальный API (Search API), прямой парсинг через Selenium и парсинг через HTTP-запросы. В этой статье разбираю каждый способ с кодом, объясняю ограничения и показываю, как сохранить результат в CSV.

Какие данные можно собрать

Карточка организации на Яндекс Картах содержит набор полей, полезных для маркетинга, аналитики и лидогенерации.

  • Название организации и категория (рубрика)
  • Адрес, координаты (широта/долгота)
  • Телефон, email, сайт
  • Рейтинг и количество отзывов
  • Часы работы
  • Фотографии
  • Текст отзывов (доступен при парсинге, ограничен в API)

Способ 1: Search API (официальный)

Search API (API Поиска по организациям) это официальный HTTP-интерфейс Яндекс Карт для поиска компаний по запросу и местоположению. Он возвращает структурированный JSON с данными организаций.

Получение API-ключа

  1. Зайдите в Кабинет разработчика: developer.tech.yandex.ru.
  2. Нажмите "Подключить API".
  3. Выберите "API Поиска по организациям" (Search API).
  4. Заполните форму и получите ключ.

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 и параметров. Структура ответа не документирована и может измениться в любой момент.

Сравнение трёх способов

Характеристика Search API Selenium HTTP-запросы (requests)
Стабильность Высокая (документирован) Средняя (классы меняются) Низкая (эндпоинты не документированы)
Скорость Быстро (JSON) Медленно (рендеринг страницы) Быстро
Данные Основные поля организации Всё, что видит пользователь Зависит от эндпоинта
Отзывы Нет Да Возможно
Стоимость Платный (после лимита) Бесплатно (но прокси стоят денег) Бесплатно
Блокировки Нет (в рамках лимита) Высокий риск Высокий риск
Законность Легально (по API) Серая зона Серая зона

Обход блокировок

Яндекс активно блокирует автоматизированные запросы. При парсинге через 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-тариф — это дешевле, чем отлаживать парсер после каждого обновления Яндекса.

Читайте также