Saltar para o conteúdo
Comece grátis
Voltar ao blogue

Web scraping imobiliário: como coletar dados de imóveis com Python

Robert MunceanuÚltima atualização em 24 min read
Web scraping imobiliário: como coletar dados de imóveis com Python
Resumo: o web scraping imobiliário coleta anúncios, preços e detalhes de imóveis em sites e os transforma em dados estruturados. Antes de fazer scraping, verifique se existem dados abertos, um feed de MLS ou uma API de dados, porque Zillow, Realtor.com e Redfin proíbem a coleta automatizada em seus termos de uso. Este guia aborda o modelo de dados, as fontes, os limites legais, o código Python e as verificações de qualidade.

Cedo ou tarde, alguém pede para você "conseguir dados de imóveis" de um mercado: preços pedidos para comparativos, o estoque de anúncios para um relatório ou um feed de novos anúncios. Dados imobiliários estão por toda a web, então o scraping parece a resposta óbvia. Mas dois erros derrubam a maioria desses projetos. O primeiro é fazer scraping de um portal cujos termos proíbem isso. O segundo é tratar o preço pedido como preço de venda e construir a análise sobre o número errado.

Comece pela decisão: quais dados existem, qual fonte serve para cada tarefa e o que os grandes portais permitem. Depois, construa um scraper em Python com requests, BeautifulSoup e pandas em uma página de exemplo que você controla e obtenha preços reais de imóveis a partir de um arquivo gratuito de dados abertos.

O que é web scraping imobiliário?

Web scraping imobiliário é a coleta automatizada de informações de imóveis em sites, como preços anunciados, endereços, quartos, área, fotos e tempo de anúncio. Um scraper busca as páginas, extrai os campos e os salva como linhas em um arquivo CSV ou banco de dados, para que você compare mercados ou acompanhe mudanças ao longo do tempo.

O scraping é apenas uma das formas de obter essas informações. Os mesmos números costumam estar disponíveis como downloads gratuitos, feeds de MLS licenciados ou APIs de dados, e esses caminhos geralmente são mais baratos de manter e mais seguros de usar. Compare as opções antes de escrever uma linha de código.

Quais dados imobiliários você pode coletar com scraping?

A maioria das páginas de anúncio mostra endereço, preço pedido ou aluguel, quartos, banheiros, área útil, área do terreno, tipo de imóvel, status, data do anúncio, fotos e o corretor responsável. Esses campos descrevem três coisas diferentes: o imóvel, o anúncio que o divulga e cada preço observado em uma determinada data.

Um imóvel pode ser anunciado três vezes em cinco anos, por duas imobiliárias, a cinco preços diferentes. Se você guardar tudo isso em uma única linha, perde o histórico que torna os dados úteis. Em vez disso, modele em três camadas:

Camada

O que é

Exemplos de campos

Chave para deduplicação

Imóvel

O imóvel físico, que raramente muda

Endereço, número do lote, quartos, banheiros, área útil, área do terreno, ano de construção

Endereço normalizado ou ID do lote

Episódio de anúncio

Um período em que o imóvel esteve à venda ou para alugar

Site de origem, ID do anúncio, status, data do anúncio, imobiliária, URL

Origem + ID do anúncio

Observação

O que você viu em um determinado momento

Preço, status, tempo de anúncio, collected_at

ID do anúncio + data e hora

Os preços exigem o mesmo cuidado. Uma página de anúncio pode mostrar vários números parecidos que significam coisas diferentes:

Tipo de preço

O que significa

É uma transação?

Preço pedido (anunciado)

O que o vendedor pede hoje; pode cair ou subir

Não

Preço de venda

O que o comprador pagou no fechamento

Sim

Aluguel pedido

O que o proprietário pede por período (mês ou semana)

Não, e precisa de um período

Estimativa automática

O palpite de um modelo sobre o valor, como um Zestimate

Não

Valor venal

O valor que a autoridade local usa para o imposto predial

Não

Mantenha cada tipo em sua própria coluna ou marque cada preço com o tipo correspondente. Nossos dados de anúncios imobiliários seguem a mesma ideia: um imóvel, os anúncios que o divulgam e observações de preço com data, porque um anúncio diz o que alguém pediu, não o que alguém pagou.

