# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/arpents_verts.py : connecteur Domaine les Arpents Verts # (domainelesarpentsverts.com — Société Immobilière GBS, ~199 portes à # Saint-Hyacinthe). WordPress rendu serveur : une page par typologie sous # /le-projet/ (« Logements 4 ½ », « Logements 5 ½ secteur St-Joseph », # « Logements 5 ½ secteur Douville »), découvertes via le menu de l'accueil. # Granularité : une annonce par page typologie/secteur (aucun prix publié, # superficie et commodités dans la prose). # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, normalize_unit_type, parse_area_sqft from .base import BaseConnector BASE = "https://domainelesarpentsverts.com" PAGE_LINK_RE = re.compile( r"https://domainelesarpentsverts\.com/le-projet/(logements-[\w\-]+)/?") UNIT_RE = re.compile(r"(\d)\s*(?:½|1/2|-1-2)") AREA_RE = re.compile(r"([\d\s,]{3,7})\s*(?:pieds?\s+carr[ée]s?|pi2|pi²|p\.?c\.?)", re.I) IMG_RE = re.compile( r"https://domainelesarpentsverts\.com/wp-content/uploads/" r'[^"\s\\)]+\.(?:jpe?g|png|webp|avif)', re.I) PHONE_RE = re.compile(r"tel:(\d{3})(\d{3})(\d{4})") EMAIL_RE = re.compile(r"mailto:([\w.\-]+@[\w.\-]+\.\w{2,})") def _sector_of(slug: str) -> str: if "douville" in slug: return "Douville" if "st-joseph" in slug or "saint-joseph" in slug: return "Saint-Joseph" return "" class ArpentsVertsConnector(BaseConnector): source_id = "arpents_verts" request_delay = 0.8 max_pages = 10 # garde-fou de crawl def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: home = self.get(BASE + "/").text except Exception: return listings slugs = list(dict.fromkeys(PAGE_LINK_RE.findall(home))) for slug in slugs[: self.max_pages]: try: lst = self._fetch_page(slug) if lst: listings.append(lst) except Exception: continue return listings def _fetch_page(self, slug: str) -> Listing | None: url = f"{BASE}/le-projet/{slug}/" html = self.get(url).text soup = BeautifulSoup(html, "html.parser") for tag in soup(["script", "style", "noscript"]): tag.decompose() h1 = soup.find("h1") title = h1.get_text(" ", strip=True) if h1 else \ slug.replace("-", " ").title() text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True)) m = UNIT_RE.search(f"{title} {slug}") unit_type = normalize_unit_type(f"{m.group(1)}½") if m else "" sector = _sector_of(slug) # superficie dans la prose (« une superficie généreuse de 1 200 # pieds carrés ») area = None m = AREA_RE.search(text) if m: area = parse_area_sqft(m.group(0)) # description : premiers paragraphes substantiels paras = [p.get_text(" ", strip=True) for p in soup.find_all("p") if len(p.get_text(strip=True)) > 80] description = " ".join(paras[:3])[:900] # commodités : items « Les atouts de nos logements » (liste large) amenities = [li.get_text(" ", strip=True) for li in soup.select("li") if 8 < len(li.get_text(strip=True)) < 90 and not li.find("a")][:18] # repli : les atouts sont parfois de simples div espacées if len(amenities) < 4: zone = text i = text.find("atouts de nos logements") if i > 0: zone = text[i:i + 1200] amenities = [a.strip() for a in re.findall( r"(?:Ascenseurs?|Air climatis[ée]|2 balcons|2 salles de bains?|" r"Walk-in[^,.]{0,40}|C[âa]blage[^,.]{0,30}|Espaces de rangement[^,.]{0,20}|" r"Syst[èe]mes? de s[ée]curit[ée]|Piscine[^,.]{0,25}|Salle Communautaire|" r"Gym|Biblioth[èe]que|D[ée]neigement|Stationnements?)", zone)] contact: dict = {} m = PHONE_RE.search(html) if m: contact["phone"] = f"{m.group(1)}-{m.group(2)}-{m.group(3)}" m = EMAIL_RE.search(html) if m: contact["email"] = m.group(1).lower() images = [u for u in dict.fromkeys(IMG_RE.findall(html)) if not re.search(r"logo|icon|favicon|cropped-|" r"-\d{2,3}x\d{2,3}\.", u, re.I)][:20] if not unit_type: return None return Listing( source=self.source_id, external_id=slug, url=url, title=f"Domaine les Arpents Verts — {title}", address="", sector=sector, city="Saint-Hyacinthe", unit_type=unit_type, area_sqft=area, description=description, amenities=list(dict.fromkeys(amenities))[:18], details={"contact": dict(contact)} if contact else {}, images=images, )