Введение: что вы получите и кому пригодится этот гайд

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

В этом руководстве мы вместе напишем собственный парсер Яндекс Карт на Python. Никаких платных программ, никаких чужих серверов. К концу гайда у вас будет:

  • Рабочий скрипт, который собирает карточки организаций по любому запросу и городу.
  • Таблица Excel с названиями, категориями, адресами, телефонами, сайтами, рейтингом и режимом работы.
  • Отдельная таблица с отзывами: автор, оценка, дата, текст.
  • Понимание, как работать аккуратно и не получать капчу и блокировки, распределяя нагрузку через мобильные прокси.

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

Для кого этот гайд

  • Маркетологи и владельцы бизнеса, которым нужна база конкурентов или партнеров в регионе с реальными телефонами и рейтингом.
  • Арбитражники, собирающие офлайн-ниши и локальные офферы для последующего анализа.
  • Разработчики, которым поручили сделать сбор данных с Карт, а начинать с нуля не хочется.
  • Аналитики, изучающие отзывы для оценки качества сервиса в нише.

Что нужно знать заранее

Ничего сложного. Достаточно уметь устанавливать программы, открывать командную строку и копировать текст. Опыт программирования не обязателен: весь код готов, вам нужно лишь подставить свои значения. Если вы хоть раз запускали скрипт на Python, вам будет совсем легко. В блоке для продвинутых мы затронем асинхронность и работу с сетевыми ответами, но его можно пропустить.

Сколько времени потребуется

  • Подготовка окружения: 30-40 минут.
  • Написание и отладка парсера по шагам: 1,5-2 часа.
  • Первый полноценный сбор на 300-500 карточек: 1-3 часа фонового времени, пока вы занимаетесь другими делами.

Итого за половину рабочего дня вы получаете инструмент, который дальше будет экономить вам недели ручного труда.

Предварительная подготовка: инструменты, доступы и требования

Прежде чем писать код, соберем все необходимое. Пропустите этот раздел только если у вас уже установлен Python и есть доступ к мобильному прокси.

Системные требования

  • Windows 10 или 11, macOS 12 и новее либо любой современный Linux.
  • Минимум 8 ГБ оперативной памяти: мы будем запускать реальный браузер Chromium.
  • 3-4 ГБ свободного места на диске под Python, браузер и результаты.
  • Стабильный интернет. Скорость не критична, важнее отсутствие обрывов.

Что нужно установить

  1. Python 3.11 или 3.12. Скачайте установщик с официального сайта python.org. При установке на Windows обязательно поставьте галочку Add Python to PATH внизу первого окна установщика, иначе команды в консоли не будут работать.
  2. Редактор кода. Подойдет Visual Studio Code, PyCharm Community или даже Notepad++. Мы будем ориентироваться на VS Code, но разницы для наших задач нет.
  3. Библиотеки Python: playwright для управления браузером, pandas и openpyxl для таблиц, requests для проверки прокси. Их установим на первом шаге.
  4. Браузер Chromium для Playwright. Скачивается отдельной командой, весит около 150 МБ.

Доступ к мобильному прокси

Это ключевой элемент раздела «без банов». Яндекс Карты чувствительны к частоте запросов с одного адреса. Мобильные прокси дают IP-адреса реальных сотовых операторов, за которыми одновременно сидят тысячи обычных пользователей, поэтому Яндекс относится к ним максимально лояльно. В личном кабинете mobileproxy.space после покупки прокси вам понадобятся четыре значения:

  • Хост (адрес сервера) и порт для HTTP-подключения.
  • Логин и пароль для авторизации.
  • Ссылка смены IP: специальный URL, при обращении к которому прокси получает новый адрес у оператора. Скопируйте ее в отдельный файл, она пригодится на пятом шаге.

Совет: Выбирайте прокси того же региона или хотя бы той же страны, что и город, который парсите. Карты подстраивают выдачу под геолокацию, и прокси из другой страны может показывать неполный список организаций или интерфейс на другом языке.

Резервные копии и рабочая папка

Создайте на диске папку, например maps_parser. Внутри нее будут лежать скрипты и результаты. Все промежуточные данные мы будем сохранять в файлы после каждой карточки, поэтому даже если скрипт упадет на трехсотой организации, первые двести девяносто девять не пропадут. Копию файла с результатами имеет смысл делать перед каждым повторным запуском: просто переименовывайте старый файл, добавляя дату.

Проверка: Откройте командную строку (в Windows нажмите Win+R, введите cmd и Enter) и наберите команду python --version. Если видите строку вида Python 3.12.x, подготовка завершена. Если вместо этого ошибка, переустановите Python с галочкой Add to PATH.

Базовые понятия: как устроены Карты и что такое парсинг карточек

Перед тем как писать код, разберемся с терминами. Они простые, но без них дальше будет непонятно, почему мы делаем именно так.

