Introduction : ce que vous obtiendrez au final

Le marché immobilier vit de chiffres. Certains cherchent un appartement en dessous du marché, d'autres évaluent la concurrence dans le neuf, d'autres encore construisent des rapports pour des investisseurs. Tous ont un point commun : ils ont besoin de données à jour sur les biens et les prix, et les collecter à la main est impossible. C'est là qu'un scraper Cian fait maison devient indispensable.

Dans ce guide, vous allez construire de zéro un outil fonctionnel capable de faire trois choses. Premièrement : parcourir les pages de résultats selon un filtre donné et récupérer une liste de biens avec prix, adresse, surface et lien. Deuxièmement : entrer dans la fiche de chaque bien et extraire des détails comme l'étage, l'année de construction et le type d'immeuble. Troisièmement, le plus précieux : enregistrer les données dans une base et accumuler jour après jour l'historique des prix, pour voir quels biens ont baissé, lesquels ont été retirés de la vente et comment le marché évolue dans un quartier précis.

Le résultat final ressemble à ceci : vous avez un dossier avec des scripts Python, un fichier de base de données SQLite et un tableau ouvrable dans Excel ou Google Sheets. Vous lancez le script le matin, vous obtenez une coupe fraîche. Au bout d'une semaine de lancements, vous avez déjà une dynamique.

À qui s'adresse ce guide

  • Aux marketeurs et analystes d'agences immobilières qui ont besoin de surveiller les prix par quartier sans acheter de rapports coûteux.
  • Aux arbitragistes et chefs d'entreprise qui cherchent des niches et veulent comprendre l'offre et la demande en chiffres.
  • Aux développeurs débutants qui veulent apprendre le scraping sur un exemple concret et compréhensible.
  • Aux investisseurs et acheteurs particuliers qui veulent repérer les biens avec baisse de prix avant les autres.

Ce qu'il faut savoir au préalable

L'expérience en programmation n'est pas obligatoire. Nous détaillerons chaque ligne de code et expliquerons à quoi elle sert. Il suffit de savoir installer des logiciels, ouvrir un terminal et copier du texte. Si vous avez déjà ouvert les outils de développement dans un navigateur, ce sera très facile. Sinon, nous vous montrerons où ils se trouvent.

Une seule exigence : de l'attention. Le scraping est sensible aux fautes de frappe dans les noms de classes et les adresses. Une lettre en trop et le script renvoie une liste vide. Ne vous inquiétez pas : chaque étape a un point de contrôle où vous vérifiez que tout se passe comme prévu.

Combien de temps cela prendra

La préparation de l'environnement prend environ 30 minutes. Vous écrirez votre premier scraper de résultats en une heure. Les fiches de biens, les proxies et la base de données demanderont encore une heure et demie à deux heures. Au total, entre trois et quatre heures de temps effectif si vous avancez sans vous presser. L'historique des prix commencera à s'accumuler de lui-même, dès le deuxième lancement.

Préparation préalable : outils et environnement

Avant d'écrire du code, rassemblons tout le nécessaire. Ne sautez pas cette section : la moitié des problèmes des débutants vient d'un Python mal installé ou de bibliothèques manquantes.

Configuration requise

  • Un ordinateur sous Windows 10 ou 11, macOS ou Linux. N'importe quel portable des huit dernières années fera l'affaire.
  • Au minimum 4 Go de mémoire vive. Pour la variante avec automatisation du navigateur, 8 Go sont préférables.
  • Environ 2 Go d'espace disque libre pour Python, les bibliothèques et la base de données.
  • Une connexion Internet stable.

Ce qu'il faut installer

  1. Python 3.11 ou plus récent. Téléchargez l'installateur sur le site officiel python.org. Sous Windows, cochez impérativement la case Add Python to PATH en bas du premier écran. Sans cela, la commande python ne fonctionnera pas dans le terminal. Sur macOS, Python est souvent déjà présent, mais mieux vaut installer une version récente.
  2. Un éditeur de code. Nous recommandons Visual Studio Code : gratuit, il colore la syntaxe et affiche les erreurs. Installez l'extension Python depuis la boutique d'extensions intégrée (icône à quatre carrés dans le panneau de gauche).
  3. Un navigateur Chrome ou Edge. Nous aurons besoin des outils de développement pour étudier la structure des pages.
  4. Les bibliothèques Python. Nous les installerons via le terminal un peu plus bas.
  5. Un accès à des proxies mobiles. Nécessaire à la cinquième étape. Il vous faut l'adresse du serveur, le port, l'identifiant, le mot de passe et le lien pour changer d'adresse IP. Tout cela est fourni dans l'espace client du service lors de l'achat. Si vous n'avez pas encore de proxies, les premières étapes peuvent être réalisées sans eux.

Créer le dossier de travail et l'environnement virtuel

  1. Créez sur votre disque un dossier nommé cian_parser. Évitez les lettres accentuées et les espaces dans le chemin : ils cassent parfois les outils.
  2. Ouvrez le terminal. Sous Windows, appuyez sur Win+R, tapez cmd et validez. Sur macOS, ouvrez Terminal via Spotlight.
  3. Placez-vous dans le dossier avec la commande cd suivie du chemin, par exemple : cd C:\projects\cian_parser sous Windows ou cd ~/projects/cian_parser sur macOS.
  4. Créez un environnement virtuel avec la commande python -m venv venv. C'est une copie isolée de Python, pour que les bibliothèques du projet n'entrent pas en conflit avec celles du système.
  5. Activez l'environnement. Sous Windows : venv\Scripts\activate. Sur macOS et Linux : source venv/bin/activate. L'indication (venv) apparaîtra au début de la ligne du terminal.
  6. Installez les bibliothèques en une seule commande : pip install requests beautifulsoup4 lxml pandas openpyxl. L'installation prend une à deux minutes.

Vérification : tapez dans le terminal python -c "import requests, bs4, pandas; print('ok')". Si le mot ok s'affiche sans erreur, l'environnement est prêt. Si vous voyez ModuleNotFoundError, l'environnement n'est pas activé ou l'installation a été interrompue. Réactivez venv et relancez pip install.

Sauvegardes

Dans ce projet, la vraie valeur n'est pas le code, mais la base accumulée avec l'historique des prix. Elle est irrécupérable : les prix passés n'apparaîtront nulle part ailleurs. Prenez donc l'habitude dès le premier jour : le fichier de base de données est copié vers le cloud ou sur un disque externe au moins une fois par semaine. Plus tard, nous ajouterons une copie automatique dans le script.