Mais uma regra antes de coletar qualquer coisa: nomes, telefones e e-mails de corretores são dados pessoais segundo a maioria das leis de privacidade. Deixe-os de fora, a menos que você tenha uma base legal para mantê-los.

Quem usa scraping imobiliário, e para quê?

Corretores e imobiliárias usam o scraping para comparativos de venda e precificação. Investidores o usam para encontrar imóveis subvalorizados e acompanhar a rentabilidade de aluguéis. Equipes de proptech criam feeds de anúncios e modelos de avaliação. Pesquisadores e jornalistas acompanham estoque e reduções de preço. Compradores e inquilinos monitoram um bairro.

O que importa é associar cada tarefa aos campos de que ela precisa e à fonte que os entrega com o menor risco:

Caso de uso

Campos necessários

Melhor primeira fonte

Comparativos e precificação

Preço de venda, quartos, banheiros, área, data da venda

Feed de MLS via imobiliária ou dados licenciados

Monitoramento de mercado

Estoque, preço mediano anunciado, tempo de anúncio por região metropolitana ou CEP

Downloads de dados abertos

Triagem de investimentos

Preço pedido, aluguel, impostos, histórico de preços

API de dados ou dados licenciados, além de índices de aluguel abertos

Agregação de anúncios

Registros completos de anúncios com fotos

IDX de MLS ou feed licenciado; aqui, o scraping é o caminho mais arriscado

Geração de leads

Contatos de proprietários ou corretores

Cuidado: regras de dados pessoais e antispam se aplicam

Busca pessoal de imóvel

Alertas sobre alguns anúncios

Os alertas de busca salva do próprio portal

Se a sua tarefa está nas três primeiras linhas, talvez você nem precise de um scraper.

Fazer scraping, baixar ou licenciar dados imobiliários?

Baixe primeiro se você precisa de números em nível de mercado, já que Zillow, Redfin e Realtor.com publicam arquivos gratuitos de dados habitacionais. Licencie um feed de MLS ou uma API de dados se precisa de registros de anúncios em escala. Faça scraping apenas de páginas cujos termos permitem, ou dos seus próprios sites, de sites de parceiros e de fontes públicas que autorizam acesso automatizado.

Caminho

O que oferece

Custo e esforço

Barreira de acesso

Ideal para

Atenção a

Downloads de dados abertos (Zillow Research, Redfin Data Center, biblioteca de dados da Realtor.com, FHFA HPI)

Agregados por região metropolitana, condado e CEP

Grátis; poucas linhas de pandas

Nenhuma

Tendências, relatórios, triagem de mercado

Atualização mensal; sem linhas por anúncio

MLS via RESO Web API

Registros oficiais de anúncios, incluindo dados de vendas

Taxas de licença e contratos

Alta: associação, imobiliária ou fornecedor aprovado

Imobiliárias, sites IDX, avaliação

Regras de exibição e uso

API ou dataset de dados imobiliários

Registros estruturados de anúncios sem rodar scrapers

Sob consulta

Baixa a média

Produtos que precisam de anúncios sem uma equipe de parsers

Termos, cobertura e atualização do fornecedor

Scraper API

HTML renderizado de páginas que você pode coletar; você escreve o parser

Pagamento por requisição

Baixa

Sites de imobiliárias, feeds de parceiros, fontes públicas permitidas

O parser e a verificação de conformidade continuam sendo seus

Scraper próprio (requests, BeautifulSoup, Playwright)

O que você construir

Seu tempo, servidores e proxies

Baixa no início, alta na manutenção

Tarefas pequenas e permitidas, protótipos

Quebras, bloqueios e conformidade

A maioria das equipes acaba combinando caminhos: dados abertos para o panorama do mercado, um feed ou API para os registros de anúncios e um scraper para a cauda longa de sites permitidos.

Os termos de uso deles proíbem isso. O Zillow proíbe "automated queries", incluindo scraping e burlar captchas. A Realtor.com proíbe scraping de tela e de banco de dados. O Redfin proíbe rastreamento automatizado sem permissão por escrito. Violar os termos pode levar a contas banidas, IPs bloqueados ou ações contratuais, mesmo onde fazer scraping de páginas públicas não é crime. Use os downloads de dados deles ou uma fonte licenciada.

Veja o que dizem os termos de cada grande portal, nas palavras deles (citações no original em inglês):