Ключевые термины простым языком

  • Парсинг (скрапинг) — автоматический сбор информации со страниц сайта. Программа открывает страницу, как это сделал бы человек, и вытаскивает из нее нужные фрагменты.
  • Карточка организации — отдельная страница в Картах с адресом вида yandex.ru/maps/org/название/числовой-идентификатор/. Именно на ней лежат телефон, адрес, отзывы и остальные данные. Список организаций слева — лишь витрина, у которой мы берем только ссылки на карточки.
  • Селектор — «адрес» элемента внутри страницы. Например, класс business-contacts-view__address указывает на блок с адресом. По селекторам скрипт находит нужный текст.
  • Headless-браузер — настоящий браузер, которым управляет программа. Может работать с окном (вы видите, что происходит) или без него.
  • Playwright — библиотека для управления браузером из Python. Мы выбрали ее, потому что Карты полностью строятся на JavaScript, и простой запрос за HTML вернет пустую страницу без данных.
  • Мобильный прокси — посредник между вашим компьютером и сайтом с IP-адресом сотовой сети. Сайт видит не ваш адрес, а адрес оператора.
  • Ротация IP — периодическая смена адреса прокси, чтобы нагрузка не концентрировалась на одном IP.
  • Капча — страница проверки «Подтвердите, что вы не робот». Для нас это сигнал, что мы слишком спешим. Мы не будем ее решать сторонними сервисами, а просто остановимся, сменим IP и снизим темп.

Основные принципы работы парсера Яндекс Карт

Логика проста и состоит из трех фаз. Сначала мы получаем список ссылок на карточки нужных организаций. Затем открываем каждую карточку по очереди и извлекаем данные. В конце складываем все в таблицу. Между этими действиями мы делаем паузы, случайные по длительности, и время от времени меняем IP через ссылку смены адреса.

Что важно понимать о законности и этике

Внимание: Собирайте только общедоступные данные организаций и используйте их для аналитики. Телефон и адрес компании не являются персональными данными, но имена авторов отзывов могут ими считаться по 152-ФЗ. Не храните их без необходимости и не используйте собранные телефоны для массовых рассылок без согласия, это нарушает закон о рекламе и правила Яндекса. Также помните: пользовательское соглашение Яндекса ограничивает автоматизированный сбор, поэтому держите нагрузку минимальной, а для коммерческих проектов с большими объемами рассматривайте официальный API Яндекса для поиска по организациям.

Шаг 1: Настраиваем окружение и подключаем мобильный прокси

Цель этапа: установить все библиотеки, скачать браузер и убедиться, что запросы идут через мобильный прокси, а не напрямую.

Установка библиотек

  1. Откройте командную строку или терминал.
  2. Перейдите в рабочую папку командой cd и путем к папке. Например: cd C:\maps_parser в Windows или cd ~/maps_parser в macOS и Linux.
  3. Создайте виртуальное окружение, чтобы библиотеки не мешали другим проектам: python -m venv venv
  4. Активируйте его. Windows: venv\Scripts\activate. macOS и Linux: source venv/bin/activate. В начале строки терминала появится надпись (venv).
  5. Установите библиотеки одной командой:
pip install playwright pandas openpyxl requests
playwright install chromium

Вторая команда скачает Chromium. Это занимает 2-5 минут в зависимости от скорости интернета. Дождитесь, пока в терминале снова появится приглашение ввода.

Проверка прокси

Создайте в редакторе файл check_proxy.py и вставьте код, заменив ЛОГИН, ПАРОЛЬ, ХОСТ и ПОРТ на значения из личного кабинета:

import requests

proxy = 'http://ЛОГИН:ПАРОЛЬ@ХОСТ:ПОРТ'
proxies = {'http': proxy, 'https': proxy}

direct = requests.get('https://api.ipify.org?format=json', timeout=30).json()
via_proxy = requests.get('https://api.ipify.org?format=json', proxies=proxies, timeout=30).json()
print('Ваш прямой IP:', direct['ip'])
print('IP через прокси:', via_proxy['ip'])

Запустите файл командой python check_proxy.py. Вы должны увидеть два разных адреса. Если адреса совпадают или появилась ошибка соединения, прокси не работает, и дальше идти бессмысленно.

Проверка смены IP

Откройте ссылку смены IP из личного кабинета в обычном браузере. Обычно она возвращает короткий ответ об успешной смене. Подождите 15-30 секунд и снова запустите check_proxy.py. Адрес через прокси должен измениться. Запомните, сколько секунд реально требуется на смену: это значение мы подставим в скрипт на пятом шаге.

Совет: Сохраните все настройки прокси в отдельный файл config.py с переменными PROXY_HOST, PROXY_PORT, PROXY_LOGIN, PROXY_PASSWORD и CHANGE_IP_URL. Тогда в остальных скриптах достаточно написать from config import * и не переписывать пароли по десять раз.

Проверка: Команда playwright --version выводит номер версии, check_proxy.py показывает IP сотового оператора, отличающийся от вашего домашнего, а после обращения к ссылке смены адрес меняется.

Возможные проблемы

  • Ошибка «pip не является внутренней командой». Python установлен без добавления в PATH. Переустановите с галочкой или используйте команду py -m pip вместо pip.
  • Ошибка 407 Proxy Authentication Required. Неверный логин или пароль. Проверьте, нет ли лишних пробелов при копировании.
  • Timeout при запросе через прокси. Порт указан неверно или прокси в этот момент меняет IP. Подождите полминуты и повторите.

Шаг 2: Собираем ссылки на карточки организаций

Цель этапа: получить файл links.json со списком адресов карточек всех организаций по нужному запросу в нужном городе.

Здесь мы лишь однажды заглянем в список результатов Карт, чтобы забрать из него ссылки. Сами данные мы будем брать исключительно из карточек, поэтому список нам нужен как оглавление, не более.

Как устроен список организаций

