Introducción: qué obtendrás y a quién le servirá esta guía

Yandex Maps hace tiempo dejó de ser solo un navegador para convertirse en el catálogo de negocios más completo de Rusia. En las fichas de organizaciones está todo lo que necesita cualquier marketero, afiliado o dueño de negocio: direcciones exactas, teléfonos, sitios web, horarios, rating y miles de reseñas honestas de clientes. El problema es que recorrer manualmente quinientas fichas de odontologías en una ciudad es imposible, y los servicios ya hechos o son caros, o entregan datos desactualizados.

En esta guía escribiremos juntos nuestro propio parser de Yandex Maps en Python. Sin programas pagos, sin servidores ajenos. Al final de la guía tendrás:

  • Un script funcional que recolecta fichas de organizaciones por cualquier consulta y ciudad.
  • Una tabla de Excel con nombres, categorías, direcciones, teléfonos, sitios web, rating y horario.
  • Una tabla aparte con reseñas: autor, calificación, fecha, texto.
  • Entender cómo trabajar con cuidado y no recibir captcha ni bloqueos, distribuyendo la carga a través de proxies móviles.

Límite importante de este artículo: analizamos específicamente las fichas de organizaciones dentro de Maps. El parsing de resultados de búsqueda normales de Yandex, el trabajo con navegadores antidetect y la configuración básica de proxies móviles están cubiertos en materiales relacionados del blog; aquí no los repetimos, solo nos apoyamos en ellos.

Para quién es esta guía

  • Marketeros y dueños de negocio que necesitan una base de competidores o socios en la región con teléfonos reales y rating.
  • Afiliados que recolectan nichos offline y ofertas locales para su posterior análisis.
  • Desarrolladores a quienes les encargaron hacer recolección de datos de Maps y no quieren empezar desde cero.
  • Analistas que estudian reseñas para evaluar la calidad del servicio en un nicho.

Qué necesitas saber de antemano

Nada complicado. Basta con saber instalar programas, abrir la línea de comandos y copiar texto. No se requiere experiencia en programación: todo el código está listo, solo necesitas poner tus valores. Si alguna vez ejecutaste un script en Python, te resultará muy fácil. En el bloque para avanzados tocaremos asincronía y trabajo con respuestas de red, pero puedes saltarlo.

Cuánto tiempo tomará

  • Preparación del entorno: 30-40 minutos.
  • Escritura y depuración del parser paso a paso: 1,5-2 horas.
  • Primera recolección completa de 300-500 fichas: 1-3 horas de tiempo en segundo plano mientras haces otras cosas.

En total, en medio día laboral obtienes una herramienta que después te ahorrará semanas de trabajo manual.

Preparación previa: herramientas, accesos y requisitos

Antes de escribir código, reunamos todo lo necesario. Salta esta sección solo si ya tienes Python instalado y acceso a un proxy móvil.

Requisitos del sistema

  • Windows 10 u 11, macOS 12 o superior, o cualquier Linux moderno.
  • Mínimo 8 GB de RAM: vamos a ejecutar un navegador Chromium real.
  • 3-4 GB de espacio libre en disco para Python, el navegador y los resultados.
  • Internet estable. La velocidad no es crítica, lo importante es que no se corte.

Qué hay que instalar

  1. Python 3.11 o 3.12. Descarga el instalador desde el sitio oficial python.org. Al instalar en Windows, asegúrate de marcar la casilla Add Python to PATH abajo de la primera ventana del instalador, si no, los comandos en la consola no funcionarán.
  2. Editor de código. Puede ser Visual Studio Code, PyCharm Community o incluso Notepad++. Nos orientaremos a VS Code, pero para nuestras tareas no hay diferencia.
  3. Librerías de Python: playwright para controlar el navegador, pandas y openpyxl para las tablas, requests para verificar el proxy. Las instalaremos en el primer paso.
  4. Navegador Chromium para Playwright. Se descarga con un comando aparte, pesa unos 150 MB.

Acceso a un proxy móvil

Este es el elemento clave de la sección «sin baneos». Yandex Maps es sensible a la frecuencia de solicitudes desde una misma dirección. Los proxies móviles proporcionan direcciones IP de operadores celulares reales, detrás de las cuales están miles de usuarios comunes al mismo tiempo, por eso Yandex los trata con máxima lealtad. En tu panel de mobileproxy.space después de comprar el proxy necesitarás cuatro valores:

  • Host (dirección del servidor) y puerto para la conexión HTTP.
  • Usuario y contraseña para autenticación.
  • Enlace de cambio de IP: una URL especial que, al visitarla, hace que el proxy obtenga una nueva dirección del operador. Cópiala en un archivo aparte, la necesitarás en el quinto paso.

Consejo: Elige un proxy de la misma región o al menos del mismo país que la ciudad que estás parseando. Los mapas adaptan los resultados a la geolocalización, y un proxy de otro país puede mostrar una lista incompleta de organizaciones o la interfaz en otro idioma.

Copias de seguridad y carpeta de trabajo