Portal

O que dizem os termos

robots.txt

Alternativa oficial

Zillow

Os termos de uso do Zillow (atualizados em 28 de outubro de 2025) proíbem realizar "automated queries (including screen and database scraping, spiders, robots, crawlers, bypassing 'captcha' or similar precautions…)". Copiar só é permitido "without the aid of any automated processes". Os dados agregados Local-Info podem ser reutilizados com o crédito "Data Provided by Zillow Group".

Arquivo longo com regras por caminho

Downloads do Zillow Research; APIs do Zillow Group sob termos próprios

Realtor.com

Os termos de serviço da Realtor.com proíbem "screen scraping" e "database scraping", além da coleta "by automated means", como um "robot, spider, script". Também afirmam que as configurações do robots.txt não ampliam seus direitos.

Existe, mas não se sobrepõe aos termos

Biblioteca de dados residenciais da Realtor.com (CSV)

Redfin

Os termos de uso do Redfin incluem "No Scrubbing or Scraping": nada de rastreamento ou consultas automatizadas "unless you have received prior express written permission". Os avisos de MLS nas páginas de anúncio repetem a proibição.

Existe, com regras por caminho

Downloads do Redfin Data Center

Rightmove (Reino Unido)

O rodapé dos termos de uso da Rightmove afirma: "Rightmove prohibits the scraping of its content."

A proibição vale independentemente do robots.txt

Pedir permissão por escrito à Rightmove

Outros portais (Zoopla, Idealista, realestate.com.au, Domain, Homes.com)

Leia a página de termos de cada site antes de coletar qualquer coisa

Verificar site a site

Verificar site a site

Nos EUA, o Tribunal de Apelações do Nono Circuito decidiu em hiQ Labs v. LinkedIn (2022) que fazer scraping de páginas de acesso público provavelmente não configura "acesso sem autorização" segundo o Computer Fraud and Abuse Act. Essa decisão não anula as outras regras. Termos de uso são contratos, fotos e descrições de anúncios são protegidas por direitos autorais, e as leis de privacidade protegem as pessoas presentes nos dados. Os próprios termos do Zillow dizem que ele não reivindica direitos autorais sobre imagens e descrições dos anúncios. Esses direitos são dos proprietários. Fora dos EUA, os direitos sobre bases de dados da UE e o GDPR acrescentam seus próprios limites.

Isto não é aconselhamento jurídico. Consulte um advogado antes de coletar dados de um site cujos termos proíbem isso. Para uma visão mais ampla, leia os fundamentos legais do web scraping.

É possível fazer scraping de dados de MLS?

Não de forma legítima. Os dados de MLS são licenciados para membros, e as regras de IDX permitem que corretores exibam anúncios aos consumidores, mas não que terceiros os copiem. O caminho legítimo é um feed RESO Web API, obtido por meio de associação a um MLS, de uma imobiliária parceira ou de um fornecedor de dados aprovado. As páginas dos portais com dados de MLS têm as mesmas restrições.

A RESO Web API é um padrão, não uma fonte de dados. A RESO publica um dicionário de dados compartilhado, de modo que campos como ListPrice, StandardStatus e BedroomsTotal significam a mesma coisa em todo MLS compatível. A Web API entrega esses campos via REST, seguindo as convenções OData. Ainda assim, alguém precisa conceder o acesso. Os caminhos usuais são:

  • Imobiliária responsável (broker of record). Uma imobiliária licenciada para a qual você trabalha ou com quem tem parceria solicita o feed ao seu MLS.
  • Fornecedor aprovado pelo MLS. Muitos MLSs licenciam dados para empresas de tecnologia por meio de um contrato de fornecedor.
  • Agregadores multi-MLS. Empresas que têm acordos com muitos MLSs e revendem acesso normalizado.

Duas outras siglas aparecem com frequência. O IDX (Internet Data Exchange) permite que corretores participantes exibam os anúncios uns dos outros em seus próprios sites. Um VOW (virtual office website) mostra mais detalhes aos clientes cadastrados de um corretor. Ambos são programas de exibição com regras definidas pelo MLS. Nenhum deles permite que terceiros copiem os anúncios para o próprio banco de dados.

