Introdução: o que você vai obter no final

O mercado imobiliário vive de números. Alguns procuram apartamentos abaixo do preço de mercado, outros avaliam concorrentes em lançamentos, outros montam relatórios para investidores. O que todos têm em comum é uma coisa: precisam de dados atualizados sobre imóveis e preços, e coletar isso manualmente é impossível. É exatamente aqui que um parser do CIAN feito por você mesmo vai ser útil.

Neste guia, você vai construir do zero uma ferramenta funcional que faz três coisas. Primeiro: percorrer as páginas de resultados por um filtro definido e coletar a lista de imóveis com preço, endereço, área e link. Segundo: entrar no anúncio de cada imóvel e extrair detalhes como andar, ano de construção e tipo de imóvel. Terceiro, o mais valioso: salvar os dados em um banco de dados e acumular dia após dia o histórico de preços, para que você veja quais imóveis ficaram mais baratos, quais foram retirados da venda e como o mercado se movimenta em um bairro específico.

O resultado final é assim: você tem uma pasta com scripts Python, um arquivo de banco de dados SQLite e uma tabela que pode ser aberta no Excel ou no Google Sheets. Você roda o script pela manhã e obtém um panorama atualizado. Depois de uma semana de execuções, você já tem a dinâmica.

Para quem é este guia

  • Para profissionais de marketing e analistas de imobiliárias que precisam monitorar preços por bairro sem comprar relatórios caros.
  • Para quem faz arbitragem e donos de negócio que buscam nichos e querem entender oferta e demanda em números.
  • Para desenvolvedores iniciantes que querem aprender raspagem com um exemplo real e compreensível.
  • Para investidores e compradores particulares que querem encontrar imóveis com redução de preço antes dos outros.

O que você precisa saber de antemão

Experiência em programação não é obrigatória. Vamos analisar cada linha de código e explicar por que ela é necessária. Basta saber instalar programas, abrir o terminal e copiar texto. Se você já abriu as ferramentas de desenvolvedor no navegador ao menos uma vez, vai ser bem mais fácil. Se não, vamos mostrar onde elas ficam.

O único requisito é atenção. A raspagem é sensível a erros de digitação em nomes de classes e endereços. Uma letra a mais e o script retorna uma lista vazia. Não se assuste: cada etapa tem um ponto de verificação onde você confirma que tudo está saindo conforme o plano.

Quanto tempo vai levar

A preparação do ambiente leva cerca de 30 minutos. O primeiro parser funcional de resultados você escreve em uma hora. Os anúncios dos imóveis, os proxies e o banco de dados vão exigir mais uma hora e meia a duas horas. No total, de três a quatro horas de tempo líquido, se for sem pressa. O histórico de preços começa a se acumular sozinho, a partir da segunda execução.

Preparação inicial: ferramentas e ambiente

Antes de escrever código, vamos reunir tudo o que é necessário. Não vale pular esta seção: metade dos problemas dos iniciantes surge por causa de Python mal instalado ou bibliotecas ausentes.

Requisitos de sistema

  • Computador com Windows 10 ou 11, macOS ou Linux. Qualquer notebook dos últimos oito anos serve.
  • No mínimo 4 GB de memória RAM. Para a variante com automação de navegador, o ideal é 8 GB.
  • Cerca de 2 GB de espaço livre em disco para Python, bibliotecas e banco de dados.
  • Conexão de internet estável.

O que é preciso instalar

  1. Python 3.11 ou mais recente. Baixe o instalador no site oficial python.org. No Windows, durante a instalação, marque obrigatoriamente a caixa Add Python to PATH na parte de baixo da primeira tela. Sem isso, o comando python não vai funcionar no terminal. No macOS, o Python muitas vezes já vem instalado, mas é melhor colocar uma versão atual.
  2. Editor de código. Recomendamos o Visual Studio Code: gratuito, destaca a sintaxe e mostra erros. Instale a extensão Python pela loja de extensões integrada (ícone com quatro quadrados no painel esquerdo).
  3. Navegador Chrome ou Edge. Vamos precisar das ferramentas de desenvolvedor para estudar a estrutura das páginas.
  4. Bibliotecas Python. Vamos instalá-las pelo terminal logo abaixo.
  5. Acesso a proxies móveis. Vai ser necessário na quinta etapa. Você precisa do endereço do servidor, porta, login, senha e do link para troca de IP. Tudo isso é fornecido no painel do serviço na hora da compra. Se você ainda não tem proxies, as primeiras etapas podem ser feitas sem eles.

Criando a pasta de trabalho e o ambiente virtual

  1. Crie uma pasta no disco com o nome cian_parser. Evite letras acentuadas e espaços no caminho: às vezes eles quebram as ferramentas.
  2. Abra o terminal. No Windows, pressione Win+R, digite cmd e pressione Enter. No macOS, abra o Terminal pelo Spotlight.
  3. Entre na pasta com o comando cd e o caminho dela, por exemplo: cd C:\projects\cian_parser no Windows ou cd ~/projects/cian_parser no macOS.
  4. Crie um ambiente virtual com o comando python -m venv venv. É uma cópia isolada do Python, para que as bibliotecas do projeto não entrem em conflito com as do sistema.
  5. Ative o ambiente. No Windows: venv\Scripts\activate. No macOS e Linux: source venv/bin/activate. No início da linha do terminal vai aparecer a inscrição (venv).
  6. Instale as bibliotecas com um único comando: pip install requests beautifulsoup4 lxml pandas openpyxl. A instalação leva um ou dois minutos.

Verificação: digite no terminal python -c "import requests, bs4, pandas; print('ok')". Se apareceu a palavra ok na tela sem erros, o ambiente está pronto. Se você vê ModuleNotFoundError, o ambiente não está ativado ou a instalação foi interrompida. Ative o venv de novo e repita o pip install.

Cópias de segurança

Neste projeto, o valor principal não é o código, mas o banco acumulado com o histórico de preços. Ele não pode ser recuperado: os preços passados não vão aparecer em nenhum outro lugar. Por isso, desde o primeiro dia combine consigo mesmo: o arquivo do banco de dados deve ser copiado para a nuvem ou para um disco externo no mínimo uma vez por semana. Mais adiante, vamos adicionar a cópia automática no script.

Conceitos básicos: o que é preciso entender antes de começar

Vamos ver os termos que vão aparecer daqui para frente. Se você já conhece raspagem, passe os olhos pela seção, mas preste atenção ao bloco jurídico.

