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/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