Depois de obter as credenciais, uma requisição fica mais ou menos assim. O exemplo mostra apenas a estrutura. Seu MLS fornece o endpoint e os campos que a sua licença permite ler.

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"]

Os parâmetros $select, $filter e $top vêm do OData, então o mesmo padrão funciona em servidores com certificação RESO, com pequenas diferenças na sintaxe dos filtros.

Como funcionam os scrapers imobiliários?

Um scraper imobiliário funciona em cinco etapas. Ele descobre as URLs dos anúncios em páginas de busca ou sitemaps, busca cada página, renderizando o JavaScript quando necessário, e extrai os campos dos dados estruturados ou do HTML. Em seguida, normaliza preços, áreas e endereços. Por fim, armazena cada observação com data e hora, para que as execuções seguintes detectem mudanças de preço e de status.

Descobrir as URLs dos anúncios

As URLs dos anúncios vêm de sitemaps, páginas de resultados de busca e links de paginação. As páginas de busca costumam parar depois de um número fixo de resultados, então divida áreas grandes em áreas menores por CEP ou faixa de preço. Não rastreie todas as combinações de filtros. Ordenações e filtros se multiplicam em milhões de URLs que, na maior parte, repetem os mesmos anúncios. Quando um site que você pode coletar publica um sitemap, comece por ele: ele lista as URLs canônicas dos anúncios, e as datas lastmod indicam quais páginas mudaram desde a sua última execução.

Buscar as páginas

Alguns sites enviam o HTML completo. Outros montam a página no navegador com JavaScript. Busque a versão estática sempre que possível, porque é mais rápido e mais barato. Faça cache das respostas, mantenha a taxa de requisições baixa e respeite o robots.txt, conforme descrito no Robots Exclusion Protocol. Lembre-se de que o robots.txt nunca concede uma permissão que os termos negam.

Extrair os campos

Verifique as fontes nesta ordem. Primeiro, blocos JSON-LD que usam tipos do schema.org, como RealEstateListing ou SingleFamilyResidence. Segundo, JSON incorporado em tags script pelo framework front-end do site. Veja como fazer parsing de JSON incorporado em uma página. Terceiro, seletores HTML. Dados estruturados mudam com menos frequência que a marcação, então seu scraper quebra menos.

Normalizar e deduplicar

Transforme "$1,250,000" em 1250000, com a respectiva moeda. Converta todas as áreas para uma única unidade. Deixe explícito o período do aluguel. Normalize os endereços para que o mesmo imóvel seja reconhecido em fontes diferentes e deduplique por esse endereço normalizado ou por um ID de lote.

Armazenar observações e detectar mudanças

Adicione uma nova linha a cada execução em vez de sobrescrever a anterior. Guarde collected_at, a URL de origem e o valor bruto ao lado do valor processado. Comparar as linhas de hoje com as da semana passada revela reduções de preço, mudanças de status e novos anúncios. No pandas, isso é um merge pelo ID do anúncio entre duas execuções. Linhas que só aparecem na execução nova são anúncios novos, linhas que só aparecem na antiga saíram do mercado, e linhas presentes em ambas com preço diferente indicam mudanças de preço.

Como fazer scraping de dados imobiliários com Python?

Instale requests, BeautifulSoup e pandas. Busque uma página de anúncios que você pode coletar, leia primeiro o JSON-LD e recorra a seletores HTML para os campos que faltarem. Converta preços e áreas em números, remova anúncios duplicados e exporte para CSV. Use uma API de scraping quando as páginas exigirem renderização de JavaScript ou resultados específicos por localização.

O tutorial usa um pequeno site de exemplo com anúncios fictícios, para que você execute cada etapa sem acessar os servidores de ninguém. O mesmo código funciona no site da sua própria imobiliária ou nas páginas de um parceiro, depois que você trocar os seletores.

Configurar o projeto

Você precisa do Python 3.12 ou mais recente. Crie uma pasta e um ambiente virtual e instale os quatro pacotes:

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

Começar com uma página de anúncios de exemplo

Salve a página de exemplo abaixo como sample_listings.html. Ela tem quatro anúncios fictícios. Dois deles também aparecem em um bloco JSON-LD, que é como muitos sites reais expõem dados estruturados. Um é para aluguel e outro é um studio, então o parser precisa lidar com os dois casos.

<!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>