Termos-chave em linguagem simples

  • Raspagem (scraping): obtenção automática de uma página do site por um programa e extração dos dados necessários. É a mesma coisa que você faz com os olhos, mas o script faz e mil vezes mais rápido.
  • HTML: linguagem de marcação da qual é feita qualquer página web. O preço do apartamento no CIAN está dentro de uma tag HTML com atributos específicos, e nossa tarefa é encontrar essa tag.
  • Seletor: endereço de um elemento dentro do HTML. Por exemplo, um span com o atributo data-mark igual a MainPrice. Pelo seletor o parser entende de onde tirar o preço.
  • Requisição HTTP: chamada ao servidor do site. O navegador faz isso quando você abre uma página. A biblioteca requests faz a mesma coisa a partir do código.
  • Cabeçalhos da requisição (headers): informação de serviço que o navegador envia junto com a requisição: tipo de navegador, idioma, formatos de dados. O servidor decide por eles o que entregar.
  • Proxy: servidor intermediário pelo qual passam as suas requisições. Os proxies móveis usam endereços IP de operadoras de celular e permitem mudar o endereço por comando.
  • Paginação: divisão dos resultados em páginas. Para coletar todos os imóveis, o parser precisa percorrer as páginas da primeira à última.
  • SQLite: banco de dados leve em um único arquivo. Não exige instalação de servidor, vem embutido no Python. Ideal para histórico de preços.

Como funciona a listagem nas plataformas imobiliárias

CIAN, Domclick, Yandex Real Estate e outras plataformas funcionam por um princípio parecido. Há uma página de busca com filtros: cidade, tipo de transação, número de quartos, faixa de preço. Cada filtro se transforma em um parâmetro na barra de endereços. Por exemplo, o parâmetro deal_type com o valor sale significa venda, e room1 igual a 1 adiciona apartamentos de um quarto. Entender esses parâmetros te dá uma ferramenta poderosa: em vez de clicar no site, você simplesmente monta o endereço desejado.

Dentro da listagem, cada imóvel é representado por um card: título, preço, endereço, algumas fotos, link para a página detalhada. A página detalhada contém as características completas e muitas vezes duplica todos os dados em um bloco JSON oculto que o site usa para renderizar a interface. Esse bloco é bem mais fácil de raspar do que o HTML.

Limites jurídicos e éticos

Atenção: colete apenas informações publicamente disponíveis sobre os imóveis: preço, área, endereço, características do imóvel. Não colete nem armazene telefones, nomes e outros dados pessoais de vendedores e corretores: isso é regulado pela lei de proteção de dados pessoais, e a violação traz responsabilidade real. Estude os termos de uso da plataforma antes de começar e use os dados para a sua própria análise, não para revenda ou para criar uma cópia do site. Respeite uma frequência razoável de requisições: seu parser não deve criar uma carga que atrapalhe o funcionamento do serviço.

Essa abordagem não é apenas legal, mas também prática. Um parser cuidadoso, com pausas e rotação de endereços, funciona durante meses, enquanto um agressivo recebe restrições temporárias em uma hora.

Etapa 1: Definindo o objetivo e a estrutura de dados

Objetivo da etapa: descrever com clareza o que exatamente vamos coletar e como isso vai ser armazenado. Sem essa etapa, você vai escrever um parser que puxa tudo, e depois passar uma semana tentando entender o amontoado de dados.

  1. Formule a pergunta de negócio. Exemplos: quais apartamentos de um quarto em São Paulo ficaram mais baratos em mais de 5 por cento no último mês; quanto custa o metro quadrado em lançamentos de um bairro específico; com que rapidez saem os imóveis abaixo de determinado valor.
  2. Defina o filtro da listagem. Para o exemplo deste guia, vamos usar: venda, imóvel usado, apartamentos de um e dois quartos, São Paulo, preço até 15 milhões de rublos. Você vai substituir pelos seus parâmetros.
  3. Monte a lista de campos. Para cada imóvel precisamos de: identificador único do anúncio, link, título, preço, endereço, área total, andar e total de andares, tipo de imóvel, ano de construção, data da primeira descoberta, data da última verificação. Para o histórico de preços: identificador do anúncio, data, preço.
  4. Abra o editor de código e crie na pasta do projeto o arquivo config.py. Escreva nele os parâmetros que vamos mudar com mais frequência:
BASE_URL = 'https://www.cian.ru/cat.php'
SEARCH_PARAMS = {'deal_type': 'sale', 'engine_version': 2, 'offer_type': 'flat', 'region': 1, 'room1': 1, 'room2': 1, 'maxprice': 15000000}
MAX_PAGES = 5
PAUSE_MIN = 4
PAUSE_MAX = 9
DB_PATH = 'realty.db'

Repare: no exemplo de código as quebras de linha são indicadas por símbolos de nova linha; no editor, basta escrever cada variável em uma nova linha. O parâmetro region igual a 1 corresponde a Moscou, 2 corresponde a São Petersburgo. Os códigos de outras regiões você encontra aplicando o filtro no site e olhando a barra de endereços.

Dica: comece com MAX_PAGES igual a 2-3. Cada página de resultados tem cerca de 28 imóveis, o suficiente para depuração. Faça a coleta completa quando tiver certeza de que todos os campos são extraídos corretamente.

Verificação: você tem o arquivo config.py, e no bloco de notas ou na cabeça está fixada uma lista de 12 campos e uma pergunta de negócio concreta. Se a pergunta soa como quero todos os dados de todo o Brasil, volte e restrinja: coleta completa do país são centenas de milhares de imóveis e uma infraestrutura totalmente diferente.

Possíveis problemas

Você não consegue entender qual parâmetro responde pelo filtro desejado. Solução: abra o site, defina o filtro manualmente, copie o endereço da barra e separe-o pelos símbolos de e comercial. Cada par chave igual a valor é um parâmetro.

Etapa 2: Estudando a estrutura da página de resultados

Objetivo da etapa: encontrar no HTML os elementos dos quais vamos tirar preço, título, endereço e link. Esta é a etapa mais investigativa, e é justamente aqui que os iniciantes mais se perdem, então vamos devagar.

  1. Abra o navegador e vá para a página de resultados do CIAN com os seus filtros. Confirme que você vê a lista de apartamentos.
  2. Passe o cursor sobre o preço de qualquer apartamento, clique com o botão direito e escolha o item Inspecionar (no Edge se chama Verificar). Vai abrir o painel de ferramentas de desenvolvedor, e o elemento com o preço vai ficar destacado.
  3. Olhe a linha destacada. No momento de escrita do guia, é a tag span com o atributo data-mark igual a MainPrice. Anote esse atributo: ele vai ser o seletor do preço.
  4. Suba pela árvore de elementos, clicando nas tags pai, até encontrar a tag que abrange todo o card do imóvel. Normalmente é um article com o atributo data-name igual a CardComponent. Quando você passa o cursor sobre ele no painel, toda a página do card com foto e preço fica destacada.
  5. Dentro do card, encontre o título (span com data-mark igual a OfferTitle), o endereço (vários links a com data-name igual a GeoLabel, dos quais o endereço é montado) e o link para o imóvel (tag a com href apontando para um endereço do tipo cian.ru/sale/flat/número). Anote os quatro seletores.
  6. Encontre o bloco de paginação no final da página. Role a listagem até o fim, clique com o botão direito no número da segunda página e veja como o endereço dela fica. Você vai ver o parâmetro p igual a 2. Ou seja, para passar pelas páginas basta mudar esse parâmetro.

