Introducción: qué vas a obtener al final

El mercado inmobiliario vive de números. Alguien busca un departamento por debajo del precio de mercado, alguien evalúa a la competencia en obra nueva, alguien arma reportes para inversionistas. Todos tienen algo en común: necesitan datos actualizados de objetos y precios, y recolectarlos a mano es imposible. Justo aquí es donde te sirve un parser de CIAN hecho por ti mismo.

En esta guía vas a construir desde cero una herramienta funcional que hace tres cosas. Primero: recorrer las páginas de resultados según un filtro determinado y recolectar una lista de objetos con precio, dirección, superficie y enlace. Segundo: entrar a la ficha de cada objeto y extraer detalles como el piso, el año de construcción y el tipo de edificio. Tercero, lo más valioso: guardar los datos en una base y acumular día tras día el historial de precios, para que veas qué objetos bajaron de precio, cuáles se retiraron de la venta y cómo se mueve el mercado en un barrio concreto.

El resultado final se ve así: tienes una carpeta con scripts de Python, un archivo de base de datos SQLite y una tabla que puedes abrir en Excel o Google Sheets. Ejecutas el script por la mañana y obtienes un corte fresco. Después de una semana de ejecuciones ya tienes dinámica.

Para quién es esta guía

  • Para marketers y analistas de agencias inmobiliarias que necesitan monitorear precios por barrio sin comprar reportes caros.
  • Para arbitrajistas y dueños de negocios que buscan nichos y quieren entender la oferta y la demanda en números.
  • Para desarrolladores principiantes que quieren aprender scraping con un ejemplo vivo y comprensible.
  • Para inversionistas y compradores particulares que quieren detectar objetos con rebaja antes que los demás.

Qué necesitas saber de antemano

La experiencia en programación no es obligatoria. Vamos a repasar cada línea de código y explicar para qué sirve. Basta con saber instalar programas, abrir la línea de comandos y copiar texto. Si al menos una vez abriste las herramientas de desarrollador en el navegador, será mucho más fácil. Si no, te mostraremos dónde están.

El único requisito es la atención. El scraping es sensible a los errores de tipeo en los nombres de clases y direcciones. Una letra de más, y el script devuelve una lista vacía. No te asustes: cada paso tiene un punto de verificación donde confirmarás que todo va según el plan.

Cuánto tiempo te llevará

Preparar el entorno lleva unos 30 minutos. El primer parser funcional de resultados lo escribirás en una hora. Las fichas de los objetos, los proxies y la base de datos requerirán otra hora y media o dos. En total, de tres a cuatro horas de tiempo limpio si avanzas sin prisa. El historial de precios empezará a acumularse solo, a partir de la segunda ejecución.

Preparación previa: herramientas y entorno

Antes de escribir código, reunamos todo lo necesario. No conviene saltarse esta sección: la mitad de los problemas de los principiantes surge de un Python mal instalado o de bibliotecas faltantes.

Requisitos del sistema

  • Una computadora con Windows 10 u 11, macOS o Linux. Sirve cualquier portátil de los últimos ocho años.
  • Mínimo 4 GB de memoria RAM. Para la variante con automatización del navegador, preferible 8 GB.
  • Unos 2 GB de espacio libre en disco para Python, las bibliotecas y la base de datos.
  • Conexión estable a internet.

Qué hay que instalar

  1. Python 3.11 o superior. Descarga el instalador del sitio oficial python.org. En Windows, durante la instalación, marca obligatoriamente la casilla Add Python to PATH en la parte inferior de la primera pantalla. Sin eso, el comando python no funcionará en la terminal. En macOS Python suele venir ya instalado, pero es mejor poner una versión reciente.
  2. Editor de código. Recomendamos Visual Studio Code: es gratis, resalta la sintaxis y muestra los errores. Instala la extensión Python desde la tienda integrada de extensiones (el ícono con cuatro cuadrados en el panel izquierdo).
  3. Navegador Chrome o Edge. Vamos a necesitar las herramientas de desarrollador para estudiar la estructura de las páginas.
  4. Bibliotecas de Python. Las instalaremos vía terminal un poco más abajo.
  5. Acceso a proxies móviles. Hará falta en el quinto paso. Necesitas la dirección del servidor, el puerto, el usuario, la contraseña y el enlace para cambiar la dirección IP. Todo eso se entrega en el panel del servicio al comprarlo. Si todavía no tienes proxies, los primeros pasos puedes hacerlos sin ellos.

Creamos la carpeta de trabajo y el entorno virtual

  1. Crea en el disco una carpeta con el nombre cian_parser. Evita letras cirílicas y espacios en la ruta: a veces rompen las herramientas.
  2. Abre la terminal. En Windows pulsa Win+R, escribe cmd y presiona Enter. En macOS abre Terminal desde Spotlight.
  3. Ve a la carpeta con el comando cd y su ruta, por ejemplo: cd C:\projects\cian_parser en Windows o cd ~/projects/cian_parser en macOS.
  4. Crea el entorno virtual con el comando python -m venv venv. Es una copia aislada de Python para que las bibliotecas del proyecto no entren en conflicto con las del sistema.
  5. Activa el entorno. En Windows: venv\Scripts\activate. En macOS y Linux: source venv/bin/activate. Al inicio de la línea de la terminal aparecerá la etiqueta (venv).
  6. Instala las bibliotecas con un solo comando: pip install requests beautifulsoup4 lxml pandas openpyxl. La instalación tardará uno o dos minutos.

Verificación: escribe en la terminal python -c "import requests, bs4, pandas; print('ok')". Si en la pantalla apareció la palabra ok sin errores, el entorno está listo. Si ves ModuleNotFoundError, el entorno no está activado o la instalación se interrumpió. Activa el venv de nuevo y repite pip install.

Copias de seguridad

En este proyecto lo más valioso no es el código, sino la base acumulada con el historial de precios. No se puede recuperar: los precios pasados ya no aparecerán en ningún otro lado. Por eso, desde el primer día llega a un acuerdo contigo mismo: el archivo de la base de datos se copia a la nube o a un disco externo al menos una vez por semana. Más adelante añadiremos la copia automática al script.

Conceptos básicos: qué hay que entender antes de empezar

Repasemos los términos que aparecerán después. Si ya estás familiarizado con el scraping, hojea la sección, pero presta atención al bloque legal.