Crea en el disco una carpeta, por ejemplo maps_parser. Dentro estarán los scripts y los resultados. Guardaremos todos los datos intermedios en archivos después de cada ficha, así que incluso si el script falla en la organización número trescientos, las primeras doscientas noventa y nueve no se perderán. Tiene sentido hacer una copia del archivo con resultados antes de cada ejecución repetida: simplemente renombra el archivo antiguo añadiendo la fecha.

Verificación: Abre la línea de comandos (en Windows presiona Win+R, escribe cmd y Enter) y teclea el comando python --version. Si ves una línea como Python 3.12.x, la preparación terminó. Si en su lugar aparece un error, reinstala Python con la casilla Add to PATH marcada.

Conceptos básicos: cómo funcionan los Mapas y qué es el parsing de fichas

Antes de escribir código, aclaremos los términos. Son simples, pero sin ellos no quedará claro por qué hacemos las cosas de esta manera.

Términos clave en lenguaje simple

  • Parsing (scraping) — recolección automática de información de páginas web. El programa abre una página como lo haría una persona y extrae de ella los fragmentos necesarios.
  • Ficha de organización — una página separada en Maps con una dirección del tipo yandex.ru/maps/org/nombre/identificador-numérico/. Ahí es donde están el teléfono, la dirección, las reseñas y el resto de datos. La lista de organizaciones de la izquierda es solo una vitrina, de la cual tomamos solo los enlaces a las fichas.
  • Selector — la «dirección» de un elemento dentro de la página. Por ejemplo, la clase business-contacts-view__address apunta al bloque con la dirección. Por los selectores el script encuentra el texto necesario.
  • Navegador headless — un navegador real controlado por un programa. Puede funcionar con ventana (ves lo que pasa) o sin ella.
  • Playwright — una librería para controlar el navegador desde Python. La elegimos porque los Mapas están totalmente construidos en JavaScript, y una simple solicitud de HTML devolvería una página vacía sin datos.
  • Proxy móvil — un intermediario entre tu computadora y el sitio con una dirección IP de red celular. El sitio no ve tu dirección, sino la del operador.
  • Rotación de IP — cambio periódico de la dirección del proxy para que la carga no se concentre en una sola IP.
  • Captcha — una página de verificación «Confirma que no eres un robot». Para nosotros es una señal de que vamos demasiado rápido. No la resolveremos con servicios externos, simplemente nos detendremos, cambiaremos la IP y bajaremos el ritmo.

Principios básicos de funcionamiento del parser de Yandex Maps

La lógica es simple y consta de tres fases. Primero obtenemos la lista de enlaces a las fichas de las organizaciones necesarias. Después abrimos cada ficha una por una y extraemos los datos. Al final lo reunimos todo en una tabla. Entre estas acciones hacemos pausas de duración aleatoria y de vez en cuando cambiamos la IP a través del enlace de cambio de dirección.

Qué es importante entender sobre legalidad y ética

Atención: Recolecta solo datos públicamente disponibles de organizaciones y úsalos para análisis. El teléfono y la dirección de una empresa no son datos personales, pero los nombres de los autores de reseñas pueden serlo según la ley 152-FZ. No los almacenes sin necesidad y no uses los teléfonos recolectados para envíos masivos sin consentimiento, esto viola la ley de publicidad y las reglas de Yandex. Recuerda también: el acuerdo de usuario de Yandex limita la recolección automatizada, así que mantén la carga al mínimo, y para proyectos comerciales con grandes volúmenes considera la API oficial de Yandex para búsqueda de organizaciones.

Paso 1: Configuramos el entorno y conectamos el proxy móvil

Objetivo de la etapa: instalar todas las librerías, descargar el navegador y asegurarnos de que las solicitudes pasan por el proxy móvil y no directamente.

Instalación de librerías

  1. Abre la línea de comandos o la terminal.
  2. Ve a la carpeta de trabajo con el comando cd y la ruta a la carpeta. Por ejemplo: cd C:\maps_parser en Windows o cd ~/maps_parser en macOS y Linux.
  3. Crea un entorno virtual para que las librerías no interfieran con otros proyectos: python -m venv venv
  4. Actívalo. Windows: venv\Scripts\activate. macOS y Linux: source venv/bin/activate. Al inicio de la línea de la terminal aparecerá el texto (venv).
  5. Instala las librerías con un solo comando:
pip install playwright pandas openpyxl requests
playwright install chromium

El segundo comando descargará Chromium. Esto toma de 2 a 5 minutos según la velocidad de internet. Espera hasta que en la terminal vuelva a aparecer el prompt de entrada.

Verificación del proxy

Crea en el editor el archivo check_proxy.py y pega el código, reemplazando USUARIO, CONTRASEÑA, HOST y PUERTO con los valores de tu panel:

import requests

proxy = 'http://USUARIO:CONTRASEÑA@HOST:PUERTO'
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('Tu IP directa:', direct['ip'])
print('IP a través del proxy:', via_proxy['ip'])

Ejecuta el archivo con el comando python check_proxy.py. Deberías ver dos direcciones diferentes. Si las direcciones coinciden o apareció un error de conexión, el proxy no funciona y seguir adelante no tiene sentido.

