Scrapping page web agence immobilière.

je vous explique précisément et pragmatiquement comment scraper ces pages web (annonces immobilières) et injecter proprement les données dans Odoo. Je donne :

  1. un rappel légal / éthique à respecter
  2. l’architecture générale du pipeline
  3. le mapping de champs recommandé (table)
  4. un script Python statique (requests + BeautifulSoup) pour pages simples
  5. variante JS dynamique (Selenium) si nécessaire
  6. un script Python d’import vers Odoo (XML-RPC) + options CSV
  7. conseils opérationnels (images, pagination, taux, logs, monitoring)

0) Avant de commencer — point légal & pratique

  • Vérifiez robots.txt du site et les conditions générales : si le site interdit le scraping à grande échelle, contactez l’agence pour demander un accès API/flux (beaucoup d’agences acceptent un partenariat).
  • Respectez un rate limit (ex. 1 requête / 2s), user-agent clair, et loggez tout.
  • Stockez la provenance (URL + date) pour traçabilité.

1) Architecture du pipeline (haute-niveau)

  1. Crawler → récupère pages listing et pages détail.
  2. Parseur → extrait champs (réf, titre, adresse, prix, desc, images, caractéristiques).
  3. Asset manager → télécharge images, normalise noms.
  4. Transform → nettoie valeurs (prix → int, surface → float, geo → lat/lon si possible).
  5. Load (Odoo) → soit via CSV import (interface Odoo), soit via API (XML-RPC / JSON-RPC) pour création automatique.
  6. Vérification humaine (HITL) : revoir nouvelles annonces avant publication.

2) Mapping champs recommandés (table — format copiable)

champ_scrap | type | destination Odoo (suggestion) --------------------|-------------|------------------------------------- ref | string | estate.property.ref (ou product.default_code) title | string | name / product.template.name city | string | city / x_city postcode | string | zip / x_postcode address | string | street / x_address price_eur | integer | list_price (product) ou x_price property_type | string | x_type (maison, terrain, etc.) surface_m2 | float | x_surface_m2 nb_rooms | integer | x_nb_rooms description | text | description / description_sale main_image_url | string | (download -> image_1920) additional_images | list(url) | image gallery -> attachments date_published | date | create_date / x_published_date agency_ref | string | company_id / seller_id url_origin | string | x_source_url status | string | state (draft/published)

Remarque : x_ = champ personnalisé si vous créez un module « estate » ou utilisez product.template + attributs.

3) Scraper : script Python (pages statiques)

Ce script parcourt une page de listing, suit chaque lien d’annonce vers la page détail, extrait champs et télécharge la photo principale. Résultat : annonces.csv + dossier images/.