Notions de base : ce qu'il faut comprendre avant de commencer

Passons en revue les termes que vous rencontrerez par la suite. Si vous connaissez déjà le scraping, parcourez cette section, mais prêtez attention au passage juridique.

Termes clés en langage simple

  • Scraping (extraction) : récupération automatique d'une page web par un programme et extraction des données utiles. C'est exactement ce que vous faites avec vos yeux, mais c'est le script qui le fait, mille fois plus vite.
  • HTML : le langage de balisage dont est composée toute page web. Le prix d'un appartement sur Cian se trouve dans une balise HTML avec des attributs précis, et notre tâche est de trouver cette balise.
  • Sélecteur : l'adresse d'un élément dans le HTML. Par exemple, un span avec l'attribut data-mark égal à MainPrice. Le sélecteur indique au scraper où prendre le prix.
  • Requête HTTP : un appel au serveur du site. Le navigateur l'envoie quand vous ouvrez une page. La bibliothèque requests fait la même chose depuis le code.
  • En-têtes de requête (headers) : les informations de service que le navigateur envoie avec la requête : type de navigateur, langue, formats de données. Le serveur s'en sert pour décider quoi renvoyer.
  • Proxy : un serveur intermédiaire par lequel passent vos requêtes. Les proxies mobiles utilisent des adresses IP d'opérateurs cellulaires et permettent de changer d'adresse sur commande.
  • Pagination : découpage des résultats en pages. Pour collecter tous les biens, le scraper doit parcourir les pages de la première à la dernière.
  • SQLite : une base de données légère dans un seul fichier. Pas besoin d'installer un serveur, elle est intégrée à Python. Idéale pour l'historique des prix.

Comment fonctionnent les résultats sur les portails immobiliers

Cian, Domclick, Yandex Real Estate et les autres plateformes fonctionnent sur un principe similaire. Il y a une page de recherche avec des filtres : ville, type de transaction, nombre de pièces, fourchette de prix. Chaque filtre se transforme en paramètre dans la barre d'adresse. Par exemple, le paramètre deal_type avec la valeur sale signifie une vente, et room1 égal à 1 ajoute les studios et une pièce. Comprendre ces paramètres vous donne un outil puissant : au lieu de cliquer sur le site, vous formez simplement l'adresse voulue.

Dans les résultats, chaque bien est présenté sous forme de carte : titre, prix, adresse, quelques photos, lien vers la page détaillée. La page détaillée contient les caractéristiques complètes et duplique souvent toutes les données dans un bloc JSON caché que le site utilise pour dessiner l'interface. Ce bloc est bien plus facile à parser que le HTML.

Cadre juridique et éthique

Attention : ne collectez que des informations publiquement accessibles sur les biens : prix, surface, adresse, caractéristiques de l'immeuble. Ne collectez pas et ne stockez pas les téléphones, noms et autres données personnelles des vendeurs et agents : cela relève de la loi sur les données personnelles, et une violation entraîne une responsabilité réelle. Lisez les conditions d'utilisation de la plateforme avant de commencer et utilisez les données pour votre propre analyse, pas pour de la revente ou pour créer une copie du site. Respectez une fréquence de requêtes raisonnable : votre scraper ne doit pas créer une charge qui gêne le fonctionnement du service.

Cette approche n'est pas seulement légale, elle est aussi pratique. Un scraper soigneux avec des pauses et une rotation d'adresses fonctionne des mois, tandis qu'un scraper agressif reçoit des restrictions temporaires dès la première heure.

Étape 1 : Définir l'objectif et la structure des données

Objectif de l'étape : décrire précisément ce que nous collectons et comment cela sera stocké. Sans cette étape, vous écrirez un scraper qui récupère tout et n'importe quoi, puis vous passerez une semaine à démêler un tas de données.

  1. Formulez la question métier. Exemples : quels appartements d'une pièce à Saint-Pétersbourg ont baissé de plus de 5 % en un mois ; combien coûte le mètre carré dans le neuf d'un quartier donné ; à quelle vitesse partent les biens en dessous d'un certain montant.
  2. Définissez le filtre de résultats. Pour l'exemple de ce guide, prenons : vente, ancien, appartements d'une et deux pièces, Moscou, prix jusqu'à 15 millions de roubles. Vous mettrez vos propres paramètres.
  3. Dressez la liste des champs. Pour chaque bien, il nous faut : un identifiant unique de l'annonce, le lien, le titre, le prix, l'adresse, la surface totale, l'étage et le nombre d'étages, le type d'immeuble, l'année de construction, la date de première détection, la date de dernière vérification. Pour l'historique des prix : l'identifiant de l'annonce, la date, le prix.
  4. Ouvrez votre éditeur de code et créez dans le dossier du projet un fichier config.py. Écrivez-y les paramètres que nous modifierons le plus souvent :
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'

Remarque : dans l'exemple de code, les retours à la ligne sont indiqués par des symboles de nouvelle ligne ; dans l'éditeur, écrivez simplement chaque variable sur une nouvelle ligne. Le paramètre region égal à 1 correspond à Moscou, 2 à Saint-Pétersbourg. Vous trouverez les codes des autres régions en appliquant le filtre sur le site et en observant la barre d'adresse.

Conseil : commencez avec MAX_PAGES égal à 2-3. Chaque page de résultats contient environ 28 biens, c'est suffisant pour déboguer. Lancez la collecte complète une fois que vous êtes sûr que tous les champs sont correctement extraits.

Vérification : vous avez un fichier config.py, et dans votre bloc-notes ou dans votre tête, une liste de 12 champs et une question métier précise. Si la question ressemble à « je veux toutes les données de toute la Russie », revenez en arrière et restreignez-la : une collecte complète à l'échelle du pays représente des centaines de milliers de biens et une infrastructure totalement différente.

Problèmes possibles

Vous n'arrivez pas à comprendre quel paramètre correspond au filtre voulu. Solution : ouvrez le site, réglez le filtre manuellement, copiez l'adresse depuis la barre d'adresse et décortiquez-la par les symboles &. Chaque paire clé égale valeur est un paramètre.

Étape 2 : Étudier la structure de la page de résultats

