# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/desroches.py : connecteur Immeubles Claude Desroches # (immeublesclaudedesroches.com — Granby, Cowansville, Drummondville). # Site PHP custom : une seule page statique /logements-a-louer/ avec un # tableau par ville (« Nos logements à louer à ») ; une ligne par # logement, repérée par son ancre stable . Colonnes : # photo (badge est-loue.png = déjà loué -> ignoré), adresse, grandeur, # prix, étage, note libre (mois de libération, inclusions, rénové…). # La section « LOCAUX À LOUER » (commercial) n'a pas d'ancres logNN et # est donc naturellement exclue. Aucune page détail n'existe. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type, parse_price from .base import BaseConnector BASE = "https://www.immeublesclaudedesroches.com" LIST_URL = f"{BASE}/logements-a-louer/" _ANCHOR_RE = re.compile(r"^log\d+$") _CITY_RE = re.compile(r"logements à louer à\s+([A-Za-zÀ-ÿ' -]+)", re.I) # segments de la note libre qui parlent de disponibilité (mois ou « libre ») — # le reste (« rénové », « chauffé », « Ascenseur »…) n'est pas une date et # fausserait la normalisation centrale _AVAIL_SEG_RE = re.compile( r"\b(janvier|f[ée]vrier|mars|avril|mai|juin|juillet|ao[ûu]t|septembre|" r"octobre|novembre|d[ée]cembre|libre|maintenant)\b", re.I) def _availability_from_note(note: str) -> str: """Ne garde de la note que les segments de disponibilité (« juillet », « libre »…) ; « rénové, juillet » -> « juillet ».""" segs = [s.strip() for s in re.split(r"[,;]", note or "")] return ", ".join(s for s in segs if s and _AVAIL_SEG_RE.search(s)) class DesrochesConnector(BaseConnector): source_id = "desroches" request_delay = 0.6 def fetch(self) -> list[Listing]: html = self.get(LIST_URL).text soup = BeautifulSoup(html, "html.parser") listings: dict[str, Listing] = {} for anchor in soup.find_all("a", attrs={"name": _ANCHOR_RE}): try: lst = self._parse_row(anchor) except Exception: continue if lst and lst.external_id not in listings: listings[lst.external_id] = lst return list(listings.values()) # -- ligne du tableau ----------------------------------------------------------- def _parse_row(self, anchor) -> Listing | None: row = anchor.find_parent("tr") if row is None: return None # badge « est loué » superposé à la photo -> logement non offert if row.find("img", src=re.compile(r"est-loue", re.I)): return None # ville : plus proche en-tête « Nos logements à louer à » # avant la ligne (les tableaux sont groupés par ville) city = "" for prev in anchor.find_all_previous(["p", "h1"]): t = " ".join(prev.get_text(" ", strip=True).split()) m = _CITY_RE.search(t) if m: city = m.group(1).strip() break if not city: return None cells = [" ".join(td.get_text(" ", strip=True).split()) for td in row.find_all("td")] # [photo, adresse, grandeur, prix, étage, note] if len(cells) < 4: return None address_raw = cells[1] if len(cells) > 1 else "" size_raw = cells[2] if len(cells) > 2 else "" price_raw = cells[3] if len(cells) > 3 else "" floor_raw = cells[4] if len(cells) > 4 else "" note_raw = cells[5] if len(cells) > 5 else "" if not address_raw: return None amenities = [] if floor_raw: amenities.append(f"Étage : {floor_raw}") # photo réelle (default.png = image de remplacement, ignorée) images = [] for img in row.find_all("img", src=True): src = img["src"] if re.search(r"est-loue|default\.png", src, re.I): continue src = re.sub(r"^(\.\./)+", "/", src) if not src.startswith("http"): src = BASE + (src if src.startswith("/") else f"/{src}") if src not in images: images.append(src) ext_id = anchor.get("name") return Listing( source=self.source_id, external_id=ext_id, url=f"{LIST_URL}#{ext_id}", title=f"{address_raw}, {city}", address=f"{address_raw}, {city}", city=city, unit_type=normalize_unit_type(size_raw), price=parse_price(price_raw), price_label=price_raw, availability=_availability_from_note(note_raw), # « juillet », … description=note_raw, # note libre : inclusions, rénové… amenities=amenities, images=images[:5], )