Saltar al contenido
Empezar gratis
Volver al blog

Web scraping inmobiliario: cómo recopilar datos de propiedades con Python

Robert MunceanuÚltima actualización el 24 min read
Web scraping inmobiliario: cómo recopilar datos de propiedades con Python
En resumen: el web scraping inmobiliario recopila anuncios, precios y características de propiedades de sitios web y los convierte en datos estructurados. Antes de hacer scraping, busca datos abiertos, un feed de MLS o una API de datos, porque Zillow, Realtor.com y Redfin prohíben la recopilación automatizada en sus términos. Esta guía cubre el modelo de datos, las fuentes, los límites legales, el código en Python y los controles de calidad.

Tarde o temprano alguien te pide "conseguir datos inmobiliarios" de un mercado: precios de oferta para comparables, el inventario para un informe o un feed de anuncios nuevos. Los datos inmobiliarios están por toda la web, así que el scraping parece la respuesta obvia. Pero dos errores hunden la mayoría de estos proyectos. El primero es hacer scraping de un portal cuyos términos lo prohíben. El segundo es tratar un precio de oferta como un precio de venta y construir el análisis sobre la cifra equivocada.

Empieza por la decisión: qué datos existen, qué fuente encaja con cada tarea y qué permiten los grandes portales. Después, crea un scraper en Python con requests, BeautifulSoup y pandas sobre una página de ejemplo que controlas y obtén precios reales de vivienda de un archivo de datos abiertos gratuito.

¿Qué es el web scraping inmobiliario?

El web scraping inmobiliario es la recopilación automatizada de información de propiedades desde sitios web, como precios de oferta, direcciones, dormitorios, superficie, fotos y días en el mercado. Un scraper descarga las páginas, extrae los campos y los guarda como filas en un archivo CSV o una base de datos, para que puedas comparar mercados o seguir cambios a lo largo del tiempo.

El scraping es solo una forma de obtener esa información. Las mismas cifras suelen estar disponibles como descargas gratuitas, feeds de MLS con licencia o API de datos, y esas vías suelen ser más baratas de mantener y más seguras. Compara las opciones antes de escribir una sola línea de código.

¿Qué datos inmobiliarios se pueden extraer con scraping?

La mayoría de las páginas de anuncios muestran la dirección, el precio de oferta o el alquiler, los dormitorios, los baños, la superficie habitable, el tamaño del terreno, el tipo de propiedad, el estado, la fecha de publicación, las fotos y el agente responsable. Esos campos describen tres cosas distintas: la propiedad, el anuncio que la promociona y cada precio que observaste en una fecha concreta.

Una vivienda puede salir al mercado tres veces en cinco años, con dos agencias y a cinco precios distintos. Si guardas todo eso en una sola fila, pierdes el historial que hace útiles los datos. Modélalo en tres capas:

Capa

Qué es

Campos de ejemplo

Clave para deduplicar

Propiedad

La vivienda física, que rara vez cambia

Dirección, número de parcela, dormitorios, baños, superficie habitable, tamaño del terreno, año de construcción

Dirección normalizada o ID de parcela

Episodio de anuncio

Una vez en que la vivienda estuvo en el mercado

Sitio de origen, ID del anuncio, estado, fecha de publicación, agencia, URL

Origen + ID del anuncio

Observación

Lo que viste en un momento dado

Precio, estado, días en el mercado, collected_at

ID del anuncio + marca de tiempo

Los precios requieren el mismo cuidado. Una página de anuncio puede mostrar varias cifras que se parecen pero significan cosas distintas:

Tipo de precio

Qué significa

¿Es una transacción?

Precio de oferta (anunciado)

Lo que el vendedor pide hoy; puede bajar o subir

No

Precio de venta

Lo que pagó el comprador al cerrar la operación

Sí

Alquiler pedido

Lo que pide un arrendador por periodo (mes o semana)

No, y necesita un periodo

Estimación automática

La estimación de valor de un modelo, como un Zestimate

No

Valor catastral

El valor que usa una autoridad local para el impuesto sobre la propiedad

No

Guarda cada tipo en su propia columna o etiqueta cada precio con su tipo. Nuestros datos de anuncios inmobiliarios siguen la misma idea: una propiedad, los anuncios que la promocionan y observaciones de precio con fecha, porque un anuncio te dice lo que alguien pidió, no lo que alguien pagó.

Una regla más antes de recopilar nada: los nombres, teléfonos y correos electrónicos de los agentes son datos personales según la mayoría de las leyes de privacidad. Déjalos fuera, salvo que tengas una base legal para conservarlos.

¿Quién usa el scraping inmobiliario y para qué?

Los agentes y las agencias lo usan para comparables de venta y fijación de precios. Los inversores, para encontrar propiedades infravaloradas y seguir la rentabilidad de los alquileres. Los equipos de proptech construyen feeds de anuncios y modelos de valoración. Investigadores y periodistas siguen el inventario y las rebajas de precio. Compradores e inquilinos vigilan un barrio.

Lo importante es asociar cada tarea con los campos que necesita y con la fuente que los ofrece con menos riesgo:

Caso de uso

Campos que necesitas

Mejor primera fuente

Comparables y precios

Precio de venta, dormitorios, baños, superficie, fecha de venta

Feed de MLS a través de una agencia o datos con licencia

Seguimiento del mercado

Inventario, precio de oferta mediano, días en el mercado por área metropolitana o código postal

Descargas de datos abiertos

Selección de inversiones

Precio de oferta, alquiler, impuestos, historial de precios

API de datos o datos con licencia, más índices de alquiler abiertos

Agregación de anuncios

Registros completos de anuncios con fotos

IDX de MLS o un feed con licencia; aquí el scraping es la vía más arriesgada

Generación de leads

Contactos de propietarios o agentes

Con cautela: se aplican normas de datos personales y antispam

Búsqueda personal de vivienda

Alertas sobre unos pocos anuncios

Las alertas de búsquedas guardadas del propio portal

Si tu tarea está en las tres primeras filas, puede que no necesites un scraper.

