SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
4.3 KB · 104 lines python
Raw Blame History
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