# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/axia.py : connecteur AXIA Appartements (axiaappartements.com) # Complexe locatif neuf à Lachine (Montréal), géré par Pur Immobilia. # Site vitrine WordPress/WPBakery une page : deux configurations # (2 chambres 4½ et 3 chambres 5½) avec superficie et prix # « à partir de » — 1 annonce par configuration. # ----------------------------------------------------------------------------- from __future__ import annotations import re from bs4 import BeautifulSoup from ..schema import Listing, parse_price from .base import BaseConnector BASE = "https://www.axiaappartements.com" ADDRESS = "200, boul. Saint-Joseph, Lachine (Québec) H8S 2L3" IMG_RE = re.compile( r"https://www\.axiaappartements\.com/wp-content/uploads/" r"[^\"\s\\]+?\.(?:jpg|jpeg|webp)", re.I) class AxiaConnector(BaseConnector): source_id = "axia" request_delay = 0.6 def fetch(self) -> list[Listing]: listings: list[Listing] = [] try: html = self.get(BASE + "/").text except Exception: return listings soup = BeautifulSoup(html, "html.parser") # Photos du complexe (icônes/logos exclus) images = [u for u in dict.fromkeys(IMG_RE.findall(html)) if not re.search(r"icone|logo|favicon|fleche|-\d+x\d+\.", u, re.I)][:25] # Description (meta + paragraphe « APPARTEMENTS » sur les unités) desc = "" og = soup.find("meta", attrs={"property": "og:description"}) or \ soup.find("meta", attrs={"name": "description"}) if og and og.get("content"): desc = og["content"].strip() for p in soup.find_all("p"): txt = re.sub(r"\s+", " ", p.get_text(" ", strip=True)) if "électroménagers" in txt: # descriptif des unités desc = (desc + " " + txt).strip() break desc = desc[:600] amenities = [] for el in soup.select(".icon-label"): txt = re.sub(r"\s+", " ", el.get_text(" ", strip=True)).strip() if txt and txt not in amenities: amenities.append(txt) # Contact (liens tel:/mailto: structurés — location résidentielle) contact: dict = {} tel = soup.select_one('a[href^="tel:"]') if tel: digits = re.sub(r"\D", "", tel.get("href", ""))[-10:] if len(digits) == 10: contact["phone"] = "-".join( (digits[:3], digits[3:6], digits[6:])) mail = soup.select_one('a[href^="mailto:"]') if mail: contact["email"] = mail.get("href", "")[7:].split("?")[0] # Disponibilité (texte du site) + promotion (dans la description, # pour ne pas fausser la date de disponibilité normalisée) page_text = re.sub(r"\s+", " ", soup.get_text(" ", strip=True)) availability = "Disponible" if re.search(r"Maintenant disponible à la location", page_text, re.I): availability = "Maintenant disponible à la location" m = re.search(r"(\d\s*MOIS OFFERTS[^<*]{0,80})", page_text, re.I) if m: promo = "Promotion : " + re.sub(r"\s+", " ", m.group(1)).strip() desc = (desc + " — " + promo)[:600] # Cartes de prix : « 2 chambres (4 ½) », superficie, « À partir de … $ » for card in soup.select(".price-card"): try: lst = self._parse_card(card, images, desc, amenities, availability, contact) except Exception: continue if lst: listings.append(lst) return listings def _parse_card(self, card, images, desc, amenities, availability, contact=None) -> Listing | None: head = card.get_text(" ", strip=True) # « 2 chambres (4 ½) » m = re.search(r"(\d)\s*chambres?", head, re.I) mtype = re.search(r"\(\s*(\d)\s*(?:½|1/2)\s*\)", head) if not (m or mtype): return None if mtype: unit_type = f"{mtype.group(1)}½" else: n = int(m.group(1)) unit_type = {0: "Studio", 1: "3½", 2: "4½", 3: "5½"}.get( n, f"{n} chambres") # superficie et prix dans les blocs suivants du même conteneur container = card.parent sqft = price_label = "" if container: sq = container.select_one(".superficie") if sq: sqft = sq.get_text(" ", strip=True) mt = container.select_one(".montant") if mt: price_label = mt.get_text(" ", strip=True) price = parse_price(price_label) n_ch = m.group(1) if m else {"4½": "2", "5½": "3"}.get(unit_type, "") return Listing( source=self.source_id, external_id=f"axia-lachine-{unit_type.replace('½', '.5')}", url=BASE + "/#appartements", title=f"AXIA Appartements — {n_ch} chambres ({unit_type})", address=ADDRESS, sector="Lachine", city="Montréal", unit_type=unit_type, price=price, price_label=re.sub(r"\s+", " ", price_label).strip(), availability=availability, description=(f"{sqft}. {desc}" if sqft else desc)[:600], amenities=amenities[:15], details={"contact": dict(contact)} if contact else {}, images=images, )