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/levivo.py : connecteur LeVivo Condos Locatifs (levivo.ca)5# Quatre projets « tout inclus » en Montérégie : Longueuil 1, Longueuil 2,6# Greenfield Park et La Prairie. Squarespace rendu serveur : chaque page7# /fr/projets/<slug> affiche l'adresse (ligne au-dessus de « Adresse »)8# puis une « Description des unités » en triplets texte (« 4 ½ / À partir9# de / 2 100,00 $ »). Granularité : typologie par projet.10# -----------------------------------------------------------------------------11from __future__ import annotations1213import re1415from bs4 import BeautifulSoup1617from .base import BaseConnector18from ..schema import Listing, normalize_unit_type1920BASE = "https://www.levivo.ca"2122PROJECTS = [23 ("longueuil-1", "LeVivo Longueuil 1", "Longueuil"),24 ("longueuil-2", "LeVivo Longueuil 2", "Longueuil"),25 ("greenfield-park", "LeVivo Greenfield Park", "Longueuil"),26 ("la-prairie", "LeVivo La Prairie", "La Prairie"),27]2829TYPE_RE = re.compile(r"^(studio|\d\s*½|\d\s*1/2)$", re.I)30# « 2 100,00 $ » (format québécois)31PRICE_RE = re.compile(r"^([\d\s ]{1,3}(?:[\s ]?\d{3})*),\d{2}\s*\$$")32IMG_RE = re.compile(r'https://images\.squarespace-cdn\.com/content/'33 r'[^"\s\\)]+\.(?:jpe?g|png|webp)', re.I)343536class LevivoConnector(BaseConnector):37 source_id = "levivo"38 request_delay = 0.63940 def fetch(self) -> list[Listing]:41 listings: list[Listing] = []42 for slug, name, city in PROJECTS:43 url = f"{BASE}/fr/projets/{slug}"44 try:45 html = self.get(url).text46 except Exception:47 continue48 soup = BeautifulSoup(html, "html.parser")49 lines = soup.get_text("\n", strip=True).split("\n")5051 # adresse : la ligne complète précède le libellé « Adresse »52 address = ""53 for i, line in enumerate(lines):54 if line.strip() == "Adresse" and i > 0:55 address = lines[i - 1].strip()56 break57 address = re.sub(r",?\s*Québec(\s+\w\d\w\s*\d\w\d)?$", "",58 address).strip().rstrip(",")5960 images = [u for u in dict.fromkeys(IMG_RE.findall(html))61 if not re.search(r"favicon|logo|icon", u, re.I)][:15]6263 # triplets « 4 ½ / À partir de / 2 100,00 $ »64 for i, line in enumerate(lines):65 raw = line.strip()66 if not TYPE_RE.match(raw):67 continue68 if i + 2 >= len(lines):69 continue70 if not lines[i + 1].strip().lower().startswith("à partir"):71 continue72 m = PRICE_RE.match(lines[i + 2].strip())73 if not m:74 continue75 try:76 price = float(m.group(1).replace(" ", "")77 .replace(" ", "").replace(" ", ""))78 except ValueError:79 continue80 unit_type = ("Studio" if raw.lower() == "studio"81 else normalize_unit_type(raw))82 type_slug = ("studio" if unit_type == "Studio"83 else unit_type.replace("½", ".5"))84 ext_id = f"{slug}-{type_slug}"85 if any(l.external_id == ext_id for l in listings):86 continue87 listings.append(Listing(88 source=self.source_id,89 external_id=ext_id,90 url=url,91 title=f"{unit_type} — {name}",92 address=address or None,93 city=city,94 unit_type=unit_type,95 price=price,96 price_label=f"À partir de {lines[i + 2].strip()}",97 description=f"Condos locatifs « tout inclus » du projet "98 f"{name} : électricité, chauffage, eau "99 f"chaude, internet et câblodistribution "100 f"inclus, avec piscine et salle d'exercice.",101 images=images,102 ))103 return listings104