Atenção: os nomes dos atributos data-mark e data-name nas plataformas mudam de tempos em tempos quando o design é atualizado. Não copie os seletores deste texto às cegas: confira sempre com a página real no painel de desenvolvedor. A habilidade de encontrar o seletor sozinho é mais importante do que qualquer lista pronta.

Verificando se existe JSON oculto

Muitas plataformas guardam os dados da listagem prontos dentro de uma tag script. Isso é mais conveniente que o HTML: não é preciso juntar o endereço de pedaços.

  1. No painel de desenvolvedor, pressione Ctrl+F (no macOS Cmd+F) e digite a palavra offers ou initialState.
  2. Se a busca encontrou uma tag script com muito texto parecido com um dicionário com chaves, então os dados estão no JSON. Anote o nome da variável no início desse bloco.
  3. Se não encontrou nada, não tem problema: a abordagem por HTML da próxima etapa funciona de qualquer forma.

Dica: abra a aba Network (Rede) no painel de desenvolvedor, atualize a página e filtre as requisições por tipo Fetch/XHR. Às vezes o site carrega a listagem em uma requisição separada no formato JSON. Se você vir essa requisição com o campo offers, raspar dela é o mais simples: você recebe dados limpos sem HTML.

Verificação: você tem anotados os seletores do card, preço, título, endereço e link, e sabe o nome do parâmetro da paginação. Ao clicar em cada seletor no painel, você vê o destaque do elemento certo na página.

Possíveis problemas

O painel de desenvolvedor mostra o HTML, mas não tem o preço. Motivo: o site renderiza parte dos dados por script depois do carregamento. Solução: na aba Network, verifique se o preço chega em uma requisição separada, ou use a automação de navegador da seção para avançados.

Etapa 3: Escrevendo o primeiro parser do CIAN para a página de resultados

Objetivo da etapa: obter um script que baixa a página de resultados, extrai a lista de imóveis e imprime no console. Depois dessa etapa, você terá um esqueleto funcional no qual vamos agregar funções.

  1. Crie na pasta do projeto o arquivo parser.py.
  2. Importe as bibliotecas e as configurações no início do arquivo:
import time
import random
import requests
from bs4 import BeautifulSoup
from config import BASE_URL, SEARCH_PARAMS, MAX_PAGES, PAUSE_MIN, PAUSE_MAX
  1. Descreva os cabeçalhos da requisição. O servidor deve ver neles um navegador normal com localidade russa, senão você pode receber uma versão da página diferente:
HEADERS = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0 Safari/537.36', 'Accept-Language': 'ru-RU,ru;q=0.9', 'Accept': 'text/html,application/xhtml+xml'}
  1. Escreva a função de download da página. Ela recebe o número da página, adiciona ele aos parâmetros e retorna o HTML. Verificamos obrigatoriamente o código de resposta: 200 significa sucesso, qualquer outra coisa é sinal para parar e investigar:
def fetch_page(page, session):
params = dict(SEARCH_PARAMS)
params['p'] = page
resp = session.get(BASE_URL, params=params, headers=HEADERS, timeout=30)
if resp.status_code != 200:
print('Статус', resp.status_code, 'на странице', page)
return None
return resp.text
  1. Escreva a função de parsing. Ela encontra todos os cards e para cada um extrai os campos. Preste atenção à construção com if: se o elemento não existe, não caímos com erro, mas gravamos None:
def parse_cards(html):
soup = BeautifulSoup(html, 'lxml')
cards = soup.select('article[data-name=CardComponent]')
result = []
for card in cards:
link_tag = card.select_one('a[href*=/sale/flat/]')
price_tag = card.select_one('span[data-mark=MainPrice]')
title_tag = card.select_one('span[data-mark=OfferTitle]')
geo_tags = card.select('a[data-name=GeoLabel]')
if not link_tag or not price_tag:
continue
url = link_tag.get('href')
offer_id = url.rstrip('/').split('/')[-1]
price_text = price_tag.get_text()
price = int(''.join(ch for ch in price_text if ch.isdigit()))
title = title_tag.get_text(strip=True) if title_tag else None
address = ', '.join(g.get_text(strip=True) for g in geo_tags)
result.append({'offer_id': offer_id, 'url': url, 'title': title, 'price': price, 'address': address})
return result
  1. Monte o laço principal. Ele percorre as páginas, faz uma pausa aleatória entre as requisições e junta os resultados em uma lista comum. A pausa aleatória é importante: intervalos iguais parecem artificiais e criam pico de carga:
def collect_listing():
session = requests.Session()
all_offers = []
for page in range(1, MAX_PAGES + 1):
html = fetch_page(page, session)
if html is None:
break
offers = parse_cards(html)
print('Страница', page, 'объектов:', len(offers))
if not offers:
break
all_offers.extend(offers)
time.sleep(random.uniform(PAUSE_MIN, PAUSE_MAX))
return all_offers
if __name__ == '__main__':
data = collect_listing()
for item in data[:5]:
print(item)
print('Всего собрано:', len(data))
  1. Salve o arquivo e rode no terminal com o comando python parser.py. Confirme que o ambiente venv está ativo.

Vamos analisar os pontos-chave. A Session mantém os cookies entre requisições, então o site vê o comportamento sequencial de um visitante, e não uma dezena de acessos soltos. A função select_one retorna o primeiro elemento correspondente ou None, por isso sempre verificamos o resultado antes de chamar get_text. O identificador do anúncio nós tiramos do link: é o último fragmento do endereço, um número como 312456789. É ele que vai ser a chave do histórico de preços.

Dica: na fase de depuração, salve o HTML da primeira página em arquivo com o comando open('page1.html', 'w', encoding='utf-8').write(html). Assim você pode depurar a função de parsing em uma cópia local, sem enviar requisições extras ao site.

��erificação: no console você vê linhas Страница 1 объектов: 28, Страница 2 объектов: 28 e assim por diante, e no final cinco dicionários com preços e endereços reais. Os preços devem ser números inteiros sem espaços e sem o símbolo de rublo. Se em vez de números vier vazio, volte aos seletores da segunda etapa.

