Food·Ka
Tous les prix d'épicerie du Québec. Un seul endroit.
Agrégateur indépendant de produits d'épicerie — chaque produit avec son prix courant, son prix régulier, son prix unitaire comparable ($/100 g) et un lien direct vers la fiche originale de la bannière. Toujours à jour, automatiquement.
Pourquoi Food-Ka ?
Comparer les prix d'épicerie au Québec, c'est ouvrir Metro, IGA, Maxi, Super C, Provigo, Walmart… chacun avec sa propre navigation, son propre panier, son propre format. Food-Ka retourne le problème : un connecteur dédié par bannière visite chaque site, normalise chaque produit vers un schéma unique, et détecte les changements de prix en continu.
Les épiceries n'offrent pas de webhooks. Food-Ka reproduit l'équivalent : synchronisation périodique + hash de contenu → nouveaux produits, changements de prix et retraits détectés automatiquement. Chaque variation de prix est historisée (
price_log) — les soldes deviennent traçables.
L'architecture en 30 secondes
flowchart LR
subgraph Sources["Bannières d'épicerie"]
S1["Metro · Super C · IGA/Voilà<br/>Maxi · Provigo · Walmart<br/>Mayrand · Avril · PA · Tau<br/>… 24 connecteurs actifs"]
end
subgraph FoodKa["Food-Ka"]
C["Connecteurs<br/><i>1 adaptateur / bannière</i>"] --> N["Normalisation<br/><i>schéma Product unique</i>"]
N --> D[("SQLite<br/>hash + diff + prix")]
D --> A["API FastAPI<br/>/api/products · /api/facets"]
A --> F["React 18 + Vite<br/>PWA mobile"]
end
W["⏱ Watcher (PM2)"] -.-> C
S1 --> C
F --> U["🛒 Consommateur"]
| Couche | Rôle | Fichiers |
|---|---|---|
| Connecteurs | 1 module Python par bannière : HTML rendu serveur, API JSON ouvertes (Shopify products.json, WooCommerce Store API), __NEXT_DATA__ (Loblaw), ou Scrapfly (anti-bot ASP + rendu JS) / Firecrawl pour les sites protégés |
foodka/connectors/*.py |
| Schéma | Product standardisé : nom, marque, format, prix, prix régulier, prix unitaire $/100 g, catégorie canonique, images |
foodka/schema.py |
| Diff engine | Upsert par hash de contenu — nouveau / modifié / disparu (désactivé), historique de prix | foodka/db.py |
| API | Filtres catégorie / bannière / marque / prix / soldes / recherche, tris (prix, prix unitaire, rabais), facettes, stats | foodka/web.py |
| Frontend | Design « éditorial sharp » : Space Grotesk, ombres décalées, accent lime, ticker temps réel, fiches produit avec comparaison inter-bannières, PWA installable | frontend/ |
Démarrage rapide
git clone https://github.com/spboucher-ai/food-ka.git && cd food-ka
# Backend
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
# Frontend
cd frontend && npm install && npm run build && cd ..
# Sites protégés (anti-bot) et rendu JavaScript
cat > .env <<'ENV'
SCRAPFLY_API_KEY=scp-live-votre-cle
FIRECRAWL_API_KEY=fc-votre-cle
ENV
# Ingestion puis service
.venv/bin/python run.py sync # toutes les sources (ou: run.py sync metro iga)
.venv/bin/python run.py serve 8080 # → http://localhost:8080
.venv/bin/python run.py watch 360 # resynchronisation en boucle (minutes)Ajouter une bannière (≈ 30 lignes)
L'enregistrement est auto-découvrant : déposez un module dans foodka/connectors/,
c'est tout — aucun fichier partagé à modifier.
# foodka/connectors/mon_epicerie.py
from ..schema import Product, normalize_category, parse_price
from .base import BaseConnector
class MonEpicerieConnector(BaseConnector):
source_id = "mon_epicerie"
def fetch(self) -> list[Product]:
html = self.get("https://mon-epicerie.ca/produits").text # throttlé, poli
# ... ou self.get_scrapfly(url) si le site est protégé ...
return [Product(
source=self.source_id, external_id="123",
url="https://mon-epicerie.ca/produit/123",
name="Beurre d'arachide croquant", brand="Kraft",
category=normalize_category("Garde-manger"),
size_label="500 g", price=parse_price("4,99 $"),
regular_price=6.49, images=[...],
)]Puis : .venv/bin/python run.py sync mon_epicerie — et les produits apparaissent sur
le site, avec prix unitaire calculé et comparaison inter-bannières. Ajoutez l'entrée
correspondante dans data/sources.json pour la page Sources.
API
| Endpoint | Description |
|---|---|
GET /api/products?category=&source=&brand=&price_min=&price_max=&on_sale=1&q=&sort= |
Recherche filtrée (tris : prix, prix unitaire, rabais, nom, récents) |
GET /api/products/{uid} |
Fiche complète + historique de prix + comparaison inter-bannières |
GET /api/facets |
Valeurs distinctes pour construire les filtres |
GET /api/sources |
Registre des bannières + compteurs + dernière sync |
GET /api/stats |
Totaux, soldes actifs, meilleures aubaines, journal de synchronisation |
POST /api/sync |
Déclenche une synchronisation en arrière-plan |
Couverture
Grandes bannières — Metro, Super C, IGA (Voilà), Maxi, Provigo, Walmart Canada, Costco Canada (épicerie livrée), Marché Adonis.
Spécialisées & indépendantes — Mayrand (grossiste), Avril Supermarché Santé, Marché Tau, PA Supermarché, Aubut, Maturin (producteurs québécois), Giant Tiger, Epipresto, Marché Nuvo, La Boîte à Grains, BocoBoco, Aliments Merci, Club Entrepôt, Épiceries LOCO, Akhavan, T&T Supermarket.
Chaque bannière non-connectable est documentée avec sa raison dans
data/sources.json (ex. : prix liés à une session Instacart/DoorDash, catalogue
sans prix, anti-bot strict).
Production
Déployé sous PM2 (3 processus) derrière ngrok :
food-ka-web .venv/bin/python run.py serve 8096 # API + frontend
food-ka-sync .venv/bin/python run.py watch 360 # resync aux 6 h
food-ka-ngrok ngrok http --url=www.food-ka.com 8096 # tunnelPhilosophie d'exploitation : on ne pousse que le code — le serveur maintient ses données lui-même.
Principes
- Politesse — délai ≥ 0,5 s entre requêtes, périmètre de crawl borné, User-Agent identifié.
- Fidélité — aucun prix inventé : si la source n'affiche pas de prix,
price = null; un « prix régulier » incohérent est rejeté. - Traçabilité — chaque fiche renvoie vers le produit original de la bannière, et chaque changement de prix est historisé.
- Robustesse — un connecteur qui casse n'affecte jamais les autres
(auto-découverte tolérante, try/except par produit, détection de dérive,
journal
sync_log).