Когда вы вводите в Картах запрос вроде «стоматология Казань», слева появляется прокручиваемая панель со сниппетами. Каждый сниппет содержит ссылку на карточку. Список подгружается порциями по мере прокрутки, поэтому скрипт будет крутить панель колесом мыши и собирать ссылки после каждой прокрутки, пока новые перестанут появляться.

Пишем скрипт сбора ссылок

Создайте файл collect_links.py:

from playwright.sync_api import sync_playwright
import time, random, json
from config import *

PROXY = {'server': f'http://{PROXY_HOST}:{PROXY_PORT}', 'username': PROXY_LOGIN, 'password': PROXY_PASSWORD}
QUERY = 'стоматология Казань'
MAX_SCROLLS = 40

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False, proxy=PROXY)
    context = browser.new_context(locale='ru-RU', viewport={'width': 1400, 'height': 900})
    page = context.new_page()
    page.goto('https://yandex.ru/maps/?text=' + QUERY, wait_until='domcontentloaded')
    time.sleep(random.uniform(5, 8))
    page.mouse.move(350, 500)
    links = set()
    stale = 0
    for i in range(MAX_SCROLLS):
        before = len(links)
        page.mouse.wheel(0, random.randint(1200, 2000))
        time.sleep(random.uniform(1.5, 3.5))
        for a in page.query_selector_all('a[href*="/maps/org/"]'):
            href = a.get_attribute('href') or ''
            clean = href.split('?')[0]
            if clean.startswith('/'):
                clean = 'https://yandex.ru' + clean
            links.add(clean)
        stale = stale + 1 if len(links) == before else 0
        print(f'Прокрутка {i+1}: ссылок {len(links)}')
        if stale >= 4:
            break
    with open('links.json', 'w', encoding='utf-8') as f:
        json.dump(sorted(links), f, ensure_ascii=False, indent=2)
    print('Итого собрано:', len(links))
    browser.close()

Разбираем, что происходит

  1. Строка headless=False открывает браузер с окном. На этапе отладки это обязательно: вы своими глазами увидите, загрузилась ли карта и нет ли капчи.
  2. page.mouse.move(350, 500) ставит курсор на левую панель. Без этого колесо мыши будет двигать саму карту, а не список.
  3. Селектор a[href*="/maps/org/"] ищет все ссылки, содержащие фрагмент /maps/org/. Он устойчивее конкретных классов, которые Яндекс меняет каждые несколько месяцев.
  4. Счетчик stale останавливает цикл, если четыре прокрутки подряд не принесли новых ссылок. Это значит, что список закончился.
  5. Ссылки очищаются от параметров после знака вопроса, чтобы одна организация не попала в файл дважды.

Запустите скрипт: python collect_links.py. Откроется окно браузера, загрузится карта с результатами, панель начнет прокручиваться. В терминале побежит счетчик ссылок. Для среднего города по одному запросу обычно собирается от 100 до 400 карточек за 2-4 минуты.

Совет: Карты редко отдают больше 500 результатов по одному запросу. Если вам нужна вся ниша в мегаполисе, разбивайте запрос по районам: «стоматология Вахитовский район Казань», «стоматология Советский район Казань». Ссылки из разных запросов объединяйте через set, как в коде выше, дубли исчезнут сами.

Проверка: В папке появился файл links.json, внутри которого список адресов вида https://yandex.ru/maps/org/название/1234567890/. Откройте два-три адреса вручную в обычном браузере и убедитесь, что это карточки нужных организаций.

Возможные проблемы

  • Собрано ноль ссылок. Скорее всего, страница не успела загрузиться или курсор был не над списком. Увеличьте первую паузу до 10 секунд и проверьте координаты мыши: панель должна быть под курсором.
  • Список не прокручивается, двигается карта. Уменьшите или увеличьте координату X в mouse.move, ориентируясь на ширину панели в вашем окне.
  • Сразу открылась капча. Смените IP через ссылку, подождите минуту и запустите снова. Если повторяется, попробуйте другой прокси-канал.

Шаг 3: Парсим карточку организации — название, адрес, телефон, сайт

Цель этапа: написать функцию, которая открывает одну карточку и возвращает словарь с основными данными организации, и прогнать ее по всем ссылкам из links.json.

Как найти селекторы самому

Яндекс периодически переименовывает классы в верстке. Поэтому важно уметь находить их самостоятельно, а не полагаться только на готовый код. Делается это так:

  1. Откройте любую карточку организации в обычном браузере Chrome.
  2. Нажмите правой кнопкой на телефон организации и выберите Просмотреть код (или нажмите F12 и щелкните по элементу инструментом выбора).
  3. В открывшейся панели вы увидите подсвеченный тег с атрибутом class. Например, class="card-phones-view__phone-number". Это и есть селектор: в коде он пишется с точкой в начале.
  4. Повторите для названия, адреса, сайта, рейтинга и часов работы. Запишите классы в блокнот.

Класс может состоять из нескольких слов через пробел. Берите первое, самое «говорящее», с двойным подчеркиванием внутри. На момент написания гайда актуальны селекторы из кода ниже, но проверьте их перед запуском.

Пишем функцию парсинга карточки

Создайте файл parse_cards.py:

from playwright.sync_api import sync_playwright
import time, random, json, csv, os
from config import *

PROXY = {'server': f'http://{PROXY_HOST}:{PROXY_PORT}', 'username': PROXY_LOGIN, 'password': PROXY_PASSWORD}
OUT = 'orgs.csv'
FIELDS = ['url', 'name', 'category', 'address', 'phones', 'site', 'rating', 'reviews_count', 'hours']