¿Conviene hacer scraping, descargar o licenciar datos inmobiliarios?

Descarga primero si necesitas cifras a nivel de mercado, ya que Zillow, Redfin y Realtor.com publican archivos gratuitos de datos de vivienda. Licencia un feed de MLS o una API de datos si necesitas registros de anuncios a escala. Haz scraping solo de páginas cuyos términos lo permitan, o de tus propios sitios, de los de tus socios y de fuentes públicas que admitan el acceso automatizado.

Vía

Qué te da

Costo y esfuerzo

Dificultad de acceso

Ideal para

Ten en cuenta

Descargas de datos abiertos (Zillow Research, Redfin Data Center, biblioteca de datos de Realtor.com, FHFA HPI)

Agregados por área metropolitana, condado y código postal

Gratis; unas pocas líneas de pandas

Ninguna

Tendencias, informes, análisis de mercado

Frecuencia mensual; sin filas por anuncio

MLS mediante RESO Web API

Registros de anuncios oficiales, incluidos datos de ventas

Tarifas de licencia y contratos

Alta: membresía, agencia o proveedor aprobado

Agencias, sitios IDX, valoración

Normas de visualización y uso

API o dataset de datos inmobiliarios

Registros de anuncios estructurados sin ejecutar scrapers

Precio a consultar

Baja a media

Productos que necesitan anuncios sin un equipo de parsers

Términos, cobertura y actualización del proveedor

Scraper API

HTML renderizado de páginas que puedes recopilar; tú escribes el parser

Pago por solicitud

Baja

Sitios de agencias, feeds de socios, fuentes públicas permitidas

El parser y la revisión de cumplimiento siguen siendo tuyos

Scraper propio (requests, BeautifulSoup, Playwright)

Lo que construyas

Tu tiempo, servidores y proxies

Baja al empezar, alta al mantener

Trabajos pequeños y permitidos, prototipos

Roturas, bloqueos y cumplimiento

La mayoría de los equipos acaba combinando vías: datos abiertos para la visión del mercado, un feed o API para los registros de anuncios y un scraper para la larga cola de sitios permitidos.

Sus términos de uso lo prohíben. Zillow prohíbe las "automated queries", incluido el scraping y la evasión de captchas. Realtor.com prohíbe el scraping de pantalla y de bases de datos. Redfin prohíbe el rastreo automatizado sin permiso por escrito. Incumplir los términos puede acarrear cuentas suspendidas, IP bloqueadas o reclamaciones contractuales, incluso donde extraer páginas públicas no es un delito. Usa sus descargas de datos o una fuente con licencia.

Esto es lo que dicen los términos de cada gran portal, con sus propias palabras (citas en el inglés original):

Portal

Qué dicen los términos

robots.txt

Alternativa oficial

Zillow

Los términos de uso de Zillow (actualizados el 28 de octubre de 2025) prohíben realizar "automated queries (including screen and database scraping, spiders, robots, crawlers, bypassing 'captcha' or similar precautions…)". Copiar solo está permitido "without the aid of any automated processes". Los datos agregados de Local-Info pueden reutilizarse con el crédito "Data Provided by Zillow Group".

Archivo largo con reglas por ruta

Descargas de Zillow Research; API de Zillow Group según sus propios términos

Realtor.com

Los términos de servicio de Realtor.com prohíben el "screen scraping" y el "database scraping", así como la recopilación "by automated means", como un "robot, spider, script". Añaden que la configuración de robots.txt no amplía tus derechos.

Existe, pero no prevalece sobre los términos

Biblioteca de datos residenciales de Realtor.com (CSV)

Redfin

Los términos de uso de Redfin incluyen "No Scrubbing or Scraping": nada de rastreo ni consultas automatizadas "unless you have received prior express written permission". Los avisos de MLS en las páginas de anuncios repiten la prohibición.

Existe, con reglas por ruta

Descargas del Redfin Data Center

Rightmove (Reino Unido)

El pie de página de los términos de uso de Rightmove indica: "Rightmove prohibits the scraping of its content."

La prohibición se aplica diga lo que diga robots.txt

Pedir permiso por escrito a Rightmove

Otros portales (Zoopla, Idealista, realestate.com.au, Domain, Homes.com)

Lee la página de términos de cada sitio antes de recopilar nada

Revisar sitio por sitio

Revisar sitio por sitio

En EE. UU., el Tribunal de Apelaciones del Noveno Circuito resolvió en hiQ Labs v. LinkedIn (2022) que extraer páginas de acceso público probablemente no constituye "acceso sin autorización" según la Computer Fraud and Abuse Act. Esa sentencia no anula las demás normas. Los términos de uso son contratos, las fotos y descripciones de los anuncios tienen derechos de autor y las leyes de privacidad protegen a las personas que aparecen en los datos. Los propios términos de Zillow indican que la empresa no reclama derechos de autor sobre las imágenes y descripciones de los anuncios. Esos derechos pertenecen a sus propietarios. Fuera de EE. UU., los derechos sobre bases de datos de la UE y el RGPD añaden sus propios límites.

Esto no es asesoramiento jurídico. Consulta con un abogado antes de recopilar datos de un sitio cuyos términos lo prohíben. Para una visión más amplia, lee los fundamentos legales del web scraping.

¿Se pueden extraer datos de MLS con scraping?

No de forma legítima. Los datos de MLS se licencian a los miembros, y las normas IDX permiten a las agencias mostrar anuncios a los consumidores, pero no que terceros los copien. La vía legítima es un feed de RESO Web API, que se obtiene mediante una membresía de MLS, una agencia asociada o un proveedor de datos aprobado. Las páginas de los portales con datos de MLS tienen las mismas restricciones.

La RESO Web API es un estándar, no una fuente de datos. RESO publica un diccionario de datos común, de modo que campos como ListPrice, StandardStatus y BedroomsTotal significan lo mismo en cualquier MLS compatible. La Web API entrega esos campos por REST siguiendo las convenciones de OData. Aun así, alguien tiene que concederte el acceso. Las vías habituales son:

  • Agencia responsable (broker of record). Una agencia con licencia para la que trabajas o con la que colaboras solicita el feed a su MLS.
  • Proveedor aprobado por el MLS. Muchos MLS licencian datos a empresas tecnológicas mediante un contrato de proveedor.
  • Agregadores multi-MLS. Empresas que tienen acuerdos con muchos MLS y revenden un acceso normalizado.