Verificación del cambio de IP

Abre el enlace de cambio de IP de tu panel en un navegador normal. Normalmente devuelve una respuesta corta sobre el cambio exitoso. Espera 15-30 segundos y ejecuta de nuevo check_proxy.py. La dirección a través del proxy debe cambiar. Recuerda cuántos segundos realmente se necesitan para el cambio: ese valor lo pondremos en el script en el quinto paso.

Consejo: Guarda toda la configuración del proxy en un archivo aparte config.py con las variables PROXY_HOST, PROXY_PORT, PROXY_LOGIN, PROXY_PASSWORD y CHANGE_IP_URL. Entonces en los demás scripts bastará con escribir from config import * y no reescribir contraseñas diez veces.

Verificación: El comando playwright --version muestra el número de versión, check_proxy.py muestra una IP de operador celular distinta de tu IP doméstica, y después de visitar el enlace de cambio la dirección cambia.

Posibles problemas

  • Error «pip no es un comando interno». Python se instaló sin añadirse al PATH. Reinstala con la casilla marcada o usa el comando py -m pip en lugar de pip.
  • Error 407 Proxy Authentication Required. Usuario o contraseña incorrectos. Verifica que no haya espacios extra al copiar.
  • Timeout al solicitar a través del proxy. El puerto está mal indicado o el proxy está cambiando de IP en ese momento. Espera medio minuto y repite.

Paso 2: Recolectamos los enlaces a las fichas de organizaciones

Objetivo de la etapa: obtener el archivo links.json con la lista de direcciones de fichas de todas las organizaciones por la consulta y ciudad necesarias.

Aquí solo miraremos una vez la lista de resultados de Maps para tomar de ella los enlaces. Los datos en sí los tomaremos exclusivamente de las fichas, por eso la lista nos sirve como índice, nada más.

Cómo está estructurada la lista de organizaciones

Cuando escribes en Maps una consulta como «odontología Kazán», a la izquierda aparece un panel desplazable con snippets. Cada snippet contiene un enlace a una ficha. La lista se carga por porciones a medida que se desplaza, así que el script girará el panel con la rueda del mouse y recolectará enlaces después de cada desplazamiento, hasta que dejen de aparecer nuevos.

Escribimos el script de recolección de enlaces

Crea el archivo 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 = 'odontología Kazán'
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'Desplazamiento {i+1}: enlaces {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('Total recolectado:', len(links))
    browser.close()

Analizamos qué pasa

  1. La línea headless=False abre el navegador con ventana. En la etapa de depuración esto es obligatorio: verás con tus propios ojos si el mapa cargó y si no hay captcha.
  2. page.mouse.move(350, 500) coloca el cursor en el panel izquierdo. Sin esto, la rueda del mouse moverá el mapa y no la lista.
  3. El selector a[href*="/maps/org/"] busca todos los enlaces que contienen el fragmento /maps/org/. Es más resistente que las clases concretas, que Yandex cambia cada pocos meses.
  4. El contador stale detiene el ciclo si cuatro desplazamientos seguidos no trajeron nuevos enlaces. Esto significa que la lista terminó.
  5. Los enlaces se limpian de parámetros después del signo de interrogación para que una organización no acabe dos veces en el archivo.

Ejecuta el script: python collect_links.py. Se abrirá la ventana del navegador, cargará el mapa con los resultados, el panel empezará a desplazarse. En la terminal correrá el contador de enlaces. Para una ciudad mediana por una consulta normalmente se recolectan de 100 a 400 fichas en 2-4 minutos.

Consejo: Los Mapas raras veces entregan más de 500 resultados por una consulta. Si necesitas todo el nicho en una megaciudad, divide la consulta por distritos: «odontología distrito Vajitovski Kazán», «odontología distrito Sovetski Kazán». Combina los enlaces de diferentes consultas con set, como en el código de arriba, los duplicados desaparecerán solos.

Verificación: En la carpeta apareció el archivo links.json, dentro hay una lista de direcciones del tipo https://yandex.ru/maps/org/nombre/1234567890/. Abre dos o tres direcciones a mano en un navegador normal y asegúrate de que son fichas de las organizaciones necesarias.

Posibles problemas

  • Cero enlaces recolectados. Lo más probable es que la página no haya terminado de cargar o el cursor no estaba sobre la lista. Aumenta la primera pausa a 10 segundos y verifica las coordenadas del mouse: el panel debe estar debajo del cursor.
  • La lista no se desplaza, se mueve el mapa. Reduce o aumenta la coordenada X en mouse.move, orientándote por el ancho del panel en tu ventana.
  • Apareció captcha de inmediato. Cambia la IP con el enlace, espera un minuto y ejecuta de nuevo. Si se repite, prueba otro canal de proxy.

Paso 3: Parseamos la ficha de organización — nombre, dirección, teléfono, sitio web

Objetivo de la etapa: escribir una función que abre una ficha y devuelve un diccionario con los datos principales de la organización, y recorrerla por todos los enlaces de links.json.