Objectif de l'étape : trouver dans le HTML les éléments d'où nous tirerons le prix, le titre, l'adresse et le lien. C'est l'étape la plus exploratoire, et c'est là que les débutants se perdent le plus souvent, donc nous avançons très lentement.

  1. Ouvrez le navigateur et allez sur la page de résultats Cian avec vos filtres. Assurez-vous que vous voyez la liste des appartements.
  2. Survolez le prix de n'importe quel appartement, faites un clic droit et choisissez Inspecter (dans Edge : Examiner). Le panneau des outils de développement s'ouvrira et l'élément du prix sera surligné.
  3. Regardez la ligne surlignée. Au moment de la rédaction du guide, c'est une balise span avec l'attribut data-mark égal à MainPrice. Notez cet attribut : il deviendra le sélecteur du prix.
  4. Remontez dans l'arbre des éléments en cliquant sur les balises parentes jusqu'à trouver la balise qui englobe toute la carte du bien. En général, c'est un article avec l'attribut data-name égal à CardComponent. Quand vous le survolez dans le panneau, toute la carte avec la photo et le prix se surligne sur la page.
  5. Dans la carte, trouvez le titre (span avec data-mark égal à OfferTitle), l'adresse (plusieurs liens a avec data-name égal à GeoLabel, qui composent l'adresse) et le lien vers le bien (balise a avec href menant à une adresse du type cian.ru/sale/flat/numéro). Notez les quatre sélecteurs.
  6. Trouvez le bloc de pagination en bas de page. Faites défiler les résultats jusqu'en bas, faites un clic droit sur le numéro de la deuxième page et regardez l'adresse. Vous verrez le paramètre p égal à 2. Donc, pour naviguer entre les pages, il suffit de changer ce paramètre.

Attention : les noms des attributs data-mark et data-name changent périodiquement sur les plateformes lors des refontes. Ne copiez pas aveuglément les sélecteurs de ce texte : vérifiez-les impérativement sur la page réelle dans le panneau de développement. Savoir trouver soi-même un sélecteur est plus important que n'importe quelle liste prête.

Vérifier s'il existe un JSON caché

De nombreuses plateformes stockent les données de résultats déjà prêtes dans une balise script. C'est plus pratique que le HTML : pas besoin de recoller l'adresse à partir de morceaux.

  1. Dans le panneau de développement, appuyez sur Ctrl+F (sur macOS Cmd+F) et tapez le mot offers ou initialState.
  2. Si la recherche a trouvé une balise script avec un gros volume de texte ressemblant à un dictionnaire avec des accolades, alors les données sont dans le JSON. Notez le nom de la variable au début de ce bloc.
  3. Si rien n'a été trouvé, ce n'est pas grave : l'approche HTML de l'étape suivante fonctionne dans tous les cas.

Conseil : ouvrez l'onglet Network (Réseau) dans le panneau de développement, rechargez la page et filtrez les requêtes par type Fetch/XHR. Parfois, le site charge les résultats via une requête séparée au format JSON. Si vous voyez une telle requête avec un champ offers, la parser est plus simple : vous obtenez des données propres sans HTML.

Vérification : vous avez noté les sélecteurs pour la carte, le prix, le titre, l'adresse et le lien, et vous connaissez le nom du paramètre de pagination. En cliquant sur chaque sélecteur dans le panneau, vous voyez le surlignage de l'élément voulu sur la page.

Problèmes possibles

Le panneau de développement affiche le HTML, mais le prix n'y est pas. Cause : le site dessine une partie des données via un script après le chargement. Solution : dans l'onglet Network, vérifiez si le prix arrive via une requête séparée, ou utilisez l'automatisation du navigateur de la section pour les avancés.

Étape 3 : Écrire le premier scraper Cian pour la page de résultats

Objectif de l'étape : obtenir un script qui télécharge la page de résultats, extrait la liste des biens et les affiche dans la console. Après cette étape, vous aurez une base fonctionnelle sur laquelle nous ajouterons des fonctions.

  1. Créez dans le dossier du projet un fichier parser.py.
  2. Importez les bibliothèques et les paramètres au début du fichier :
import time
import random
import requests
from bs4 import BeautifulSoup
from config import BASE_URL, SEARCH_PARAMS, MAX_PAGES, PAUSE_MIN, PAUSE_MAX
  1. Décrivez les en-têtes de requête. Le serveur doit y voir un navigateur normal avec une locale russe, sinon vous risquez d'obtenir une version différente de la page :
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. Écrivez la fonction de chargement de page. Elle prend le numéro de page, l'ajoute aux paramètres et renvoie le HTML. On vérifie impérativement le code de réponse : 200 signifie succès, tout le reste est un signal pour s'arrêter et comprendre :
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. Écrivez la fonction d'analyse. Elle trouve toutes les cartes et extrait les champs de chacune. Faites attention à la construction avec if : si un élément manque, on ne plante pas, on écrit 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. Assemblez la boucle principale. Elle parcourt les pages, fait une pause aléatoire entre les requêtes et empile les résultats dans une liste globale. La pause aléatoire est importante : des intervalles réguliers paraissent artificiels et créent une charge en pics :
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. Enregistrez le fichier et lancez dans le terminal la commande python parser.py. Assurez-vous que l'environnement venv est activé.

Analysons les points clés. Session conserve les cookies entre les requêtes, donc le site voit le comportement cohérent d'un même visiteur, et non une dizaine d'appels dispersés. La fonction select_one renvoie le premier élément correspondant ou None, c'est pourquoi nous vérifions toujours le résultat avant d'appeler get_text. L'identifiant de l'annonce est extrait du lien : c'est le dernier fragment de l'adresse, un nombre du type 312456789. C'est lui qui deviendra la clé de l'historique des prix.

Conseil : en phase de débogage, enregistrez le HTML de la première page dans un fichier avec la commande open('page1.html', 'w', encoding='utf-8').write(html). Vous pourrez alors déboguer la fonction d'analyse sur une copie locale, sans envoyer de requêtes inutiles au site.

Vérification : dans la console, vous voyez les lignes Страница 1 объектов: 28, Страница 2 объектов: 28, etc., et en bas cinq dictionnaires avec de vrais prix et adresses. Les prix doivent être des nombres entiers sans espaces ni signe rouble. Si au lieu de nombres vous avez du vide, revenez aux sélecteurs de la deuxième étape.

Problèmes possibles