Hay dos siglas más que aparecen a menudo. IDX (Internet Data Exchange) permite a las agencias participantes mostrar los anuncios de las demás en sus propios sitios web. Un VOW (virtual office website) muestra más detalles a los clientes registrados de una agencia. Ambos son programas de visualización con normas fijadas por el MLS. Ninguno permite que un tercero copie los anuncios en su propia base de datos.

Cuando tengas las credenciales, una solicitud se parece más o menos a esto. Solo muestra la estructura. Tu MLS te da el endpoint y los campos que tu licencia te permite leer.

import os

import requests

# Shape only: your MLS or data vendor supplies the endpoint, token, and fields.
BASE_URL = os.environ["RESO_BASE_URL"]
TOKEN = os.environ["RESO_ACCESS_TOKEN"]

response = requests.get(
    f"{BASE_URL}/Property",
    headers={"Authorization": f"Bearer {TOKEN}"},
    params={
        "$select": "ListingKey,ListPrice,StandardStatus,BedroomsTotal,City",
        "$filter": "StandardStatus eq 'Active' and City eq 'Springfield'",
        "$top": 100,
    },
    timeout=30,
)
listings = response.json()["value"]

Los parámetros $select, $filter y $top provienen de OData, así que el mismo patrón funciona en servidores certificados por RESO, con pequeñas diferencias en la sintaxis de los filtros.

¿Cómo funcionan los scrapers inmobiliarios?

Un scraper inmobiliario funciona en cinco pasos. Descubre las URL de los anuncios a partir de páginas de búsqueda o sitemaps, descarga cada página renderizando JavaScript cuando hace falta y extrae los campos de los datos estructurados o del HTML. Luego normaliza precios, superficies y direcciones. Por último, guarda cada observación con una marca de tiempo para que las ejecuciones posteriores detecten cambios de precio y de estado.

Descubrir las URL de los anuncios

Las URL de los anuncios salen de sitemaps, páginas de resultados de búsqueda y enlaces de paginación. Las páginas de búsqueda suelen cortarse tras un número fijo de resultados, así que divide las zonas grandes en otras más pequeñas por código postal o franja de precio. No rastrees todas las combinaciones de filtros. Los órdenes y filtros se multiplican hasta millones de URL que repiten casi siempre los mismos anuncios. Cuando un sitio que puedes recopilar publica un sitemap, empieza por ahí: enumera las URL canónicas de los anuncios y las fechas lastmod te indican qué páginas han cambiado desde tu última ejecución.

Descargar las páginas

Algunos sitios envían el HTML completo. Otros construyen la página en el navegador con JavaScript. Descarga la versión estática siempre que puedas, porque es más rápida y barata. Guarda las respuestas en caché, mantén baja la frecuencia de solicitudes y respeta robots.txt, tal como describe el Robots Exclusion Protocol. Recuerda que robots.txt nunca concede un permiso que los términos niegan.

Extraer los campos

Revisa las fuentes en este orden. Primero, bloques JSON-LD que usan tipos de schema.org como RealEstateListing o SingleFamilyResidence. Segundo, JSON incrustado en etiquetas script por el framework front-end del sitio. Consulta cómo analizar JSON incrustado en una página. Tercero, selectores HTML. Los datos estructurados cambian con menos frecuencia que el marcado, así que tu scraper se rompe menos.

Normalizar y deduplicar

Convierte "$1,250,000" en 1250000 con su moneda. Pasa todas las superficies a una sola unidad. Haz explícito el periodo del alquiler. Normaliza las direcciones para que la misma vivienda coincida entre fuentes y deduplica por esa dirección normalizada o por un ID de parcela.

Guardar observaciones y detectar cambios

Añade una fila nueva en cada ejecución en lugar de sobrescribir la anterior. Conserva collected_at, la URL de origen y el valor bruto junto al valor procesado. Comparar las filas de hoy con las de la semana pasada muestra rebajas de precio, cambios de estado y anuncios nuevos. En pandas, basta con un merge por ID de anuncio entre dos ejecuciones. Las filas que solo están en la ejecución nueva son anuncios nuevos, las que solo están en la antigua salieron del mercado y las que están en ambas con un precio distinto reflejan cambios de precio.

¿Cómo se hace scraping de datos inmobiliarios con Python?

Instala requests, BeautifulSoup y pandas. Descarga una página de anuncios que puedas recopilar, lee primero su JSON-LD y recurre a selectores HTML para los campos que falten. Convierte precios y superficies en números, elimina anuncios duplicados y exporta a CSV. Usa una API de scraping cuando las páginas necesiten renderizar JavaScript o resultados específicos por ubicación.

El tutorial usa un pequeño sitio de ejemplo con anuncios ficticios, para que puedas ejecutar cada paso sin tocar los servidores de nadie. El mismo código funciona en el sitio de tu propia agencia o en las páginas de un socio una vez que cambies los selectores.

Configurar el proyecto

Necesitas Python 3.12 o posterior. Crea una carpeta y un entorno virtual, e instala los cuatro paquetes:

mkdir property-scraper && cd property-scraper
python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install requests beautifulsoup4 lxml pandas

Empezar con una página de anuncios de ejemplo