def grab(page, selector):
    el = page.query_selector(selector)
    return el.inner_text().strip() if el else ''

def parse_card(page, url):
    page.goto(url, wait_until='domcontentloaded')
    time.sleep(random.uniform(3, 6))
    more = page.query_selector('.card-phones-view__more')
    if more:
        more.click()
        time.sleep(random.uniform(1, 2))
    phones = [e.inner_text().strip() for e in page.query_selector_all('.card-phones-view__phone-number')]
    return {
        'url': url,
        'name': grab(page, 'h1.orgpage-header-view__header'),
        'category': grab(page, '.orgpage-categories-info-view'),
        'address': grab(page, '.business-contacts-view__address'),
        'phones': '; '.join(dict.fromkeys(phones)),
        'site': grab(page, '.business-urls-view__text'),
        'rating': grab(page, '.business-rating-badge-view__rating-text'),
        'reviews_count': grab(page, '.business-header-rating-view__text'),
        'hours': grab(page, '.business-working-status-view'),
    }

def load_done():
    if not os.path.exists(OUT):
        return set()
    with open(OUT, encoding='utf-8') as f:
        return {row['url'] for row in csv.DictReader(f)}

links = json.load(open('links.json', encoding='utf-8'))
done = load_done()
todo = [u for u in links if u not in done]
print(f'Всего {len(links)}, осталось {len(todo)}')

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False, proxy=PROXY)
    page = browser.new_context(locale='ru-RU').new_page()
    new_file = not os.path.exists(OUT)
    with open(OUT, 'a', newline='', encoding='utf-8') as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        if new_file:
            writer.writeheader()
        for n, url in enumerate(todo, 1):
            try:
                row = parse_card(page, url)
                writer.writerow(row)
                f.flush()
                print(f'{n}/{len(todo)} {row["name"]} | {row["phones"]}')
            except Exception as e:
                print('Ошибка на', url, e)
            time.sleep(random.uniform(4, 9))
    browser.close()

Что здесь важно

  1. Кнопка «Показать телефон». В части карточек полный номер скрыт за кнопкой. Мы ищем ее по классу card-phones-view__more и кликаем, если нашли. Только после этого собираем номера.
  2. Запись после каждой карточки. Функция f.flush() принудительно сбрасывает данные на диск. Если скрипт упадет, все собранное останется в orgs.csv.
  3. Возобновление работы. Функция load_done читает уже собранные ссылки, и при повторном запуске скрипт продолжает с места остановки, а не начинает сначала.
  4. Паузы 4-9 секунд между карточками. Это темп внимательного человека, который читает информацию. Не уменьшайте его на первых запусках.

Запустите python parse_cards.py и понаблюдайте первые пять-десять карточек в окне браузера. Вы должны видеть, как страница открывается, при необходимости раскрывается телефон, а в терминале появляется название и номер.

Внимание: Если пять карточек подряд вернули пустые названия, немедленно остановите скрипт сочетанием Ctrl+C. Почти наверняка Яндекс изменил верстку, и селекторы устарели. Найдите новые по инструкции выше и обновите код. Продолжать сбор пустых строк бессмысленно и только увеличивает нагрузку на прокси.

Совет: Помимо текста, полезно сохранить сам идентификатор организации: это последняя числовая часть URL. По нему легко сверять базы между сборами и отслеживать закрывшиеся компании. Добавьте поле 'org_id': url.rstrip('/').split('/')[-1] в словарь.

Проверка: Файл orgs.csv открывается в Excel, в нем заполнены столбцы name, address, phones не менее чем у 90 процентов строк. Телефоны отображаются в формате +7 (843) 000-00-00. Рейтинг выглядит как число с запятой, например 4,7.

Возможные проблемы

  • Телефоны пустые, хотя на сайте они есть. Кнопка «Показать телефон» имеет другой класс. Найдите его через F12 и замените в коде.
  • Кириллица в CSV отображается кракозябрами. Excel не распознал кодировку. Откройте файл через меню Данные, Из текста/CSV и выберите UTF-8, либо дождитесь шага 6, где мы конвертируем данные в xlsx.
  • Скрипт зависает на одной карточке. Добавьте параметр timeout=45000 в page.goto, чтобы через 45 секунд возникало исключение и цикл шел дальше.

Шаг 4: Собираем отзывы из карточек

Цель этапа: для каждой организации получить список отзывов с оценкой, датой и текстом и сохранить их в отдельный файл reviews.csv.

Где живут отзывы

У каждой карточки есть вкладка «Отзывы» с адресом вида https://yandex.ru/maps/org/название/идентификатор/reviews/. Отзывы там подгружаются порциями при прокрутке, как и список организаций на втором шаге. По умолчанию они отсортированы по актуальности, а свежие можно получить, переключив сортировку на «По новизне».

Пишем функцию сбора отзывов

Создайте файл parse_reviews.py. Основа та же, что в предыдущем шаге, поэтому приведем только функцию и цикл:

def parse_reviews(page, url, max_scrolls=15):
    page.goto(url.rstrip('/') + '/reviews/', wait_until='domcontentloaded')
    time.sleep(random.uniform(3, 6))
    page.mouse.move(350, 600)
    seen = 0
    for _ in range(max_scrolls):
        page.mouse.wheel(0, random.randint(1500, 2500))
        time.sleep(random.uniform(1.5, 3))
        cards = page.query_selector_all('.business-review-view')
        if len(cards) == seen:
            break
        seen = len(cards)
    result = []
    for c in page.query_selector_all('.business-review-view'):
        def sub(sel):
            el = c.query_selector(sel)
            return el.inner_text().strip() if el else ''
        stars = c.query_selector_all('.business-rating-badge-view__star._full')
        result.append({
            'org_url': url,
            'author': sub('.business-review-view__author-name'),
            'date': sub('.business-review-view__date'),
            'stars': len(stars),
            'text': sub('.business-review-view__body-text'),
        })
    return result

В цикле по ссылкам вызывайте parse_reviews вместо parse_card и записывайте каждый элемент списка отдельной строкой в reviews.csv с полями org_url, author, date, stars, text. Логика возобновления и запись после каждой организации остаются прежними.

Разбираем детали

  1. Ограничение max_scrolls=15. У популярных заведений бывает две-три тысячи отзывов. Собирать их все редко нужно, а времени и запросов это съедает много. Пятнадцати прокруток обычно хватает на 100-150 последних отзывов.
  2. Оценка через звезды. Число баллов в отзыве не написано текстом, а нарисовано звездами. Мы считаем элементы с модификатором _full, то есть закрашенные звезды.
  3. Длинные отзывы. Часть текстов свернута и требует нажатия «Ещё». Если нужен полный текст, перед сбором кликните все кнопки с классом business-review-view__expand внутри карточки.

Внимание: Имена авторов отзывов — это данные пользователей, а не организаций. Если ваша задача — анализ тональности или поиск типичных жалоб, поле author вам не нужно. Не собирайте его без цели, так вы снимаете с себя лишние вопросы по 152-ФЗ. Если поле все же нужно, храните файл локально и не передавайте третьим лицам.

Совет: Запускайте сбор отзывов не по всем организациям, а по отфильтрованному списку: например, только с рейтингом ниже 4,0 или только у прямых конкурентов. Так вы сократите объем запросов в разы, а ценность данных не потеряете.

Проверка: В reviews.csv у каждой организации от 20 до 150 строк, столбец stars содержит числа от 1 до 5, в text есть осмысленный русский текст. Дата выглядит как «15 января» или «3 марта 2026».

Возможные проблемы

  • Нашлось ноль отзывов. Проверьте, что URL заканчивается на /reviews/, а панель отзывов находится под курсором при прокрутке.
  • У всех отзывов stars равно 0. Класс закрашенной звезды изменился. Найдите его через F12, кликнув по звезде.
  • Дублируются отзывы. Панель прокрутилась не полностью и один блок посчитан дважды. Уберите дубликаты на шестом шаге по паре author плюс text.

Шаг 5: Настраиваем ротацию IP и защиту от блокировок

Цель этапа: научить парсер вести себя как аккуратный пользователь: менять IP по расписанию, распознавать капчу и автоматически снижать темп вместо того, чтобы упираться в блокировку.

Почему вообще возникают баны

Яндекс не запрещает смотреть карточки, но следит за аномалиями: сотни страниц в минуту с одного адреса, одинаковые интервалы между запросами, отсутствие движений мыши, пустые куки. Когда подозрение накапливается, появляется страница SmartCaptcha, а при упорстве — временное ограничение для IP. Наша стратегия не в том, чтобы «пробивать» защиту, а в том, чтобы не давать поводов ее включать.

Три правила спокойного парсера Яндекс Карт

  1. Темп человека. Не более 8-12 карточек в минуту на один IP. Случайные паузы, а не фиксированные.
  2. Регулярная смена IP. Каждые 25-40 карточек или каждые 10-15 минут обращаемся к ссылке смены адреса. Мобильный прокси за секунды получа��т новый IP оператора, и история запросов «обнуляется» с точки зрения сайта.
  3. Немедленный откат при капче. Увидели проверку — не пытаемся ее пройти, а ставим паузу на 2-3 минуты, меняем IP и продолжаем с той же карточки в новом контексте браузера.

Добавляем ротацию в код

Вставьте в parse_cards.py и parse_reviews.py следующие функции и вызывайте их в основном цикле:

import requests

def change_ip():
    try:
        r = requests.get(CHANGE_IP_URL, timeout=30)
        print('Смена IP:', r.status_code)
    except Exception as e:
        print('Не удалось сменить IP:', e)
    time.sleep(IP_CHANGE_WAIT)

def is_captcha(page):
    if 'showcaptcha' in page.url or 'checkcaptcha' in page.url:
        return True
    return page.query_selector('.CheckboxCaptcha') is not None

def new_page(browser):
    ctx = browser.new_context(locale='ru-RU', viewport={'width': random.choice([1366, 1440, 1536]), 'height': 900})
    return ctx.new_page()

А главный цикл приобретает такой вид:

page = new_page(browser)
since_change = 0
for n, url in enumerate(todo, 1):
    if since_change >= random.randint(25, 40):
        page.context.close()
        change_ip()
        page = new_page(browser)
        since_change = 0
    row = parse_card(page, url)
    if is_captcha(page):
        print('Капча! Пауза и смена IP')
        page.context.close()
        time.sleep(random.uniform(120, 180))
        change_ip()
        page = new_page(browser)
        row = parse_card(page, url)
    writer.writerow(row)
    f.flush()
    since_change += 1
    time.sleep(random.uniform(4, 9))

