spb/lou-ka Public
Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 99.7%
1# -----------------------------------------------------------------------------2# Lou-Ka — Agrégateur de logements à louer (province de Québec)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# connectors/gestion_valco.py : connecteur Gestion Valco (gestionvalco.ca)5# WordPress Divi/Elementor, page « Logements à louer » rédigée à la main :6# chaque logement = une section Elementor « titre » (« 402 Laviolette7# Trois-Rivières 4 1/2 »), suivie d'un bloc texte (inclusions, dispo…)8# et de galeries d'images. 1 seule requête par sync ; external_id =9# data-id Elementor de la section titre (stable tant que la section vit).10# -----------------------------------------------------------------------------11from __future__ import annotations1213import re1415from bs4 import BeautifulSoup1617from ..schema import Listing, normalize_unit_type18from .base import BaseConnector1920BASE = "https://gestionvalco.ca"21LIST_URL = f"{BASE}/logements-a-louer/" # redirige vers /elementor-344/logements-a-louer/2223# titre d'annonce : contient un type d'unité (« 4 1/2 », « 3½ », studio…)24_TYPE_RE = re.compile(r"\d\s*(?:½|1/2)|studio|loft", re.I)25# villes desservies par Gestion Valco (Mauricie / Centre-du-Québec)26_CITIES = [27 ("trois-rivieres", "Trois-Rivières"),28 ("trois- rivieres", "Trois-Rivières"),29 ("cap-de-la-madeleine", "Trois-Rivières"),30 ("shawinigan", "Shawinigan"),31 ("nicolet", "Nicolet"),32 ("louiseville", "Louiseville"),33 ("st-narcisse", "Saint-Narcisse"),34 ("saint-narcisse", "Saint-Narcisse"),35]363738def _norm(txt: str) -> str:39 """Aplatis les <font> de Google Translate : espaces multiples, « Trois- Rivières »."""40 txt = re.sub(r"\s+", " ", txt).strip()41 return re.sub(r"(\w)-\s+(\w)", r"\1-\2", txt)424344def _strip_accents(s: str) -> str:45 import unicodedata46 return "".join(c for c in unicodedata.normalize("NFD", s)47 if unicodedata.category(c) != "Mn")484950class GestionValcoConnector(BaseConnector):51 source_id = "gestion_valco"52 request_delay = 0.75354 def fetch(self) -> list[Listing]:55 html = self.get(LIST_URL).text # requests suit la redirection56 soup = BeautifulSoup(html, "html.parser")57 main = soup.find("main") or soup5859 listings: list[Listing] = []60 current: Listing | None = None61 desc_lines: list[str] = []6263 def _flush() -> None:64 nonlocal current, desc_lines65 if current is not None:66 current.description = "\n".join(desc_lines)[:900]67 listings.append(current)68 current, desc_lines = None, []6970 for sec in main.select("section.elementor-top-section"):71 head = sec.select_one(".elementor-widget-heading .elementor-heading-title")72 title = _norm(head.get_text(" ", strip=True)) if head else ""73 if title and _TYPE_RE.search(title) and not re.search(74 r"commercial|stationnement|rangement|local\b", title, re.I):75 # nouvelle annonce : section titre76 _flush()77 low = _strip_accents(title.lower())78 city, city_pos = "", -179 for key, name in _CITIES:80 p = low.find(key)81 if p >= 0:82 city, city_pos = name, p83 break84 # adresse civique : début du titre jusqu'au nom de ville85 address = ""86 if city_pos > 0 and re.match(r"\d", title):87 address = title[:city_pos].strip(" ,-")88 # disponibilité : mention « Libre … » du titre (texte source)89 m = re.search(r"\b(Libre\b.*)$", title, re.I)90 availability = m.group(1).strip() if m else ""91 ext_id = sec.get("data-id") or ""92 if not ext_id:93 continue94 current = Listing(95 source=self.source_id,96 external_id=str(ext_id),97 url=f"{BASE}/elementor-344/logements-a-louer/#{ext_id}",98 title=title,99 address=address,100 city=city,101 unit_type=normalize_unit_type(title),102 availability=availability,103 )104 # la section titre peut aussi porter le bloc texte : on continue105 if current is None:106 continue107 # même section ou sections suivantes : description (texte), galeries108 for txt in sec.select(".elementor-widget-text-editor"):109 for p in txt.find_all(["p", "li"]) or [txt]:110 t = re.sub(r"\s+", " ", p.get_text(" ", strip=True))111 if t and t not in desc_lines:112 desc_lines.append(t)113 if not current.availability:114 for t in desc_lines:115 if re.search(r"\b(libre|disponible)\b", t, re.I):116 current.availability = t117 break118 for a in sec.select("a.e-gallery-item[href]"):119 u = a["href"]120 if u.startswith("http") and u not in current.images:121 current.images.append(u)122 current.images = current.images[:30]123 _flush()124 return listings125