Guarda la siguiente página de ejemplo como sample_listings.html. Tiene cuatro anuncios ficticios. Dos de ellos también aparecen en un bloque JSON-LD, que es como muchos sitios reales exponen datos estructurados. Uno es un alquiler y otro un estudio, así que el parser tiene que manejar ambos.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Homes for sale and rent in Springfield (sample page)</title>
  <script type="application/ld+json">
  {
    "@context": "https://schema.org",
    "@type": "ItemList",
    "itemListElement": [
      {
        "@type": "RealEstateListing",
        "identifier": "SPR-1001",
        "url": "https://example.com/listings/SPR-1001",
        "datePosted": "2026-10-12",
        "offers": {"@type": "Offer", "price": 425000, "priceCurrency": "USD"},
        "about": {
          "@type": "SingleFamilyResidence",
          "address": {"@type": "PostalAddress", "streetAddress": "123 Example St",
                      "addressLocality": "Springfield", "addressRegion": "ST", "postalCode": "00001"},
          "numberOfBedrooms": 3,
          "numberOfBathroomsTotal": 2,
          "floorSize": {"@type": "QuantitativeValue", "value": 1850, "unitCode": "FTK"}
        }
      },
      {
        "@type": "RealEstateListing",
        "identifier": "SPR-1002",
        "url": "https://example.com/listings/SPR-1002",
        "datePosted": "2026-10-20",
        "offers": {"@type": "Offer", "price": 1250000, "priceCurrency": "USD"},
        "about": {
          "@type": "SingleFamilyResidence",
          "address": {"@type": "PostalAddress", "streetAddress": "45 Sample Ave",
                      "addressLocality": "Springfield", "addressRegion": "ST", "postalCode": "00002"},
          "numberOfBedrooms": 5,
          "numberOfBathroomsTotal": 4.5,
          "floorSize": {"@type": "QuantitativeValue", "value": 4100, "unitCode": "FTK"}
        }
      }
    ]
  }
  </script>
</head>
<body>
  <ul class="results">
    <li class="listing-card" data-listing-id="SPR-1001" data-status="for_sale">
      <a class="listing-link" href="https://example.com/listings/SPR-1001">123 Example St, Springfield, ST 00001</a>
      <span class="price">$425,000</span>
      <span class="beds">3 bd</span> <span class="baths">2 ba</span> <span class="area">1,850 sqft</span>
    </li>
    <li class="listing-card" data-listing-id="SPR-1002" data-status="for_sale">
      <a class="listing-link" href="https://example.com/listings/SPR-1002">45 Sample Ave, Springfield, ST 00002</a>
      <span class="price">$1,250,000</span>
      <span class="beds">5 bd</span> <span class="baths">4.5 ba</span> <span class="area">4,100 sqft</span>
    </li>
    <li class="listing-card" data-listing-id="SPR-1003" data-status="for_rent">
      <a class="listing-link" href="https://example.com/listings/SPR-1003">7 Placeholder Ln, Unit 2B, Springfield, ST 00001</a>
      <span class="price">$2,400/mo</span>
      <span class="beds">2 bd</span> <span class="baths">1 ba</span> <span class="area">950 sqft</span>
    </li>
    <li class="listing-card" data-listing-id="SPR-1004" data-status="for_sale">
      <a class="listing-link" href="https://example.com/listings/SPR-1004">88 Demo Rd, Springfield, ST 00003</a>
      <span class="price">$389,900</span>
      <span class="beds">Studio</span> <span class="baths">1 ba</span> <span class="area">610 sqft</span>
    </li>
  </ul>
  <a class="next" href="sample_listings_2.html">Next</a>
</body>
</html>

El enlace "Next" apunta a una segunda página de ejemplo. Guárdala como sample_listings_2.html. Repite un anuncio de la página 1, algo que ocurre en sitios reales cuando los anuncios nuevos desplazan a los antiguos hacia abajo:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><title>Homes in Springfield, page 2 (sample page)</title></head>
<body>
  <ul class="results">
    <li class="listing-card" data-listing-id="SPR-1004" data-status="for_sale">
      <a class="listing-link" href="https://example.com/listings/SPR-1004">88 Demo Rd, Springfield, ST 00003</a>
      <span class="price">$389,900</span>
      <span class="beds">Studio</span> <span class="baths">1 ba</span> <span class="area">610 sqft</span>
    </li>
    <li class="listing-card" data-listing-id="SPR-1005" data-status="for_sale">
      <a class="listing-link" href="https://example.com/listings/SPR-1005">19 Fixture Ct, Springfield, ST 00002</a>
      <span class="price">$615,000</span>
      <span class="beds">4 bd</span> <span class="baths">2.5 ba</span> <span class="area">2,300 sqft</span>
    </li>
  </ul>
</body>
</html>

Sirve la carpeta en local para que tu scraper descargue las páginas por HTTP, como lo haría en un sitio real:

python -m http.server 8000

Deja el servidor en marcha y abre una segunda terminal para los siguientes pasos. Pon el código de este paso y de los cinco siguientes, en orden, en un único archivo llamado scraper.py. Empieza con las importaciones y una función de descarga que envía un User-Agent honesto y se detiene con un error explícito ante fallos HTTP:

import json
import re
import time
from datetime import datetime, timezone
from urllib.parse import urljoin

import requests
from bs4 import BeautifulSoup

HEADERS = {"User-Agent": "property-research/1.0 (you@example.com)"}


def fetch(url: str) -> str:
    response = requests.get(url, headers=HEADERS, timeout=30)
    response.raise_for_status()
    return response.text

Extraer primero el JSON-LD

El JSON-LD está en etiquetas <script type="application/ld+json">. Una página puede contener un objeto, una lista de objetos o una ItemList que agrupa los anuncios, así que la función auxiliar desempaqueta las tres formas. Después, convierte cada RealEstateListing en campos planos:

def iter_jsonld(soup: BeautifulSoup):
    """Yield every JSON-LD object on the page, unwrapping lists and ItemLists."""
    for tag in soup.find_all("script", type="application/ld+json"):
        try:
            data = json.loads(tag.string or "")
        except json.JSONDecodeError:
            continue
        for item in data if isinstance(data, list) else [data]:
            if item.get("@type") == "ItemList":
                for element in item.get("itemListElement", []):
                    yield element.get("item", element)
            else:
                yield item