O link "Next" aponta para uma segunda página de exemplo. Salve-a como sample_listings_2.html. Ela repete um anúncio da página 1, o que acontece em sites reais quando anúncios novos empurram os mais antigos para baixo:

<!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>

Sirva a pasta localmente para que o scraper busque as páginas via HTTP, como faria em um site real:

python -m http.server 8000

Deixe o servidor rodando e abra um segundo terminal para as próximas etapas. Coloque o código desta etapa e das cinco seguintes, em ordem, em um único arquivo chamado scraper.py. Comece pelos imports e por uma função de busca que envia um User-Agent honesto e para com um erro explícito diante de falhas 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

Extrair o JSON-LD primeiro

O JSON-LD fica em tags <script type="application/ld+json">. Uma página pode ter um objeto, uma lista de objetos ou uma ItemList que agrupa os anúncios, então a função auxiliar trata os três formatos. Depois, ela converte cada RealEstateListing em campos simples:

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

Os valores do JSON-LD já são números, o que poupa a maior parte do trabalho de limpeza.

Recorrer aos seletores HTML

Dois dos quatro cards da página 1 não têm JSON-LD, e nenhum traz o status do anúncio nos dados estruturados. O parser HTML lê cada card pelos atributos data-* e nomes de classe e guarda o texto bruto de cada campo. Se você está começando com seletores, veja nosso 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

Em um site real, prefira atributos data-* e nomes de classe estáveis a caminhos CSS longos. Eles resistem melhor a redesigns.

Limpar e normalizar os campos

Agora transforme as strings brutas em números. parse_price remove tudo exceto os dígitos, então "$1,250,000" vira 1250000 e "$2,400/mo" vira 2400. rent_period guarda a informação "/mo" em uma coluna própria. normalize mescla as duas fontes, dá prioridade ao JSON-LD quando ele tem valor, rotula cada preço como preço pedido ou aluguel pedido e marca a linha com data e hora em 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()]

A coluna source registra de onde vieram os valores de cada linha. Quando o JSON-LD some de uma página depois de um redesign, essa coluna avisa antes que seus números fiquem errados.

Lidar com paginação e duplicatas

O crawler segue o link "Next" até não haver mais nenhum, espera entre as requisições e ignora IDs de anúncios que já viu. Ele também para em uma página vazia e depois de max_pages, para que um link quebrado não o coloque em loop:

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

Em um site real, um intervalo de dois segundos ou mais é um padrão sensato. Aumente-o se o servidor ficar lento ou retornar erros.

Exportar para CSV

O último bloco executa o rastreamento e grava listings.csv com 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=""))

Execute python scraper.py. Saída com as páginas de exemplo:

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

Cinco anúncios únicos saíram de seis cards: o SPR-1004 repetido na página 2 foi ignorado. As duas primeiras linhas tiraram os valores do JSON-LD, e as demais, do HTML. O aluguel mantém seu período mensal, e o studio tem zero quartos em vez de um valor ausente. O CSV também traz status, currency, address, url, collected_at e o texto bruto do preço em cada linha.

Buscar páginas com muito JavaScript usando uma API de scraping

O site de exemplo é estático. Muitas páginas de anúncio reais carregam os dados com JavaScript, e algumas mostram resultados diferentes conforme a localização do visitante. Nesses casos, uma API de scraping pode buscar a página por meio de um navegador real, a partir do país que você escolher, e devolver o HTML final. Renderização e localização também são as duas configurações que mais costumam determinar se você vê a mesma página que um visitante. Para mais contexto, veja como evitar bloqueios e limites de taxa.

A requisição abaixo usa o endpoint v2 da WebScrapingAPI. Leia a chave de uma variável de ambiente e nunca a imprima. TARGET_URL é um exemplo a ser substituído por uma página que você pode coletar, como o site da sua própria imobiliária ou de um parceiro:

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")

Defina a chave no shell primeiro com export WSA_API_KEY=.... O corpo da resposta é o HTML renderizado, então a função parse_page do seu scraper o processa sem alterações. Só os seletores precisam corresponder ao site de destino.

Como obter preços de imóveis sem scraping?

