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/logicom.py : connecteur Les Immeubles Logicom5# (immeubleslogicom.com — Sainte-Foy, Sillery, Lévis…). WordPress rendu6# serveur : /appartements-a-louer/ liste les immeubles (/immeuble/<slug>/),7# et chaque page immeuble expose ses unités disponibles en cartes8# `.appartement` (no d'unité dans le titre, disponibilité, type n ½,9# superficie, balcon, prix « À partir de … $ », plan PDF/PNG) + un marqueur10# carte data-lat/data-lng. Granularité : unité.11# -----------------------------------------------------------------------------12from __future__ import annotations1314import hashlib15import re1617from bs4 import BeautifulSoup1819from ..schema import Listing, infer_city, normalize_unit_type, parse_price20from .base import BaseConnector2122BASE = "https://immeubleslogicom.com"23LIST_URL = f"{BASE}/appartements-a-louer/"2425UNIT_NO_RE = re.compile(r"(\d{1,4}[A-Z]?)\s*$")26TYPE_OK_RE = re.compile(r"studio|loft|\d\s*(?:½|1/2)", re.I)27SQFT_RE = re.compile(r"([\d\s]+)\s*pi²", re.I)282930class LogicomConnector(BaseConnector):31 source_id = "logicom"32 request_delay = 0.633 max_buildings = 15 # garde-fou de crawl3435 def fetch(self) -> list[Listing]:36 listings: list[Listing] = []37 try:38 html = self.get(LIST_URL).text39 except Exception:40 return listings4142 slugs = list(dict.fromkeys(43 re.findall(r"/immeuble/([a-z0-9-]+)/", html)))[:self.max_buildings]44 seen: set[str] = set()45 for slug in slugs:46 url = f"{BASE}/immeuble/{slug}/"47 try:48 page = self.get(url).text49 except Exception:50 continue51 for lst in self._parse_building(slug, url, page):52 # certaines unités apparaissent en double sur la page53 if lst.external_id in seen:54 continue55 seen.add(lst.external_id)56 listings.append(lst)57 return listings5859 def _parse_building(self, slug: str, url: str, page: str) -> list[Listing]:60 out: list[Listing] = []61 soup = BeautifulSoup(page, "html.parser")6263 h1 = soup.select_one("h1")64 building = h1.get_text(" ", strip=True) if h1 else slug6566 # secteur : bloc « Localisation » du survol (hero-info)67 sector = ""68 for blk in soup.select(".hero-info span"):69 if "localisation" in blk.get_text(strip=True).lower():70 p = blk.find_next("p")71 if p:72 sector = p.get_text(" ", strip=True)73 break74 city = infer_city(sector)7576 lat = lng = None77 marker = soup.select_one(".marker[data-lat]")78 if marker:79 try:80 lat = float(marker["data-lat"])81 lng = float(marker["data-lng"])82 except (KeyError, ValueError):83 lat = lng = None8485 # photos de l'immeuble (carrousel du projet)86 photos = [img.get("src", "") for img in87 soup.select(".slider-container img")88 if img.get("src", "").startswith("http")][:15]8990 for card in soup.select("div.appartement"):91 try:92 title_el = card.select_one("[data-title]")93 if not title_el:94 continue95 title = title_el.get_text(" ", strip=True)96 m = UNIT_NO_RE.search(title)97 unit_no = m.group(1) if m else ""9899 type_el = card.select_one(".type p")100 raw_type = type_el.get_text(" ", strip=True) if type_el else \101 card.get("data-type", "")102 # résidentiel seulement (Studio / Loft / n ½)103 if not TYPE_OK_RE.search(raw_type):104 continue105106 avail_el = title_el.find_next("p")107 availability = avail_el.get_text(" ", strip=True) \108 if avail_el else ""109110 sizes = [p.get_text(" ", strip=True)111 for p in card.select(".taille p")]112 area = None113 for s in sizes:114 sm = SQFT_RE.search(s)115 if sm and "$" not in s:116 area = float(sm.group(1).replace(" ", "")117 .replace(" ", ""))118 break119 price_label = next((re.sub(r"\s+", " ", s) for s in sizes120 if "$" in s), "")121122 plan = card.select_one('a[href$=".pdf"]')123 plan_img = card.select_one("img")124 images = list(photos)125 if plan_img and plan_img.get("src", "").startswith("http"):126 images = [plan_img["src"]] + photos127128 if not unit_no:129 unit_no = hashlib.sha1(130 f"{title}|{raw_type}|{area}".encode()).hexdigest()[:8]131 extras = [f"Balcon : {sizes[1]}"] \132 if len(sizes) >= 3 and "pi²" in sizes[1] else []133 details: dict = {}134 if plan:135 details["floor_plan"] = plan.get("href", "")136137 out.append(Listing(138 source=self.source_id,139 external_id=f"{slug}-{unit_no}",140 url=url,141 title=title,142 sector=sector,143 city=city,144 unit_type=normalize_unit_type(raw_type),145 price=parse_price(price_label),146 price_label=price_label,147 availability=availability,148 area_sqft=area,149 description=f"Unité {unit_no} — {building} "150 f"({sector}).",151 amenities=extras,152 details=details,153 images=images,154 lat=lat,155 lng=lng,156 ))157 except Exception:158 continue159 return out160