Possíveis problemas

O script imprime объектов: 0 na primeira página. Motivos: o seletor do card mudou ou o servidor entregou uma página-fantasma. Abra o page1.html salvo no navegador e veja o que você recebeu. Se for uma página pedindo confirmação de que você não é um robô, aumente as pausas e passe para a quinta etapa com proxies. Se for uma listagem normal, confira os seletores.

Erro ValueError na conversão do preço. Motivo: no texto do preço não há dígitos, por exemplo está escrito Preço sob consulta. Solução: envolva a conversão em try e grave None nesses casos.

Etapa 4: Coletando os anúncios dos imóveis

Objetivo da etapa: ensinar o parser a entrar na página de cada imóvel e extrair características detalhadas: área, andar, tipo de imóvel, ano de construção. Esses campos são necessários para calcular o preço por metro quadrado e comparar apartamentos parecidos.

  1. Abra no navegador a página de qualquer imóvel da listagem. Pressione Ctrl+U para ver o código-fonte da página.
  2. Pressione Ctrl+F e digite a palavra totalArea. No momento de escrita do guia, os dados do card ficam na tag script dentro do objeto de configuração do frontend, em uma chave com um nome do tipo frontend-offer-card. Você vai ver os campos totalArea, floorNumber, floorsCount, buildYear, materialType e outros.
  3. Se a busca por totalArea não deu nada, procure as características no HTML: normalmente é um bloco com pares nome e valor, por exemplo Общая площадь e 38,5 м². Anote o seletor desse bloco.
  4. Adicione ao parser.py uma função de extração de JSON do card. Encontramos o script certo, recortamos o objeto pelas chaves e fazemos o parsing com o módulo json:
import json
import re
def parse_offer_page(html):
soup = BeautifulSoup(html, 'lxml')
details = {}
for script in soup.find_all('script'):
text = script.string or ''
if 'totalArea' in text and 'offerData' in text:
m = re.search(r'totalArea[^0-9]*([0-9.,]+)', text)
if m:
details['area'] = float(m.group(1).replace(',', '.'))
m = re.search(r'floorNumber[^0-9]*([0-9]+)', text)
if m:
details['floor'] = int(m.group(1))
m = re.search(r'floorsCount[^0-9]*([0-9]+)', text)
if m:
details['floors_total'] = int(m.group(1))
m = re.search(r'buildYear[^0-9]*([0-9]{4})', text)
if m:
details['build_year'] = int(m.group(1))
break
return details
  1. Aqui usamos de propósito expressões regulares em vez de parsing completo de JSON. O motivo é simples: o script na página contém não só JSON, mas também código, e isolar o objeto puro às vezes é difícil. As expressões regulares buscam a chave e o primeiro número depois dela, o que é razoavelmente confiável para campos numéricos.
  2. Adicione a função que pega a lista de imóveis da listagem e enriquece cada um com os dados do card. As pausas aqui são ainda mais importantes, porque o número de requisições fica 28 vezes maior:
def enrich_offers(offers, session):
for i, offer in enumerate(offers, 1):
try:
resp = session.get(offer['url'], headers=HEADERS, timeout=30)
if resp.status_code == 200:
offer.update(parse_offer_page(resp.text))
else:
print('Карточка', offer['offer_id'], 'статус', resp.status_code)
except requests.RequestException as e:
print('Ошибка сети на', offer['offer_id'], e)
if i % 10 == 0:
print('Обработано карточек:', i)
time.sleep(random.uniform(PAUSE_MIN, PAUSE_MAX))
return offers
  1. No bloco if __name__ depois do collect_listing adicione a chamada enrich_offers(data, requests.Session()) e rode o script de novo com MAX_PAGES igual a 1, para não esperar muito.

Quanto tempo isso leva: 28 cards com pausa média de 6 segundos são cerca de 3 minutos. A coleta completa de 5 páginas de resultados com os cards leva aproximadamente 15 minutos. Isso é normal. Um parser imobiliário não precisa ser rápido, precisa ser estável.

Dica: não raspe os cards de novo a cada execução. As características do apartamento não mudam: área e ano de construção bastam ser coletados uma vez. O que muda é só o preço, e ele está na listagem. Na sexta etapa vamos fazer com que o card seja requisitado apenas para imóveis novos. Isso reduz o número de requisições em dezenas de vezes.

Verificação: na saída dos dicionários apareceram as chaves area, floor, floors_total e build_year com valores plausíveis: área de 15 a 200, andar não maior que o total de andares, ano de 1900 a 2026. Se em parte dos imóveis os campos estiverem ausentes, é normal: nem todos os vendedores preenchem o ano de construção.

Possíveis problemas

A expressão regular encontra a área do cômodo em vez da total. Motivo: no JSON há chaves parecidas como livingArea ou kitchenArea. Solução: refine o padrão adicionando aspas ou dois-pontos antes da chave, para que ele não coincida com parte de outra palavra.

Etapa 5: Conectando proxies móveis e tornando a coleta resiliente

Objetivo da etapa: distribuir as requisições por proxies móveis com rotação de IP, adicionar tentativas repetidas e tratamento correto das respostas. Depois dessa etapa, o parser vai conseguir rodar com regularidade e por muito tempo, sem criar carga excessiva de um só endereço.

Por que um parser imobiliário precisa de proxies móveis

Qualquer plataforma grande limita a frequência de requisições de um mesmo endereço IP. É proteção contra sobrecarga, e ela dispara em qualquer automação. O endereço residencial, depois de algumas centenas de requisições, começa a receber respostas 429 ou páginas com verificação. Os proxies móveis resolvem a questão de outra forma: você recebe um IP do pool de uma operadora de celular, e por comando ou por temporizador o endereço muda. Suas requisições se distribuem entre os endereços, a carga sobre cada um fica baixa, e o parser trabalha com regularidade. Para monitoramento regular de preços isso é fundamental: você precisa não de dados pontuais, mas de cortes diários durante meses.

Configuração

  1. Abra o painel do seu serviço de proxies móveis e encontre o proxy comprado. Copie quatro valores: host, porta, login, senha. Copie também o link para troca de IP: normalmente é um endereço com uma chave, e ao acessá-lo o proxy recebe um novo endereço.
  2. Adicione ao config.py os parâmetros do proxy. Nunca grave senhas em código que você publica em algum lugar: mantenha-as em um arquivo separado ou em variáveis de ambiente:
PROXY_HOST = 'seu_host'
PROXY_PORT = 'sua_porta'
PROXY_USER = 'seu_login'
PROXY_PASS = 'sua_senha'
ROTATE_URL = 'link_para_troca_de_ip'
ROTATE_EVERY = 25
  1. Adicione ao parser.py a função de criação de sessão com proxy. A biblioteca requests aceita um dicionário com endereços para http e https:
from config import PROXY_HOST, PROXY_PORT, PROXY_USER, PROXY_PASS, ROTATE_URL, ROTATE_EVERY
def make_session():
session = requests.Session()
proxy_url = 'http://' + PROXY_USER + ':' + PROXY_PASS + '@' + PROXY_HOST + ':' + PROXY_PORT
session.proxies = {'http': proxy_url, 'https': proxy_url}
return session
def rotate_ip():
try:
r = requests.get(ROTATE_URL, timeout=20)
print('Смена IP:', r.status_code)
time.sleep(5)
except requests.RequestException as e:
print('Не удалось сменить IP:', e)
  1. Verifique que o proxy funciona. Crie um arquivo temporário check_proxy.py com o código que consulta um serviço de detecção de IP pela sessão e imprime a resposta:
from parser import make_session
s = make_session()
print(s.get('https://api.ipify.org', timeout=20).text)
  1. Rode ele. Você deve ver um endereço IP diferente do seu residencial. Chame rotate_ip e rode a verificação de novo: o endereço deve mudar.
  2. Agora adicione a função de requisição com tentativas repetidas. Ela trata três situações: resposta bem-sucedida, resposta 429 ou 403 (precisa esperar e mudar o endereço), erro de rede (repetir):
def safe_get(session, url, params=None, retries=3):
for attempt in range(1, retries + 1):
try:
resp = session.get(url, params=params, headers=HEADERS, timeout=30)
if resp.status_code == 200:
return resp
if resp.status_code in (429, 403):
print('Статус', resp.status_code, 'попытка', attempt, 'меняем IP и ждем')
rotate_ip()
time.sleep(30 * attempt)
continue
print('Неожиданный статус', resp.status_code)
return None
except requests.RequestException as e:
print('Сетевая ошибка', e, 'попытка', attempt)
time.sleep(10 * attempt)
return None
  1. Substitua as chamadas session.get em fetch_page e enrich_offers por safe_get. Adicione ao enrich_offers um contador: a cada ROTATE_EVERY requisições chame rotate_ip. É uma rotação planejada, que não deixa o endereço acumular requisições demais.

Atenção: se você recebeu uma resposta 429 ou uma página com verificação, não tente furá-la com repetições frequentes. Isso só vai piorar a situação do endereço atual. A reação correta é: parar, aumentar as pausas, mudar o IP e continuar em ritmo tranquilo. Um parser que respeita os limites do site vive mais e coleta mais.

Dica: use um canal de proxy por thread de raspagem. A tentação de rodar dez threads por um mesmo endereço é grande, mas é caminho direto para restrições. Se precisar de velocidade, compre vários canais e distribua por eles regiões diferentes ou filtros diferentes.

Verificação: o check_proxy.py mostra um endereço de operadora móvel, e depois da rotação o endereço muda. O parser passa por duas páginas de resultados com cards sem um único status 429. No log aparecem as linhas Смена IP: 200 a cada 25 cards.

Possíveis problemas

Erro ProxyError ou 407. Motivo: login ou senha errados, ou porta errada. Solução: verifique os dados no painel, confirme que está usando a porta para proxy HTTP, e não SOCKS. Se for SOCKS5, instale a biblioteca pysocks e use o prefixo socks5h em vez de http no proxy_url.

Depois da rotação o endereço não muda. Motivo: a operadora entregou o mesmo endereço ou a rotação ainda não foi aplicada. Solução: aumente a pausa depois do rotate_ip para 10 segundos e verifique se a frequência de troca de IP não está limitada no seu plano.

Etapa 6: Salvando os dados e construindo o histórico de preços

Objetivo da etapa: migrar o parser da impressão no console para a gravação em banco de dados SQLite, de modo que cada execução adicione um novo ponto ao histórico de preços, em vez de sobrescrever o antigo. É o coração de todo o projeto.

Projetando as tabelas

Precisamos de duas tabelas. A primeira, offers, guarda o imóvel: uma linha por anúncio com características e datas. A segunda, prices, guarda o preço na data: várias linhas por anúncio. A separação é necessária para não duplicar área e endereço a cada gravação de preço.

  1. Crie o arquivo storage.py e descreva a criação das tabelas:
import sqlite3
from datetime import date
from config import DB_PATH
def get_conn():
conn = sqlite3.connect(DB_PATH)
conn.execute('CREATE TABLE IF NOT EXISTS offers (offer_id TEXT PRIMARY KEY, url TEXT, title TEXT, address TEXT, area REAL, floor INTEGER, floors_total INTEGER, build_year INTEGER, first_seen TEXT, last_seen TEXT, is_active INTEGER DEFAULT 1)')
conn.execute('CREATE TABLE IF NOT EXISTS prices (offer_id TEXT, checked_on TEXT, price INTEGER, PRIMARY KEY (offer_id, checked_on))')
conn.commit()
return conn
  1. Adicione a função que retorna o conjunto de offer_id já conhecidos. Ela é necessária para requisitar os cards apenas dos imóveis novos:
def known_ids(conn):
rows = conn.execute('SELECT offer_id FROM offers').fetchall()
return set(r[0] for r in rows)
  1. Escreva a função de gravação. Para um imóvel novo inserimos uma linha em offers. Para qualquer imóvel atualizamos last_seen e gravamos o preço de hoje. A construção INSERT OR REPLACE em prices significa: se o preço de hoje já foi gravado, atualizar, senão adicionar:
def save_offers(conn, offers):
today = date.today().isoformat()
for o in offers:
conn.execute('INSERT OR IGNORE INTO offers (offer_id, url, title, address, area, floor, floors_total, build_year, first_seen, last_seen) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', (o['offer_id'], o['url'], o.get('title'), o.get('address'), o.get('area'), o.get('floor'), o.get('floors_total'), o.get('build_year'), today, today))
conn.execute('UPDATE offers SET last_seen = ?, is_active = 1 WHERE offer_id = ?', (today, o['offer_id']))
if o.get('price'):
conn.execute('INSERT OR REPLACE INTO prices (offer_id, checked_on, price) VALUES (?, ?, ?)', (o['offer_id'], today, o['price']))
conn.commit()
  1. Adicione a função que marca os imóveis retirados. Se o anúncio não apareceu na listagem por mais de três dias, consideramos ele inativo. Isso dá a você dados sobre a velocidade de venda:
def mark_inactive(conn, days=3):
conn.execute('UPDATE offers SET is_active = 0 WHERE julianday(?) - julianday(last_seen) > ?', (date.today().isoformat(), days))
conn.commit()
  1. Reescreva o bloco principal do parser.py para que ele colete a listagem, identifique os imóveis novos, enriqueça só eles e salve tudo:
from storage import get_conn, known_ids, save_offers, mark_inactive
if __name__ == '__main__':
conn = get_conn()
session = make_session()
listing = collect_listing(session)
old = known_ids(conn)
new_offers = [o for o in listing if o['offer_id'] not in old]
print('Новых объектов:', len(new_offers), 'из', len(listing))
enrich_offers(new_offers, session)
save_offers(conn, listing)
mark_inactive(conn)
print('Сохранено. Всего в базе:', conn.execute('SELECT COUNT(*) FROM offers').fetchone()[0])
  1. Não esqueça de alterar collect_listing para que ela receba session por parâmetro, em vez de criar a própria. Rode o script. Na pasta do projeto vai aparecer o arquivo realty.db.

Vendo o histórico de preços

Crie o arquivo report.py, que exporta as alterações de preço para o Excel. A consulta abaixo encontra os imóveis cujo último preço difere do primeiro:

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

Depois da primeira execução a coluna change_pct vai ser zero: ainda não há histórico. A partir do segundo dia aparecem as primeiras alterações. Em duas semanas você vai ver o quadro: quantos imóveis baixaram o preço, em quanto em média, quais bairros se movem mais rápido.

Dica: adicione no final do parser.py a cópia do banco: shutil.copy(DB_PATH, 'backup_' + date.today().isoformat() + '.db'). Uma linha de código protege semanas de dados acumulados. Uma vez por mês apague as cópias antigas, deixando as cinco últimas.

Verificação: o arquivo realty.db existe, o report.xlsx abre no Excel, e nele há colunas com endereço, área, preços e preço por metro quadrado. Rode o parser.py de novo depois de alguns minutos: a linha Новых объектов deve mostrar 0 ou um número pequeno, e os cards não devem ser requisitados de novo. Isso confirma que a desduplicação funciona.

Possíveis problemas

Erro database is locked. Motivo: o banco está aberto em outro programa, por exemplo em um visualizador de SQLite, ou duas instâncias do script rodam ao mesmo tempo. Solução: feche os programas extras e não rode o script em paralelo consigo mesmo.

No report.xlsx todos os imóveis mostram a mesma data. Motivo: o script rodou apenas um dia. Isso é esperado, basta esperar as próximas execuções.

Etapa 7: Automatizando a execução e expandindo para outras plataformas

Objetivo da etapa: fazer com que o parser rode sozinho toda manhã, e preparar o código para conectar outras plataformas imobiliárias. O histórico de preços só tem valor com regularidade, por isso a automação é obrigatória.

Agendamento da execução

  1. No Windows, abra o Agendador de Tarefas pela busca no menu Iniciar. Clique em Criar Tarefa Simples. Digite um nome, por exemplo Parser Imobiliário.
  2. Escolha o gatilho Diariamente, defina o horário, por exemplo 07:30. De manhã cedo é conveniente: a carga nos sites é mínima, e no começo do expediente você tem dados atualizados.
  3. Na ação escolha Iniciar um programa. No campo Programa indique o caminho completo para python.exe dentro da pasta venv, por exemplo C:\projects\cian_parser\venv\Scripts\python.exe. No campo Argumentos digite parser.py. No campo Iniciar em indique a pasta do projeto.
  4. Salve a tarefa e clique em Executar no painel à direita para testar. Na pasta do projeto o banco deve ser atualizado.
  5. No macOS e Linux use o cron. Digite no terminal crontab -e e adicione a linha: 30 7 * * * cd /путь/к/cian_parser && ./venv/bin/python parser.py >> run.log 2>&1. O log de todas as execuções vai se acumular em run.log.

Preparação para outras plataformas

Domclick, Yandex Real Estate, Metr Kvadratny e portais regionais funcionam de forma parecida, mas os seletores e parâmetros de filtro são próprios. Para não reescrever o parser para cada uma, isole o que é diferente em módulos separados.

  1. Crie dentro do projeto a pasta sites. Nela crie o arquivo cian.py e mova para lá as funções fetch_page e parse_cards junto com os parâmetros de busca. Deixe nelas uma interface igual: a função parse_cards recebe HTML e retorna uma lista de dicionários com as mesmas chaves offer_id, url, title, price, address.
  2. Para uma nova plataforma crie um arquivo, por exemplo domclick.py, e repita a investigação da segunda etapa: abra a listagem, encontre o card, o preço, o link e o parâmetro de paginação. Escreva as suas versões de fetch_page e parse_cards.
  3. No offer_id adicione um prefixo da plataforma, por exemplo cian_312456789 e domclick_98765. Senão os identificadores de sites diferentes podem coincidir e misturar o histórico.
  4. No parser.py importe os módulos de sites e rode a coleta por cada um em um laço. As tabelas do banco são comuns: um esquema para todas as fontes, e a coluna source indica de onde veio o imóvel.

Observação importante sobre Domclick e Yandex Real Estate: essas plataformas usam ativamente APIs internas em formato JSON, visíveis na aba Network. A listagem delas muitas vezes é mais conveniente para raspagem do que o HTML, mas a estrutura das respostas muda com mais frequência. Verifique seletores e campos uma vez por mês.

Dica: para um mesmo imóvel anunciado em várias plataformas, os preços podem ser diferentes. Comparar esses pares dá uma análise interessante e ajuda a encontrar vendedores que em algum lugar baixaram o preço, mas esqueceram de atualizar em outro. Dá para cruzar os imóveis por endereço, área e andar.

Verificação: a tarefa no agendador executou manualmente sem erros, no run.log ou no histórico do agendador há uma marca de sucesso. A estrutura de pastas contém sites com pelo menos um módulo, o parser roda da raiz do projeto e funciona como antes.

Possíveis problemas

O agendador diz que a tarefa foi executada, mas o banco não foi atualizado. Motivo: o script rodou de outra pasta de trabalho e criou um novo banco vazio em outro lugar. Solução: preencha o campo Iniciar em na tarefa ou use o caminho absoluto para o banco no config.py.

Verificação do resultado: checklist de um parser pronto

