SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
8.3 KB · 203 lines python
Raw Blame History
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