def listings_from_jsonld(soup: BeautifulSoup) -> dict:
    found = {}
    for item in iter_jsonld(soup):
        if item.get("@type") != "RealEstateListing":
            continue
        home = item.get("about", {})
        addr = home.get("address", {})
        offer = item.get("offers", {})
        found[item["identifier"]] = {
            "listing_id": item["identifier"],
            "url": item.get("url"),
            "address": f"{addr.get('streetAddress')}, {addr.get('addressLocality')}, "
                       f"{addr.get('addressRegion')} {addr.get('postalCode')}",
            "price": offer.get("price"),
            "currency": offer.get("priceCurrency"),
            "beds": home.get("numberOfBedrooms"),
            "baths": home.get("numberOfBathroomsTotal"),
            "sqft": home.get("floorSize", {}).get("value"),
            "source": "json-ld",
        }
    return found

Los valores del JSON-LD ya son números, lo que te ahorra la mayor parte de la limpieza.

Recurrir a los selectores HTML

Dos de las cuatro tarjetas de la página 1 no tienen JSON-LD, y ninguna incluye el estado del anuncio en los datos estructurados. El parser HTML lee cada tarjeta mediante sus atributos data-* y nombres de clase, y conserva el texto bruto de cada campo. Si los selectores son nuevos para ti, empieza por nuestro tutorial de BeautifulSoup.

def text_of(card, selector: str) -> str | None:
    node = card.select_one(selector)
    return node.get_text(strip=True) if node else None


def listings_from_html(soup: BeautifulSoup, base_url: str) -> dict:
    found = {}
    for card in soup.select("li.listing-card"):
        link = card.select_one("a.listing-link")
        listing_id = card["data-listing-id"]
        found[listing_id] = {
            "listing_id": listing_id,
            "status": card.get("data-status"),
            "url": urljoin(base_url, link["href"]),
            "address": link.get_text(strip=True),
            "price_raw": text_of(card, ".price"),
            "beds_raw": text_of(card, ".beds"),
            "baths_raw": text_of(card, ".baths"),
            "sqft_raw": text_of(card, ".area"),
        }
    return found

En un sitio real, prefiere los atributos data-* y los nombres de clase estables a las rutas CSS largas. Aguantan mejor los rediseños.

Limpiar y normalizar los campos

Ahora convierte las cadenas brutas en números. parse_price elimina todo salvo los dígitos, así que "$1,250,000" pasa a 1250000 y "$2,400/mo" pasa a 2400. rent_period guarda la información "/mo" en su propia columna. normalize combina ambas fuentes, da prioridad al JSON-LD cuando tiene un valor, etiqueta cada precio como precio de oferta o alquiler pedido y añade a la fila una marca de tiempo en UTC:

def parse_price(text: str | None) -> int | None:
    """'$1,250,000' -> 1250000 and '$2,400/mo' -> 2400."""
    if not text:
        return None
    digits = re.sub(r"[^\d]", "", text.split("/")[0])
    return int(digits) if digits else None


def rent_period(text: str | None) -> str | None:
    if text and re.search(r"/\s*(mo|month)", text, re.I):
        return "month"
    if text and re.search(r"/\s*(wk|week)", text, re.I):
        return "week"
    return None


def parse_number(text: str | None) -> float | None:
    if not text:
        return None
    if "studio" in text.lower():
        return 0.0
    match = re.search(r"\d[\d,]*(\.\d+)?", text)
    return float(match.group().replace(",", "")) if match else None


def normalize(html_row: dict, jsonld_row: dict | None) -> dict:
    raw_price = html_row.get("price_raw")
    row = {
        "listing_id": html_row["listing_id"],
        "status": html_row.get("status"),
        "price": parse_price(raw_price),
        "currency": "USD" if raw_price and "$" in raw_price else None,
        "rent_period": rent_period(raw_price),
        "beds": parse_number(html_row.get("beds_raw")),
        "baths": parse_number(html_row.get("baths_raw")),
        "sqft": parse_number(html_row.get("sqft_raw")),
        "address": html_row.get("address"),
        "url": html_row.get("url"),
        "source": "html",
    }
    # Structured data wins wherever it has a value.
    for key, value in (jsonld_row or {}).items():
        if value is not None:
            row[key] = value
    row["price_type"] = "asking_rent" if row["status"] == "for_rent" else "asking_price"
    row["sqft"] = int(row["sqft"]) if row["sqft"] is not None else None
    row["collected_at"] = datetime.now(timezone.utc).isoformat(timespec="seconds")
    row["price_raw"] = raw_price
    return row


def parse_page(html: str, base_url: str) -> list[dict]:
    soup = BeautifulSoup(html, "lxml")
    structured = listings_from_jsonld(soup)
    cards = listings_from_html(soup, base_url)
    return [normalize(card, structured.get(lid)) for lid, card in cards.items()]

La columna source registra de dónde vienen los valores de cada fila. Cuando el JSON-LD desaparece de una página tras un rediseño, esa columna te avisa antes de que tus cifras sean erróneas.

Gestionar la paginación y los duplicados

El crawler sigue el enlace "Next" hasta que no hay más, espera entre solicitudes y omite los ID de anuncio que ya ha visto. También se detiene ante una página vacía y al llegar a max_pages, para que un enlace roto no lo meta en un bucle:

def crawl(start_url: str, max_pages: int = 20, delay: float = 2.0) -> list[dict]:
    rows, seen, url = [], set(), start_url
    for _ in range(max_pages):
        html = fetch(url)
        page_rows = parse_page(html, url)
        new_rows = [r for r in page_rows if r["listing_id"] not in seen]
        seen.update(r["listing_id"] for r in new_rows)
        rows.extend(new_rows)
        print(f"{url}: {len(page_rows)} cards, {len(new_rows)} new")

        next_link = BeautifulSoup(html, "lxml").select_one("a.next")
        if not page_rows or not next_link:
            break
        url = urljoin(url, next_link["href"])
        time.sleep(delay)
    return rows

En un sitio real, un retardo de dos segundos o más es un valor predeterminado sensato. Auméntalo si el servidor se ralentiza o devuelve errores.

Exportar a CSV

El último bloque ejecuta el rastreo y escribe listings.csv con pandas:

if __name__ == "__main__":
    import pandas as pd

    rows = crawl("http://localhost:8000/sample_listings.html", delay=1.0)
    df = pd.DataFrame(rows)
    df.to_csv("listings.csv", index=False)
    print(df[["listing_id", "price_type", "price", "rent_period",
              "beds", "baths", "sqft", "source"]].to_string(index=False, na_rep=""))

Ejecuta python scraper.py. Salida con las páginas de ejemplo:

http://localhost:8000/sample_listings.html: 4 cards, 4 new
http://localhost:8000/sample_listings_2.html: 2 cards, 1 new
listing_id   price_type   price rent_period  beds  baths  sqft  source
  SPR-1001 asking_price  425000               3.0    2.0  1850 json-ld
  SPR-1002 asking_price 1250000               5.0    4.5  4100 json-ld
  SPR-1003  asking_rent    2400       month   2.0    1.0   950    html
  SPR-1004 asking_price  389900               0.0    1.0   610    html
  SPR-1005 asking_price  615000               4.0    2.5  2300    html

De seis tarjetas salieron cinco anuncios únicos: el SPR-1004 repetido en la página 2 se omitió. Las dos primeras filas tomaron sus valores del JSON-LD y el resto, del HTML. El alquiler conserva su periodo mensual y el estudio tiene cero dormitorios en lugar de un valor vacío. El CSV también incluye status, currency, address, url, collected_at y el texto bruto del precio en cada fila.

Descargar páginas con mucho JavaScript con una API de scraping

El sitio de ejemplo es estático. Muchas páginas de anuncios reales cargan sus datos con JavaScript, y algunas muestran resultados distintos según la ubicación del visitante. En esos casos, una API de scraping puede descargar la página mediante un navegador real desde el país que elijas y devolver el HTML final. El renderizado y la ubicación son además los dos ajustes que más a menudo deciden si ves la misma página que un visitante. Para más contexto, consulta cómo evitar bloqueos y límites de velocidad.

La siguiente solicitud usa el endpoint v2 de WebScrapingAPI. Lee la clave desde una variable de entorno y no la imprimas nunca. TARGET_URL es un marcador para una página que tienes permiso para recopilar, como el sitio de tu propia agencia o el de un socio:

import os

import requests

from scraper import parse_page

TARGET_URL = "https://www.your-brokerage.example/listings?city=springfield"

response = requests.get(
    "https://api.webscrapingapi.com/v2",
    params={
        "api_key": os.environ["WSA_API_KEY"],
        "url": TARGET_URL,
        "render_js": 1,
        "country": "us",
    },
    timeout=90,
)
response.raise_for_status()
rows = parse_page(response.text, TARGET_URL)
print(f"{len(rows)} listings parsed")

Define primero la clave en tu shell con export WSA_API_KEY=.... El cuerpo de la respuesta es el HTML renderizado, así que parse_page de tu scraper lo procesa sin cambios. Solo los selectores tienen que coincidir con el sitio de destino.

¿Cómo obtener precios de vivienda sin scraping?

Descárgalos. Realtor.com, Zillow y Redfin publican archivos gratuitos del mercado de la vivienda con precios de oferta medianos, inventario, días en el mercado y rebajas de precio por área metropolitana, condado o código postal, actualizados cada mes. El House Price Index de la FHFA sigue los precios de reventa. Estas fuentes responden a preguntas de mercado sin ningún scraping.

Las principales fuentes:

  • Biblioteca de datos residenciales de Realtor.com. La biblioteca de datos residenciales de Realtor.com ofrece archivos CSV mensuales por país, estado, área metropolitana, condado y código postal, en versiones actuales e históricas.
  • Zillow Research. Los datos de Zillow Research incluyen índices de valor de vivienda y de alquiler, e informes de inventario y precios de venta, publicados para tu propio análisis.
  • Redfin Data Center. El Redfin Data Center publica archivos de seguimiento del mercado con precios de venta, inventario y rebajas de precio.
  • FHFA. El House Price Index de la FHFA mide la variación de precios a partir de ventas repetidas de las mismas viviendas.

Este ejemplo lee el archivo metropolitano de Realtor.com directamente en pandas y muestra las 10 áreas metropolitanas con el precio de oferta mediano más alto:

import pandas as pd

CSV_URL = ("https://econdata.s3-us-west-2.amazonaws.com/"
           "Reports/Core/RDC_Inventory_Core_Metrics_Metro.csv")

metros = pd.read_csv(CSV_URL)
month = metros["month_date_yyyymm"].iloc[0]
top = (metros.dropna(subset=["median_listing_price"])
             .nlargest(10, "median_listing_price")
             [["cbsa_title", "median_listing_price",
               "active_listing_count", "median_days_on_market"]])
print(f"Data month: {month}")
print(top.to_string(index=False))

Salida:

Data month: 202609
                        cbsa_title  median_listing_price  active_listing_count  median_days_on_market
                     Nantucket, MA             5873250.0                   145                   84.0
                Vineyard Haven, MA             2495000.0                   260                   99.0
                    Jackson, WY-ID             2150000.0                   418                  108.0
     Santa Maria-Santa Barbara, CA             1720750.0                   718                   58.0
                         Heber, UT             1594500.0                  1595                   90.0
                         Rifle, CO             1510750.0                   875                   93.0
                         Kapaa, HI             1507750.0                   385                  116.0
San Jose-Sunnyvale-Santa Clara, CA             1388472.0                  2112                   35.0
                       Salinas, CA             1289500.0                   551                   66.0
                          Napa, CA             1272500.0                   572                   81.0

Fuente: Realtor.com Residential Data Library, indicadores principales por área metropolitana, septiembre de 2026 (month_date_yyyymm 202609).

El archivo también incluye price_reduced_share, new_listing_count, pending_listing_count y median_listing_price_per_square_foot, además de sus variaciones mensuales e interanuales. Imprime el mes cada vez que ejecutes el script, porque el archivo se actualiza mensualmente y tu informe debe indicar qué mes describe. Cita la fuente bajo cualquier gráfico o tabla que publiques.