Baixe-os. Realtor.com, Zillow e Redfin publicam arquivos gratuitos do mercado imobiliário com preços medianos anunciados, estoque, tempo de anúncio e reduções de preço por região metropolitana, condado ou CEP, atualizados mensalmente. O House Price Index da FHFA acompanha preços de revendas. Essas fontes respondem a perguntas sobre o mercado sem nenhum scraping.

As principais fontes:

  • Biblioteca de dados residenciais da Realtor.com. A biblioteca de dados residenciais da Realtor.com oferece arquivos CSV mensais por país, estado, região metropolitana, condado e CEP, em versões atuais e históricas.
  • Zillow Research. Os dados do Zillow Research incluem índices de valor de imóveis e de aluguel, arquivos de estoque e de preços de venda, publicados para a sua própria análise.
  • Redfin Data Center. O Redfin Data Center publica arquivos de acompanhamento de mercado com preços de venda, estoque e reduções de preço.
  • FHFA. O House Price Index da FHFA mede a variação de preços com base em vendas repetidas dos mesmos imóveis.

Este exemplo lê o arquivo metropolitano da Realtor.com direto no pandas e lista as 10 regiões metropolitanas com o maior preço mediano anunciado:

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))

Saída:

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

Fonte: Realtor.com Residential Data Library, indicadores principais por região metropolitana, setembro de 2026 (month_date_yyyymm 202609).

O arquivo também inclui price_reduced_share, new_listing_count, pending_listing_count e median_listing_price_per_square_foot, além das variações mensais e anuais de cada um. Imprima o mês sempre que executar o script, porque o arquivo é atualizado mensalmente e o seu relatório deve dizer a qual mês se refere. Dê crédito à fonte abaixo de qualquer gráfico ou tabela que publicar.

Para tendências ao longo do tempo, a mesma biblioteca tem uma versão _History de cada arquivo, com uma linha por região metropolitana por mês. Troque o nome do arquivo em CSV_URL, filtre por cbsa_title e plote median_listing_price em função de month_date_yyyymm. Os arquivos por CEP usam as mesmas colunas de métricas com suas próprias colunas geográficas, então o código quase não muda.

Como verificar se os dados imobiliários coletados estão corretos?

Verifique cada execução com algumas regras. Cada linha tem um endereço, um preço e uma URL de origem. Os preços ficam em uma faixa plausível para o mercado. O número de linhas fica próximo do total informado pelo site. Nenhum ID de anúncio aparece duas vezes. Valores de venda, aluguel e estimativa nunca dividem a mesma coluna de preço.

Um scraper que recebe HTTP 200 ainda pode entregar lixo. Estas verificações pegam as falhas mais comuns:

Verificação

Regra

Ação em caso de falha

Campos obrigatórios

Toda linha tem endereço, preço e URL de origem

Suspender a execução e inspecionar as linhas

Coerência do preço

Preço acima de zero, dentro da faixa normal do mercado, moeda definida

Sinalizar as linhas; verificar se falta um dígito ou se há um aluguel na coluna de venda

Período do aluguel

Todo aluguel tem um período (mês ou semana)

Corrigir o parser antes de carregar

Número de linhas

Próximo do total que o site mostra para a busca

Procurar um limite de paginação ou uma página perdida

IDs duplicados

Nenhum ID de anúncio aparece duas vezes na mesma execução

Corrigir a deduplicação; verificar se os IDs mudam entre páginas

Desvio de campos

A proporção de valores vazios por campo fica próxima à da última execução

Comparar com a execução anterior; um salto indica um seletor quebrado

Páginas desatualizadas

Anúncios vendidos e retirados do mercado são marcados, não contados como ativos

Atualizar o status e manter o histórico

Parser quebrado

Zero linhas ou queda repentina no número de linhas

Disparar um alerta e manter os dados de ontem no ar

Algumas dessas verificações cabem em um script curto de pandas que roda depois de cada rastreamento:

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)

Saída com os dados de exemplo:

{'rent_period': 0.8}
OK

Só rent_period tem valores vazios, e isso é esperado: quatro dos cinco anúncios são de venda. Guarde as proporções de valores vazios de cada execução para que a verificação de desvio de campos tenha uma base de comparação.

Quais ferramentas de scraping imobiliário usar?