Что важно понимать

  • Новый контекст вместе с новым IP. Закрывая контекст, мы сбрасываем куки и локальное хранилище. Смена адреса без сброса куки бессмысленна: сайт продолжит узнавать вас по сессии.
  • Переменная IP_CHANGE_WAIT в config.py — это время, замеренное на первом шаге, обычно 15-30 секунд. Меньше ставить нельзя: браузер откроет страницу через старый адрес.
  • Случайный размер окна добавляет разнообразия отпечатку браузера. Это мягкая мера, но она бесплатна.
  • Мобильные прокси с ротацией по ссылке здесь удобнее любых других: вы не переключаетесь между десятками адресов вручную, а просто дергаете один URL.

Совет: Заведите простой лог: записывайте в файл время, номер карточки и событие (успех, капча, смена IP). Через неделю по логу вы точно увидите, при каком темпе капча не появляется вообще, и подстроите паузы под свой прокси-канал.

Проверка: За час непрерывной работы скрипт собрал 400-600 карточек, в терминале не более одного-двух сообщений о капче, после каждого из них сбор продолжился автоматически. IP через прокси менялся минимум десять раз (это видно по строкам «Смена IP: 200»).

Возможные проблемы

  • Капча появляется каждые 10 карточек. Темп слишком высокий для вашего канала. Увеличьте паузы до 8-15 секунд и меняйте IP каждые 15 карточек.
  • После смены IP страницы не грузятся. Увеличьте IP_CHANGE_WAIT: оператор еще не выдал новый адрес.
  • Ссылка смены IP возвращает ошибку о частых обращениях. У большинства тарифов есть минимальный интервал между сменами, обычно от одной до двух минут. Не меняйте IP чаще.

Шаг 6: Очищаем данные и сохраняем в Excel

Цель этапа: превратить сырые CSV в аккуратные таблицы Excel без дубликатов, с нормализованными телефонами и числовым рейтингом.

Пишем скрипт очистки

Создайте файл clean_export.py:

import pandas as pd

def norm_phone(value):
    out = []
    for raw in str(value).split(';'):
        digits = ''.join(ch for ch in raw if ch.isdigit())
        if len(digits) == 11 and digits[0] in '78':
            out.append('+7' + digits[1:])
        elif len(digits) == 10:
            out.append('+7' + digits)
    return '; '.join(dict.fromkeys(out))

orgs = pd.read_csv('orgs.csv', encoding='utf-8')
orgs = orgs.drop_duplicates(subset='url')
orgs['phones'] = orgs['phones'].fillna('').apply(norm_phone)
orgs['rating'] = pd.to_numeric(orgs['rating'].astype(str).str.replace(',', '.'), errors='coerce')
orgs['reviews_count'] = pd.to_numeric(orgs['reviews_count'].astype(str).str.extract('(\d+)')[0], errors='coerce')
orgs = orgs.sort_values('rating', ascending=False)

reviews = pd.read_csv('reviews.csv', encoding='utf-8')
reviews = reviews.drop_duplicates(subset=['org_url', 'author', 'text'])

with pd.ExcelWriter('yandex_maps_result.xlsx', engine='openpyxl') as w:
    orgs.to_excel(w, sheet_name='Организации', index=False)
    reviews.to_excel(w, sheet_name='Отзывы', index=False)
print('Организаций:', len(orgs), 'Отзывов:', len(reviews))

В строке с извлечением числа отзывов используется регулярное выражение из одной обратной косой черты и буквы d в скобках: оно вытаскивает первое число из текста вроде «312 отзывов». Скопируйте его внимательно.

Что делает скрипт

  1. Убирает повторы организаций по URL.
  2. Приводит все телефоны к формату +7XXXXXXXXXX, убирая скобки, пробелы и дефисы. Такой формат удобен для CRM и сверки баз.
  3. Меняет запятую на точку в рейтинге, чтобы Excel воспринимал его как число и позволял сортировать.
  4. Вытаскивает количество отзывов из текстовой строки.
  5. Удаляет дубликаты отзывов и записывает два листа в один файл xlsx.

Запустите python clean_export.py и откройте yandex_maps_result.xlsx. На первом листе организации отсортированы по рейтингу, телефоны единообразны, на втором листе — отзывы с привязкой к организации по столбцу org_url.

Совет: Добавьте в скрипт столбец «дата сбора» с текущей датой. Через месяц повторите сбор и сравните таблицы функцией merge в pandas: вы увидите новые организации, закрывшиеся точки и изменения рейтинга конкурентов. Это уже полноценный мониторинг рынка.

Проверка: Файл xlsx открывается без предупреждений, кириллица читается, столбец rating сортируется как число, в столбце phones нет скобок и пробелов, число строк на листе «Организации» совпадает с количеством уникальных ссылок в links.json минус ошибки.

Проверка результата: чек-лист готового парсера

Пройдитесь по списку. Если на каждый пункт вы ответили «да», парсер Яндекс Карт готов к регулярной работе.

