SPB Git

spb/ka2 Public

ka2 — explorateur structuré du web québécois (édition légère, Groupe KA). Bot scraper+IA → graphe de connaissances. Claude Haiku + Firecrawl/Scrapfly.

Python 98.1% Shell 1.9%
3.3 KB · 83 lines python
Raw Blame History
1# Author: Simon-Pierre Boucher2# Contact: contact@spboucher.ai3"""Normalisation pour la résolution d'entités et le regroupement géographique.45- normalize_name : clé canonique d'un nom (accents, ponctuation, suffixes légaux retirés).6- canonical_location : regroupe les variantes ("Montreal"/"Montréal"/"Montréal, QC") en un7  libellé unique, et rattache les grandes villes du Québec à un libellé stable.8- domain : domaine racine d'une URL (pour relier des entités du même site).9"""1011from __future__ import annotations1213import re14import unicodedata15from urllib.parse import urlparse1617_LEGAL = {18    "inc", "ltee", "ltd", "limited", "limitee", "enr", "senc", "sencrl", "srl",19    "corp", "corporation", "cie", "co", "llc", "llp", "sec", "sa", "sas",20}212223def strip_accents(s: str) -> str:24    return "".join(c for c in unicodedata.normalize("NFD", s or "") if unicodedata.category(c) != "Mn")252627def normalize_name(name: str) -> str:28    """Clé de résolution : minuscules, sans accents/ponctuation ni suffixe légal."""29    s = strip_accents((name or "").lower())30    s = re.sub(r"[^a-z0-9 ]+", " ", s)31    toks = [t for t in s.split() if t and t not in _LEGAL]32    return " ".join(toks).strip()333435def domain(url: str | None) -> str:36    if not url:37        return ""38    u = url.strip()39    if "://" not in u:40        u = "http://" + u41    try:42        net = urlparse(u).netloc.lower()43    except Exception:44        return ""45    if net.startswith("www."):46        net = net[4:]47    return net484950# Grandes villes / variantes fréquentes -> libellé canonique51_CITY_MAP = {52    "montreal": "Montréal", "mtl": "Montréal", "ville-marie": "Montréal",53    "quebec": "Québec", "quebec city": "Québec", "ville de quebec": "Québec",54    "laval": "Laval", "gatineau": "Gatineau", "hull": "Gatineau",55    "sherbrooke": "Sherbrooke", "trois rivieres": "Trois-Rivières",56    "saguenay": "Saguenay", "chicoutimi": "Saguenay", "jonquiere": "Saguenay",57    "levis": "Lévis", "longueuil": "Longueuil", "terrebonne": "Terrebonne",58    "brossard": "Brossard", "repentigny": "Repentigny", "drummondville": "Drummondville",59    "saint jean sur richelieu": "Saint-Jean-sur-Richelieu", "granby": "Granby",60    "blainville": "Blainville", "saint jerome": "Saint-Jérôme", "mirabel": "Mirabel",61    "rimouski": "Rimouski", "victoriaville": "Victoriaville", "shawinigan": "Shawinigan",62    "rouyn noranda": "Rouyn-Noranda", "sept iles": "Sept-Îles", "val d or": "Val-d'Or",63    "boucherville": "Boucherville", "mascouche": "Mascouche", "salaberry": "Salaberry-de-Valleyfield",64    "chateauguay": "Châteauguay", "saint hyacinthe": "Saint-Hyacinthe", "sorel": "Sorel-Tracy",65    "joliette": "Joliette", "magog": "Magog", "alma": "Alma", "thetford": "Thetford Mines",66}676869def canonical_location(text: str | None) -> str:70    """Retourne un libellé de lieu canonique (ville QC connue, sinon 1er segment nettoyé)."""71    if not text:72        return ""73    base = strip_accents(text.lower())74    base = re.sub(r"[^a-z0-9 ]+", " ", base)75    base = re.sub(r"\s+", " ", base).strip()76    for kw, label in _CITY_MAP.items():77        if re.search(rf"\b{re.escape(kw)}\b", base):78            return label79    # sinon : 1er segment avant virgule, sans mentions province/pays80    seg = text.split(",")[0].strip()81    seg = re.sub(r"(?i)\b(qc|québec|quebec|canada)\b", "", seg).strip(" ,-")82    return seg[:60]83