Términos clave en lenguaje sencillo

  • Parsing (scraping): obtención automática de la página de un sitio por parte de un programa y extracción de los datos necesarios. Lo mismo que haces con los ojos, pero lo hace el script y mil veces más rápido.
  • HTML: el lenguaje de marcado del que está hecha cualquier página web. El precio del departamento en CIAN está dentro de una etiqueta HTML con ciertos atributos, y nuestra tarea es encontrar esa etiqueta.
  • Selector: la dirección del elemento dentro del HTML. Por ejemplo, un span con el atributo data-mark igual a MainPrice. Gracias al selector, el parser entiende de dónde tomar el precio.
  • Petición HTTP: una llamada al servidor del sitio. El navegador la hace cuando abres la página. La biblioteca requests hace lo mismo desde el código.
  • Encabezados de la petición (headers): información de servicio que el navegador envía junto con la petición: tipo de navegador, idioma, formatos de datos. A partir de ellos el servidor decide qué devolver.
  • Proxy: un servidor intermediario por el que pasan tus peticiones. Los proxies móviles usan direcciones IP de operadores celulares y permiten cambiar la dirección con un comando.
  • Paginación: la división de los resultados en páginas. Para recolectar todos los objetos, el parser debe recorrer las páginas de la primera a la última.
  • SQLite: una base de datos ligera en un solo archivo. No requiere instalar un servidor, viene integrada en Python. Ideal para el historial de precios.

Cómo están organizados los resultados en los portales inmobiliarios

CIAN, Domclick, Yandex Realty y otros portales funcionan con un principio parecido. Hay una página de búsqueda con filtros: ciudad, tipo de operación, número de habitaciones, rango de precio. Cada filtro se convierte en un parámetro de la barra de direcciones. Por ejemplo, el parámetro deal_type con el valor sale significa venta, y room1 igual a 1 agrega departamentos de una habitación. Entender estos parámetros te da una herramienta poderosa: en vez de hacer clics en el sitio, simplemente armas la dirección que necesitas.

Dentro de los resultados, cada objeto está representado por una tarjeta: título, precio, dirección, varias fotos, enlace a la página de detalle. La página de detalle contiene las características completas y a menudo duplica todos los datos en un bloque JSON oculto que el sitio usa para dibujar la interfaz. Ese bloque es mucho más cómodo de parsear que el HTML.

Marcos legales y éticos

Atención: recolecta solo información pública de los objetos: precio, superficie, dirección, características del edificio. No recolectes ni almacenes teléfonos, nombres ni otros datos personales de vendedores y agentes: esto está regulado por la ley de protección de datos personales, y su incumplimiento conlleva responsabilidad real. Estudia los términos de uso del portal antes de empezar y usa los datos para tu propia analítica, no para revenderlos o crear una copia del sitio. Respeta una frecuencia razonable de peticiones: tu parser no debe crear una carga que impida el funcionamiento del servicio.

Este enfoque no solo es legal, también es práctico. Un parser cuidadoso, con pausas y rotación de direcciones, funciona durante meses, mientras que uno agresivo recibe restricciones temporales en una hora.

Paso 1: Definimos el objetivo y la estructura de datos

Objetivo de la etapa: describir con claridad qué estamos recolectando exactamente y cómo se almacenará. Sin este paso escribirás un parser que arrastra todo sin criterio, y después pasarás una semana ordenando el desastre de datos.

  1. Formula la pregunta de negocio. Ejemplos: qué departamentos de una habitación en San Petersburgo bajaron de precio más del 5 por ciento en un mes; cuánto cuesta el metro cuadrado en obra nueva de un barrio concreto; qué tan rápido se venden los objetos por debajo de cierta suma.
  2. Define el filtro de resultados. Para el ejemplo de esta guía tomaremos: venta, mercado secundario, departamentos de una y dos habitaciones, Moscú, precio hasta 15 millones de rublos. Tú pondrás tus propios parámetros.
  3. Arma la lista de campos. Para cada objeto necesitamos: identificador único del anuncio, enlace, título, precio, dirección, superficie total, piso y cantidad de pisos, tipo de edificio, año de construcción, fecha de primera detección, fecha de última verificación. Para el historial de precios: identificador del anuncio, fecha, precio.
  4. Abre el editor de código y crea en la carpeta del proyecto el archivo config.py. Escribe en él los parámetros que cambiaremos con más frecuencia:
BASE_URL = 'https://www.cian.ru/cat.php'
SEARCH_PARAMS = {'deal_type': 'sale', 'engine_version': 2, 'offer_type': 'flat', 'region': 1, 'room1': 1, 'room2': 1, 'maxprice': 15000000}
MAX_PAGES = 5
PAUSE_MIN = 4
PAUSE_MAX = 9
DB_PATH = 'realty.db'

Presta atención: en el ejemplo de código los saltos de línea están indicados con símbolos de nueva línea; en el editor simplemente escribe cada variable en una línea nueva. El parámetro region igual a 1 corresponde a Moscú, 2 corresponde a San Petersburgo. Los códigos de otras regiones los encontrarás aplicando el filtro en el sitio y mirando la barra de direcciones.

Consejo: empieza con MAX_PAGES igual a 2-3. En cada página de resultados hay unos 28 objetos, para depurar es suficiente. Lanza la recolección completa cuando confirmes que todos los campos se extraen correctamente.

Verificación: tienes el archivo config.py y en tu cuaderno o en la cabeza está fijada una lista de 12 campos y una pregunta de negocio concreta. Si la pregunta suena como quiero todos los datos de toda Rusia, vuelve y acótala: la recolección completa del país son cientos de miles de objetos y una infraestructura totalmente distinta.

Posibles problemas

No logras entender qué parámetro corresponde al filtro que necesitas. Solución: abre el sitio, aplica el filtro a mano, copia la dirección de la barra de navegación y divídela por el símbolo ampersand. Cada par clave igual a valor es un parámetro.

Paso 2: Estudiamos la estructura de la página de resultados

Objetivo de la etapa: encontrar en el HTML los elementos de los que tomaremos el precio, el título, la dirección y el enlace. Este es el paso más de investigación, y justo aquí los principiantes suelen perderse, así que avanzamos muy despacio.

  1. Abre el navegador y ve a la página de resultados de CIAN con tus filtros. Asegúrate de ver la lista de departamentos.
  2. Pasa el cursor sobre el precio de cualquier departamento, haz clic con el botón derecho y elige la opción Ver código (en Edge se llama Inspeccionar). Se abrirá el panel de herramientas de desarrollador, y dentro de él se resaltará el elemento con el precio.
  3. Mira la línea resaltada. Al momento de escribir esta guía es una etiqueta span con el atributo data-mark igual a MainPrice. Anota ese atributo: será el selector para el precio.
  4. Sube por el árbol de elementos haciendo clic en las etiquetas padre hasta encontrar la etiqueta que abarca toda la tarjeta del objeto por completo. Normalmente es un article con el atributo data-name igual a CardComponent. Cuando pasas el cursor sobre él en el panel, en la página se resalta toda la tarjeta con la foto y el precio.
  5. Dentro de la tarjeta encuentra el título (span con data-mark igual a OfferTitle), la dirección (varios enlaces a con data-name igual a GeoLabel, de los que se compone la dirección) y el enlace al objeto (etiqueta a con href que lleva a una dirección del tipo cian.ru/sale/flat/número). Anota los cuatro selectores.
  6. Encuentra el bloque de paginación al final de la página. Recorre los resultados hasta el final, haz clic con el botón derecho en el número de la segunda página y mira cómo se ve su dirección. Verás el parámetro p igual a 2. Eso significa que para pasar de página basta con cambiar ese parámetro.