# scrape_listings.py import requests, os, csv, time from bs4 import BeautifulSoup from urllib.parse import urljoin BASE = "https://www.example-agency.com" # remplacer START_URL = "https://www.example-agency.com/offres/ville/rodez" HEADERS = {"User-Agent": "MyBot/1.0 (+email@example.com)"} OUT_CSV = "annonces.csv" IMG_DIR = "images" os.makedirs(IMG_DIR, exist_ok=True) def fetch(url): r = requests.get(url, headers=HEADERS, timeout=15) r.raise_for_status() return r.text def parse_listing_page(html): soup = BeautifulSoup(html, "html.parser") # selector: a chaque vignette d'annonce -> lien vers détail cards = soup.select("div.listing-card a[href*='/annonce/']") # à adapter links = [urljoin(BASE, a['href']) for a in cards] return links def parse_detail(html, url): soup = BeautifulSoup(html, "html.parser") # Exemples de selecteurs — adapter au site ref = (soup.select_one(".ref") or soup.select_one(".property-ref") ) ref = ref.get_text(strip=True) if ref else "" title = (soup.select_one("h1.title") or soup.select_one(".product-title")).get_text(strip=True) price_raw = (soup.select_one(".price") or soup.select_one(".property-price")).get_text(strip=True) # nettoyez le prix : ex "Prix : 227 000 €" import re price = int(re.sub(r"[^\d]", "", price_raw)) if price_raw else 0 address = (soup.select_one(".address") or soup.select_one(".location")).get_text(strip=True) if soup.select_one(".address") else "" desc = (soup.select_one(".description") or soup.select_one("#desc")).get_text("\n", strip=True) main_img_el = soup.select_one(".gallery img") or soup.select_one(".main-photo img") main_img = urljoin(url, main_img_el['src']) if main_img_el and main_img_el.get('src') else "" # autres champs (surface, rooms) via regex sur description surface = None m = re.search(r"(\d+)\s*m²", desc) if m: surface = float(m.group(1)) return { "ref": ref, "title": title, "price": price, "address": address, "description": desc, "main_image": main_img, "surface_m2": surface, "url": url } def download_image(url, outdir=IMG_DIR): if not url: return "" name = url.split("/")[-1].split("?")[0] path = os.path.join(outdir, name) if os.path.exists(path): return path r = requests.get(url, headers=HEADERS, stream=True, timeout=20) r.raise_for_status() with open(path, "wb") as f: for chunk in r.iter_content(1024*8): f.write(chunk) time.sleep(1.2) # rate limit return path def main(): # pagination simplifiée (ex: page=1..N) next_url = START_URL all_links = set() while next_url: print("Fetch listing:", next_url) html = fetch(next_url) links = parse_listing_page(html) all_links.update(links) # detecter lien "suivant" soup = BeautifulSoup(html, "html.parser") nxt = soup.select_one("a.next") next_url = urljoin(BASE, nxt['href']) if nxt else None time.sleep(1.5) print("Total annonces:", len(all_links)) rows = [] for url in list(all_links): try: html = fetch(url) data = parse_detail(html, url) img_path = download_image(data["main_image"]) data["main_image_local"] = img_path rows.append(data) time.sleep(1.5) except Exception as e: print("Erreur:", url, e) # écrire CSV keys = ["ref","title","price","address","surface_m2","description","main_image","main_image_local","url"] with open(OUT_CSV,"w",newline="",encoding="utf-8") as f: writer = csv.DictWriter(f, keys) writer.writeheader() for r in rows: writer.writerow(r) print("Fini - CSV:", OUT_CSV) if __name__ == "__main__": main()

À adapter : selecteurs CSS (.listing-card, .price, etc.) selon le HTML réel (utilisez l’inspecteur du navigateur).

4) Si la page est dynamique (JS) — Selenium (headless)

from selenium import webdriver from selenium.webdriver.chrome.options import Options from bs4 import BeautifulSoup import time options = Options() options.headless = True driver = webdriver.Chrome(options=options) # assurez-vous d'avoir chromedriver driver.get(START_URL) time.sleep(3) # attendre le chargement # éventuellement scroll pour lazy-load driver.execute_script("window.scrollTo(0, document.body.scrollHeight);") time.sleep(1.5) html = driver.page_source soup = BeautifulSoup(html, "html.parser") # ensuite récupérer liens comme avant driver.quit()

5) Importer dans Odoo — 2 options

Option A — Importer le CSV manuellement via l’interface Odoo

  • Avantages : rapide, pas de code serveur, mapping via UI.
  • Étapes :
    1. Menu Produits (ou module État immobilier si installé).
    2. Importer annonces.csv.
    3. Mapper colonnes CSV → champs Odoo (ex. main_image_local → Image).
    4. Vérifier doublons (clé ref).

Option B — Automatisation via XML-RPC (Python) — création de records automatiquement

Exemple (Odoo 14+) : créer un produit (product.template) et assigner l’image (base64).

import xmlrpc.client, base64, csv, os url = "https://your-odoo.example" db = "your_db" username = "admin@example.com" password = "your_password" common = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/common") uid = common.authenticate(db, username, password, {}) models = xmlrpc.client.ServerProxy(f"{url}/xmlrpc/2/object") def image_to_base64(path): with open(path, "rb") as f: return base64.b64encode(f.read()).decode("utf-8") with open("annonces.csv", newline="", encoding="utf-8") as csvfile: reader = csv.DictReader(csvfile) for row in reader: # check existing via ref (default_code) existing = models.execute_kw(db, uid, password, 'product.template', 'search', [[['default_code', '=', row['ref']]]], {'limit':1}) if existing: print("exists", row['ref']); continue vals = { 'name': row['title'], 'type': 'product', 'list_price': float(row['price'] or 0), 'default_code': row['ref'], 'description_sale': row['description'] or '', # champs custom: x_address, x_surface_m2 etc. 'x_address': row.get('address',''), 'x_surface_m2': float(row.get('surface_m2') or 0), 'sale_ok': True, 'purchase_ok': False, } # image img_local = row.get('main_image_local') or '' if img_local and os.path.exists(img_local): vals['image_1920'] = image_to_base64(img_local) product_id = models.execute_kw(db, uid, password, 'product.template', 'create', [vals]) print("created product", product_id, row['ref'])

