Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Location court terme3# connectors/alouerauxiles.py : À louer aux Îles (alouerauxiles.com)4#5# Annuaire local des Îles-de-la-Madeleine (~100 maisons/chalets, région très6# mal couverte ailleurs). Site statique « Tactical Soft » : cartes par île.7#8# Méthode :9# 1. LISTE : les pages d'île FR (/havre-aubert, /cap-aux-meules, …) sont du10# HTML serveur contenant un bloc `<div class=item id=<id> tag=rent …>`11# par annonce : id stable, nom (alt="…"), capacité/chambres12# (cap="3 chambres (5 pers. max)"), prix (dayrate=186, $/nuit calculé)13# et lien public (href=https://alouerauxiles.com/<slug>). Dédup par id14# (une annonce peut apparaître sur plusieurs pages).15# 2. DÉTAIL (cache self.detail) : /php/page_fr.php?id=<id> → type16# d'hébergement, personnes/chambres/salles de bain (attributs title=),17# animaux, description, permis CITQ, adresse + ville (après le code18# postal), commodités (« Commodités : ») et photos (pages/idlm/rent/…).19# External_id = id du bloc item (numérique ou slug, stable).20# -----------------------------------------------------------------------------21from __future__ import annotations2223import html as _html24import re2526from ..schema import StListing27from .base import StConnector2829SITE = "https://alouerauxiles.com"30WWW = "https://www.alouerauxiles.com"3132# pages d'île francophones (les slugs anglais sont des doublons)33ISLANDS = ["havre-aubert", "cap-aux-meules", "havre-aux-maisons",34 "pointe-aux-loups", "grosse-ile", "grande-entree", "ile-d-entree"]3536_TAG_RE = re.compile(r"<[^>]+>")3738# libellé du site → type canonique Lou-Ka39_TYPES = {"chalet": "Chalet", "maison": "Maison", "résidence": "Maison",40 "studio": "Studio", "appartement": "Appartement", "loft": "Loft",41 "chambre": "Chambre", "gîte": "Gîte", "auberge": "Auberge"}424344def _text(fragment: str) -> str:45 return _html.unescape(re.sub(r"\s+", " ", _TAG_RE.sub(" ", fragment))46 ).replace("", "").strip()474849def _num(raw) -> float | None:50 m = re.search(r"\d+(?:[.,]\d+)?", str(raw or ""))51 return float(m.group(0).replace(",", ".")) if m else None525354class ALouerAuxIles(StConnector):55 source_id = "alouerauxiles"5657 # -- liste (pages d'île) --------------------------------------------------58 def _island_items(self) -> dict[str, dict]:59 items: dict[str, dict] = {}60 for island in ISLANDS:61 try:62 h = self.get(f"{WWW}/{island}").text63 except Exception:64 continue65 for block in re.findall(r"<div class=item ([^>]+)>", h):66 if "tag=rent" not in block:67 continue68 m = re.search(r"\bid=([\w-]+)", block)69 if not m:70 continue71 lid = m.group(1)72 it = items.setdefault(lid, {"island": island})73 m = re.search(r'alt="([^"]+)"', block)74 if m:75 it["name"] = _text(m.group(1))76 m = re.search(r'cap="(\d+)\s*chambres?\s*\((\d+)\s*pers',77 block)78 if m:79 it["bedrooms"] = float(m.group(1))80 it["capacity"] = float(m.group(2))81 m = re.search(r"\bdayrate=(\d+(?:\.\d+)?)", block)82 if m:83 it["dayrate"] = float(m.group(1))84 m = re.search(r"href=(https://alouerauxiles\.com/[\w-]+)\b",85 block)86 if m:87 it["url"] = m.group(1)88 return items8990 # -- page détail ------------------------------------------------------91 def _detail(self, lid: str) -> dict:92 h = self.get(f"{SITE}/php/page_fr.php", params={"id": lid}).text93 d: dict = {}9495 m = re.search(r"<b class=t1>([^<]+)</b>", h)96 if m:97 d["type_label"] = _text(m.group(1))9899 m = re.search(r'title="(\d+)\s*personnes', h)100 if m:101 d["capacity"] = float(m.group(1))102 m = re.search(r'title="(\d+)\s*chambres?"', h)103 if m:104 d["bedrooms"] = float(m.group(1))105 m = re.search(r'title="(\d+)\s*salle\(?s?\)?\s*de\s*bain', h)106 if m:107 d["bathrooms"] = float(m.group(1))108 m = re.search(r'title="\s*animaux([^"]*)"', h)109 if m:110 d["pets"] = "non" if "non" in m.group(1).lower() else "oui"111112 # description : bloc txtdiv (nom + texte de présentation)113 m = re.search(r"(?s)<div id=txtdiv>(.*?)</div>", h)114 if m:115 frag = re.sub(r"(?s)<b class=t2>.*?</b>", " ", m.group(1))116 d["description"] = _text(frag)[:4000]117 m = re.search(r"(?s)<b class=t2>([^<]+)</b>",118 h[h.find("txtdiv"):] if "txtdiv" in h else "")119 if m:120 d["title"] = _text(m.group(1))121122 m = re.search(r"CITQ\D{0,12}(\d{6})", h, re.I)123 if m:124 d["citq"] = m.group(1)125126 # adresse : rue + « G4T 3H6 l'Étang-du-Nord » → ville après le code127 m = re.search(r"(?s)Adresse\s*:\s*</b>(.*?)(?:<b class|Contact)", h)128 if m:129 lines = [_text(x) for x in re.split(r"<br\s*/?>", m.group(1))]130 lines = [x for x in lines if x]131 if lines:132 d["address"] = lines[0]133 for x in lines:134 pm = re.search(r"[A-Z]\d[A-Z]\s?\d[A-Z]\d\s+(.{3,40})$", x)135 if pm:136 d["city"] = pm.group(1).strip()137 break138139 # commodités : lignes « - … » entre « Commodités : » et « À proximité »140 m = re.search(r"(?s)Commodités\s*:(.*?)(?:À proximité|Contact\s*:|$)",141 h)142 if m:143 amens = [_text(x).lstrip("- ").strip()144 for x in re.split(r"<br\s*/?>", m.group(1))]145 d["amenities"] = [a for a in amens if 2 <= len(a) <= 80][:50]146147 # photos du dossier de l'annonce (originaux img/, pas les vignettes)148 imgs: list[str] = []149 for u in re.findall(r"[\"'=](?:\.\./)?(pages/idlm/rent/[\w-]+/img/"150 r"[^\"'\s>]+\.(?:jpe?g|png|webp))", h, re.I):151 full = f"{SITE}/{u}"152 if full not in imgs:153 imgs.append(full)154 d["images"] = imgs[:20]155 return d156157 # -- contrat ----------------------------------------------------------158 def fetch(self) -> list[StListing]:159 listings: list[StListing] = []160 for lid, it in self._island_items().items():161 key = f"{it.get('name', '')}|{it.get('capacity', '')}|" \162 f"{it.get('dayrate', '')}|{it.get('bedrooms', '')}"163 try:164 det = self.detail(lid, key, lambda i=lid: self._detail(i))165 except Exception:166 det = {}167 title = det.get("title") or it.get("name") or ""168 if not title:169 continue170171 type_label = (det.get("type_label") or "").lower()172 ptype = ""173 for needle, canon in _TYPES.items():174 if needle in type_label:175 ptype = canon176 break177178 price = it.get("dayrate")179 city = det.get("city") or it["island"].replace("-", " ").title()180 listings.append(StListing(181 source=self.source_id,182 external_id=lid,183 url=it.get("url") or f"{SITE}/php/page_fr.php?id={lid}",184 title=title,185 property_type=ptype or "Maison",186 address=det.get("address") or "",187 city=city,188 region="Îles-de-la-Madeleine",189 price_night=price,190 price_label=(f"à partir de {price:.0f} $ / nuit"191 if price else ""),192 capacity=det.get("capacity") or it.get("capacity"),193 bedrooms=det.get("bedrooms") or it.get("bedrooms"),194 bathrooms=det.get("bathrooms"),195 pets=det.get("pets"),196 citq=det.get("citq") or "",197 description=det.get("description") or "",198 amenities=det.get("amenities") or [],199 details={"ile": it["island"]},200 images=det.get("images") or [],201 ))202 return listings203