Para tendencias a lo largo del tiempo, la misma biblioteca tiene una versión _History de cada archivo, con una fila por área metropolitana y mes. Cambia el nombre del archivo en CSV_URL, filtra por cbsa_title y representa median_listing_price frente a month_date_yyyymm. Los archivos por código postal usan las mismas columnas de métricas con sus propias columnas geográficas, así que el código apenas cambia.

¿Cómo comprobar que los datos inmobiliarios extraídos son correctos?

Revisa cada ejecución con unas pocas reglas. Cada fila tiene una dirección, un precio y una URL de origen. Los precios están en un rango plausible para el mercado. El número de filas se acerca al total que indica el sitio. Ningún ID de anuncio aparece dos veces. Los valores de venta, alquiler y estimación nunca comparten la misma columna de precio.

Un scraper que recibe un HTTP 200 puede seguir devolviendo basura. Estos controles detectan los fallos más comunes:

Control

Regla

Acción si falla

Campos obligatorios

Cada fila tiene una dirección, un precio y una URL de origen

Detener la ejecución e inspeccionar las filas

Coherencia del precio

Precio mayor que cero, dentro del rango normal del mercado y con moneda

Marcar las filas; buscar un dígito que falte o un alquiler en la columna de venta

Periodo del alquiler

Cada alquiler tiene un periodo (mes o semana)

Corregir el parser antes de cargar

Número de filas

Cercano al total que muestra el sitio para la búsqueda

Buscar un límite de paginación o una página omitida

ID duplicados

Ningún ID de anuncio aparece dos veces en una ejecución

Corregir la deduplicación; comprobar si los ID cambian entre páginas

Deriva de campos

La proporción de valores vacíos por campo se mantiene cerca de la ejecución anterior

Comparar con la ejecución anterior; un salto indica que un selector se ha roto

Páginas obsoletas

Los anuncios vendidos o retirados se marcan, no se cuentan como activos

Actualizar el estado y conservar el historial

Parser roto

Cero filas o una caída repentina en el número de filas

Lanzar una alerta y mantener publicados los datos de ayer

Varios de estos controles caben en un breve script de pandas que se ejecuta después de cada rastreo:

import pandas as pd

df = pd.read_csv("listings.csv")
problems = []

missing = df[df[["address", "price", "url"]].isna().any(axis=1)]
if len(missing):
    problems.append(f"{len(missing)} rows missing address, price, or URL")

sale = df[df["price_type"] == "asking_price"]
if not sale["price"].between(10_000, 50_000_000).all():
    problems.append("sale price outside the plausible range")

rent = df[df["price_type"] == "asking_rent"]
if rent["rent_period"].isna().any():
    problems.append("rental without a rent period")

if df["listing_id"].duplicated().any():
    problems.append("duplicate listing IDs")

null_share = df.isna().mean().round(2)
print(null_share[null_share > 0].to_dict())
print("OK" if not problems else problems)

Salida con los datos de ejemplo:

{'rent_period': 0.8}
OK

Solo rent_period tiene valores vacíos, y es lo esperado: cuatro de los cinco anuncios son de venta. Guarda las proporciones de valores vacíos de cada ejecución para que el control de deriva de campos tenga con qué comparar.

¿Qué herramientas de scraping inmobiliario deberías usar?

Elige según cuánto quieras gestionar por tu cuenta. Las bibliotecas de Python como requests y BeautifulSoup sirven para páginas estáticas. Playwright maneja sitios con mucho JavaScript. Una API de scraping añade renderizado, proxies y geolocalización sin infraestructura. Una API de datos o un dataset entrega anuncios estructurados sin parser. Los feeds de MLS siguen siendo la fuente oficial de datos de anuncios con licencia.

Categoría

Ejemplos

Lo que mantienes

Ideal para

Límites

Bibliotecas HTTP + parser

requests, httpx, BeautifulSoup, lxml

Todo: descarga, análisis, programación, proxies

Páginas estáticas, trabajos pequeños y permitidos

Sin JavaScript; los bloqueos corren de tu cuenta

Automatización de navegador

Playwright, Selenium

Navegadores, análisis, servidores

Páginas con mucho JavaScript

Más lento y pesado por página

API de scraping

Scraper API de WebScrapingAPI y servicios similares

Análisis y programación

Páginas renderizadas de sitios permitidos a cualquier volumen

Sigues escribiendo y corrigiendo el parser

API de datos y datasets

Data API de WebScrapingAPI (Zillow, Redfin, Zoopla) y otros proveedores

Nada más allá de tu pipeline

Registros de anuncios estructurados sin equipo de parsers

Cobertura y campos definidos por el proveedor; precio a consultar

Herramientas no-code

Scrapers visuales de apuntar y hacer clic y extensiones de navegador

Configuraciones de apuntar y hacer clic

Extracciones puntuales por personas que no programan

Difíciles de versionar, probar y escalar

Feeds de MLS y agregadores

RESO Web API a través de un MLS o proveedor aprobado

Integración y cumplimiento de visualización

Datos oficiales de anuncios y ventas

Licencias, contratos, normas de visualización

Si necesitas anuncios de Zillow a escala, un feed de datos de Zillow mantenido te da registros estructurados de un proveedor que se encarga de la recopilación, en lugar de un scraper que tendrías que construir y defender tú mismo. Eso no cambia lo que dicen los términos de Zillow sobre tu propio scraping. Si estás valorando tu propio código frente a un servicio, lee cómo decidir entre desarrollar o comprar un scraper.

¿Dónde encontrar guías para sitios inmobiliarios concretos?

Tenemos guías paso a paso para varios sitios inmobiliarios, cada una con sus propias notas legales:

FAQ

Depende del sitio, de los datos y de tu país. Muchos grandes portales prohíben el scraping en sus términos, y las fotos de los anuncios suelen tener derechos de autor. Los datos públicos y no personales de sitios que permiten el acceso automatizado conllevan mucho menos riesgo. Cuando los términos lo prohíben, usa datos abiertos o un feed con licencia, y consulta con un abogado.

¿Puedo hacer scraping de Zillow?

