Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/ocartier.py : connecteur OCartier 2 (ocartier.ca — TDR5# Développements). Tour locative de 200 appartements au 30, rue St-Hubert6# à Laval (métro Cartier, secteur Pont-Viau), loyer tout inclus7# (électricité, chauffage, climatisation, eau, internet).8# WordPress/Elementor rendu serveur : la page /plans/ affiche une9# fourchette de prix par TYPOLOGIE (« Studios / 3 1/2 / 4 1/2 / 5 1/2 »,10# « À partir de X $ jusqu'à Y $ »). Granularité TYPOLOGIE.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import re1516from bs4 import BeautifulSoup1718from ..schema import Listing, normalize_unit_type19from .base import BaseConnector2021BASE = "https://ocartier.ca"22PAGE_URL = f"{BASE}/plans/"2324ADDRESS = "30, rue St-Hubert"25CITY = "Laval"26SECTOR = "Pont-Viau"2728BUILDING_AMENITIES = [29 "Électricité incluse", "Chauffage inclus", "Climatisation incluse",30 "Eau incluse", "Internet inclus",31]3233TYPE_RE = re.compile(r"^(?:Studios?|\d\s*(?:1/2|½))$", re.I)34RANGE_RE = re.compile(35 r"À partir de\s*([\d\s ,]+?)\s*\$.*?jusqu[’']à\s*([\d\s ,]+?)\s*\$",36 re.I | re.S)37BED_RE = re.compile(r"(\d)\s*chambres?", re.I)38IMG_RE = re.compile(39 r"https://ocartier\.ca/wp-content/uploads/[^\"'\s\\)]+?"40 r"\.(?:jpg|jpeg|png|webp)", re.I)414243def _num(txt: str) -> float | None:44 digits = re.sub(r"[^\d]", "", txt or "")45 return float(digits) if digits else None464748class OCartierConnector(BaseConnector):49 source_id = "ocartier"50 request_delay = 0.65152 def fetch(self) -> list[Listing]:53 listings: list[Listing] = []54 try:55 html = self.get(PAGE_URL).text56 except Exception:57 return listings58 soup = BeautifulSoup(html, "html.parser")5960 # photos : la page /plans/ est pauvre en visuels, on complète avec la61 # galerie de la page /appartements/ (rendue serveur elle aussi)62 pool = IMG_RE.findall(html)63 try:64 pool += IMG_RE.findall(self.get(f"{BASE}/appartements/").text)65 except Exception:66 pass67 images = [u for u in dict.fromkeys(pool)68 if not re.search(r"logo|favicon|icon|metro|-\d+x\d+\.", u,69 re.I)][:20]7071 seen: set[str] = set()72 # blocs typologie : <h4>Studios</h4> … « À partir de 1 630 $ jusqu'à … »73 # — le prix est dans un widget texte qui SUIT le heading dans l'ordre74 # du document (structure de conteneurs Elementor variable) : on75 # concatène le texte jusqu'au prochain heading de typologie.76 headings = [x for x in soup.select(".elementor-heading-title")77 if TYPE_RE.match(x.get_text(" ", strip=True))]78 for heading in headings:79 label = heading.get_text(" ", strip=True)80 texts: list[str] = []81 for node in heading.find_all_next(string=True):82 t = str(node).strip()83 if not t:84 continue85 if TYPE_RE.match(t) and t != label:86 break87 texts.append(t)88 if len(texts) > 40:89 break90 blob = " ".join(texts)91 m = RANGE_RE.search(blob)92 if not m:93 continue94 lo, hi = _num(m.group(1)), _num(m.group(2))95 if not lo:96 continue97 unit_type = ("Studio" if label.lower().startswith("studio")98 else normalize_unit_type(label))99 ext_id = ("ocartier2-"100 + unit_type.replace("½", ".5").replace(" ", "").lower())101 if ext_id in seen: # les blocs sont répétés desktop/mobile102 continue103 seen.add(ext_id)104 bedrooms = None105 bm = BED_RE.search(blob.split("À partir")[0])106 if bm:107 bedrooms = float(bm.group(1))108 price_label = (f"À partir de {lo:.0f} $"109 + (f" jusqu'à {hi:.0f} $" if hi else "")110 + " /mois tout inclus")111 listings.append(Listing(112 source=self.source_id,113 external_id=ext_id,114 url=PAGE_URL,115 title=f"{unit_type} — OCartier 2, appartements locatifs à Laval",116 address=ADDRESS,117 sector=SECTOR,118 city=CITY,119 unit_type=unit_type,120 bedrooms=bedrooms,121 price=lo,122 price_label=price_label,123 description=("OCartier 2 propose 200 appartements locatifs "124 "haut de gamme à Laval, du studio au 5½, à deux "125 "pas du métro Cartier. Prix tout inclus : "126 "électricité, chauffage, climatisation, eau et "127 "internet."),128 amenities=list(BUILDING_AMENITIES),129 images=images,130 ))131 return listings132