Atención: los nombres de los atributos data-mark y data-name en los portales cambian periódicamente cuando actualizan el diseño. No copies los selectores de este texto a ciegas: verifícalos obligatoriamente con la página real en el panel de desarrollador. La habilidad de encontrar un selector por tu cuenta es más importante que cualquier lista prefabricada.

Verificamos si hay un JSON oculto

Muchos portales guardan los datos de los resultados ya listos dentro de una etiqueta script. Es más cómodo que el HTML: no hay que pegar la dirección a partir de fragmentos.

  1. En el panel de desarrollador pulsa Ctrl+F (en macOS Cmd+F) y escribe la palabra offers o initialState.
  2. Si la búsqueda encontró una etiqueta script con un volumen grande de texto parecido a un diccionario con llaves, significa que los datos están en JSON. Anota el nombre de la variable al inicio de ese bloque.
  3. Si no encontró nada, no pasa nada: el enfoque HTML del siguiente paso funciona en cualquier caso.

Consejo: abre la pestaña Network (Red) en el panel de desarrollador, actualiza la página y filtra las peticiones por tipo Fetch/XHR. A veces el sitio carga los resultados con una petición aparte en formato JSON. Si ves una petición así con el campo offers, parsearla es lo más sencillo: obtienes datos limpios sin HTML.

Verificación: tienes anotados los selectores de la tarjeta, el precio, el título, la dirección y el enlace, y sabes el nombre del parámetro de paginación. Al hacer clic en cada selector en el panel, ves el resaltado del elemento correspondiente en la página.

Posibles problemas

El panel de desarrollador muestra el HTML, pero allí no está el precio. Causa: el sitio dibuja parte de los datos con un script después de la carga. Solución: en la pestaña Network verifica si el precio llega en una petición aparte, o usa la automatización del navegador de la sección para avanzados.

Paso 3: Escribimos el primer parser de CIAN para la página de resultados

Objetivo de la etapa: obtener un script que descargue la página de resultados, extraiga la lista de objetos y los imprima en la consola. Después de este paso tendrás un esqueleto funcional sobre el que iremos añadiendo funciones.

  1. Crea en la carpeta del proyecto el archivo parser.py.
  2. Importa las bibliotecas y la configuración al inicio del archivo:
import time
import random
import requests
from bs4 import BeautifulSoup
from config import BASE_URL, SEARCH_PARAMS, MAX_PAGES, PAUSE_MIN, PAUSE_MAX
  1. Describe los encabezados de la petición. El servidor debe ver en ellos un navegador normal con configuración regional rusa, de lo contrario puedes recibir una versión distinta de la página:
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0 Safari/537.36', 'Accept-Language': 'ru-RU,ru;q=0.9', 'Accept': 'text/html,application/xhtml+xml'}
  1. Escribe la función de carga de la página. Recibe el número de página, lo añade a los parámetros y devuelve el HTML. Verificamos obligatoriamente el código de respuesta: 200 significa éxito, todo lo demás es una señal para detenerse y averiguar qué pasa:
def fetch_page(page, session):
params = dict(SEARCH_PARAMS)
params['p'] = page
resp = session.get(BASE_URL, params=params, headers=HEADERS, timeout=30)
if resp.status_code != 200:
print('Статус', resp.status_code, 'на странице', page)
return None
return resp.text
  1. Escribe la función de análisis. Encuentra todas las tarjetas y para cada una extrae los campos. Presta atención a la construcción con if: si el elemento no existe, no nos caemos con un error, sino que registramos None:
def parse_cards(html):
soup = BeautifulSoup(html, 'lxml')
cards = soup.select('article[data-name=CardComponent]')
result = []
for card in cards:
link_tag = card.select_one('a[href*=/sale/flat/]')
price_tag = card.select_one('span[data-mark=MainPrice]')
title_tag = card.select_one('span[data-mark=OfferTitle]')
geo_tags = card.select('a[data-name=GeoLabel]')
if not link_tag or not price_tag:
continue
url = link_tag.get('href')
offer_id = url.rstrip('/').split('/')[-1]
price_text = price_tag.get_text()
price = int(''.join(ch for ch in price_text if ch.isdigit()))
title = title_tag.get_text(strip=True) if title_tag else None
address = ', '.join(g.get_text(strip=True) for g in geo_tags)
result.append({'offer_id': offer_id, 'url': url, 'title': title, 'price': price, 'address': address})
return result
  1. Arma el bucle principal. Recorre las páginas, hace una pausa aleatoria entre peticiones y acumula los resultados en una lista común. La pausa aleatoria es importante: los intervalos iguales se ven poco naturales y crean una carga en picos:
def collect_listing():
session = requests.Session()
all_offers = []
for page in range(1, MAX_PAGES + 1):
html = fetch_page(page, session)
if html is None:
break
offers = parse_cards(html)
print('Страница', page, 'объектов:', len(offers))
if not offers:
break
all_offers.extend(offers)
time.sleep(random.uniform(PAUSE_MIN, PAUSE_MAX))
return all_offers
if __name__ == '__main__':
data = collect_listing()
for item in data[:5]:
print(item)
print('Всего собрано:', len(data))
  1. Guarda el archivo y ejecútalo en la terminal con el comando python parser.py. Asegúrate de que el entorno venv esté activo.

Repasemos los puntos clave. Session conserva las cookies entre peticiones, por eso el sitio ve el comportamiento consecuente de un mismo visitante y no una decena de llamadas dispersas. La función select_one devuelve el primer elemento que coincide o None, por eso siempre verificamos el resultado antes de llamar a get_text. El identificador del anuncio lo tomamos del enlace: es el último fragmento de la dirección, un número como 312456789. Precisamente ese será la clave para el historial de precios.

Consejo: en la etapa de depuración guarda el HTML de la primera página en un archivo con el comando open('page1.html', 'w', encoding='utf-8').write(html). Así podrás depurar la función de análisis en una copia local, sin enviar peticiones de más al sitio.

Verificación: en la consola ves las líneas Страница 1 объектов: 28, Страница 2 объектов: 28 y así sucesivamente, y al final cinco diccionarios con precios y direcciones reales. Los precios deben ser números enteros sin espacios ni el signo del rublo. Si en lugar de números aparece vacío, vuelve a los selectores del segundo paso.

Posibles problemas

El script imprime объектов: 0 en la primera página. Causas: cambió el selector de la tarjeta o el servidor devolvió una página señuelo. Abre el page1.html guardado en el navegador y mira qué recibiste. Si es una página que pide confirmar que no eres un robot, aumenta las pausas y pasa al quinto paso con proxies. Si es una página de resultados normal, verifica los selectores.