Escolha conforme o quanto você quer operar por conta própria. Bibliotecas Python como requests e BeautifulSoup servem para páginas estáticas. O Playwright lida com sites com muito JavaScript. Uma API de scraping acrescenta renderização, proxies e geolocalização sem infraestrutura. Uma API de dados ou um dataset entrega anúncios estruturados sem parser. Os feeds de MLS continuam sendo a fonte oficial de dados licenciados de anúncios.

Categoria

Exemplos

O que você mantém

Ideal para

Limitações

Bibliotecas HTTP + parser

requests, httpx, BeautifulSoup, lxml

Tudo: busca, parsing, agendamento, proxies

Páginas estáticas, tarefas pequenas e permitidas

Sem JavaScript; bloqueios ficam por sua conta

Automação de navegador

Playwright, Selenium

Navegadores, parsing, servidores

Páginas com muito JavaScript

Mais lento e pesado por página

APIs de scraping

Scraper API da WebScrapingAPI e serviços semelhantes

Parsing e agendamento

Páginas renderizadas de sites permitidos, em qualquer volume

Você ainda escreve e corrige o parser

APIs de dados e datasets

Data API da WebScrapingAPI (Zillow, Redfin, Zoopla) e outros fornecedores

Nada além do seu pipeline

Registros estruturados de anúncios sem equipe de parsers

Cobertura e campos definidos pelo fornecedor; sob consulta

Ferramentas no-code

Scrapers visuais de apontar e clicar e extensões de navegador

Configurações de apontar e clicar

Extrações pontuais feitas por quem não é desenvolvedor

Difíceis de versionar, testar e escalar

Feeds de MLS e agregadores

RESO Web API via MLS ou fornecedor aprovado

Integração e conformidade de exibição

Dados oficiais de anúncios e vendas

Licenças, contratos, regras de exibição

Se você precisa de anúncios do Zillow em escala, um feed de dados do Zillow mantido por um fornecedor entrega registros estruturados de quem cuida da coleta, em vez de um scraper que você teria de construir e defender sozinho. Isso não muda o que os termos do Zillow dizem sobre o seu próprio scraping. Se você está pesando entre código próprio e um serviço, leia sobre desenvolver ou comprar um scraper.

Onde encontrar guias para sites imobiliários específicos?

Temos guias passo a passo para vários sites de imóveis, cada um com suas próprias observações legais:

FAQ

Depende do site, dos dados e do seu país. Muitos grandes portais proíbem o scraping em seus termos, e as fotos dos anúncios costumam ser protegidas por direitos autorais. Dados públicos e não pessoais de sites que permitem acesso automatizado envolvem bem menos risco. Quando os termos proíbem, use dados abertos ou um feed licenciado e consulte um advogado.

Posso fazer scraping do Zillow?

Os termos de uso do Zillow proíbem consultas automatizadas, incluindo scraping de tela, crawlers e burlar captchas. Para tendências de mercado, baixe os dados do Zillow Research, que o Zillow publica para a sua própria análise. Para registros por anúncio, use um fornecedor de dados licenciado ou um feed de MLS. Não construa um negócio sobre dados coletados em violação aos termos.

Qual é o melhor scraper imobiliário?

Não existe uma única melhor ferramenta. Para páginas estáticas que você pode coletar, Python com requests e BeautifulSoup é suficiente. Para páginas com muito JavaScript, use Playwright ou uma API de scraping. Se você prefere não manter parsers, uma API de dados imobiliários ou um dataset licenciado entrega anúncios estruturados diretamente.

Existe uma API de dados imobiliários gratuita?

Poucas são gratuitas no nível do anúncio. As opções gratuitas são, em geral, de nível de mercado: Realtor.com, Zillow e Redfin publicam arquivos habitacionais para download, e a FHFA publica seu House Price Index. APIs por anúncio e feeds de MLS normalmente exigem um plano pago, uma licença ou um vínculo com uma imobiliária. Verifique os termos de cada fonte antes de reutilizar os dados.

Com que frequência você deve fazer scraping de anúncios de imóveis?

Ajuste a frequência à velocidade com que os dados mudam. Execuções diárias capturam novos anúncios, reduções de preço e mudanças de status em mercados aquecidos. Execuções semanais servem para mercados mais lentos e para pesquisa. Arquivos mensais de dados abertos bastam para análise de tendências. Busque novamente só os anúncios que mudaram, para reduzir a carga no site de origem.

Você precisa de proxies para fazer scraping de sites imobiliários?