Чек-лист

  • check_proxy.py показывает IP сотового оператора, отличный от домашнего.
  • collect_links.py собирает более 50 ссылок по среднему запросу и останавливается сам.
  • parse_cards.py заполняет название, адрес и телефон не менее чем у 90 процентов карточек.
  • Кнопка «Показать телефон» раскрывается автоматически.
  • parse_reviews.py возвращает отзывы с оценкой от 1 до 5.
  • При капче скрипт делает паузу, меняет IP и продолжает без вашего участия.
  • После аварийной остановки повторный запуск продолжает с места обрыва.
  • clean_export.py создает xlsx с двумя листами и нормализованными телефонами.

Как протестировать целиком

  1. Возьмите небольшой запрос, по которому в городе 30-60 организаций, например «шиномонтаж» в райцентре.
  2. Прогоните все скрипты по очереди и засеките время. На 50 карточек с отзывами должно уйти 15-25 минут.
  3. Выберите пять случайных организаций из таблицы и сверьте телефоны и адреса с карточками вручную.
  4. Остановите parse_cards.py на середине сочетанием Ctrl+C и запустите снова. Убедитесь, что счетчик «осталось» уменьшился, а не сбросился.

Показатели успешного выполнения

  • Точность данных при ручной сверке: 100 процентов совпадений по телефонам и адресам.
  • Доля пустых названий: менее 3 процентов.
  • Частота капчи: не более одной на 200-300 карточек.
  • Скорость: 400-600 карточек в час на один прокси-канал при безопасном темпе.

Типичные ошибки и их решения

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

Пустые поля у большинства карточек

Причина: Яндекс обновил верстку, классы элементов изменились. Решение: откройте карточку в Chrome, нажмите F12, найдите новые классы и замените их в функции parse_card. Держите селекторы в одном словаре в начале файла, чтобы править одно место.

Капча появляется с первой же страницы

Причина: IP прокси уже «уставший» от предыдущей активности либо браузер запускается с подозрительными параметрами. Решение: смените IP перед стартом, убедитесь, что locale='ru-RU' задан, и не используйте режим headless на первых запусках: он дает больше сигналов автоматизации.

Прокрутка списка двигает карту, а не панель

Причина: курсор мыши находится вне левой панели. Решение: подберите координаты page.mouse.move под ваш размер окна. При ширине 1400 пикселей панель занимает примерно первые 450 пикселей по горизонтали.

Собираются одни и те же 20 организаций

Причина: панель не прокручивается до конца, счетчик stale срабатывает раньше времени. Решение: увеличьте паузу после прокрутки до 3-4 секунд и порог stale до 6, Карты иногда подгружают следующую порцию медленно.

Телефоны видны в браузере, но в таблице пусто

Причина: номер появляется только после клика, а скрипт собирает данные до его завершения, либо кнопка имеет другой класс. Решение: увеличьте паузу после клика до 2-3 секунд и проверьте класс кнопки.

Ошибка Target closed или Browser has been closed

Причина: вы закрыли контекст при смене IP, но продолжаете использовать старый объект page. Решение: убедитесь, что после каждого page.context.close() переменная page переназначается через new_page(browser), как в примере шага 5.

После смены IP страницы грузятся вечно

Причина: оператор еще не выделил новый адрес, прокси в переходном состоянии. Решение: увеличьте IP_CHANGE_WAIT до 30-40 секунд и добавьте timeout в page.goto, чтобы зависание не блокировало цикл.

Excel открывает CSV с кракозябрами

Причина: Excel не распознает UTF-8 без метки BOM. Решение: используйте clean_export.py и работайте с xlsx либо при записи CSV укажите encoding='utf-8-sig'.

Дополнительные возможности для продвинутых

Базовый парсер уже решает 90 процентов задач. Если хочется больше скорости и данных, вот куда развиваться.

Перехват сетевых ответов вместо разбора верстки

Карты загружают данные карточек в формате JSON отдельными запросами. Playwright умеет подписываться на них через page.on('response', handler). Внутри обработчика проверяйте, содержит ли response.url фрагмент /maps/api/, и сохраняйте response.json(). Плюс метода: данные структурированы и не зависят от классов верстки. Минус: внутренние адреса меняются без предупреждения, а разбирать вложенный JSON сложнее, чем читать текст со страницы. Подход хорошо сочетать с основным: если JSON-ответ пришел — берем его, иначе падаем на разбор HTML.

Параллельная работа с несколькими прокси-каналами

Один мобильный прокси при безопасном темпе дает 400-600 карточек в час. Если нужно быстрее, купите два-три канала и запустите по отдельному процессу на каждый, поделив links.json на равные части. Не запускайте несколько браузеров через один канал: суммарный темп на IP вырастет и капча вернется. Для оркестрации подойдет простой скрипт-запускальщик на subprocess либо библиотека asyncio с async_playwright, где каждый воркер получает свой контекст и свой прокси в параметре proxy у new_context.

Мониторинг изменений

Сохраняйте результаты каждого сбора в отдельный файл с датой и сравнивайте их по org_id. Появившиеся идентификаторы — новые игроки на рынке, пропавшие — закрывшиеся, изменение rating и reviews_count — динамика репутации. Настройте запуск раз в две недели через Планировщик заданий Windows или cron, и у вас будет живая карта ниши.

Расширение полей

В карточках есть и другие полезные блоки: список услуг с ценами, ссылки на соцсети, признак «Проверенная организация», количество фотографий, ближайшее метро. Каждое поле добавляется по той же схеме: найти класс через F12, добавить grab в словарь. Особенно ценен блок цен для арбитражников, оценивающих средний чек ниши.

Анализ отзывов