Error ValueError al convertir el precio. Causa: en el texto del precio no hay dígitos, por ejemplo dice Precio a consultar. Solución: envuelve la conversión en try y registra None para esos casos.

Paso 4: Recolectamos las fichas de los objetos

Objetivo de la etapa: enseñar al parser a entrar a la página de cada objeto y extraer características detalladas: superficie, piso, tipo de edificio, año de construcción. Estos campos se necesitan para calcular el precio por metro cuadrado y comparar departamentos similares.

  1. Abre en el navegador la página de cualquier objeto de los resultados. Pulsa Ctrl+U para ver el código fuente de la página.
  2. Pulsa Ctrl+F y escribe la palabra totalArea. Al momento de escribir esta guía los datos de la ficha están en una etiqueta script dentro del objeto de configuración del frontend, en una clave con un nombre del tipo frontend-offer-card. Verás los campos totalArea, floorNumber, floorsCount, buildYear, materialType y otros.
  3. Si la búsqueda por totalArea no dio nada, busca las características en el HTML: normalmente es un bloque con pares nombre y valor, por ejemplo Superficie total y 38,5 m². Anota el selector de ese bloque.
  4. Añade en parser.py una función de extracción del JSON de la ficha. Encontramos el script necesario, recortamos de él el objeto por las llaves y lo analizamos con el módulo json:
import json
import re
def parse_offer_page(html):
soup = BeautifulSoup(html, 'lxml')
details = {}
for script in soup.find_all('script'):
text = script.string or ''
if 'totalArea' in text and 'offerData' in text:
m = re.search(r'totalArea[^0-9]*([0-9.,]+)', text)
if m:
details['area'] = float(m.group(1).replace(',', '.'))
m = re.search(r'floorNumber[^0-9]*([0-9]+)', text)
if m:
details['floor'] = int(m.group(1))
m = re.search(r'floorsCount[^0-9]*([0-9]+)', text)
if m:
details['floors_total'] = int(m.group(1))
m = re.search(r'buildYear[^0-9]*([0-9]{4})', text)
if m:
details['build_year'] = int(m.group(1))
break
return details
  1. Aquí usamos a propósito expresiones regulares en lugar de un análisis completo del JSON. La razón es simple: el script de la página contiene no solo el JSON, sino también código, y aislar un objeto limpio a veces es difícil. Las expresiones regulares buscan la clave y el primer número después de ella, lo que es bastante fiable para campos numéricos.
  2. Añade una función que toma la lista de objetos de los resultados y enriquece cada uno con los datos de la ficha. Las pausas aquí son aún más importantes, porque el número de peticiones se multiplica por 28:
def enrich_offers(offers, session):
for i, offer in enumerate(offers, 1):
try:
resp = session.get(offer['url'], headers=HEADERS, timeout=30)
if resp.status_code == 200:
offer.update(parse_offer_page(resp.text))
else:
print('Карточка', offer['offer_id'], 'статус', resp.status_code)
except requests.RequestException as e:
print('Ошибка сети на', offer['offer_id'], e)
if i % 10 == 0:
print('Обработано карточек:', i)
time.sleep(random.uniform(PAUSE_MIN, PAUSE_MAX))
return offers
  1. En el bloque if __name__ después de collect_listing añade la llamada enrich_offers(data, requests.Session()) y ejecuta el script de nuevo con MAX_PAGES igual a 1, para no esperar mucho.

Cuánto tarda: 28 fichas con una pausa media de 6 segundos son unos 3 minutos. La recolección completa de 5 páginas de resultados con fichas tardará unos 15 minutos. Eso es normal. Un parser inmobiliario no debe ser rápido, debe ser estable.

Consejo: no vuelvas a parsear las fichas en cada ejecución. Las características del departamento no cambian: la superficie y el año de construcción basta con recolectarlos una vez. Solo cambia el precio, y está en los resultados. En el sexto paso haremos que la ficha se solicite solo para los objetos nuevos. Eso reducirá el número de peticiones decenas de veces.

Verificación: en la salida de los diccionarios aparecieron las claves area, floor, floors_total y build_year con valores plausibles: superficie de 15 a 200, piso no mayor que la cantidad de pisos, año de 1900 a 2026. Si en parte de los objetos faltan campos, es normal: no todos los vendedores completan el año de construcción.

Posibles problemas

La expresión regular encuentra la superficie de la habitación en lugar de la total. Causa: en el JSON hay claves parecidas como livingArea o kitchenArea. Solución: precisa el patrón añadiendo una comilla o dos puntos antes de la clave, para que no coincida con parte de otra palabra.

Paso 5: Conectamos proxies móviles y hacemos la recolección sostenible

Objetivo de la etapa: distribuir las peticiones a través de proxies móviles con rotación de IP, añadir reintentos y un manejo correcto de las respuestas. Después de este paso el parser podrá trabajar de forma regular y prolongada, sin crear una carga excesiva desde una sola dirección.

Por qué un parser inmobiliario necesita proxies móviles

Cualquier portal grande limita la frecuencia de peticiones desde una misma dirección IP. Es una protección contra la sobrecarga, y se activa ante cualquier automatización. La dirección de casa, después de varios cientos de peticiones, empieza a recibir respuestas 429 o páginas con verificación. Los proxies móviles resuelven la tarea de otra forma: obtienes una IP de un pool de un operador celular, y por comando o por temporizador la dirección cambia. Tus peticiones se distribuyen entre direcciones, la carga sobre cada una se mantiene baja, y el parser funciona con normalidad. Para el monitoreo regular de precios esto es fundamental: no necesitas datos únicos, sino cortes diarios durante meses.

Configuración

  1. Abre el panel de tu servicio de proxies móviles y encuentra el proxy comprado. Copia cuatro valores: host, puerto, usuario, contraseña. Copia también el enlace para el cambio de IP: normalmente se ve como una dirección con una clave, y al acceder a ella el proxy recibe una nueva dirección.
  2. Añade en config.py los parámetros del proxy. Nunca escribas las contraseñas en el código que publicas en algún lugar: mantenlas en un archivo aparte o en variables de entorno:
PROXY_HOST = 'ваш_хост'
PROXY_PORT = 'ваш_порт'
PROXY_USER = 'ваш_логин'
PROXY_PASS = 'ваш_пароль'
ROTATE_URL = 'ссылка_для_смены_ip'
ROTATE_EVERY = 25
  1. Añade en parser.py la función de creación de sesión con proxy. La biblioteca requests acepta un diccionario con las direcciones para http y https:
from config import PROXY_HOST, PROXY_PORT, PROXY_USER, PROXY_PASS, ROTATE_URL, ROTATE_EVERY
def make_session():
session = requests.Session()
proxy_url = 'http://' + PROXY_USER + ':' + PROXY_PASS + '@' + PROXY_HOST + ':' + PROXY_PORT
session.proxies = {'http': proxy_url, 'https': proxy_url}
return session
def rotate_ip():
try:
r = requests.get(ROTATE_URL, timeout=20)
print('Смена IP:', r.status_code)
time.sleep(5)
except requests.RequestException as e:
print('Не удалось сменить IP:', e)
  1. Verifica que el proxy funciona. Crea un archivo temporal check_proxy.py con el código que solicita un servicio de detección de IP a través de la sesión e imprime la respuesta:
from parser import make_session
s = make_session()
print(s.get('https://api.ipify.org', timeout=20).text)
  1. Ejecútalo. Debes ver una dirección IP distinta de la de tu casa. Llama a rotate_ip y ejecuta la verificación otra vez: la dirección debe cambiar.
  2. Ahora añade la función de petición con reintentos. Maneja tres situaciones: respuesta exitosa, respuesta 429 o 403 (hay que esperar y cambiar de dirección), error de red (repetir):
def safe_get(session, url, params=None, retries=3):
for attempt in range(1, retries + 1):
try:
resp = session.get(url, params=params, headers=HEADERS, timeout=30)
if resp.status_code == 200:
return resp
if resp.status_code in (429, 403):
print('Статус', resp.status_code, 'попытка', attempt, 'меняем IP и ждем')
rotate_ip()
time.sleep(30 * attempt)
continue
print('Неожиданный статус', resp.status_code)
return None
except requests.RequestException as e:
print('Сетевая ошибка', e, 'попытка', attempt)
time.sleep(10 * attempt)
return None
  1. Reemplaza las llamadas a session.get en fetch_page y enrich_offers por safe_get. Añade en enrich_offers un contador: cada ROTATE_EVERY peticiones llama a rotate_ip. Esta es una rotación planificada que evita que la dirección acumule demasiadas llamadas.

Atención: si recibiste una respuesta 429 o una página con verificación, no intentes atravesarla con repeticiones frecuentes. Eso solo empeorará la situación para la dirección actual. La reacción correcta es detenerse, aumentar las pausas, cambiar de IP y continuar a un ritmo tranquilo. Un parser que respeta los límites del sitio vive más y recolecta más.

Consejo: usa un canal de proxy por cada flujo de parsing. La tentación de lanzar diez flujos a través de una sola dirección es grande, pero es el camino directo a las restricciones. Si necesitas velocidad, compra varios canales y distribuye entre ellos distintas regiones o distintos filtros.

Verificación: check_proxy.py muestra una dirección de operador móvil, y después de la rotación la dirección cambia. El parser recorre dos páginas de resultados con fichas sin un solo estado 429. En el log se ven las líneas Смена IP: 200 cada 25 fichas.

Posibles problemas

Error ProxyError o 407. Causa: usuario o contraseña incorrectos, o puerto equivocado. Solución: verifica los datos en el panel, asegúrate de usar el puerto para proxy HTTP, no SOCKS. Si tienes SOCKS5, instala la biblioteca pysocks y usa el prefijo socks5h en lugar de http en proxy_url.

Después de la rotación la dirección no cambia. Causa: el operador entregó la misma dirección o la rotación todavía no se aplicó. Solución: aumenta la pausa después de rotate_ip hasta 10 segundos y verifica si la frecuencia de cambio de IP está limitada en tu tarifa.

Paso 6: Guardamos los datos y construimos el historial de precios

Objetivo de la etapa: pasar el parser de imprimir en la consola a escribir en una base de datos SQLite, de modo que cada ejecución añada un nuevo punto en el historial de precios en vez de sobrescribir el anterior. Este es el corazón de todo el proyecto.

Diseñamos las tablas

Necesitamos dos tablas. La primera, offers, almacena el objeto: una fila por anuncio con las características y las fechas. La segunda, prices, almacena el precio por fecha: varias filas por anuncio. La separación es necesaria para no duplicar la superficie y la dirección en cada registro de precio.

  1. Crea el archivo storage.py y describe la creación de las tablas:
import sqlite3
from datetime import date
from config import DB_PATH
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.execute('CREATE TABLE IF NOT EXISTS offers (offer_id TEXT PRIMARY KEY, url TEXT, title TEXT, address TEXT, area REAL, floor INTEGER, floors_total INTEGER, build_year INTEGER, first_seen TEXT, last_seen TEXT, is_active INTEGER DEFAULT 1)')
conn.execute('CREATE TABLE IF NOT EXISTS prices (offer_id TEXT, checked_on TEXT, price INTEGER, PRIMARY KEY (offer_id, checked_on))')
conn.commit()
return conn
  1. Añade una función que devuelva el conjunto de offer_id ya conocidos. Se necesita para solicitar las fichas solo de los objetos nuevos:
def known_ids(conn):
rows = conn.execute('SELECT offer_id FROM offers').fetchall()
return set(r[0] for r in rows)
  1. Escribe la función de guardado. Para un objeto nuevo insertamos una fila en offers. Para cualquier objeto actualizamos last_seen y registramos el precio de hoy. La construcción INSERT OR REPLACE en prices significa: si hoy ya se registró el precio, actualizar, de lo contrario añadir:
def save_offers(conn, offers):
today = date.today().isoformat()
for o in offers:
conn.execute('INSERT OR IGNORE INTO offers (offer_id, url, title, address, area, floor, floors_total, build_year, first_seen, last_seen) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', (o['offer_id'], o['url'], o.get('title'), o.get('address'), o.get('area'), o.get('floor'), o.get('floors_total'), o.get('build_year'), today, today))
conn.execute('UPDATE offers SET last_seen = ?, is_active = 1 WHERE offer_id = ?', (today, o['offer_id']))
if o.get('price'):
conn.execute('INSERT OR REPLACE INTO prices (offer_id, checked_on, price) VALUES (?, ?, ?)', (o['offer_id'], today, o['price']))
conn.commit()
  1. Añade una función que marque los objetos retirados. Si el anuncio no apareció en los resultados durante más de tres días, lo consideramos inactivo. Esto te da datos sobre la velocidad de venta:
def mark_inactive(conn, days=3):
conn.execute('UPDATE offers SET is_active = 0 WHERE julianday(?) - julianday(last_seen) > ?', (date.today().isoformat(), days))
conn.commit()
  1. Reescribe el bloque principal de parser.py para que recolecte los resultados, determine los objetos nuevos, enriquezca solo esos y guarde todo:
from storage import get_conn, known_ids, save_offers, mark_inactive
if __name__ == '__main__':
conn = get_conn()
session = make_session()
listing = collect_listing(session)
old = known_ids(conn)
new_offers = [o for o in listing if o['offer_id'] not in old]
print('Новых объектов:', len(new_offers), 'из', len(listing))
enrich_offers(new_offers, session)
save_offers(conn, listing)
mark_inactive(conn)
print('Сохранено. Всего в базе:', conn.execute('SELECT COUNT(*) FROM offers').fetchone()[0])
  1. No olvides modificar collect_listing para que reciba session como parámetro en lugar de crear la suya. Ejecuta el script. En la carpeta del proyecto aparecerá el archivo realty.db.