Los términos de uso de Zillow prohíben las consultas automatizadas, incluidos el scraping de pantalla, los crawlers y la evasión de captchas. Para tendencias de mercado, descarga los datos de Zillow Research, que Zillow publica para tu propio análisis. Para registros a nivel de anuncio, usa un proveedor de datos con licencia o un feed de MLS. No construyas un negocio sobre datos recopilados en contra de los términos.

¿Cuál es el mejor scraper inmobiliario?

No hay una única herramienta mejor. Para páginas estáticas que puedes recopilar, basta con Python, requests y BeautifulSoup. Para páginas con mucho JavaScript, usa Playwright o una API de scraping. Si prefieres no mantener parsers, una API de datos inmobiliarios o un dataset con licencia te entrega anuncios estructurados directamente.

¿Existe una API de datos inmobiliarios gratuita?

Pocas son gratuitas a nivel de anuncio. Las opciones gratuitas son sobre todo a nivel de mercado: Realtor.com, Zillow y Redfin publican archivos de vivienda descargables, y la FHFA publica su House Price Index. Las API a nivel de anuncio y los feeds de MLS suelen requerir un plan de pago, una licencia o una relación con una agencia. Revisa los términos de cada fuente antes de reutilizar los datos.

¿Con qué frecuencia conviene hacer scraping de anuncios inmobiliarios?

Ajusta la frecuencia a la velocidad con la que cambian los datos. Las ejecuciones diarias captan anuncios nuevos, rebajas de precio y cambios de estado en mercados activos. Las semanales sirven para mercados más lentos y para investigación. Los archivos mensuales de datos abiertos bastan para analizar tendencias. Vuelve a descargar solo los anuncios que hayan cambiado para reducir la carga en el sitio de origen.

¿Se necesitan proxies para hacer scraping de sitios inmobiliarios?

No para trabajos pequeños y respetuosos en sitios que permiten la recopilación. Las ejecuciones grandes suelen necesitar proxies, renderizado de JavaScript y resultados específicos por ubicación, algo que una API de scraping reúne en un solo servicio. Los proxies no cambian los términos de un sitio. Si un portal prohíbe el scraping, rotar IP para esquivar sus bloqueos aumenta el riesgo legal.

¿Cómo recopilar fotos de propiedades a escala?

Guarda las URL y los metadatos de las imágenes, no copias, salvo que tengas derechos sobre las fotos. Las fotos de los anuncios suelen pertenecer al fotógrafo, al agente o al MLS, no al portal. Para reutilizarlas con licencia, obtén las imágenes mediante un feed de MLS o IDX, o de un proveedor de datos cuya licencia cubra la visualización, y conserva la atribución.

¿Qué enfoque deberías elegir?

Parte de la pregunta que necesitas responder, no del scraper:

  • Cifras a nivel de mercado, como precios medianos, inventario y días en el mercado: descarga datos abiertos.
  • Registros a nivel de anuncio a escala: licencia un feed de MLS o usa una API de datos.
  • Tus propios sitios, los de tus socios y fuentes públicas permitidas: usa una API de scraping o el scraper en Python de esta guía.

Elijas la vía que elijas, mantén separadas las tres capas, etiqueta cada precio con su tipo y ejecuta los controles de calidad en cada carga.

Obtén datos inmobiliarios sin mantener scrapers

¿Necesitas registros de anuncios, precios e historial de tus mercados? Cuéntanos qué fuentes y campos necesitas y definiremos un feed a tu medida. Explorar datos inmobiliarios

¿Recopilas datos de tus propios sitios o de sitios permitidos? Empieza con Scraper API y 5000 solicitudes gratuitas. Sin créditos adicionales por renderizado de JavaScript ni geolocalización, y las solicitudes fallidas no se facturan. Ver planes de Scraper API

Acerca del autor

Robert Munceanu, Desarrollador full-stack @ WebScrapingAPI

Robert Munceanu

Desarrollador full-stack

Robert Munceanu es desarrollador full stack en WebScrapingAPI, donde colabora en todas las áreas del producto y ayuda a crear herramientas y funciones fiables que respaldan la plataforma.

Extracción de datos web con AWS Lambda: guía para Python y Java 2026
Guías

Extracción de datos web con AWS Lambda: guía para Python y Java 2026

TL;DR: En resumen: el web scraping con AWS Lambda funciona mejor cuando cada invocación es breve, está delimitada y se puede reintentar de forma independiente. Empieza con HTTP directo, AWS SAM y S3, y añade SQS, contenedores, renderización en navegador, proxies o una capa de recuperación gestionada solo cuando la carga de trabajo demuestre que los necesita.

Suciu Dan33 min read
Leer artículo
Cómo utilizar GoSpider: rastrear, depurar URL y extraer datos
Guías

Cómo utilizar GoSpider: rastrear, depurar URL y extraer datos

TL;DR: En resumen: GoSpider es un rastreador de línea de comandos diseñado para descubrir URL, no un extractor completo de datos estructurados. Esta guía sobre cómo utilizar GoSpider muestra cómo realizar un rastreo limitado, gestionar correctamente los resultados, pasar los datos de Colly a CSV y seguir una ruta de diagnóstico para respuestas 403 o páginas que requieren renderización con JavaScript.

Suciu Dan24 min read
Leer artículo
Cómo raspar Redfin: Guía Python de Datos Inmobiliarios
Guías

Cómo raspar Redfin: Guía Python de Datos Inmobiliarios

TL;DR: Redfin expone puntos finales de API ocultos que devuelven JSON estructurado para los listados de propiedades, lo que permite omitir por completo el frágil análisis HTML. Esta guía te guía a través de la construcción de un raspador de Python que extrae datos de alquiler y venta, busca por ubicación, supervisa los nuevos listados a través de mapas de sitio XML y exporta resultados limpios a CSV o JSON.

Suciu Dan14 min read
Leer artículo

Empieza a crear

¿Estás listo para ampliar tu recopilación de datos?

Únete a más de 2000 empresas que utilizan WebScrapingAPI para extraer datos de la web a escala empresarial sin ningún gasto de infraestructura.