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%
5.3 KB · 138 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/arpents_verts.py : connecteur Domaine les Arpents Verts5#   (domainelesarpentsverts.com — Société Immobilière GBS, ~199 portes à6#   Saint-Hyacinthe). WordPress rendu serveur : une page par typologie sous7#   /le-projet/ (« Logements 4 ½ », « Logements 5 ½ secteur St-Joseph »,8#   « Logements 5 ½ secteur Douville »), découvertes via le menu de l'accueil.9#   Granularité : une annonce par page typologie/secteur (aucun prix publié,10#   superficie et commodités dans la prose).11# -----------------------------------------------------------------------------12from __future__ import annotations1314import re1516from bs4 import BeautifulSoup1718from ..schema import Listing, normalize_unit_type, parse_area_sqft19from .base import BaseConnector2021BASE = "https://domainelesarpentsverts.com"2223PAGE_LINK_RE = re.compile(24    r"https://domainelesarpentsverts\.com/le-projet/(logements-[\w\-]+)/?")25UNIT_RE = re.compile(r"(\d)\s*(?:½|1/2|-1-2)")26AREA_RE = re.compile(r"([\d\s,]{3,7})\s*(?:pieds?\s+carr[ée]s?|pi2|pi²|p\.?c\.?)", re.I)27IMG_RE = re.compile(28    r"https://domainelesarpentsverts\.com/wp-content/uploads/"29    r'[^"\s\\)]+\.(?:jpe?g|png|webp|avif)', re.I)30PHONE_RE = re.compile(r"tel:(\d{3})(\d{3})(\d{4})")31EMAIL_RE = re.compile(r"mailto:([\w.\-]+@[\w.\-]+\.\w{2,})")323334def _sector_of(slug: str) -> str:35    if "douville" in slug:36        return "Douville"37    if "st-joseph" in slug or "saint-joseph" in slug:38        return "Saint-Joseph"39    return ""404142class ArpentsVertsConnector(BaseConnector):43    source_id = "arpents_verts"44    request_delay = 0.845    max_pages = 10             # garde-fou de crawl4647    def fetch(self) -> list[Listing]:48        listings: list[Listing] = []49        try:50            home = self.get(BASE + "/").text51        except Exception:52            return listings5354        slugs = list(dict.fromkeys(PAGE_LINK_RE.findall(home)))55        for slug in slugs[: self.max_pages]:56            try:57                lst = self._fetch_page(slug)58                if lst:59                    listings.append(lst)60            except Exception:61                continue62        return listings6364    def _fetch_page(self, slug: str) -> Listing | None:65        url = f"{BASE}/le-projet/{slug}/"66        html = self.get(url).text67        soup = BeautifulSoup(html, "html.parser")68        for tag in soup(["script", "style", "noscript"]):69            tag.decompose()7071        h1 = soup.find("h1")72        title = h1.get_text(" ", strip=True) if h1 else \73            slug.replace("-", " ").title()74        text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True))7576        m = UNIT_RE.search(f"{title} {slug}")77        unit_type = normalize_unit_type(f"{m.group(1)}½") if m else ""78        sector = _sector_of(slug)7980        # superficie dans la prose (« une superficie généreuse de 1 20081        # pieds carrés »)82        area = None83        m = AREA_RE.search(text)84        if m:85            area = parse_area_sqft(m.group(0))8687        # description : premiers paragraphes substantiels88        paras = [p.get_text(" ", strip=True) for p in soup.find_all("p")89                 if len(p.get_text(strip=True)) > 80]90        description = " ".join(paras[:3])[:900]9192        # commodités : items « Les atouts de nos logements » (liste large)93        amenities = [li.get_text(" ", strip=True)94                     for li in soup.select("li")95                     if 8 < len(li.get_text(strip=True)) < 9096                     and not li.find("a")][:18]97        # repli : les atouts sont parfois de simples div espacées98        if len(amenities) < 4:99            zone = text100            i = text.find("atouts de nos logements")101            if i > 0:102                zone = text[i:i + 1200]103            amenities = [a.strip() for a in re.findall(104                r"(?:Ascenseurs?|Air climatis[ée]|2 balcons|2 salles de bains?|"105                r"Walk-in[^,.]{0,40}|C[âa]blage[^,.]{0,30}|Espaces de rangement[^,.]{0,20}|"106                r"Syst[èe]mes? de s[ée]curit[ée]|Piscine[^,.]{0,25}|Salle Communautaire|"107                r"Gym|Biblioth[èe]que|D[ée]neigement|Stationnements?)", zone)]108109        contact: dict = {}110        m = PHONE_RE.search(html)111        if m:112            contact["phone"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}"113        m = EMAIL_RE.search(html)114        if m:115            contact["email"] = m.group(1).lower()116117        images = [u for u in dict.fromkeys(IMG_RE.findall(html))118                  if not re.search(r"logo|icon|favicon|cropped-|"119                                   r"-\d{2,3}x\d{2,3}\.", u, re.I)][:20]120121        if not unit_type:122            return None123        return Listing(124            source=self.source_id,125            external_id=slug,126            url=url,127            title=f"Domaine les Arpents Verts — {title}",128            address="",129            sector=sector,130            city="Saint-Hyacinthe",131            unit_type=unit_type,132            area_sqft=area,133            description=description,134            amenities=list(dict.fromkeys(amenities))[:18],135            details={"contact": dict(contact)} if contact else {},136            images=images,137        )138