Remarques :

  • Idéalement, installez un module estate / real_estate qui apporte un modèle estate.property plus adapté ; sinon utilisez product.template ou sale.order selon votre usage.
  • Créez des champs personnalisés (x_*) pour stocker adresse, surface, rooms, url_origin, etc. (via Studio ou module).

6) Gestion des images & galerie

  • Téléchargez images et stockez localement ou sur un bucket S3.
  • Dans Odoo, vous pouvez attacher plusieurs images via le modèle product.image (ou via ir.attachment sur res.model = estate.property).
  • En XML-RPC : créer ir.attachment records liés à res_model/res_id.

Exemple rapide d’attachement supplémentaire :

attachment_vals = { 'name': 'img1.jpg', 'datas': base64.b64encode(open(img_local,'rb').read()).decode(), 'res_model': 'product.template', 'res_id': product_id, 'type': 'binary', } att_id = models.execute_kw(db, uid, password, 'ir.attachment', 'create', [attachment_vals])

7) Robustesse & production

  • Déduplication : utiliser ref ou url_origin comme clé unique.
  • Retry & backoff sur erreurs réseau.
  • Monitoring : logs (success/fail), métriques (nb importés), tableau d’erreurs CSV.
  • Interface de validation : ajouter une appli Odoo (Kanban) pour relire les annonces importées en draft avant published.
  • Planification : cron (ex. n8n ou Airflow) pour exécution quotidienne.

8) Checklist pratique pour vous lancer (ordre d’exécution)

  1. Inspecter le code HTML de la page (F12) → collecter selecteurs exacts.
  2. Tester requests + BeautifulSoup localement sur 5 annonces.
  3. Adapter script (selectors) et exécuter pour 1 page complète.
  4. Générer annonces.csv + images.
  5. Test manuel : importer CSV dans Odoo (sandbox).
  6. Si OK, automatiser via XML-RPC et prévoir staging → prod.
  7. Surveiller robots.txt et contacter l’agence si volume élevé.

je peux faire tout de suite > Analyser le HTML concret si vous me donnez l’URL ou le HTML (je vous fournis les selecteurs CSS exacts).

Les grandes histoires ont une personnalité. Envisagez de raconter une belle histoire qui donne de la personnalité. Écrire une histoire avec de la personnalité pour des clients potentiels aidera à établir un lien relationnel. Cela se traduit par de petites spécificités comme le choix des mots ou des phrases. Écrivez de votre point de vue, pas de l'expérience de quelqu'un d'autre.

Les grandes histoires sont pour tout le monde, même lorsqu'elles ne sont écrites que pour une seule personne. Si vous essayez d'écrire en pensant à un public large et général, votre histoire sonnera fausse et manquera d'émotion. Personne ne sera intéressé. Ecrire pour une personne en particulier signifie que si c'est authentique pour l'un, c'est authentique pour le reste.

Générer un script d’import Odoo adapté à votre version Odoo (XML-RPC) et au modèle que vous préférez (product.template vs estate.property).

Remarques liminaires:  Odoo Online (SaaS) accepte les APIs XML-RPC et JSON-RPC.

La seule vraie limite d’Online : pas de modules custom installables (sauf via Odoo Studio). Donc :

  • utilisez des modèles natifs (ex. product.template, crm.lead) ou
  • créez votre propre modèle avec Studio (les champs auront des noms techniques x_...), puis l’API peut écrire dedans.

odoo Studio pour faire une base Product.template / immobilier


Ci-dessous je vous fournis un script prêt à adapter pour Odoo Online en XML-RPC (et un équivalent JSON-RPC si vous préférez). Il gère la création d’articles immobiliers dans product.template (simple et disponible en Online), avec image principale en base64, déduplication par default_code (votre référence annonce) et support des champs Studio.


Découvrir plus