Le script affiche объектов: 0 sur la première page. Causes : le sélecteur de la carte a changé, ou le serveur a renvoyé une page de substitution. Ouvrez le page1.html enregistré dans le navigateur et regardez ce que vous avez reçu. Si c'est une page demandant de confirmer que vous n'êtes pas un robot, augmentez les pauses et passez à la cinquième étape avec les proxies. Si c'est une page de résultats normale, vérifiez les sélecteurs.

Erreur ValueError lors de la conversion du prix. Cause : le texte du prix ne contient aucun chiffre, par exemple « Prix sur demande ». Solution : enveloppez la conversion dans un try et écrivez None pour ces cas.

Étape 4 : Collecter les fiches des biens

Objectif de l'étape : apprendre au scraper à entrer sur la page de chaque bien et à extraire des caractéristiques détaillées : surface, étage, type d'immeuble, année de construction. Ces champs sont nécessaires pour calculer le prix au mètre carré et comparer des appartements similaires.

  1. Ouvrez dans le navigateur la page de n'importe quel bien issu des résultats. Appuyez sur Ctrl+U pour voir le code source de la page.
  2. Appuyez sur Ctrl+F et tapez le mot totalArea. Au moment de la rédaction du guide, les données de la fiche se trouvent dans une balise script à l'intérieur de l'objet de configuration du frontend, dans une clé du type frontend-offer-card. Vous verrez les champs totalArea, floorNumber, floorsCount, buildYear, materialType et d'autres.
  3. Si la recherche par totalArea ne donne rien, cherchez les caractéristiques dans le HTML : c'est en général un bloc avec des paires nom et valeur, par exemple « Surface totale » et « 38,5 m² ». Notez le sélecteur de ce bloc.
  4. Ajoutez dans parser.py une fonction d'extraction du JSON de la fiche. Nous trouvons le script voulu, découpons l'objet par les accolades et l'analysons avec le module 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. Ici, nous utilisons volontairement des expressions régulières au lieu d'une analyse JSON complète. La raison est simple : le script de la page contient non seulement du JSON, mais aussi du code, et isoler l'objet pur est parfois difficile. Les expressions régulières cherchent la clé et le premier nombre après elle, ce qui est suffisamment fiable pour les champs numériques.
  2. Ajoutez une fonction qui prend la liste des biens des résultats et enrichit chacun avec les données de la fiche. Les pauses sont ici encore plus importantes, car le nombre de requêtes est multiplié par 28 :
def enrich_offers(offers, session):
for i, offer in enumerate(offers, 1):
try:
resp = session.get(offer['url'], headers=HEADERS, timeout=30)
if resp.status_code == 200:
offer.update(parse_offer_page(resp.text))
else:
print('Карточка', offer['offer_id'], 'статус', resp.status_code)
except requests.RequestException as e:
print('Ошибка сети на', offer['offer_id'], e)
if i % 10 == 0:
print('Обработано карточек:', i)
time.sleep(random.uniform(PAUSE_MIN, PAUSE_MAX))
return offers
  1. Dans le bloc if __name__, après collect_listing, ajoutez l'appel enrich_offers(data, requests.Session()) et relancez le script avec MAX_PAGES égal à 1, pour ne pas attendre longtemps.

Combien de temps cela prend : 28 fiches avec une pause moyenne de 6 secondes, soit environ 3 minutes. Une collecte complète de 5 pages de résultats avec les fiches prendra environ 15 minutes. C'est normal. Un scraper immobilier ne doit pas être rapide, il doit être stable.

Conseil : ne reparsez pas les fiches à chaque lancement. Les caractéristiques d'un appartement ne changent pas : la surface et l'année de construction suffisent à être collectées une fois. Seul le prix change, et il est dans les résultats. À la sixième étape, nous ferons en sorte que la fiche ne soit demandée que pour les nouveaux biens. Cela réduira le nombre de requêtes d'un facteur plusieurs dizaines.

Vérification : dans l'affichage des dictionnaires, les clés area, floor, floors_total et build_year sont apparues avec des valeurs plausibles : surface de 15 à 200, étage pas supérieur au nombre d'étages, année de 1900 à 2026. Si certains biens n'ont pas ces champs, c'est normal : tous les vendeurs ne renseignent pas l'année de construction.

Problèmes possibles

L'expression régulière trouve la surface d'une pièce au lieu de la surface totale. Cause : dans le JSON, il y a des clés similaires comme livingArea ou kitchenArea. Solution : précisez le motif en ajoutant un guillemet ou un deux-points avant la clé, pour qu'il ne corresponde pas à une partie d'un autre mot.

Étape 5 : Connecter des proxies mobiles et rendre la collecte robuste

Objectif de l'étape : répartir les requêtes via des proxies mobiles avec rotation d'IP, ajouter des tentatives répétées et une gestion correcte des réponses. Après cette étape, le scraper pourra fonctionner régulièrement et longtemps, sans créer de charge excessive depuis une seule adresse.

Pourquoi un scraper immobilier a besoin de proxies mobiles

Toute grande plateforme limite la fréquence des requêtes depuis une même adresse IP. C'est une protection contre la surcharge, et elle se déclenche sur n'importe quelle automatisation. Une adresse domestique, après quelques centaines de requêtes, commence à recevoir des réponses 429 ou des pages de vérification. Les proxies mobiles résolvent le problème autrement : vous obtenez une IP issue d'un pool d'opérateur cellulaire, et l'adresse change sur commande ou selon un minuteur. Vos requêtes se répartissent entre les adresses, la charge sur chacune reste faible, et le scraper fonctionne régulièrement. Pour un suivi régulier des prix, c'est fondamental : vous avez besoin non pas de données ponctuelles, mais de coupes quotidiennes pendant des mois.

Configuration

  1. Ouvrez l'espace client de votre service de proxies mobiles et trouvez le proxy acheté. Copiez quatre valeurs : l'hôte, le port, l'identifiant, le mot de passe. Copiez aussi le lien de changement d'IP : en général, c'est une adresse avec une clé, et en l'appelant, le proxy obtient une nouvelle adresse.
  2. Ajoutez dans config.py les paramètres du proxy. N'écrivez jamais les mots de passe dans un code que vous publiez quelque part : gardez-les dans un fichier séparé ou des variables d'environnement :