Passe pela lista e marque cada item. Se tudo foi cumprido, você tem um parser do CIAN completo com histórico de preços.

  • O script parser.py roda por comando a partir do ambiente ativado sem erros de importação.
  • A listagem é coletada de várias páginas, e o número de imóveis por página corresponde ao que você vê no navegador.
  • Os preços são salvos como números inteiros, os endereços são legíveis, os links abrem.
  • Os cards são requisitados apenas para imóveis novos, no log aparece a linha Новых объектов com um número que diminui nas execuções repetidas.
  • As requisições passam por proxy móvel, o check_proxy.py mostra um endereço de operadora, e a rotação o altera.
  • Em caso de status 429 o parser faz uma pausa e troca o IP, em vez de cair.
  • O arquivo realty.db cresce, a tabela prices recebe novas linhas todos os dias.
  • O report.xlsx é gerado e abre, e em alguns dias aparecem nele alterações de preço diferentes de zero.
  • A execução automática está configurada, o log registra cada rodada.
  • A cópia de segurança do banco é criada automaticamente.

Como testar de ponta a ponta

  1. Apague ou renomeie o realty.db, para começar do zero.
  2. Defina MAX_PAGES igual a 2 e rode o parser.py. Marque o tempo: deve levar cerca de 6-8 minutos com os cards.
  3. Rode o report.py e confirme que o relatório tem cerca de 56 linhas.
  4. Abra o realty.db em qualquer visualizador de SQLite ou execute no Python a consulta SELECT COUNT(*) FROM prices. O número deve coincidir com o número de imóveis.
  5. Rode o parser.py de novo. O tempo de execução deve cair para um minuto, porque os cards não são requisitados. O número de linhas em prices não vai mudar, já que a data é a mesma.
  6. Altere no banco o preço de um imóvel manualmente para qualquer outro número, mude a data do registro para ontem e rode o parser. No report.xlsx esse imóvel deve mostrar alteração de preço. Assim você confirma que a lógica do histórico funciona, sem esperar alterações reais.

Indicadores de sucesso

Proporção de imóveis com área preenchida acima de 90 por cento. Proporção de requisições com status 200 acima de 97 por cento. Nenhuma exceção não tratada durante a rodada. O tempo de uma rodada completa é previsível e não cresce de uma execução para outra. Se os indicadores estiverem abaixo, volte à seção com os erros típicos.

Erros típicos e soluções

Reunimos os problemas que praticamente todo mundo encontra ao escrever um parser de anúncios imobiliários pela primeira vez. Formato: problema, causa, solução.

O parser retorna 0 imóveis, embora funcionasse ontem

Motivo: a plataforma atualizou o layout e mudou os atributos data-mark ou data-name. Solução: abra a listagem no navegador, repita a segunda etapa e atualize os seletores. Crie o hábito de guardar os seletores em um só lugar no início do módulo, para que as correções levem um minuto. Adicione ao parser uma verificação: se na primeira página vierem 0 imóveis, envie uma notificação para você em um mensageiro.

Os preços são salvos com erro de mil vezes

Motivo: em parte dos imóveis o preço está indicado em milhares ou com a observação por mês, ou no texto entrou o preço por metro quadrado. Solução: confirme que você está pegando exatamente o MainPrice, e não o elemento vizinho com o preço por metro. Adicione uma verificação de razoabilidade: preço de venda de apartamento em Moscou abaixo de um milhão de rublos quase certamente é erro de parsing; registre esses casos em log.

O status 429 chega já na terceira página

Motivo: as pausas são curtas demais ou o proxy ainda não está conectado, e todas as requisições saem do mesmo IP residencial. Solução: aumente PAUSE_MIN e PAUSE_MAX para 6 e 12, conecte o proxy móvel, ative a rotação planejada a cada 20-25 requisições. Verifique se você não iniciou várias instâncias do script ao mesmo tempo.

Erro UnicodeEncodeError ao imprimir no console do Windows

Motivo: o console padrão do Windows nem sempre exibe cirílico corretamente. Solução: execute no terminal chcp 65001 antes de rodar ou adicione no início do script a linha sys.stdout.reconfigure(encoding='utf-8'). Também é possível escrever os logs em arquivo em vez do console.

O endereço é coletado incompleto ou com duplicatas

Motivo: o endereço no card é composto por vários links GeoLabel, e parte deles duplica cidade e distrito. Solução: remova as duplicatas mantendo a ordem, ou pegue o endereço do card do imóvel, onde ele aparece como uma única string. Para análise por bairro, adicione um campo separado district, recortando-o do endereço pela lista conhecida de bairros.

O parser funciona na execução manual, mas não no agendador

Motivo: o agendador usa outro interpretador Python sem as bibliotecas instaladas, ou outra pasta de trabalho. Solução: indique o caminho absoluto para python.exe dentro do venv e preencha o campo Iniciar em. Redirecione a saída para um arquivo de log, para ver os erros.

O banco chega a gigabytes em um mês

Motivo: você está salvando o HTML completo das páginas ou todos os campos do JSON no banco. Solução: guarde apenas os campos necessários. Se quiser salvar as páginas originais para reprocessar, coloque-as em arquivos compactados no disco, e não no SQLite. Uma vez por trimestre execute o comando VACUUM para compactar o banco.

Imóveis iguais duplicam sob identificadores diferentes

Motivo: o vendedor retirou o anúncio e publicou de novo, recebendo um novo número. Solução: adicione uma chave adicional de correspondência a partir do endereço, área e andar. Imóveis com a mesma chave, mas offer_id diferentes, podem ser ligados em uma tabela separada, e a história de preço pode ser contada pelo conjunto. Essa já é uma análise avançada, mas é ela que mostra as reduções reais de preço escondidas atrás da republicação.

Recursos adicionais para avançados

O parser básico está pronto. Se você quer mais, aqui vão direções que dão o maior retorno.

Automação de navegador com Playwright

Algumas páginas carregam dados por scripts já depois do carregamento, e o requests recebe um esqueleto vazio. Nesses casos use o Playwright: ele controla um navegador de verdade. Instale com os comandos pip install playwright e playwright install chromium. O proxy é passado na inicialização do navegador no parâmetro proxy com o dicionário server, username, password. Espere o aparecimento dos cards com page.wait_for_selector e passe page.content() para a função parse_cards que você já escreveu. Lembre que o navegador consome dez vezes mais recursos, então use-o de forma pontual, só para as páginas problemáticas.

Coleta paralela por vários canais de proxy

Se você precisa coletar várias regiões, compre um proxy móvel separado para cada região e rode um processo separado por canal. Não use multithreading dentro de um mesmo canal: o sentido da distribuição de carga se perde. Uma forma simples: o parâmetro de região é passado ao script como argumento de linha de comando, e o agendador dispara várias tarefas com argumentos diferentes e um pequeno deslocamento de horário.

Notificações de redução de preço