Cómo encontrar los selectores por tu cuenta

Yandex renombra periódicamente las clases en el marcado. Por eso es importante saber encontrarlas por tu cuenta y no depender solo del código listo. Se hace así:

  1. Abre cualquier ficha de organización en un navegador normal Chrome.
  2. Haz clic derecho sobre el teléfono de la organización y elige Inspeccionar (o presiona F12 y haz clic sobre el elemento con la herramienta de selección).
  3. En el panel que se abre verás la etiqueta resaltada con el atributo class. Por ejemplo, class="card-phones-view__phone-number". Ese es el selector: en el código se escribe con un punto al principio.
  4. Repite para el nombre, la dirección, el sitio, el rating y el horario. Anota las clases en el bloc de notas.

La clase puede estar formada por varias palabras separadas por espacios. Toma la primera, la más «elocuente», con doble guion bajo dentro. Al momento de escribir esta guía son actuales los selectores del código de abajo, pero verifícalos antes de ejecutar.

Escribimos la función de parsing de la ficha

Crea el archivo 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'Total {len(links)}, quedan {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('Error en', url, e)
            time.sleep(random.uniform(4, 9))
    browser.close()

Qué es importante aquí

  1. Botón «Mostrar teléfono». En algunas fichas el número completo está oculto detrás de un botón. Lo buscamos por la clase card-phones-view__more y hacemos clic si lo encontramos. Solo después recolectamos los números.
  2. Escritura después de cada ficha. La función f.flush() fuerza el volcado de datos al disco. Si el script falla, todo lo recolectado quedará en orgs.csv.
  3. Reanudación del trabajo. La función load_done lee los enlaces ya recolectados, y al reiniciar el script continúa desde donde se detuvo, no desde el principio.
  4. Pausas de 4-9 segundos entre fichas. Este es el ritmo de una persona atenta que lee información. No lo reduzcas en las primeras ejecuciones.

Ejecuta python parse_cards.py y observa las primeras cinco o diez fichas en la ventana del navegador. Deberías ver cómo la página se abre, si es necesario se despliega el teléfono, y en la terminal aparece el nombre y el número.

Atención: Si cinco fichas seguidas devolvieron nombres vacíos, detén el script inmediatamente con Ctrl+C. Casi con seguridad Yandex cambió el marcado y los selectores quedaron obsoletos. Encuentra los nuevos siguiendo las instrucciones de arriba y actualiza el código. Seguir recolectando líneas vacías no tiene sentido y solo aumenta la carga sobre el proxy.

Consejo: Además del texto, es útil guardar el propio identificador de la organización: es la última parte numérica de la URL. Con él es fácil cotejar bases entre recolecciones y rastrear empresas cerradas. Añade el campo 'org_id': url.rstrip('/').split('/')[-1] al diccionario.

Verificación: El archivo orgs.csv se abre en Excel, están rellenadas las columnas name, address, phones en al menos el 90 por ciento de las filas. Los teléfonos se muestran en formato +7 (843) 000-00-00. El rating se ve como un número con coma, por ejemplo 4,7.

Posibles problemas

  • Los teléfonos están vacíos aunque en el sitio estén. El botón «Mostrar teléfono» tiene otra clase. Encuéntrala con F12 y reemplázala en el código.
  • El cirílico en CSV se ve como caracteres extraños. Excel no reconoció la codificación. Abre el archivo a través del menú Datos, Desde texto/CSV y elige UTF-8, o espera al paso 6, donde convertimos los datos a xlsx.
  • El script se cuelga en una ficha. Añade el parámetro timeout=45000 en page.goto para que tras 45 segundos se lance una excepción y el ciclo siga adelante.

Paso 4: Recolectamos las reseñas de las fichas

Objetivo de la etapa: para cada organización obtener una lista de reseñas con calificación, fecha y texto, y guardarlas en un archivo aparte reviews.csv.

Dónde viven las reseñas

Cada ficha tiene una pestaña «Reseñas» con una dirección del tipo https://yandex.ru/maps/org/nombre/identificador/reviews/. Ahí las reseñas se cargan por porciones al desplazar, igual que la lista de organizaciones en el segundo paso. Por defecto están ordenadas por relevancia, y las más recientes se pueden obtener cambiando el orden a «Por novedad».

Escribimos la función de recolección de reseñas

Crea el archivo parse_reviews.py. La base es la misma que en el paso anterior, por eso mostramos solo la función y el ciclo:

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

En el ciclo por enlaces llama a parse_reviews en lugar de parse_card y escribe cada elemento de la lista en una línea separada de reviews.csv con los campos org_url, author, date, stars, text. La lógica de reanudación y escritura después de cada organización se mantiene igual.