Miramos el historial de precios

Crea el archivo report.py, que exporta los cambios de precio a Excel. La consulta de abajo encuentra los objetos en los que el último precio difiere del primero:

import sqlite3
import pandas as pd
from config import DB_PATH
conn = sqlite3.connect(DB_PATH)
query = 'SELECT o.offer_id, o.address, o.area, o.url, MIN(p.checked_on) AS first_date, MAX(p.checked_on) AS last_date, (SELECT price FROM prices WHERE offer_id = o.offer_id ORDER BY checked_on ASC LIMIT 1) AS first_price, (SELECT price FROM prices WHERE offer_id = o.offer_id ORDER BY checked_on DESC LIMIT 1) AS last_price FROM offers o JOIN prices p ON p.offer_id = o.offer_id GROUP BY o.offer_id'
df = pd.read_sql(query, conn)
df['change_pct'] = ((df['last_price'] - df['first_price']) / df['first_price'] * 100).round(1)
df['price_per_m2'] = (df['last_price'] / df['area']).round(0)
df = df.sort_values('change_pct')
df.to_excel('report.xlsx', index=False)
print(df.head(10))

Después de la primera ejecución la columna change_pct será cero: todavía no hay historial. A partir del segundo día aparecerán los primeros cambios. En dos semanas verás el panorama: cuántos objetos bajaron de precio, en cuánto en promedio, qué barrios se mueven más rápido.

Consejo: añade al final de parser.py la copia de la base: shutil.copy(DB_PATH, 'backup_' + date.today().isoformat() + '.db'). Una sola línea de código protegerá semanas de datos acumulados. Una vez al mes borra las copias antiguas, dejando las últimas cinco.

Verificación: el archivo realty.db existe, report.xlsx se abre en Excel y contiene columnas con dirección, superficie, precios y precio por metro cuadrado. Ejecuta parser.py otra vez después de unos minutos: la línea Новых объектов debe mostrar 0 o un número pequeño, y las fichas no deben solicitarse de nuevo. Esto confirma que la deduplicación funciona.

Posibles problemas

Error database is locked. Causa: la base está abierta en otro programa, por ejemplo en un visor de SQLite, o dos instancias del script trabajan al mismo tiempo. Solución: cierra los programas de más y no ejecutes el script en paralelo consigo mismo.

En report.xlsx todos los objetos muestran la misma fecha. Causa: el script se ejecutó solo un día. Es lo esperado, simplemente espera las siguientes ejecuciones.

Paso 7: Automatizamos la ejecución y ampliamos a otros portales

Objetivo de la etapa: hacer que el parser se ejecute solo cada mañana y preparar el código para conectar otros portales inmobiliarios. El historial de precios solo vale si es regular, por eso la automatización es obligatoria.

Programación de la ejecución

  1. En Windows abre el Programador de tareas desde la búsqueda en el menú Inicio. Pulsa Crear tarea básica. Escribe un nombre, por ejemplo Parser inmobiliario.
  2. Elige el desencadenador Diariamente, fija una hora, por ejemplo 07:30. La madrugada es cómoda: la carga sobre los sitios es mínima, y al inicio de la jornada laboral ya tienes datos frescos.
  3. En la acción elige Iniciar un programa. En el campo Programa indica la ruta completa a python.exe dentro de la carpeta venv, por ejemplo C:\projects\cian_parser\venv\Scripts\python.exe. En el campo Argumentos escribe parser.py. En el campo Carpeta de inicio indica la carpeta del proyecto.
  4. Guarda la tarea y pulsa Ejecutar en el panel derecho para verificarla. En la carpeta del proyecto debe actualizarse la base.
  5. En macOS y Linux usa cron. Escribe en la terminal crontab -e y añade la línea: 30 7 * * * cd /путь/к/cian_parser && ./venv/bin/python parser.py >> run.log 2>&1. El log de todas las ejecuciones se acumulará en run.log.

Preparación para otros portales

Domclick, Yandex Realty, Metr Kvadratny y los portales regionales están organizados de forma parecida, pero sus selectores y parámetros de filtro son propios. Para no reescribir el parser para cada uno, separa lo que difiere en módulos aparte.

  1. Crea la carpeta sites dentro del proyecto. En ella crea el archivo cian.py y traslada allí las funciones fetch_page y parse_cards junto con los parámetros de búsqueda. Déjales una interfaz idéntica: la función parse_cards recibe HTML y devuelve una lista de diccionarios con las mismas claves offer_id, url, title, price, address.
  2. Para un nuevo portal crea un archivo, por ejemplo domclick.py, y repite la investigación del segundo paso: abre los resultados, encuentra la tarjeta, el precio, el enlace y el parámetro de paginación. Escribe tus propias versiones de fetch_page y parse_cards.
  3. En offer_id añade el prefijo del portal, por ejemplo cian_312456789 y domclick_98765. De lo contrario los identificadores de sitios distintos pueden coincidir y mezclar el historial.
  4. En parser.py importa los módulos de sites y lanza la recolección de cada uno en un ciclo. Las tablas de la base son comunes: un esquema para todas las fuentes, y la columna source te dirá de dónde viene el objeto.

Una observación importante sobre Domclick y Yandex Realty: estos portales usan activamente API internas en formato JSON, que se ven en la pestaña Network. Sus resultados suelen ser más cómodos de parsear que el HTML, pero la estructura de las respuestas cambia más seguido. Verifica los selectores y los campos una vez al mes.

Consejo: para un mismo objeto publicado en varios portales, los precios pueden diferir. La comparación de esos pares da una analítica interesante y ayuda a encontrar vendedores que en algún lugar bajaron el precio, pero olvidaron actualizarlo en otro. Los objetos se pueden emparejar por dirección, superficie y piso.

Verificación: la tarea del programador se ejecutó manualmente sin errores, en run.log o en el historial del programador se ve una marca exitosa. La estructura de carpetas contiene sites con al menos un módulo, el parser se lanza desde la raíz del proyecto y funciona como antes.

Posibles problemas

El programador dice que la tarea se completó, pero la base no se actualizó. Causa: el script arrancó desde otra carpeta de trabajo y creó una nueva base vacía en otro lugar. Solución: completa el campo Carpeta de inicio en la tarea o usa la ruta absoluta a la base en config.py.

Verificación del resultado: checklist del parser terminado