Adicione no final do parser a comparação do preço de hoje com o anterior para cada imóvel. Se a redução for maior que um limite definido, por exemplo 3 por cento, monte uma mensagem com endereço, preço antigo e novo, link, e envie para você por um bot em um mensageiro. Isso transforma o parser de ferramenta de análise em ferramenta de ação: você fica sabendo de imóveis vantajosos em até uma hora após a alteração.

Análise e visualização

Com o pandas você pode agrupar dados por bairro e calcular o preço mediano do metro quadrado, a proporção de imóveis com redução, o tempo médio de exposição (diferença entre first_seen e last_seen para imóveis inativos). A biblioteca matplotlib monta um gráfico de dinâmica do mês em três linhas de código. Suba o relatório no Google Sheets, e colegas sem habilidades de programação terão um painel vivo de indicadores.

Armazenamento em PostgreSQL

Quando os imóveis passarem de cem mil, o SQLite vai começar a travar em consultas analíticas. A migração para PostgreSQL é simples: o esquema das tabelas é o mesmo, muda só a string de conexão e a biblioteca (psycopg2 em vez de sqlite3). Faça isso apenas com necessidade real: para uma cidade o SQLite dá conta por anos.

Monitoramento da saúde do parser

Grave em uma tabela separada runs o horário de início, o horário de término, o número de imóveis coletados, o número de erros e o número de rotações de IP. Se o número de imóveis cair de repente ou os erros passarem de 5 por cento, envie uma notificação. Esse monitoramento permite perceber a mudança de layout no dia em que ela acontece, e não em duas semanas por um relatório vazio.

FAQ: perguntas frequentes sobre criar um parser imobiliário

É legal raspar o CIAN e outras plataformas?

A coleta de informação publicamente disponível sobre imóveis para análise pessoal em geral é admissível, mas as condições de cada plataforma estão descritas nos termos de uso dela, e é preciso ler. É terminantemente proibido coletar dados pessoais de vendedores, publicar a base copiada como se fosse sua e criar carga que atrapalhe o funcionamento do serviço. Se você planeja uso comercial dos dados, consulte um advogado.

Por que proxies móveis e não de servidor?

Os endereços de operadoras móveis são compartilhados por milhares de assinantes reais e mudam constantemente. As plataformas os tratam com mais leniência do que endereços de data centers, de onde compradores reais quase não acessam. Além disso, a possibilidade de trocar o IP por link dá uma rotação controlada sem comprar centenas de endereços.

Com que frequência rodar o parser para o histórico de preços?

Uma vez por dia é o ideal. Os preços de imóveis mudam raramente; coletar mais de uma vez por dia não faz sentido, e a carga aumenta. Se você precisa capturar reduções com agilidade, rode duas vezes por dia, de manhã e à noite, mas só com um filtro estreito.

Quantos imóveis dá para coletar por dia com um proxy?

Com pausas de 4-9 segundos e rotação planejada são cerca de 500-700 requisições por hora sem problemas, ou 8-12 mil por dia em trabalho contínuo. Para monitorar uma cidade normalmente bastam 2-3 mil requisições por dia, porque os cards são requisitados apenas para imóveis novos.

O que fazer se os seletores mudaram e eu não consigo encontrá-los?

Volte à segunda etapa e vá a partir do preço: clique com o botão direito no preço, Inspecionar, e suba pela árvore até o card. Procure atributos com a palavra data e nomes significativos: eles são mais estáveis que classes com caracteres aleatórios. Confira também a aba Network: talvez os dados tenham passado a chegar em uma requisição JSON separada, e raspar deles vai ficar até mais simples.

Dá para ficar sem proxy em um projeto pequeno?

Para uma coleta pontual de duas ou três páginas, dá. Para monitoramento diário com centenas de requisições, o endereço residencial vai começar a receber restrições rapidamente, e os dados ficam incompletos. Um histórico de preços com lacunas perde valor, por isso para trabalho regular o proxy é necessário.

Como raspar aluguel, e não venda?

Altere o parâmetro deal_type para rent e adicione o tipo de aluguel nos parâmetros de busca: de longo prazo ou por diária. Os links dos imóveis vão conter rent em vez de sale, por isso atualize o seletor do link em parse_cards. O resto da lógica, incluindo o histórico de preços, funciona sem alterações.

É preciso guardar as fotos dos imóveis?

Para análise de preços, não. As fotos ocupam muito espaço e não são necessárias para os cálculos. Se você está montando um catálogo para uso interno, guarde só os links das imagens, e não os arquivos.

Como entender que o imóvel foi vendido, e não apenas retirado?

As plataformas não informam o motivo da retirada. Um indício indireto: o imóvel desapareceu da listagem e não voltou em um mês. Imóveis que desaparecem e voltam depois de alguns dias com outro preço provavelmente foram republicados. Ligue-os por endereço e área, como descrito na seção de erros.

O que fazer se os dados são necessários para dez cidades?

Monte uma lista de regiões no config.py e rode a coleta em um laço, com um canal de proxy separado a cada dois ou três municípios. Desloque as execuções no tempo, para não coletar tudo ao mesmo tempo. O banco continua comum; adicione o campo de região à tabela offers.

Conclusão

Vamos ver o que você fez. Você preparou o ambiente com Python e bibliotecas, entendeu como funciona a listagem de uma plataforma imobiliária e aprendeu a encontrar seletores sozinho. Escreveu um parser do CIAN que coleta a listagem e os cards dos imóveis. Conectou proxies móveis com rotação e tentativas repetidas, graças ao que a coleta ficou estável e previsível. Projetou um banco com histórico de preços, configurou relatórios e execução automática por agendamento. É uma ferramenta de trabalho completa, e não um exemplo didático.

O que fazer a seguir: deixe o parser rodar duas semanas sem alterações. Nesse tempo o histórico vai se acumular, e você vai ver os pontos fracos: onde cai a proporção de campos preenchidos, quais imóveis duplicam, onde o relatório pede novas colunas. Só depois disso adicione funções. Em seguida conecte a segunda plataforma, usando a estrutura modular da sétima etapa: você vai se surpreender com a rapidez na segunda vez.

Para onde evoluir: notificações de redução de preço transformam a ferramenta em fonte de negócios. Análise por bairro e tipo de imóvel faz de você um especialista de mercado com números nas mãos. A ligação de imóveis republicados mostra os descontos reais que não aparecem no site. E o cuidado com a plataforma, as pausas, a rotação de endereços por proxies móveis e a coleta apenas dos dados necessários vão permitir que a ferramenta funcione por meses sem falhas.

Raspagem imobiliária não é sobre velocidade, mas sobre regularidade e qualidade dos dados. Você lançou a base certa. Boa sorte na coleta, e que a sua base cresça a cada manhã.