Analizamos los detalles

  1. Límite max_scrolls=15. Los establecimientos populares pueden tener dos o tres mil reseñas. Recolectarlas todas raras veces es necesario, y consume mucho tiempo y solicitudes. Quince desplazamientos normalmente alcanzan para 100-150 reseñas recientes.
  2. Calificación por estrellas. El número de puntos en la reseña no está escrito como texto, sino dibujado con estrellas. Contamos los elementos con el modificador _full, es decir, las estrellas pintadas.
  3. Reseñas largas. Parte de los textos está colapsada y requiere pulsar «Más». Si necesitas el texto completo, antes de recolectar haz clic en todos los botones con la clase business-review-view__expand dentro de la ficha.

Atención: Los nombres de los autores de reseñas son datos de usuarios, no de organizaciones. Si tu tarea es análisis de sentimiento o búsqueda de quejas típicas, el campo author no te hace falta. No lo colectes sin propósito, así te quitas preguntas innecesarias respecto a la ley 152-FZ. Si de todos modos necesitas el campo, guarda el archivo localmente y no lo compartas con terceros.

Consejo: Ejecuta la recolección de reseñas no por todas las organizaciones, sino por una lista filtrada: por ejemplo, solo con rating por debajo de 4,0 o solo de competidores directos. Así reduces el volumen de solicitudes varias veces, y no pierdes el valor de los datos.

Verificación: En reviews.csv cada organización tiene entre 20 y 150 líneas, el campo stars contiene números del 1 al 5, en text hay texto ruso con sentido. La fecha se ve como «15 de enero» o «3 de marzo de 2026».

Posibles problemas

  • Se encontraron cero reseñas. Verifica que la URL termine en /reviews/ y que el panel de reseñas esté debajo del cursor al desplazar.
  • En todas las reseñas stars es 0. La clase de la estrella pintada cambió. Encuéntrala con F12 haciendo clic sobre la estrella.
  • Se duplican reseñas. El panel no se desplazó del todo y un bloque se contó dos veces. Elimina los duplicados en el sexto paso por el par author más text.

Paso 5: Configuramos la rotación de IP y la protección contra bloqueos

Objetivo de la etapa: enseñar al parser a comportarse como un usuario cuidadoso: cambiar IP según lo previsto, reconocer el captcha y bajar automáticamente el ritmo en lugar de chocar contra un bloqueo.

Por qué aparecen los baneos en general

Yandex no prohíbe mirar fichas, pero vigila anomalías: cientos de páginas por minuto desde una dirección, intervalos iguales entre solicitudes, ausencia de movimientos del mouse, cookies vacías. Cuando la sospecha se acumula, aparece la página SmartCaptcha, y con insistencia, una restricción temporal para la IP. Nuestra estrategia no es «romper» la protección, sino no dar motivos para que se active.

Tres reglas del parser tranquilo de Yandex Maps

  1. Ritmo humano. No más de 8-12 fichas por minuto por una IP. Pausas aleatorias, no fijas.
  2. Cambio regular de IP. Cada 25-40 fichas o cada 10-15 minutos visitamos el enlace de cambio de dirección. El proxy móvil en segundos obtiene una nueva IP del operador, y el historial de solicitudes se «resetea» desde el punto de vista del sitio.
  3. Reversión inmediata ante captcha. Vimos la verificación: no intentamos pasarla, sino que hacemos una pausa de 2-3 minutos, cambiamos IP y continuamos con la misma ficha en un nuevo contexto del navegador.

Añadimos la rotación al código

Inserta en parse_cards.py y parse_reviews.py las siguientes funciones y llámalas en el ciclo principal:

import requests

