SPB Git

spb/food-ka Public

Food-Ka — agrégateur de produits d'épicerie du Québec — www.food-ka.com

Python 57.7% TypeScript 24.9% CSS 16.7% HTML 0.6%
4.4 KB · 113 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Food-Ka — connecteur Aubut (aubut.ca — grossiste cash & carry, Montréal)3# Auteur : Simon-Pierre Boucher — contact@spboucher.ai4# Site maison (Vue.js) : les grilles de catégories (/produits/<cat>?p=N,5# 32 tuiles/page) embarquent un composant <app-product-ecommerce> dont6# l'attribut :product contient TOUT le produit en JSON (prix, format, solde,7# inventaire, image) — parsing trivial et fiable.8# -----------------------------------------------------------------------------9from __future__ import annotations1011import json1213from bs4 import BeautifulSoup1415from ..schema import Product16from .base import BaseConnector1718BASE = "https://www.aubut.ca"1920# (slug de catégorie, catégorie canonique)21CATEGORIES = [22    ("fruits-et-legumes-9", "Fruits et légumes"),23    ("viandes-poissons-frais-8", "Viandes et volailles"),24    ("charcuteries-7", "Charcuteries et fromages"),25    ("produits-laitiers-6", "Produits laitiers et œufs"),26    ("boulangerie-5", "Boulangerie"),27    ("surgele-4", "Surgelés"),28    ("epicerie-1", "Garde-manger"),29    ("boissons-2", "Boissons"),30]313233def _price(value) -> float | None:34    """« 31.99 » (chaîne du JSON Aubut) -> float, None si vide/zéro."""35    try:36        price = float(value)37    except (TypeError, ValueError):38        return None39    return price if price > 0 else None404142class AubutConnector(BaseConnector):43    source_id = "aubut"4445    max_categories: int | None = None    # borne pour les tests46    pages_per_category: int = 2          # 32 tuiles/page -> ~512 produits max4748    def _parse_tiles(self, html: str, category: str, slug: str) -> list[Product]:49        soup = BeautifulSoup(html, "html.parser")50        products: list[Product] = []51        for tag in soup.select("app-product-ecommerce[\\:product]"):52            try:53                data = json.loads(tag[":product"])54            except (ValueError, KeyError):55                continue56            name = (data.get("title") or "").strip()57            pid = str(data.get("product_id") or data.get("id") or "")58            if not name or not pid:59                continue6061            # prix « unité » ; *_special > 0 = prix en solde62            regular = _price(data.get("unite_price"))63            special = _price(data.get("unite_price_special"))64            price = special if special is not None else regular65            if special is None:66                regular = None      # pas de solde -> pas de prix régulier distinct6768            image = data.get("picture_thumb_path") or ""69            in_stock = None70            if str(data.get("has_inventaire") or "") in ("0", "1"):71                in_stock = data.get("has_inventaire") == "1"7273            products.append(Product(74                source=self.source_id,75                external_id=pid,76                url=data.get("detail") or f"{BASE}/produits/details/{data.get('slug', '')}-{pid}",77                name=name,78                brand=(data.get("brand_title") or "").strip(),79                category=category,80                category_raw=slug,81                size_label=(data.get("unite_format") or "").strip(),82                price=price,83                regular_price=regular,84                on_sale=special is not None,85                unit_price_label=(data.get("price_100") or "").strip(),86                in_stock=in_stock,87                images=[image] if image.startswith("http") else [],88            ))89        return products9091    def fetch(self) -> list[Product]:92        products: list[Product] = []93        seen: set[str] = set()94        categories = CATEGORIES[: self.max_categories]95        for slug, category in categories:96            for page in range(1, self.pages_per_category + 1):97                url = f"{BASE}/produits/{slug}"98                if page > 1:99                    url += f"?p={page}"100                try:101                    html = self.get(url).text102                except Exception:   # une page qui casse ne bloque pas le reste103                    break104                new = 0105                for prod in self._parse_tiles(html, category, slug):106                    if prod.uid not in seen:107                        seen.add(prod.uid)108                        products.append(prod)109                        new += 1110                if new == 0:        # page vide ou répétée : catégorie épuisée111                    break112        return products113