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
3 days agolast push
HTML 98.9% Python 0.6%

airbnb : polygone frontière Québec (élagage cellules + filtre cartes) et budget mélangé

Le bbox provincial englobe l'est de l'Ontario, le nord des É.-U. et le NB :
4 857 annonces hors province (Ottawa & cie étiquetées « Outaouais ») ont été
récoltées puis purgées. QC_POLY filtre désormais les cartes et élague les
cellules entièrement hors Québec ; l'ordre des cellules est mélangé à chaque
run pour que les cellules restantes tournent, budget par défaut 900 → 1200.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 23, 2026) parent 5c571bc

1 changed file +52 −6

modified louka/shortterm/connectors/airbnb.py +52 −6
@@ -16,8 +16,14 @@
16 16 # par carte, 100 % des cartes ont des coordonnées → la région touristique
17 17 # est déduite du point (centroïde le plus proche). Dédoublonnage par room id.
18 18 #
19 +# Le bbox englobe l'est de l'Ontario, le nord des États-Unis et le NB : un
20 +# polygone frontière grossier (QC_POLY) élague les cellules hors province et
21 +# filtre les cartes (sinon Ottawa & cie se font étiqueter « Outaouais »).
22 +# L'ordre des cellules est mélangé à chaque run pour que les cellules restantes
23 +# après épuisement du budget tournent d'un run à l'autre.
24 +#
19 25 # Réglages env : LOUKA_AIRBNB_PAGES (pages par cellule feuille, défaut 15),
20 −# LOUKA_AIRBNB_BUDGET (budget de requêtes HTML, défaut 900),
26 +# LOUKA_AIRBNB_BUDGET (budget de requêtes HTML, défaut 1200),
21 27 # LOUKA_AIRBNB_DEPTH (profondeur max du quadtree, défaut 7).
22 28 # -----------------------------------------------------------------------------
23 29 from __future__ import annotations
@@ -26,6 +32,7 @@ import base64
26 32 import json
27 33 import math
28 34 import os
35 +import random
29 36 import re
30 37 import sys
31 38 import time
@@ -68,6 +75,41 @@ REGION_CENTROIDS: list[tuple[str, float, float]] = [
68 75 ]
69 76
70 77
78 +# Polygone frontière grossier du Québec (lat, lng) : suit approximativement
79 +# la rivière des Outaouais / lac Témiscamingue (Ontario), la frontière
80 +# américaine, le NB, puis englobe golfe + Îles-de-la-Madeleine + Anticosti +
81 +# Côte-Nord. Les petites erreurs près des frontières sont acceptables.
82 +QC_POLY: list[tuple[float, float]] = [
83 + (52.20, -79.50), (47.55, -79.58), (46.75, -79.15), (46.15, -77.90),
84 + (45.85, -77.20), (45.45, -75.90), (45.50, -74.90), (45.35, -74.50),
85 + (45.00, -74.35), (45.00, -71.50), (45.30, -71.10), (46.00, -70.50),
86 + (47.30, -68.50), (47.50, -68.30), (47.50, -66.50), (47.90, -66.40),
87 + (48.00, -64.50), (46.90, -62.50), (47.00, -61.30), (48.50, -61.00),
88 + (49.30, -61.60), (50.00, -58.50), (51.20, -56.90), (52.20, -57.00),
89 +]
90 +
91 +
92 +def _in_quebec(lat: float | None, lng: float | None) -> bool:
93 + if lat is None or lng is None:
94 + return False
95 + inside = False
96 + n = len(QC_POLY)
97 + for i in range(n):
98 + la1, lo1 = QC_POLY[i]
99 + la2, lo2 = QC_POLY[(i + 1) % n]
100 + if (la1 > lat) != (la2 > lat):
101 + x = lo1 + (lat - la1) / (la2 - la1) * (lo2 - lo1)
102 + if lng < x:
103 + inside = not inside
104 + return inside
105 +
106 +
107 +def _cell_touches_qc(s: float, w: float, n: float, e: float) -> bool:
108 + """Vrai si au moins un point d'un échantillon 4×4 de la cellule est au QC."""
109 + return any(_in_quebec(s + (n - s) * i / 3, w + (e - w) * j / 3)
110 + for i in range(4) for j in range(4))
111 +
112 +
71 113 def _region_from_latlng(lat: float | None, lng: float | None) -> str:
72 114 if lat is None or lng is None:
73 115 return ""
@@ -327,9 +369,10 @@ class Airbnb(StConnector):
327 369 for card in cards:
328 370 coord = (((card.get("demandStayListing") or {})
329 371 .get("location") or {}).get("coordinate") or {})
330 − region = _region_from_latlng(coord.get("latitude"),
331 − coord.get("longitude"))
332 − lst = self._to_listing(card, region)
372 + lat, lng = coord.get("latitude"), coord.get("longitude")
373 + if lat is not None and lng is not None and not _in_quebec(lat, lng):
374 + continue # Ontario / États-Unis / NB dans le bbox
375 + lst = self._to_listing(card, _region_from_latlng(lat, lng))
333 376 if lst is None or lst.external_id in seen:
334 377 continue
335 378 seen.add(lst.external_id)
@@ -340,7 +383,8 @@ class Airbnb(StConnector):
340 383 # -- contrat ------------------------------------------------------------
341 384 def fetch(self) -> list[StListing]:
342 385 pages = max(1, int(os.environ.get("LOUKA_AIRBNB_PAGES", "15") or 15))
343 − budget = max(1, int(os.environ.get("LOUKA_AIRBNB_BUDGET", "900") or 900))
386 + budget = max(1, int(os.environ.get("LOUKA_AIRBNB_BUDGET", "1200")
387 + or 1200))
344 388 max_depth = max(0, int(os.environ.get("LOUKA_AIRBNB_DEPTH", "7") or 7))
345 389
346 390 s0, w0, n0, e0 = QC_BBOX
@@ -351,7 +395,7 @@ class Airbnb(StConnector):
351 395 s0 + (i + 1) * dlat, w0 + (j + 1) * dlng, 0)
352 396 for i in range(rows) for j in range(cols)
353 397 ]
354 − stack.reverse()
398 + random.shuffle(stack)
355 399
356 400 out: list[StListing] = []
357 401 seen: set[str] = set()
@@ -359,6 +403,8 @@ class Airbnb(StConnector):
359 403
360 404 while stack and used < budget:
361 405 cs, cw, cn, ce, depth = stack.pop()
406 + if not _cell_touches_qc(cs, cw, cn, ce):
407 + continue # cellule entièrement hors Québec
362 408 base = self._map_url(cs, cw, cn, ce)
363 409 used += 1
364 410 try:
365 411