spb/lou-ka Public
Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 99.7%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/atlas_immo.py : connecteur Atlas société immobilière5# (atlasimmo.ca — Beauce : Saint-Georges, Saint-Victor, Notre-Dame-des-Pins,6# ~50-100 logements 2½–6½). CMS custom « webchef », pages .php statiques7# servies en HTTP (pas de TLS). Page /logements-a-louer-en-beauce.php : toutes8# les unités disponibles sont rendues serveur (cartes `div.resultat` — nom9# d'immeuble, rue + ville, description « 4X 4 et demi moderne », drapeaux10# Disponible/Chauffé/Éclairé avec icônes ok/cancel, photo) ; le filtre11# ville/grandeur/prix est purement client (JS). Quand aucun logement n'est12# libre, seule la mention « Aucun appartement n'est disponible… » s'affiche13# et le connecteur retourne [] — situation normale (parc beauceron plein).14# Prix jamais affiché sur les cartes -> None (rien d'inventé) ; granularité15# immeuble assumée (précédent immeubles_guillot). Les fiches16# /logement-a-louer/<id>-<slug>.php existent mais leur structure n'a jamais17# été observée (aucune archive) : pas d'appel détail pour l'instant.18# robots.txt : Disallow répertoires internes (/webchef, /inc…) seulement.19# -----------------------------------------------------------------------------20from __future__ import annotations2122import re2324from bs4 import BeautifulSoup2526from ..schema import Listing, normalize_unit_type27from .base import BaseConnector2829BASE = "http://www.atlasimmo.ca"30LIST_URL = f"{BASE}/logements-a-louer-en-beauce.php"3132# type d'unité dans la description d'immeuble (« 4X 4 et demi moderne »)33_TYPE_RE = re.compile(r"(\d)\s*(?:½|1/2|et\s+demi)", re.I)343536class AtlasImmoConnector(BaseConnector):37 source_id = "atlas_immo"38 request_delay = 0.63940 def fetch(self) -> list[Listing]:41 html = self.get(LIST_URL).text42 soup = BeautifulSoup(html, "html.parser")4344 listings: dict[str, Listing] = {}45 for card in soup.select("div.resultat"):46 try:47 self._parse_card(card, listings)48 except Exception:49 continue50 return list(listings.values())5152 # -- carte (div.resultat, id bloc-<n>) -----------------------------------------53 def _parse_card(self, card, listings: dict[str, Listing]) -> None:54 link = card.select_one('a[href*="/logement-a-louer/"]')55 if not link:56 return57 url = link["href"]58 if url.startswith("/"):59 url = BASE + url60 m = re.search(r"/logement-a-louer/([^/?#]+?)(?:\.php)?$", url)61 if not m:62 return63 ext_id = m.group(1)64 if not ext_id or ext_id in listings:65 return6667 title_el = card.select_one("h2")68 title = title_el.get_text(strip=True) if title_el else ""6970 # adresse : <li>Rue Ambroise<br><strong>Saint-Victor</strong>, ---</li>71 address, city = "", ""72 li = card.select_one(".description ul li")73 if li:74 strong = li.select_one("strong")75 city = strong.get_text(strip=True) if strong else ""76 street = li.get_text("\n", strip=True).split("\n")[0].strip()77 street = street.rstrip(",")78 address = f"{street}, {city}".strip(", ") if city else street7980 # description libre de l'immeuble (« 4X 4 et demi moderne »)81 desc_el = card.select_one(".description p:not(.aligneDroit)")82 description = desc_el.get_text(" ", strip=True) if desc_el else ""8384 m_type = _TYPE_RE.search(description)85 unit_type = normalize_unit_type(f"{m_type.group(1)} ½") if m_type else ""8687 # drapeaux : <span><i class="icon-ok"/> Disponible</span> — icon-cancel = non88 availability = ""89 amenities: list[str] = []90 for span in card.select(".options span"):91 label = span.get_text(" ", strip=True)92 icon = span.select_one("i")93 ok = bool(icon and "icon-ok" in (icon.get("class") or []))94 if re.search(r"disponible", label, re.I):95 if ok:96 availability = label97 elif ok and label:98 amenities.append(label)99100 images: list[str] = []101 img = card.select_one("img[src]")102 if img:103 u = img["src"]104 if u.startswith("/"):105 u = BASE + u106 if u.startswith("http"):107 images.append(u)108109 listings[ext_id] = Listing(110 source=self.source_id,111 external_id=ext_id,112 url=url,113 title=title,114 address=address,115 city=city,116 unit_type=unit_type,117 price=None, # jamais affiché sur les cartes118 price_label="",119 availability=availability,120 description=description,121 amenities=amenities,122 images=images,123 )124