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>
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 | |