Não para tarefas pequenas e respeitosas em sites que permitem a coleta. Execuções grandes costumam exigir proxies, renderização de JavaScript e resultados específicos por localização, que uma API de scraping reúne em um só serviço. Proxies não mudam os termos de um site. Se um portal proíbe o scraping, alternar IPs para driblar os bloqueios aumenta o risco jurídico.

Como coletar fotos de imóveis em escala?

Armazene URLs e metadados das imagens, não cópias, a menos que você tenha direitos sobre as fotos. As fotos dos anúncios geralmente pertencem ao fotógrafo, ao corretor ou ao MLS, não ao portal. Para reutilização licenciada, obtenha as imagens por um feed de MLS ou IDX ou por um fornecedor de dados cuja licença cubra a exibição, e mantenha a atribuição.

Qual abordagem escolher?

Comece pela pergunta que você precisa responder, não pelo scraper:

  • Números em nível de mercado, como preços medianos, estoque e tempo de anúncio: baixe dados abertos.
  • Registros por anúncio em escala: licencie um feed de MLS ou use uma API de dados.
  • Seus próprios sites, sites de parceiros e fontes públicas permitidas: use uma API de scraping ou o scraper Python deste guia.

Seja qual for o caminho, mantenha as três camadas separadas, rotule cada preço com o seu tipo e rode as verificações de qualidade a cada carga.

Obtenha dados imobiliários sem manter scrapers

Precisa de registros de anúncios, preços e histórico dos seus mercados? Diga quais fontes e campos você quer, e nós montamos um feed sob medida. Conheça os dados imobiliários

Vai coletar dos seus próprios sites ou de sites permitidos? Comece a usar a Scraper API com 5.000 requisições gratuitas. Sem créditos extras para renderização de JavaScript ou geolocalização, e requisições com falha não são cobradas. Ver os planos da Scraper API

Sobre o autor

Robert Munceanu, Desenvolvedor Full-Stack @ WebScrapingAPI

Robert Munceanu

Desenvolvedor Full-Stack

Robert Munceanu é um programador Full Stack na WebScrapingAPI, contribuindo em todas as áreas do produto e ajudando a criar ferramentas e funcionalidades fiáveis que sustentam a plataforma.

Web Scraping com AWS Lambda: Guia para Python e Java 2026
Guias

Web Scraping com AWS Lambda: Guia para Python e Java 2026

TL;DR: Resumo: A extração de dados da Web com o AWS Lambda funciona melhor quando cada invocação é curta, delimitada e pode ser repetida de forma independente. Comece com HTTP direto, AWS SAM e S3 e, só depois, adicione SQS, contentores, renderização no navegador, proxies ou uma camada de recuperação gerida, apenas quando a carga de trabalho demonstrar que precisa deles.

Suciu Dan32 min read
Ler artigo
Como utilizar o GoSpider: rastrear, limpar URLs e extrair dados
Guias

Como utilizar o GoSpider: rastrear, limpar URLs e extrair dados

TL;DR: Resumo: O GoSpider é um rastreador de linha de comandos destinado a descobrir URLs, não um extrator completo de dados estruturados. Este guia sobre como utilizar o GoSpider mostra como realizar um rastreio limitado, gerir resultados de forma organizada, fazer a transferência de dados do Colly para CSV e seguir um percurso de diagnóstico para respostas 403 ou páginas que exijam renderização em JavaScript.

Suciu Dan23 min read
Ler artigo
Como fazer o Scrape Redfin: Guia Python para Dados de Propriedade
Guias

Como fazer o Scrape Redfin: Guia Python para Dados de Propriedade

TL;DR: A Redfin expõe pontos de extremidade de API ocultos que retornam JSON estruturado para listagens de propriedades, tornando possível ignorar totalmente a análise HTML frágil. Este guia orienta-o na construção de um scraper Python que extrai dados de aluguer e venda, pesquisa por localização, monitoriza novas listagens através de sitemaps XML e exporta resultados limpos para CSV ou JSON.

Suciu Dan14 min read
Ler artigo

Comece a construir

Pronto para expandir a sua recolha de dados?

Junte-se a mais de 2.000 empresas que utilizam a WebScrapingAPI para extrair dados da Web à escala empresarial, sem quaisquer custos de infraestrutura.