# ----------------------------------------------------------------------------- # Lou-Ka — Location court terme # connectors/alouerauxiles.py : À louer aux Îles (alouerauxiles.com) # # Annuaire local des Îles-de-la-Madeleine (~100 maisons/chalets, région très # mal couverte ailleurs). Site statique « Tactical Soft » : cartes par île. # # Méthode : # 1. LISTE : les pages d'île FR (/havre-aubert, /cap-aux-meules, …) sont du # HTML serveur contenant un bloc `
tag=rent …>` # par annonce : id stable, nom (alt="…"), capacité/chambres # (cap="3 chambres (5 pers. max)"), prix (dayrate=186, $/nuit calculé) # et lien public (href=https://alouerauxiles.com/). Dédup par id # (une annonce peut apparaître sur plusieurs pages). # 2. DÉTAIL (cache self.detail) : /php/page_fr.php?id= → type # d'hébergement, personnes/chambres/salles de bain (attributs title=), # animaux, description, permis CITQ, adresse + ville (après le code # postal), commodités (« Commodités : ») et photos (pages/idlm/rent/…). # External_id = id du bloc item (numérique ou slug, stable). # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import re from ..schema import StListing from .base import StConnector SITE = "https://alouerauxiles.com" WWW = "https://www.alouerauxiles.com" # pages d'île francophones (les slugs anglais sont des doublons) ISLANDS = ["havre-aubert", "cap-aux-meules", "havre-aux-maisons", "pointe-aux-loups", "grosse-ile", "grande-entree", "ile-d-entree"] _TAG_RE = re.compile(r"<[^>]+>") # libellé du site → type canonique Lou-Ka _TYPES = {"chalet": "Chalet", "maison": "Maison", "résidence": "Maison", "studio": "Studio", "appartement": "Appartement", "loft": "Loft", "chambre": "Chambre", "gîte": "Gîte", "auberge": "Auberge"} def _text(fragment: str) -> str: return _html.unescape(re.sub(r"\s+", " ", _TAG_RE.sub(" ", fragment)) ).replace("", "").strip() def _num(raw) -> float | None: m = re.search(r"\d+(?:[.,]\d+)?", str(raw or "")) return float(m.group(0).replace(",", ".")) if m else None class ALouerAuxIles(StConnector): source_id = "alouerauxiles" # -- liste (pages d'île) -------------------------------------------------- def _island_items(self) -> dict[str, dict]: items: dict[str, dict] = {} for island in ISLANDS: try: h = self.get(f"{WWW}/{island}").text except Exception: continue for block in re.findall(r"
]+)>", h): if "tag=rent" not in block: continue m = re.search(r"\bid=([\w-]+)", block) if not m: continue lid = m.group(1) it = items.setdefault(lid, {"island": island}) m = re.search(r'alt="([^"]+)"', block) if m: it["name"] = _text(m.group(1)) m = re.search(r'cap="(\d+)\s*chambres?\s*\((\d+)\s*pers', block) if m: it["bedrooms"] = float(m.group(1)) it["capacity"] = float(m.group(2)) m = re.search(r"\bdayrate=(\d+(?:\.\d+)?)", block) if m: it["dayrate"] = float(m.group(1)) m = re.search(r"href=(https://alouerauxiles\.com/[\w-]+)\b", block) if m: it["url"] = m.group(1) return items # -- page détail ------------------------------------------------------ def _detail(self, lid: str) -> dict: h = self.get(f"{SITE}/php/page_fr.php", params={"id": lid}).text d: dict = {} m = re.search(r"([^<]+)", h) if m: d["type_label"] = _text(m.group(1)) m = re.search(r'title="(\d+)\s*personnes', h) if m: d["capacity"] = float(m.group(1)) m = re.search(r'title="(\d+)\s*chambres?"', h) if m: d["bedrooms"] = float(m.group(1)) m = re.search(r'title="(\d+)\s*salle\(?s?\)?\s*de\s*bain', h) if m: d["bathrooms"] = float(m.group(1)) m = re.search(r'title="\s*animaux([^"]*)"', h) if m: d["pets"] = "non" if "non" in m.group(1).lower() else "oui" # description : bloc txtdiv (nom + texte de présentation) m = re.search(r"(?s)
(.*?)
", h) if m: frag = re.sub(r"(?s).*?", " ", m.group(1)) d["description"] = _text(frag)[:4000] m = re.search(r"(?s)([^<]+)", h[h.find("txtdiv"):] if "txtdiv" in h else "") if m: d["title"] = _text(m.group(1)) m = re.search(r"CITQ\D{0,12}(\d{6})", h, re.I) if m: d["citq"] = m.group(1) # adresse : rue + « G4T 3H6 l'Étang-du-Nord » → ville après le code m = re.search(r"(?s)Adresse\s*:\s*(.*?)(?:", m.group(1))] lines = [x for x in lines if x] if lines: d["address"] = lines[0] for x in lines: pm = re.search(r"[A-Z]\d[A-Z]\s?\d[A-Z]\d\s+(.{3,40})$", x) if pm: d["city"] = pm.group(1).strip() break # commodités : lignes « - … » entre « Commodités : » et « À proximité » m = re.search(r"(?s)Commodités\s*:(.*?)(?:À proximité|Contact\s*:|$)", h) if m: amens = [_text(x).lstrip("- ").strip() for x in re.split(r"", m.group(1))] d["amenities"] = [a for a in amens if 2 <= len(a) <= 80][:50] # photos du dossier de l'annonce (originaux img/, pas les vignettes) imgs: list[str] = [] for u in re.findall(r"[\"'=](?:\.\./)?(pages/idlm/rent/[\w-]+/img/" r"[^\"'\s>]+\.(?:jpe?g|png|webp))", h, re.I): full = f"{SITE}/{u}" if full not in imgs: imgs.append(full) d["images"] = imgs[:20] return d # -- contrat ---------------------------------------------------------- def fetch(self) -> list[StListing]: listings: list[StListing] = [] for lid, it in self._island_items().items(): key = f"{it.get('name', '')}|{it.get('capacity', '')}|" \ f"{it.get('dayrate', '')}|{it.get('bedrooms', '')}" try: det = self.detail(lid, key, lambda i=lid: self._detail(i)) except Exception: det = {} title = det.get("title") or it.get("name") or "" if not title: continue type_label = (det.get("type_label") or "").lower() ptype = "" for needle, canon in _TYPES.items(): if needle in type_label: ptype = canon break price = it.get("dayrate") city = det.get("city") or it["island"].replace("-", " ").title() listings.append(StListing( source=self.source_id, external_id=lid, url=it.get("url") or f"{SITE}/php/page_fr.php?id={lid}", title=title, property_type=ptype or "Maison", address=det.get("address") or "", city=city, region="Îles-de-la-Madeleine", price_night=price, price_label=(f"à partir de {price:.0f} $ / nuit" if price else ""), capacity=det.get("capacity") or it.get("capacity"), bedrooms=det.get("bedrooms") or it.get("bedrooms"), bathrooms=det.get("bathrooms"), pets=det.get("pets"), citq=det.get("citq") or "", description=det.get("description") or "", amenities=det.get("amenities") or [], details={"ile": it["island"]}, images=det.get("images") or [], )) return listings