Recorre la lista y marca cada punto. Si todo está hecho, tienes un parser de CIAN completo con historial de precios.

  • El script parser.py se ejecuta con un comando desde el entorno activado sin errores de importación.
  • Los resultados se recolectan de varias páginas, la cantidad de objetos por página coincide con lo que ves en el navegador.
  • Los precios se guardan como números enteros, las direcciones son legibles, los enlaces se abren.
  • Las fichas se solicitan solo para los objetos nuevos, en el log se ve la línea Новых объектов con un número que disminuye en las ejecuciones repetidas.
  • Las peticiones pasan por un proxy móvil, check_proxy.py muestra la dirección del operador, la rotación la cambia.
  • Ante un estado 429 el parser hace una pausa y cambia de IP, en lugar de caerse.
  • El archivo realty.db crece, la tabla prices recibe nuevas filas cada día.
  • report.xlsx se genera y se abre, en unos días aparecerán en él cambios de precio distintos de cero.
  • La ejecución automática está configurada, el log registra cada corrida.
  • La copia de seguridad de la base se crea automáticamente.

Cómo probarlo de punta a punta

  1. Borra o renombra realty.db para empezar de cero.
  2. Fija MAX_PAGES igual a 2 y ejecuta parser.py. Cronometra: debe tardar unos 6-8 minutos contando las fichas.
  3. Ejecuta report.py y confirma que en el reporte hay unas 56 filas.
  4. Abre realty.db con cualquier visor de SQLite o ejecuta en Python la consulta SELECT COUNT(*) FROM prices. El número debe coincidir con la cantidad de objetos.
  5. Ejecuta parser.py otra vez. El tiempo de ejecución debe reducirse a un minuto, porque las fichas no se solicitan. El número de filas en prices no cambiará, ya que la fecha es la misma.
  6. Cambia a mano el precio de un objeto en la base por cualquier otro número, cambia la fecha del registro a la de ayer y ejecuta el parser. En report.xlsx ese objeto debe mostrar el cambio de precio. Así confirmarás que la lógica del historial funciona sin esperar cambios reales.

Indicadores de éxito

La proporción de objetos con la superficie completada supera el 90 por ciento. La proporción de peticiones con estado 200 supera el 97 por ciento. Ninguna excepción sin manejar durante la corrida. El tiempo de la corrida completa es predecible y no crece de una ejecución a otra. Si los indicadores están por debajo, vuelve a la sección de errores típicos.

Errores típicos y soluciones

Reunimos los problemas con los que se topan prácticamente todos los que escriben un scraper de anuncios inmobiliarios por primera vez. Formato: problema, causa, solución.

El parser devuelve 0 objetos, aunque ayer funcionaba

Causa: el portal actualizó el maquetado y cambió los atributos data-mark o data-name. Solución: abre los resultados en el navegador, repite el segundo paso y actualiza los selectores. Adquiere la costumbre de guardar los selectores en un solo lugar al inicio del módulo, para que las correcciones tomen un minuto. Añade al parser una verificación: si en la primera página hay 0 objetos, enviarte una notificación a un mensajero.

Los precios se guardan con un error de mil veces

Causa: en parte de los objetos el precio está indicado en miles o con la nota por mes, o en el texto se coló el precio por metro cuadrado. Solución: asegúrate de tomar precisamente MainPrice, y no el elemento vecino con el precio por metro. Añade una verificación de razonabilidad: un precio de venta de un departamento en Moscú por menos de un millón de rublos casi seguro es un error de análisis; registra esos casos en el log.

El estado 429 llega ya en la tercera página

Causa: las pausas son demasiado cortas o el proxy todavía no está conectado, todas las peticiones van desde una misma IP de casa. Solución: aumenta PAUSE_MIN y PAUSE_MAX hasta 6 y 12, conecta un proxy móvil, activa la rotación planificada cada 20-25 peticiones. Verifica que no lanzaste varias instancias del script al mismo tiempo.

Error UnicodeEncodeError al imprimir en la consola de Windows

Causa: la consola estándar de Windows no siempre muestra correctamente el cirílico. Solución: ejecuta en la terminal chcp 65001 antes de lanzar, o añade al inicio del script la línea con sys.stdout.reconfigure(encoding='utf-8'). También puedes escribir los logs en un archivo en lugar de la consola.

La dirección se arma incompleta o con duplicados

Causa: la dirección en la tarjeta se compone de varios enlaces GeoLabel, parte de ellos duplica la ciudad y el distrito. Solución: elimina los duplicados conservando el orden, o toma la dirección de la ficha del objeto, donde está representada como una sola cadena. Para la analítica por barrios añade un campo aparte district, recortándolo de la dirección según una lista conocida de barrios.

El parser funciona en ejecución manual, pero no en el programador

Causa: el programador usa otro intérprete de Python sin las bibliotecas instaladas u otra carpeta de trabajo. Solución: indica la ruta absoluta a python.exe dentro del venv y completa el campo de la carpeta de trabajo. Redirige la salida a un archivo de log para ver los errores.

La base pesa gigabytes después de un mes

Causa: guardas el HTML completo de las páginas o todos los campos del JSON en la base. Solución: almacena solo los campos necesarios. Si quieres guardar las páginas originales para un análisis posterior, ponlas en archivos comprimidos en el disco, no en SQLite. Una vez al trimestre ejecuta el comando VACUUM para compactar la base.

Objetos idénticos se duplican bajo identificadores distintos

Causa: el vendedor retiró el anuncio y lo publicó de nuevo, obteniendo un número nuevo. Solución: añade una clave adicional de emparejamiento a partir de la dirección, la superficie y el piso. Los objetos con la misma clave, pero con offer_id distintos, se pueden vincular en una tabla aparte y calcular el historial de precio por el vínculo. Esto ya es analítica avanzada, pero justamente ella muestra las rebajas reales escondidas detrás de la republicación.

Funciones adicionales para avanzados

El parser básico está listo. Si quieres más, aquí tienes las direcciones que dan el mayor retorno.

Automatización del navegador con Playwright

Algunas páginas cargan los datos con scripts después de la carga, y requests recibe un esqueleto vacío. En esos casos usa Playwright: controla un navegador real. Instálalo con los comandos pip install playwright y playwright install chromium. El proxy se pasa al lanzar el navegador en el parámetro proxy con un diccionario server, username, password. Espera la aparición de las tarjetas con page.wait_for_selector y pasa page.content() a la función parse_cards que ya escribiste. Ten en cuenta que el navegador consume diez veces más recursos, así que úsalo de forma puntual, solo para las páginas problemáticas.

Recolección paralela por varios canales de proxy

Si necesitas recolectar varias regiones, compra un proxy móvil aparte para cada región y lanza un proceso separado por canal. No uses multihilo dentro de un mismo canal: el sentido de distribuir la carga se pierde. Una forma simple: el parámetro de la región se pasa al script como argumento de línea de comandos, y el programador lanza varias tareas con argumentos distintos y un pequeño desfase en el tiempo.

Notificaciones de bajadas de precio