def change_ip():
    try:
        r = requests.get(CHANGE_IP_URL, timeout=30)
        print('Cambio de IP:', r.status_code)
    except Exception as e:
        print('No se pudo cambiar la 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()

Y el ciclo principal adquiere esta forma:

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('¡Captcha! Pausa y cambio de 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))

Qué es importante entender

  • Nuevo contexto junto con nueva IP. Al cerrar el contexto, reseteamos cookies y almacenamiento local. Cambiar la dirección sin resetear cookies no tiene sentido: el sitio seguirá reconociéndote por la sesión.
  • La variable IP_CHANGE_WAIT en config.py es el tiempo medido en el primer paso, normalmente 15-30 segundos. No se puede poner menos: el navegador abrirá la página a través de la dirección antigua.
  • Tamaño de ventana aleatorio añade variedad a la huella del navegador. Es una medida suave, pero es gratis.
  • Los proxies móviles con rotación por enlace son aquí más cómodos que cualquier otro: no te cambias entre decenas de direcciones a mano, simplemente tiras de una URL.

Consejo: Lleva un log simple: apunta en un archivo la hora, el número de ficha y el evento (éxito, captcha, cambio de IP). En una semana por el log verás con precisión a qué ritmo no aparece captcha en absoluto y ajustarás las pausas a tu canal de proxy.

Verificación: En una hora de trabajo continuo el script recolectó 400-600 fichas, en la terminal no hay más de uno o dos mensajes sobre captcha, después de cada uno la recolección continuó automáticamente. La IP a través del proxy cambió al menos diez veces (esto se ve en las líneas «Cambio de IP: 200»).

Posibles problemas

  • Aparece captcha cada 10 fichas. El ritmo es demasiado alto para tu canal. Aumenta las pausas a 8-15 segundos y cambia la IP cada 15 fichas.
  • Después del cambio de IP las páginas no cargan. Aumenta IP_CHANGE_WAIT: el operador aún no entregó la nueva dirección.
  • El enlace de cambio de IP devuelve error de solicitudes demasiado frecuentes. La mayoría de las tarifas tienen un intervalo mínimo entre cambios, normalmente de uno a dos minutos. No cambies la IP con más frecuencia.

Paso 6: Limpiamos los datos y guardamos en Excel

Objetivo de la etapa: convertir los CSV crudos en tablas de Excel ordenadas, sin duplicados, con teléfonos normalizados y rating numérico.

Escribimos el script de limpieza

Crea el archivo 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('Organizaciones:', len(orgs), 'Reseñas:', len(reviews))

En la línea de extracción del número de reseñas se usa una expresión regular de una barra invertida y la letra d entre paréntesis: extrae el primer número de un texto como «312 reseñas». Cópiala con atención.

Qué hace el script

  1. Elimina repeticiones de organizaciones por URL.
  2. Lleva todos los teléfonos al formato +7XXXXXXXXXX, quitando paréntesis, espacios y guiones. Este formato es cómodo para CRM y cotejo de bases.
  3. Cambia la coma por punto en el rating para que Excel lo perciba como número y permita ordenar.
  4. Extrae la cantidad de reseñas de la cadena de texto.
  5. Elimina duplicados de reseñas y escribe dos hojas en un solo archivo xlsx.

Ejecuta python clean_export.py y abre yandex_maps_result.xlsx. En la primera hoja las organizaciones están ordenadas por rating, los teléfonos son uniformes; en la segunda hoja están las reseñas con vínculo a la organización por la columna org_url.

Consejo: Añade al script una columna «fecha de recolección» con la fecha actual. En un mes repite la recolección y compara las tablas con la función merge de pandas: verás nuevas organizaciones, locales cerrados y cambios de rating de los competidores. Esto ya es un monitoreo de mercado completo.

Verificación: El archivo xlsx se abre sin advertencias, el cirílico se lee, la columna rating se ordena como número, en la columna phones no hay paréntesis ni espacios, el número de filas en la hoja «Организации» coincide con la cantidad de enlaces únicos en links.json menos los errores.

Verificación del resultado: checklist del parser terminado

Repasa la lista. Si respondiste «sí» a cada punto, el parser de Yandex Maps está listo para el trabajo regular.

Checklist

  • check_proxy.py muestra una IP de operador celular, distinta de la doméstica.
  • collect_links.py recolecta más de 50 enlaces por una consulta mediana y se detiene solo.
  • parse_cards.py rellena nombre, dirección y teléfono en al menos el 90 por ciento de las fichas.
  • El botón «Mostrar teléfono» se despliega automáticamente.
  • parse_reviews.py devuelve reseñas con calificación del 1 al 5.
  • Ante captcha el script hace una pausa, cambia IP y continúa sin tu intervención.
  • Después de una parada de emergencia, una nueva ejecución continúa desde donde se cortó.
  • clean_export.py crea un xlsx con dos hojas y teléfonos normalizados.

Cómo probar todo de principio a fin

  1. Toma una consulta pequeña con la que en la ciudad haya 30-60 organizaciones, por ejemplo «vulcanizadora» en una capital de distrito.
  2. Ejecuta todos los scripts en orden y cronometra el tiempo. Para 50 fichas con reseñas deben pasar 15-25 minutos.
  3. Elige cinco organizaciones al azar de la tabla y coteja teléfonos y direcciones con las fichas a mano.
  4. Detén parse_cards.py a la mitad con Ctrl+C y ejecútalo de nuevo. Asegúrate de que el contador «quedan» disminuyó y no se reseteó.

Indicadores de ejecución exitosa

  • Precisión de los datos en el cotejo manual: 100 por ciento de coincidencias en teléfonos y direcciones.
  • Proporción de nombres vacíos: menos del 3 por ciento.
  • Frecuencia de captcha: no más de una por cada 200-300 fichas.
  • Velocidad: 400-600 fichas por hora por un canal de proxy a ritmo seguro.

Errores típicos y sus soluciones

Reunimos los problemas con los que se topan casi todos los que escriben un parser de Yandex Maps por primera vez, y las formas de resolverlos.

Campos vacíos en la mayoría de las fichas

Causa: Yandex actualizó el marcado, las clases de los elementos cambiaron. Solución: abre la ficha en Chrome, presiona F12, encuentra las nuevas clases y reemplázalas en la función parse_card. Mantén los selectores en un solo diccionario al inicio del archivo para editar un solo lugar.

Aparece captcha desde la primera página

Causa: la IP del proxy ya está «cansada» de actividad anterior o el navegador arranca con parámetros sospechosos. Solución: cambia la IP antes de arrancar, asegúrate de que locale='ru-RU' esté configurado y no uses el modo headless en las primeras ejecuciones: da más señales de automatización.

El desplazamiento de la lista mueve el mapa y no el panel

Causa: el cursor del mouse está fuera del panel izquierdo. Solución: ajusta las coordenadas de page.mouse.move a tu tamaño de ventana. Con un ancho de 1400 píxeles el panel ocupa aproximadamente los primeros 450 píxeles en horizontal.

Se recolectan las mismas 20 organizaciones

Causa: el panel no se desplaza hasta el final, el contador stale se dispara demasiado pronto. Solución: aumenta la pausa después del desplazamiento a 3-4 segundos y el umbral stale a 6; los Mapas a veces cargan la siguiente porción despacio.

Los teléfonos se ven en el navegador, pero en la tabla están vacíos

Causa: el número aparece solo después del clic, y el script recolecta datos antes de que termine, o el botón tiene otra clase. Solución: aumenta la pausa después del clic a 2-3 segundos y verifica la clase del botón.

Error Target closed o Browser has been closed

Causa: cerraste el contexto al cambiar de IP, pero sigues usando el objeto page antiguo. Solución: asegúrate de que después de cada page.context.close() la variable page se reasigna mediante new_page(browser), como en el ejemplo del paso 5.

Después del cambio de IP las páginas cargan eternamente

Causa: el operador aún no asignó la nueva dirección, el proxy está en estado transitorio. Solución: aumenta IP_CHANGE_WAIT a 30-40 segundos y añade timeout en page.goto para que el cuelgue no bloquee el ciclo.

Excel abre el CSV con caracteres extraños

Causa: Excel no reconoce UTF-8 sin marca BOM. Solución: usa clean_export.py y trabaja con xlsx, o al escribir CSV indica encoding='utf-8-sig'.

Funcionalidades adicionales para avanzados

El parser básico ya resuelve el 90 por ciento de las tareas. Si quieres más velocidad y datos, aquí hacia dónde desarrollarte.

Interceptar respuestas de red en lugar de analizar el marcado

Los Mapas cargan los datos de las fichas en formato JSON mediante solicitudes separadas. Playwright sabe suscribirse a ellas con page.on('response', handler). Dentro del handler verifica si response.url contiene el fragmento /maps/api/ y guarda response.json(). La ventaja del método: los datos están estructurados y no dependen de las clases del marcado. La desventaja: las direcciones internas cambian sin aviso, y analizar el JSON anidado es más difícil que leer texto de la página. El enfoque conviene combinarlo con el principal: si llegó la respuesta JSON, la tomamos; si no, caemos al análisis del HTML.

Trabajo paralelo con varios canales de proxy

Un proxy móvil a ritmo seguro da 400-600 fichas por hora. Si necesitas más rapidez, compra dos o tres canales y ejecuta un proceso separado para cada uno, dividiendo links.json en partes iguales. No ejecutes varios navegadores por un mismo canal: el ritmo total por IP crecerá y el captcha volverá. Para la orquestación sirve un script sencillo lanzador con subprocess o la librería asyncio con async_playwright, donde cada worker recibe su propio contexto y su propio proxy en el parámetro proxy de new_context.

Monitoreo de cambios

Guarda los resultados de cada recolección en un archivo aparte con la fecha y compáralos por org_id. Los identificadores aparecidos son nuevos jugadores en el mercado, los desaparecidos son locales cerrados, el cambio de rating y reviews_count es dinámica de reputación. Configura la ejecución cada dos semanas mediante el Programador de tareas de Windows o cron, y tendrás un mapa vivo del nicho.

Ampliación de campos

En las fichas hay otros bloques útiles: lista de servicios con precios, enlaces a redes sociales, marca de «Organización verificada», cantidad de fotos, metro más cercano. Cada campo se añade con el mismo esquema: encontrar la clase con F12, añadir grab al diccionario. El bloque de precios es especialmente valioso para los afiliados que evalúan el ticket medio del nicho.

Análisis de reseñas

Los textos recolectados encajan perfectamente en un análisis simple: cuenta la frecuencia de palabras entre las reseñas con una o dos estrellas y obtén la lista de las principales quejas de los clientes hacia los competidores. Para esto basta pandas y Counter de la librería estándar. La variante avanzada es pasar los textos por un modelo de lenguaje para clasificarlos por temas: precio, calidad, servicio, espera.

La API oficial como alternativa

Para grandes proyectos comerciales considera la API de Yandex para búsqueda de organizaciones. Devuelve nombre, dirección, teléfono y categorías en forma estructurada y no genera riesgos de bloqueo en absoluto. No tiene reseñas, los límites son pagos, pero para recolectar una base de contactos es el camino más limpio. El parser de fichas en ese caso queda como herramienta para reseñas y campos que en la API no existen.

FAQ: preguntas frecuentes sobre el parsing de Yandex Maps

¿Es legal recolectar teléfonos de organizaciones de Yandex Maps?

Los datos de contacto de las empresas son publicados públicamente por las propias organizaciones y no son datos personales. La recolección para análisis propio es admisible. Pero el uso de estos números para llamadas y envíos masivos sin consentimiento viola la ley de publicidad. Recuerda también las limitaciones del acuerdo de usuario de Yandex sobre la recolección automatizada y mantén la carga al mínimo.

¿Por qué no basta con solicitudes requests normales sin navegador?

Los Mapas se dibujan completamente con código JavaScript. El servidor entrega un HTML casi vacío, y los datos se cargan después. requests obtendrá el esqueleto sin teléfonos ni direcciones. Por eso se necesita Playwright con un Chromium real.

¿Es obligatorio usar proxies móviles, se puede parsear desde la IP doméstica?

Técnicamente las primeras 50-100 fichas se recolectarán igual. Pero después aparecerá el captcha, y la IP doméstica quedará bajo restricciones durante varias horas, y no podrás usar Yandex con normalidad. Los proxies móviles resuelven el problema de dos maneras: la dirección del operador celular genera más confianza desde el principio, y la rotación por enlace permite distribuir la carga entre direcciones sin detener la recolección.

¿Cuántas fichas se pueden recolectar por día con un proxy?

A ritmo seguro de 8-12 fichas por minuto con pausas y cambio de IP cada 30 fichas, unas 5-8 mil por día de trabajo continuo. Junto con las reseñas el volumen se reduce dos o tres veces, porque cada página de reseñas requiere desplazamiento.

¿Qué hacer si Yandex cambió el marcado y el parser se rompió?

Es una situación normal, pasa varias veces al año. Abre la ficha, presiona F12, encuentra las nuevas clases de los elementos necesarios y reemplázalas en el código. Toma 10-15 minutos. Para simplificar el proceso, guarda todos los selectores en un solo diccionario al inicio del archivo.

¿Cómo recolectar organizaciones de toda la región, no de una sola ciudad?

Haz una lista de localidades y ejecuta collect_links.py en un ciclo, sustituyendo el nombre en QUERY. Combina los enlaces en un solo set. Para ciudades grandes divide además por distritos, ya que una sola consulta raras veces entrega más de 500 resultados.

¿Se puede ejecutar el parser en segundo plano sin ventana del navegador?

Sí, configura headless=True. Pero hazlo solo después de depurar los selectores y asegurarte de que el captcha no aparece. En modo sin ventana es más difícil notar un problema, y algunas señales de automatización se manifiestan con más fuerza. Compromiso: headless=True más una captura de pantalla de la página ante cada error mediante page.screenshot(path='error.png').

¿Cómo saber que el script recibió captcha si no miro la pantalla?

La función is_captcha del paso 5 verifica la dirección de la página y la presencia del bloque de verificación. Añade el envío de una notificación a ti mismo, por ejemplo escritura en un archivo de log o un mensaje a un mensajero a través de un bot, y sabrás del problema de inmediato.

No se recolectan todas las reseñas, solo las últimas cien. ¿Cómo obtener más?

Aumenta max_scrolls en parse_reviews a 50-100. Ten en cuenta que cada desplazamiento es una solicitud adicional al servidor, por eso para organizaciones con miles de reseñas la recolección tomará varios minutos y requerirá cambios de IP más frecuentes.

¿En qué es mejor este método que los servicios de parsing ya hechos?

Controlas por completo los campos, el ritmo y la frescura de los datos, no pagas por cada línea y no dependes del calendario de actualizaciones de otros. Los servicios ya hechos son cómodos para una tarea puntual de un par de cientos de fichas, mientras que tu propio parser de Yandex Maps se amortiza ya en la segunda recolección.

Conclusión

Resumamos. Instalaste Python y Playwright, conectaste un proxy móvil y verificaste el cambio de IP. Recolectaste los enlaces a las fichas de organizaciones por consulta y ciudad. Escribiste una función que abre cada ficha, despliega el teléfono oculto y toma el nombre, la dirección, el sitio, el rating y el horario. Añadiste la recolección de reseñas con calificaciones. Enseñaste al parser a cambiar de IP según lo previsto y a reaccionar correctamente ante el captcha. Finalmente, limpiaste los datos y obtuviste un Excel ordenado con dos hojas.

Lo principal que vale la pena recordar: la estabilidad del parser de Yandex Maps no se apoya en trucos, sino en tres cosas: ritmo humano, rotación regular de direcciones a través de proxies móviles y disposición a detenerse ante la primera señal. Un script que respeta el recurso funciona meses sin intervención.

Qué hacer después

  • Ejecuta la primera recolección completa por tu nicho y verifica cinco o diez fichas a mano.
  • Pon la recolección en un calendario cada dos semanas y empieza a acumular historial de cambios.
  • Prueba la intercepción de respuestas JSON del bloque para avanzados, para depender menos del marcado.
  • Si los volúmenes crecen, añade un segundo canal de proxy y paraleliza el trabajo.

Hacia dónde desarrollarte

El siguiente paso lógico es el análisis automático de las reseñas recolectadas y la conexión de la tabla con tu CRM o panel publicitario. Y si trabajas con varias plataformas, el mismo enfoque con Playwright y proxies móviles se traslada a cualquier sitio con carga dinámica. Los principios son los mismos, solo cambian los selectores. ¡Éxitos en las recolecciones y datos limpios!