Собранные тексты отлично ложатся в простую аналитику: посчитайте частоту слов среди отзывов с одной-двумя звездами и получите список главных претензий клиентов к конкурентам. Для этого хватит pandas и Counter из стандартной библиотеки. Продвинутый вариант — прогнать тексты через языковую модель для классификации по темам: цена, качество, сервис, ожидание.

Официальный API как альтернатива

Для крупных коммерческих проектов рассмотрите API Яндекса для поиска по организациям. Он отдает название, адрес, телефон и категории в структурированном виде и не создает рисков блокировок вовсе. Отзывов в нем нет, лимиты платные, но для сбора базы контактов это самый чистый путь. Парсер карточек в таком случае остается инструментом для отзывов и полей, которых в API нет.

FAQ: частые вопросы по парсингу Яндекс Карт

Законно ли собирать телефоны организаций с Яндекс Карт?

Контактные данные компаний размещены публично самими организациями и не являются персональными данными. Сбор для собственного анализа допустим. Но использование этих номеров для массовых звонков и рассылок без согласия нарушает закон о рекламе. Также помните об ограничениях пользовательского соглашения Яндекса на автоматизированный сбор и держите нагрузку минимальной.

Почему нельзя обойтись обычными запросами requests без браузера?

Карты полностью рисуются JavaScript-кодом. Сервер отдает почти пустой HTML, а данные подгружаются потом. requests получит каркас без телефонов и адресов. Поэтому нужен Playwright с реальным Chromium.

Обязательно ли использовать мобильные прокси, можно ли парсить с домашнего IP?

Технически первые 50-100 карточек соберутся и так. Но затем появится капча, а домашний IP на несколько часов попадет под ограничения, и вы не сможете нормально пользоваться Яндексом. Мобильные прокси решают проблему двумя способами: адрес сотового оператора изначально вызывает больше доверия, а ротация по ссылке позволяет распределять нагрузку между адресами без остановки сбора.

Сколько карточек можно собрать за день с одного прокси?

При безопасном темпе 8-12 карточек в минуту с паузами и сменой IP каждые 30 карточек — около 5-8 тысяч за сутки непрерывной работы. Вместе с отзывами объем уменьшится в два-три раза, потому что каждая страница отзывов требует прокрутки.

Что делать, если Яндекс поменял верстку и парсер сломался?

Это штатная ситуация, происходит несколько раз в год. Откройте карточку, нажмите F12, найдите новые классы нужных элементов и замените их в коде. Уходит 10-15 минут. Чтобы упростить процесс, храните все селекторы в одном словаре в начале файла.

Как собрать организации по всей области, а не по одному городу?

Составьте список населенных пунктов и запускайте collect_links.py в цикле, подставляя название в QUERY. Объединяйте ссылки в один set. Для крупных городов дополнительно разбивайте по районам, так как один запрос редко отдает больше 500 результатов.

Можно ли запускать парсер в фоне без окна браузера?

Да, установите headless=True. Но делайте это только после того, как отладили селекторы и убедились, что капча не появляется. В режиме без окна сложнее заметить проблему, а некоторые признаки автоматизации проявляются сильнее. Компромисс: headless=True плюс скриншот страницы при каждой ошибке через page.screenshot(path='error.png').

Как понять, что скрипт получил капчу, если я не смотрю на экран?

Функция is_captcha из шага 5 проверяет адрес страницы и наличие блока проверки. Добавьте отправку уведомления себе, например запись в лог-файл или сообщение в мессенджер через бот, и вы узнаете о проблеме сразу.

Отзывы собираются не все, только последние сто. Как получить больше?

Увеличьте max_scrolls в parse_reviews до 50-100. Учтите, что каждая прокрутка — это дополнительный запрос к серверу, поэтому для организаций с тысячами отзывов сбор займет несколько минут и потребует более частой смены IP.

Чем этот способ лучше готовых сервисов парсинга?

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

Заключение

Давайте подведем итог. Вы установили Python и Playwright, подключили мобильный прокси и проверили смену IP. Собрали ссылки на карточки организаций по запросу и городу. Написали функцию, которая открывает каждую карточку, раскрывает скрытый телефон и забирает название, адрес, сайт, рейтинг и режим работы. Добавили сбор отзывов с оценками. Научили парсер менять IP по расписанию и корректно реагировать на капчу. Наконец, очистили данные и получили аккуратный Excel с двумя листами.

Главное, что стоит запомнить: устойчивость парсера Яндекс Карт держится не на хитростях, а на трех вещах — человеческом темпе, регулярной ротации адресов через мобильные прокси и готовности остановиться при первом сигнале. Скрипт, который уважает ресурс, работает месяцами без вмешательства.

Что делать дальше

  • Запустите первый полноценный сбор по своей нише и проверьте пять-десять карточек вручную.
  • Поставьте сбор на расписание раз в две недели и начните накапливать историю изменений.
  • Попробуйте перехват JSON-ответов из блока для продвинутых, чтобы меньше зависеть от верстки.
  • Если объемы растут, добавьте второй прокси-канал и распараллельте работу.

Куда развиваться

Следующий логичный шаг — автоматическая аналитика собранных отзывов и связка таблицы с вашей CRM или рекламным кабинетом. А если вы работаете с несколькими площадками, тот же подход с Playwright и мобильными прокси переносится на любые сайты с динамической загрузкой. Принципы одинаковы, меняются лишь селекторы. Удачных сборов и чистых данных!