Añade al final del parser una comparación del precio de hoy con el anterior para cada objeto. Si la bajada supera un umbral dado, por ejemplo el 3 por ciento, arma un mensaje con la dirección, el precio viejo y el nuevo, el enlace, y envíatelo a través de un bot en un mensajero. Esto convierte el parser de herramienta de analítica en herramienta de acción: te enterarás de los objetos convenientes dentro de la hora siguiente al cambio.

Analítica y visualización

Con pandas puedes agrupar los datos por barrios y calcular el precio mediano del metro cuadrado, la proporción de objetos con rebaja, el tiempo medio de exposición (la diferencia entre first_seen y last_seen para los objetos inactivos). La biblioteca matplotlib construirá una gráfica de la dinámica del mes en tres líneas de código. Sube el reporte a Google Sheets, y tus colegas sin habilidades de programación obtendrán un panel vivo de indicadores.

Almacenamiento en PostgreSQL

Cuando los objetos superen los cien mil, SQLite empezará a ralentizarse en las consultas analíticas. La migración a PostgreSQL es simple: el esquema de las tablas es el mismo, solo cambia la cadena de conexión y la biblioteca (psycopg2 en lugar de sqlite3). Hazlo solo si realmente lo necesitas: para una ciudad, SQLite alcanza para años.

Monitoreo de la salud del parser

Registra en una tabla aparte runs la hora de inicio, la hora de fin, el número de objetos recolectados, el número de errores y el número de rotaciones de IP. Si los objetos disminuyeron bruscamente o los errores superan el 5 por ciento, envía una notificación. Un monitoreo así permite notar el cambio de maquetado el día en que aparece, y no dos semanas después por un reporte vacío.

FAQ: preguntas frecuentes sobre crear un parser inmobiliario

¿Es legal hacer scraping en CIAN y otros portales?

La recolección de información pública sobre los objetos para analítica personal en general es admisible, pero las condiciones de cada portal están descritas en sus términos de uso, y hay que leerlos. Está terminantemente prohibido recolectar datos personales de vendedores, publicar una base copiada como propia y crear una carga que impida el funcionamiento del servicio. Si planeas un uso comercial de los datos, consulta con un abogado.

¿Por qué proxies móviles y no de servidor?

Las direcciones de los operadores móviles son comunes para miles de abonados reales y cambian constantemente. Los portales las tratan con más indulgencia que a las direcciones de centros de datos, desde donde los compradores reales casi no entran. Además, la posibilidad de cambiar de IP por un enlace da una rotación controlada sin comprar cientos de direcciones.

¿Con qué frecuencia ejecutar el parser para el historial de precios?

Una vez al día es óptimo. Los precios inmobiliarios cambian poco; recolectar más de una vez al día no tiene sentido y la carga crece. Si necesitas detectar bajadas con rapidez, ejecútalo dos veces al día, mañana y tarde, pero solo con un filtro estrecho.

¿Cuántos objetos se pueden recolectar al día a través de un proxy?

Con pausas de 4-9 segundos y rotación planificada son unas 500-700 peticiones por hora sin problemas, o 8-12 mil al día con trabajo continuo. Para monitorear una ciudad suelen bastar 2-3 mil peticiones al día, porque las fichas se solicitan solo para los objetos nuevos.

¿Qué hacer si los selectores cambiaron y no logro encontrarlos?

Vuelve al segundo paso y avanza desde el precio: clic derecho en el precio, Ver código, sube por el árbol hasta la tarjeta. Busca atributos con la palabra data y nombres con sentido: son más estables que las clases con símbolos aleatorios. Revisa también la pestaña Network: quizá los datos empezaron a llegar en una petición JSON aparte, y parsearlos será incluso más fácil.

¿Se puede prescindir de los proxies en un proyecto pequeño?

Para una recolección puntual de dos o tres páginas, sí. Para un monitoreo diario con cientos de peticiones, la dirección de casa empezará a recibir restricciones rápidamente, y los datos quedarán incompletos. Un historial de precios con huecos pierde valor, por eso para el trabajo regular se necesitan proxies.

¿Cómo parsear alquiler en lugar de venta?

Cambia el parámetro deal_type a rent y añade el tipo de alquiler en los parámetros de búsqueda: de larga duración o por día. Los enlaces a los objetos contendrán rent en lugar de sale, así que actualiza el selector del enlace en parse_cards. El resto de la lógica, incluido el historial de precios, funciona sin cambios.

¿Hay que guardar las fotos de los objetos?

Para la analítica de precios, no. Las fotos ocupan mucho espacio y no se necesitan para los cálculos. Si construyes un catálogo para uso interno, guarda solo los enlaces a las imágenes, no los archivos en sí.

¿Cómo entender que el objeto se vendió y no solo se retiró?

Los portales no informan la causa del retiro. Un indicio indirecto: el objeto desapareció de los resultados y no reapareció en un mes. Los objetos que desaparecen y vuelven a aparecer a los pocos días con otro precio probablemente fueron republicados. Vincúlalos por dirección y superficie, como se describe en la sección de errores.

¿Qué hacer si necesito datos de diez ciudades?

Arma una lista de regiones en config.py y lanza la recolección en un ciclo con un canal de proxy aparte cada dos o tres ciudades. Desfasa las ejecuciones en el tiempo para no recolectar todo a la vez. La base sigue siendo común, añade el campo de región a la tabla offers.

Conclusión

Veamos lo que hiciste. Preparaste el entorno con Python y las bibliotecas, entendiste cómo están organizados los resultados de un portal inmobiliario y aprendiste a encontrar los selectores por tu cuenta. Escribiste un parser de CIAN que recolecta los resultados y las fichas de los objetos. Conectaste proxies móviles con rotación y reintentos, gracias a lo cual la recolección se volvió sostenible y predecible. Diseñaste una base con historial de precios, configuraste reportes y la ejecución automática por horario. Esta es una herramienta de trabajo completa, no un ejemplo de estudio.

Qué hacer después: deja que el parser trabaje dos semanas sin cambios. En ese tiempo se acumulará historial, y verás los puntos débiles: dónde cae la proporción de campos completados, qué objetos se duplican, dónde el reporte requiere nuevas columnas. Solo después de eso añade funciones. Luego conecta un segundo portal usando la estructura modular del séptimo paso: te sorprenderá lo mucho más rápido que irá la segunda vez.

Hacia dónde crecer: las notificaciones de bajadas de precio convertirán la herramienta en una fuente de operaciones. La analítica por barrios y tipos de edificio te hará un experto del mercado con números en la mano. El vínculo de objetos republicados mostrará los descuentos reales que no se ven en el sitio. Y una actitud cuidadosa con el portal, con pausas, rotación de direcciones a través de proxies móviles y recolección solo de los datos necesarios, permitirá que la herramienta funcione durante meses sin fallas.

El scraping inmobiliario no se trata de velocidad, sino de regularidad y calidad de los datos. Pusiste una base correcta. Mucha suerte con la recolección, y que tu base crezca cada mañana.