PROXY_HOST = 'ваш_хост'
PROXY_PORT = 'ваш_порт'
PROXY_USER = 'ваш_логин'
PROXY_PASS = 'ваш_пароль'
ROTATE_URL = 'ссылка_для_смены_ip'
ROTATE_EVERY = 25
  1. Ajoutez dans parser.py une fonction de création de session avec proxy. La bibliothèque requests accepte un dictionnaire avec les adresses pour http et 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. Vérifiez que le proxy fonctionne. Créez un fichier temporaire check_proxy.py avec un code qui demande un service de détection d'IP via la session et affiche la réponse :
from parser import make_session
s = make_session()
print(s.get('https://api.ipify.org', timeout=20).text)
  1. Lancez-le. Vous devez voir une adresse IP différente de votre adresse domestique. Appelez rotate_ip et relancez la vérification : l'adresse doit changer.
  2. Maintenant, ajoutez une fonction de requête avec tentatives répétées. Elle gère trois situations : réponse réussie, réponse 429 ou 403 (il faut attendre et changer d'adresse), erreur réseau (réessayer) :
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. Remplacez les appels session.get dans fetch_page et enrich_offers par safe_get. Ajoutez dans enrich_offers un compteur : toutes les ROTATE_EVERY requêtes, appelez rotate_ip. C'est une rotation planifiée, qui empêche l'adresse d'accumuler trop d'appels.

Attention : si vous recevez une réponse 429 ou une page de vérification, n'essayez pas de la percer avec des répétitions fréquentes. Cela ne fera qu'aggraver la situation pour l'adresse actuelle. La bonne réaction : s'arrêter, augmenter les pauses, changer d'IP et continuer à un rythme calme. Un scraper qui respecte les limites du site vit plus longtemps et collecte plus.

Conseil : utilisez un canal proxy par flux de scraping. La tentation de lancer dix flux via une même adresse est grande, mais c'est le chemin direct vers les restrictions. Si vous avez besoin de vitesse, achetez plusieurs canaux et répartissez-y différentes régions ou différents filtres.

Vérification : check_proxy.py affiche une adresse d'opérateur mobile, après rotation l'adresse change. Le scraper parcourt deux pages de résultats avec fiches sans aucun statut 429. Dans le log, on voit les lignes Смена IP: 200 toutes les 25 fiches.

Problèmes possibles

Erreur ProxyError ou 407. Cause : identifiant ou mot de passe incorrect, ou mauvais port. Solution : vérifiez les données dans l'espace client, assurez-vous d'utiliser le port du proxy HTTP et non SOCKS. Si vous avez du SOCKS5, installez la bibliothèque pysocks et utilisez le préfixe socks5h au lieu de http dans proxy_url.

Après la rotation, l'adresse ne change pas. Cause : l'opérateur a attribué la même adresse ou la rotation n'est pas encore appliquée. Solution : augmentez la pause après rotate_ip jusqu'à 10 secondes et vérifiez si la fréquence de changement d'IP n'est pas limitée dans votre forfait.

Étape 6 : Enregistrer les données et construire l'historique des prix

Objectif de l'étape : passer du scraper qui affiche dans la console à un scraper qui écrit dans une base SQLite, de sorte que chaque lancement ajoute un nouveau point dans l'historique des prix au lieu d'écraser l'ancien. C'est le cœur de tout le projet.

Concevoir les tables

Nous avons besoin de deux tables. La première, offers, stocke le bien : une ligne par annonce avec les caractéristiques et les dates. La seconde, prices, stocke le prix à une date : plusieurs lignes par annonce. Cette séparation évite de dupliquer la surface et l'adresse à chaque enregistrement de prix.

  1. Créez un fichier storage.py et décrivez la création des tables :
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. Ajoutez une fonction qui renvoie l'ensemble des offer_id déjà connus. Elle sert à ne demander les fiches que pour les nouveaux biens :
def known_ids(conn):
rows = conn.execute('SELECT offer_id FROM offers').fetchall()
return set(r[0] for r in rows)
  1. Écrivez la fonction d'enregistrement. Pour un nouveau bien, on insère une ligne dans offers. Pour tout bien, on met à jour last_seen et on enregistre le prix du jour. La construction INSERT OR REPLACE dans prices signifie : si le prix a déjà été enregistré aujourd'hui, mettre à jour, sinon ajouter :
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. Ajoutez une fonction qui marque les biens retirés. Si une annonce n'est pas apparue dans les résultats pendant plus de trois jours, on la considère inactive. Cela vous donne des données sur la vitesse de vente :
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. Réécrivez le bloc principal de parser.py pour qu'il collecte les résultats, détermine les nouveaux biens, enrichisse uniquement ceux-là et enregistre tout :
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'oubliez pas de modifier collect_listing pour qu'elle accepte session en paramètre au lieu d'en créer une. Lancez le script. Le fichier realty.db apparaîtra dans le dossier du projet.

Consulter l'historique des prix

Créez un fichier report.py qui exporte les variations de prix vers Excel. La requête ci-dessous trouve les biens dont le dernier prix diffère du premier :

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

Après le premier lancement, la colonne change_pct sera nulle : l'historique n'existe pas encore. Dès le deuxième jour, les premières variations apparaîtront. Au bout de deux semaines, vous verrez le tableau : combien de biens ont baissé leur prix, de combien en moyenne, quels quartiers bougent le plus vite.

Conseil : ajoutez à la fin de parser.py la copie de la base : shutil.copy(DB_PATH, 'backup_' + date.today().isoformat() + '.db'). Une ligne de code protégera des semaines de données accumulées. Une fois par mois, supprimez les anciennes copies en gardant les cinq dernières.

Vérification : le fichier realty.db existe, report.xlsx s'ouvre dans Excel, il contient des colonnes avec l'adresse, la surface, les prix et le prix au mètre carré. Relancez parser.py quelques minutes plus tard : la ligne Новых объектов doit afficher 0 ou un petit nombre, et les fiches ne doivent pas être redemandées. Cela confirme que la déduplication fonctionne.

Problèmes possibles

Erreur database is locked. Cause : la base est ouverte dans un autre programme, par exemple un visualiseur SQLite, ou deux instances du script tournent en même temps. Solution : fermez les programmes inutiles et ne lancez pas le script en parallèle avec lui-même.

Dans report.xlsx, tous les biens affichent la même date. Cause : le script n'a tourné qu'un seul jour. C'est attendu, attendez simplement les prochains lancements.

Étape 7 : Automatiser le lancement et étendre à d'autres plateformes

Objectif de l'étape : faire en sorte que le scraper se lance tout seul chaque matin, et préparer le code à connecter d'autres portails immobiliers. L'historique des prix n'a de valeur que s'il est régulier, donc l'automatisation est obligatoire.

Planification du lancement

  1. Sous Windows, ouvrez le Planificateur de tâches via la recherche dans le menu Démarrer. Cliquez sur Créer une tâche simple. Saisissez un nom, par exemple Scraper immobilier.
  2. Choisissez le déclencheur Quotidien, définissez l'heure, par exemple 07:30. Le petit matin est pratique : la charge sur les sites est minimale, et vous avez des données fraîches au début de la journée de travail.
  3. Dans l'action, choisissez Lancer un programme. Dans le champ Programme, indiquez le chemin complet vers python.exe dans le dossier venv, par exemple C:\projects\cian_parser\venv\Scripts\python.exe. Dans le champ Arguments, saisissez parser.py. Dans le champ Dossier de travail, indiquez le dossier du projet.
  4. Enregistrez la tâche et cliquez sur Exécuter dans le panneau de droite pour vérifier. La base doit se mettre à jour dans le dossier du projet.
  5. Sur macOS et Linux, utilisez cron. Tapez dans le terminal crontab -e et ajoutez la ligne : 30 7 * * * cd /путь/к/cian_parser && ./venv/bin/python parser.py >> run.log 2>&1. Le log de tous les lancements s'accumulera dans run.log.

Préparation à d'autres plateformes

Domclick, Yandex Real Estate, Metr Kvadratny et les portails régionaux fonctionnent de manière similaire, mais leurs sélecteurs et paramètres de filtres sont propres à chacun. Pour ne pas réécrire le scraper pour chaque site, isolez ce qui diffère dans des modules séparés.

  1. Créez un dossier sites dans le projet. Dedans, créez un fichier cian.py et déplacez-y les fonctions fetch_page et parse_cards avec les paramètres de recherche. Laissez-leur une interface identique : la fonction parse_cards prend du HTML et renvoie une liste de dictionnaires avec les mêmes clés offer_id, url, title, price, address.
  2. Pour une nouvelle plateforme, créez un fichier, par exemple domclick.py, et refaites l'étude de la deuxième étape : ouvrez les résultats, trouvez la carte, le prix, le lien et le paramètre de pagination. Écrivez vos versions de fetch_page et parse_cards.
  3. Dans offer_id, ajoutez un préfixe de plateforme, par exemple cian_312456789 et domclick_98765. Sinon, les identifiants de sites différents peuvent coïncider et mélanger l'historique.
  4. Dans parser.py, importez les modules depuis sites et lancez la collecte pour chacun dans une boucle. Les tables de la base sont communes : un schéma pour toutes les sources, et une colonne source indiquera d'où vient le bien.

Remarque importante sur Domclick et Yandex Real Estate : ces plateformes utilisent activement des API internes au format JSON, visibles dans l'onglet Network. Leurs résultats sont souvent plus pratiques à parser que le HTML, mais la structure des réponses change plus souvent. Vérifiez les sélecteurs et les champs une fois par mois.

Conseil : pour un même bien publié sur plusieurs plateformes, les prix peuvent différer. Comparer ces paires donne une analyse intéressante et aide à trouver les vendeurs qui ont baissé le prix quelque part mais oublié de le mettre à jour ailleurs. On peut apparier les biens par adresse, surface et étage.

Vérification : la tâche du planificateur s'est exécutée manuellement sans erreur, dans run.log ou dans l'historique du planificateur on voit une marque de succès. La structure des dossiers contient sites avec au moins un module, le scraper se lance depuis la racine du projet et fonctionne comme avant.

Problèmes possibles

Le planificateur indique que la tâche est terminée, mais la base n'est pas mise à jour. Cause : le script s'est lancé depuis un autre dossier de travail et a créé une nouvelle base vide ailleurs. Solution : remplissez le champ Dossier de travail dans la tâche ou utilisez un chemin absolu vers la base dans config.py.

Vérification du résultat : checklist du scraper prêt

Parcourez la liste et cochez chaque point. Si tout est fait, vous avez un scraper Cian complet avec historique des prix.

  • Le script parser.py se lance par commande depuis l'environnement activé, sans erreur d'import.
  • Les résultats sont collectés sur plusieurs pages, le nombre de biens par page correspond à ce que vous voyez dans le navigateur.
  • Les prix sont enregistrés en nombres entiers, les adresses sont lisibles, les liens s'ouvrent.
  • Les fiches ne sont demandées que pour les nouveaux biens, on voit dans le log la ligne Новых объектов avec un nombre qui diminue aux lancements suivants.
  • Les requêtes passent par un proxy mobile, check_proxy.py affiche une adresse d'opérateur, la rotation la change.
  • En cas de statut 429, le scraper fait une pause et change d'IP au lieu de planter.
  • Le fichier realty.db grandit, la table prices reçoit de nouvelles lignes chaque jour.
  • report.xlsx se forme et s'ouvre, au bout de quelques jours des variations de prix non nulles y apparaissent.
  • Le lancement automatique est configuré, le log enregistre chaque exécution.
  • La sauvegarde de la base est créée automatiquement.

Comment tester de bout en bout

  1. Supprimez ou renommez realty.db pour repartir de zéro.
  2. Réglez MAX_PAGES à 2 et lancez parser.py. Chronométrez : cela doit prendre environ 6-8 minutes avec les fiches.
  3. Lancez report.py et vérifiez que le rapport contient environ 56 lignes.
  4. Ouvrez realty.db avec n'importe quel visualiseur SQLite ou exécutez en Python la requête SELECT COUNT(*) FROM prices. Le nombre doit correspondre au nombre de biens.
  5. Relancez parser.py. Le temps d'exécution doit tomber à une minute, car les fiches ne sont pas demandées. Le nombre de lignes dans prices ne changera pas, la date étant la même.
  6. Modifiez dans la base le prix d'un bien à la main, mettez n'importe quel autre nombre, changez la date d'enregistrement à hier et relancez le scraper. Dans report.xlsx, ce bien doit afficher une variation de prix. Vous vérifiez ainsi que la logique d'historique fonctionne sans attendre de vraies variations.

Indicateurs de succès

Part des biens avec surface renseignée supérieure à 90 %. Part des requêtes avec statut 200 supérieure à 97 %. Aucune exception non gérée sur une exécution. Temps d'exécution complet prévisible et qui n'augmente pas d'un lancement à l'autre. Si les indicateurs sont inférieurs, revenez à la section des erreurs typiques.

Erreurs typiques et solutions

Nous avons rassemblé les problèmes auxquels sont confrontés pratiquement tous ceux qui écrivent un scraper d'annonces immobilières pour la première fois. Format : problème, cause, solution.

Le scraper renvoie 0 objet alors qu'il fonctionnait hier

Cause : la plateforme a mis à jour la mise en page et changé les attributs data-mark ou data-name. Solution : ouvrez les résultats dans le navigateur, refaites la deuxième étape et mettez à jour les sélecteurs. Prenez l'habitude de stocker les sélecteurs au même endroit en début de module, pour que les corrections prennent une minute. Ajoutez au scraper une vérification : si la première page contient 0 objet, envoyez-vous une notification par messagerie.

Les prix sont enregistrés avec une erreur d'un facteur mille

Cause : pour certains biens, le prix est indiqué en milliers ou avec la mention « par mois », ou le texte a capté le prix au mètre carré. Solution : assurez-vous de prendre MainPrice et non l'élément voisin avec le prix au mètre. Ajoutez une vérification de plausibilité : un prix de vente d'appartement à Moscou inférieur à un million de roubles est presque sûrement une erreur d'analyse, journalisez ces cas.

Le statut 429 arrive dès la troisième page

Cause : les pauses sont trop courtes ou le proxy n'est pas encore connecté, toutes les requêtes partent de la même IP domestique. Solution : augmentez PAUSE_MIN et PAUSE_MAX à 6 et 12, connectez un proxy mobile, activez la rotation planifiée toutes les 20-25 requêtes. Vérifiez que vous n'avez pas lancé plusieurs instances du script en même temps.

Erreur UnicodeEncodeError lors de l'affichage dans la console Windows

Cause : la console Windows standard n'affiche pas toujours correctement les caractères accentués. Solution : exécutez dans le terminal chcp 65001 avant le lancement, ou ajoutez au début du script la ligne sys.stdout.reconfigure(encoding='utf-8'). Vous pouvez aussi écrire les logs dans un fichier plutôt que dans la console.

L'adresse est collectée incomplète ou avec des doublons

Cause : l'adresse sur la carte se compose de plusieurs liens GeoLabel, certains dupliquent la ville et l'arrondissement. Solution : supprimez les doublons en conservant l'ordre, ou prenez l'adresse depuis la fiche du bien, où elle est présentée en une seule chaîne. Pour l'analyse par quartier, ajoutez un champ district séparé, en le découpant depuis l'adresse selon une liste connue de quartiers.

Le scraper fonctionne en lancement manuel mais pas dans le planificateur

Cause : le planificateur utilise un autre interpréteur Python sans les bibliothèques installées, ou un autre dossier de travail. Solution : indiquez le chemin absolu vers python.exe dans venv et remplissez le champ du dossier de travail. Redirigez la sortie vers un fichier de log pour voir les erreurs.

La base pèse des gigaoctets au bout d'un mois

Cause : vous stockez tout le HTML des pages ou tous les champs JSON dans la base. Solution : ne gardez que les champs nécessaires. Si vous voulez conserver les pages brutes pour une réanalyse, rangez-les dans des fichiers compressés sur disque, pas dans SQLite. Une fois par trimestre, exécutez la commande VACUUM pour compacter la base.

Les mêmes biens sont dupliqués sous différents identifiants

Cause : le vendeur a retiré l'annonce et l'a republiée, obtenant un nouveau numéro. Solution : ajoutez une clé de correspondance supplémentaire à partir de l'adresse, de la surface et de l'étage. Les biens avec la même clé mais des offer_id différents peuvent être reliés dans une table séparée et l'historique de prix calculé par association. C'est déjà de l'analyse avancée, mais c'est elle qui révèle les vraies baisses de prix cachées derrière une republication.

Fonctionnalités supplémentaires pour les avancés

Le scraper de base est prêt. Si vous voulez plus, voici les directions qui rapportent le plus.

Automatisation du navigateur via Playwright

Certaines pages chargent les données via des scripts après le chargement, et requests obtient une coquille vide. Dans ces cas, utilisez Playwright : il pilote un vrai navigateur. Installez-le avec pip install playwright et playwright install chromium. Le proxy est passé au lancement du navigateur dans le paramètre proxy avec un dictionnaire server, username, password. Attendez l'apparition des cartes avec page.wait_for_selector et passez page.content() à la fonction parse_cards déjà écrite. Notez que le navigateur consomme dix fois plus de ressources, donc utilisez-le de manière ciblée, uniquement pour les pages problématiques.

Collecte parallèle sur plusieurs canaux proxy

Si vous devez collecter plusieurs régions, achetez un proxy mobile distinct par région et lancez un processus séparé par canal. N'utilisez pas le multithreading au sein d'un même canal : le sens de la répartition de charge disparaît. Méthode simple : le paramètre de région est passé au script en argument de ligne de commande, et le planificateur lance plusieurs tâches avec des arguments différents et un léger décalage horaire.

Notifications de baisse de prix

Ajoutez à la fin du scraper la comparaison du prix du jour avec le précédent pour chaque bien. Si la baisse dépasse un seuil donné, par exemple 3 %, formez un message avec l'adresse, l'ancien et le nouveau prix, le lien, et envoyez-vous-le via un bot de messagerie. Cela transforme le scraper d'un outil d'analyse en outil d'action : vous êtes informé des biens intéressants dans l'heure suivant la modification.

Analyse et visualisation

Avec pandas, vous pouvez grouper les données par quartier et calculer le prix médian au mètre carré, la part des biens avec baisse, le temps d'exposition moyen (différence entre first_seen et last_seen pour les biens inactifs). La bibliothèque matplotlib construira un graphique de dynamique sur un mois en trois lignes de code. Chargez le rapport dans Google Sheets, et vos collègues sans compétences en programmation auront un tableau de bord vivant.

Stockage dans PostgreSQL

Quand le nombre de biens dépassera cent mille, SQLite commencera à ralentir sur les requêtes analytiques. Le passage à PostgreSQL est simple : le schéma des tables reste le même, seule la chaîne de connexion et la bibliothèque changent (psycopg2 au lieu de sqlite3). Ne le faites que si c'est réellement nécessaire : pour une seule ville, SQLite suffit pendant des années.

Surveillance de la santé du scraper

Enregistrez dans une table séparée runs l'heure de début, l'heure de fin, le nombre de biens collectés, le nombre d'erreurs et le nombre de rotations d'IP. Si le nombre de biens chute brutalement ou si les erreurs dépassent 5 %, envoyez une notification. Une telle surveillance permet de remarquer un changement de mise en page le jour même, et non deux semaines plus tard devant un rapport vide.

FAQ : questions fréquentes sur la création d'un scraper immobilier

Est-il légal de scraper Cian et d'autres plateformes ?

La collecte d'informations publiquement accessibles sur les biens pour une analyse personnelle est généralement acceptable, mais les conditions de chaque plateforme sont décrites dans son contrat d'utilisation, et il faut le lire. Il est formellement interdit de collecter les données personnelles des vendeurs, de publier une base copiée comme la sienne et de créer une charge qui gêne le fonctionnement du service. Si vous envisagez une utilisation commerciale des données, consultez un juriste.

Pourquoi des proxies mobiles et pas des proxies serveur ?

Les adresses des opérateurs mobiles sont partagées par des milliers d'abonnés réels et changent constamment. Les plateformes les traitent avec plus d'indulgence que les adresses de centres de données, depuis lesquelles les acheteurs réels ne se connectent presque jamais. De plus, la possibilité de changer d'IP par lien permet une rotation maîtrisée sans acheter des centaines d'adresses.

À quelle fréquence lancer le scraper pour l'historique des prix ?

Une fois par jour est optimal. Les prix immobiliers changent rarement, collecter plus d'une fois par jour n'a pas de sens et la charge augmente. Si vous devez repérer les baisses rapidement, lancez deux fois par jour, matin et soir, mais uniquement sur un filtre étroit.

Combien de biens peut-on collecter par jour via un seul proxy ?

Avec des pauses de 4 à 9 secondes et une rotation planifiée, c'est environ 500-700 requêtes par heure sans problème, soit 8-12 milliers par jour en fonctionnement 24h/24. Pour le suivi d'une seule ville, 2-3 milliers de requêtes par jour suffisent généralement, car les fiches ne sont demandées que pour les nouveaux biens.

Que faire si les sélecteurs ont changé et que je n'arrive pas à les trouver ?

Revenez à la deuxième étape et partez du prix : clic droit sur le prix, Inspecter, remontez l'arbre jusqu'à la carte. Cherchez des attributs avec le mot data et des noms porteurs de sens : ils sont plus stables que les classes aux caractères aléatoires. Vérifiez aussi l'onglet Network : peut-être que les données arrivent maintenant via une requête JSON séparée, et les parser sera même plus simple.

Peut-on se passer de proxies pour un petit projet ?

Pour une collecte ponctuelle de deux ou trois pages, oui. Pour un suivi quotidien avec des centaines de requêtes, l'adresse domestique commencera vite à recevoir des restrictions, et les données deviendront incomplètes. Un historique de prix avec des trous perd sa valeur, donc pour un travail régulier, les proxies sont nécessaires.

Comment scraper la location plutôt que la vente ?

Changez le paramètre deal_type en rent et ajoutez le type de location dans les paramètres de recherche : longue durée ou courte durée. Les liens vers les biens contiendront rent au lieu de sale, donc mettez à jour le sélecteur de lien dans parse_cards. Le reste de la logique, y compris l'historique des prix, fonctionne sans changement.

Faut-il stocker les photos des biens ?

Pour l'analyse des prix, non. Les photos prennent beaucoup de place et ne sont pas nécessaires aux calculs. Si vous construisez un catalogue pour un usage interne, ne conservez que les liens vers les images, pas les fichiers eux-mêmes.

Comment savoir si un bien est vendu plutôt que simplement retiré ?

Les plateformes ne communiquent pas la raison du retrait. Un indice indirect : le bien a disparu des résultats et n'est pas réapparu pendant un mois. Les biens qui disparaissent et réapparaissent quelques jours plus tard avec un autre prix sont plutôt republiés. Reliez-les par adresse et surface, comme décrit dans la section des erreurs.

Que faire si j'ai besoin de données sur dix villes ?

Établissez une liste de régions dans config.py et lancez la collecte en boucle avec un canal proxy séparé pour deux ou trois villes. Décalez les lancements dans le temps pour ne pas tout collecter en même temps. La base reste commune, ajoutez le champ région dans la table offers.

Conclusion

Regardons ce que vous avez fait. Vous avez préparé l'environnement avec Python et les bibliothèques, compris comment fonctionnent les résultats d'une plateforme immobilière et appris à trouver les sélecteurs vous-même. Vous avez écrit un scraper Cian qui collecte les résultats et les fiches des biens. Vous avez connecté des proxies mobiles avec rotation et tentatives répétées, grâce à quoi la collecte est devenue robuste et prévisible. Vous avez conçu une base avec historique des prix, configuré les rapports et le lancement automatique selon un planning. C'est un outil de travail complet, pas un exemple pédagogique.

Que faire ensuite : laissez le scraper tourner deux semaines sans modifications. Pendant ce temps, l'historique s'accumulera, et vous verrez les points faibles : où la part des champs remplis baisse, quels biens se dupliquent, où le rapport demande de nouvelles colonnes. C'est seulement après que vous ajouterez des fonctions. Ensuite, connectez une deuxième plateforme, en utilisant la structure modulaire de la septième étape : vous serez surpris de voir à quel point cela ira plus vite la deuxième fois.

Vers quoi évoluer : les notifications de baisse de prix transformeront l'outil en source d'opportunités. L'analyse par quartier et type d'immeuble fera de vous un expert du marché avec des chiffres en main. La liaison des biens republiés montrera les vraies remises, invisibles sur le site. Et une attitude soigneuse envers la plateforme, des pauses, une rotation d'adresses via des proxies mobiles et la collecte des seules données nécessaires permettront à l'outil de fonctionner des mois sans incident.

Le scraping immobilier, ce n'est pas une question de vitesse, mais de régularité et de qualité des données. Vous avez posé les bonnes fondations. Bonne collecte, et que votre base grandisse chaque matin.