SPB Git

spb/qc-influenceurs Public

InfluenceursQC — classement des influenceurs du Québec.

JavaScript 83.3% Python 16.7%
7.4 KB · 190 lines python
Raw Blame History
1#!/usr/bin/env python32"""Fusionne les extractions (data/extracted/*.json) en un dataset unique dédupliqué.34Sorties : data/qc_influenceurs.json + data/qc_influenceurs.csv5"""6import csv7import glob8import json9import os10import re11import unicodedata1213BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))14EXTRACTED = os.path.join(BASE, "data", "extracted")15OUT_JSON = os.path.join(BASE, "data", "qc_influenceurs.json")16OUT_CSV = os.path.join(BASE, "data", "qc_influenceurs.csv")1718PLATFORMS = ["instagram", "tiktok", "youtube", "facebook", "x", "twitch"]19METRIC_KEYS = {20    "instagram": "instagram_followers",21    "tiktok": "tiktok_followers",22    "youtube": "youtube_subscribers",23    "facebook": "facebook_followers",24    "x": "x_followers",25    "twitch": "twitch_followers",26}272829# variantes connues du même créateur (clé normalisée -> clé canonique)30MANUAL_ALIASES = {31    "carl is cooking": "carl arsenault",32}3334CATEGORY_FIXES = {35    "tennis / sport": "sport",36    "art / miniatures": "autre",37    "docuséries / arts": "autre",38}394041def norm_name(name: str) -> str:42    s = re.sub(r"\(.*?\)", " ", name or "")  # "Nabil Lahrech (Aiekillu)" == "Nabil Lahrech"43    s = unicodedata.normalize("NFKD", s)44    s = "".join(c for c in s if not unicodedata.combining(c))45    s = re.sub(r"[^a-z0-9]+", " ", s.lower()).strip()46    return s474849def name_aliases(name: str):50    """Clés supplémentaires : contenu des parenthèses (pseudo) et nom complet."""51    keys = {norm_name(name)}52    for inner in re.findall(r"\((.*?)\)", name or ""):53        k = norm_name(inner)54        if k:55            keys.add(k)56    return keys575859def norm_handle(h):60    if not h:61        return None62    h = str(h).strip().lstrip("@").lower()63    h = re.sub(r"^https?://(www\.)?(instagram|tiktok|youtube|facebook|x|twitter|twitch)\.(com|tv)/(@)?", "", h)64    h = h.strip("/").split("/")[0].split("?")[0]65    return h or None666768def merge_record(dst, src):69    for p in PLATFORMS:70        h = norm_handle((src.get("handles") or {}).get(p))71        if h and not dst["handles"].get(p):72            dst["handles"][p] = h73    for mk in METRIC_KEYS.values():74        v = (src.get("metrics") or {}).get(mk)75        if isinstance(v, (int, float)) and v > 0:76            cur = dst["metrics"].get(mk)77            # garder la valeur la plus élevée (les listes datées sous-estiment)78            if not cur or v > cur:79                dst["metrics"][mk] = int(v)80    if src.get("category") and (not dst.get("category") or dst["category"] == "autre"):81        dst["category"] = src["category"]82    if src.get("location") and dst.get("location") in (None, "", "Québec"):83        dst["location"] = src["location"]84    note = (src.get("notes") or "").strip()85    if note and note not in dst["notes"]:86        dst["notes"] = (dst["notes"] + " | " + note).strip(" |")87    for s in src.get("sources") or []:88        if s not in dst["sources"]:89            dst["sources"].append(s)909192def main():93    records = []94    for f in sorted(glob.glob(os.path.join(EXTRACTED, "*.json"))):95        try:96            data = json.load(open(f))97        except Exception as e:98            print(f"ERREUR lecture {f}: {e}")99            continue100        if isinstance(data, dict):101            data = data.get("influencers") or data.get("data") or []102        print(f"{os.path.basename(f)}: {len(data)} records")103        records.extend(data)104105    by_key = {}106    handle_index = {}  # (platform, handle) -> key107    alias_index = {}   # alias normalisé -> key108    NON_QC = ("toronto", "vancouver", "calgary", "winnipeg", "scarborough",109              "ajax", "ontario", "terre-neuve", "alberta", "colombie-britannique")110    for r in records:111        if not r.get("name"):112            continue113        loc = (r.get("location") or "").lower()114        if any(x in loc for x in NON_QC):115            continue  # hors Québec (listes canadiennes mal filtrées)116        key = norm_name(r["name"])117        if not key:118            continue119        key = MANUAL_ALIASES.get(key, key)120        # match par handle ou alias (pseudo entre parenthèses) si le nom diffère121        matched = None122        for p in PLATFORMS:123            h = norm_handle((r.get("handles") or {}).get(p))124            if h and (p, h) in handle_index:125                matched = handle_index[(p, h)]126                break127        if not matched:128            for a in name_aliases(r["name"]):129                if a in alias_index:130                    matched = alias_index[a]131                    break132        key = matched or key133        if key not in by_key:134            by_key[key] = {135                "name": r["name"].strip(),136                "handles": {p: None for p in PLATFORMS},137                "metrics": {mk: None for mk in METRIC_KEYS.values()},138                "category": None,139                "location": None,140                "notes": "",141                "sources": [],142            }143        merge_record(by_key[key], r)144        for p in PLATFORMS:145            h = by_key[key]["handles"].get(p)146            if h:147                handle_index[(p, h)] = key148        for a in name_aliases(r["name"]) | name_aliases(by_key[key]["name"]):149            alias_index[a] = key150151    out = list(by_key.values())152    for r in out:153        vals = [v for v in r["metrics"].values() if v]154        r["total_followers"] = sum(vals) if vals else None155        r["platform_count"] = sum(1 for p in PLATFORMS if r["handles"].get(p) or r["metrics"].get(METRIC_KEYS[p]))156        t = r["total_followers"] or 0157        r["tier"] = "méga (1M+)" if t >= 1_000_000 else "macro (100K+)" if t >= 100_000 else "micro (10K+)" if t >= 10_000 else "nano/inconnu"158        r["category"] = CATEGORY_FIXES.get(r["category"] or "autre", r["category"] or "autre")159        r["location"] = r["location"] or "Québec"160161    out.sort(key=lambda r: -(r["total_followers"] or 0))162163    json.dump(out, open(OUT_JSON, "w"), ensure_ascii=False, indent=2)164165    with open(OUT_CSV, "w", newline="") as fh:166        w = csv.writer(fh)167        w.writerow(["nom", "categorie", "localisation", "tier", "total_followers",168                    "instagram_handle", "instagram_followers", "tiktok_handle", "tiktok_followers",169                    "youtube_handle", "youtube_subscribers", "facebook_handle", "facebook_followers",170                    "x_handle", "x_followers", "twitch_handle", "twitch_followers",171                    "nb_plateformes", "notes", "sources"])172        for r in out:173            w.writerow([r["name"], r["category"], r["location"], r["tier"], r["total_followers"] or "",174                        r["handles"]["instagram"] or "", r["metrics"]["instagram_followers"] or "",175                        r["handles"]["tiktok"] or "", r["metrics"]["tiktok_followers"] or "",176                        r["handles"]["youtube"] or "", r["metrics"]["youtube_subscribers"] or "",177                        r["handles"]["facebook"] or "", r["metrics"]["facebook_followers"] or "",178                        r["handles"]["x"] or "", r["metrics"]["x_followers"] or "",179                        r["handles"]["twitch"] or "", r["metrics"]["twitch_followers"] or "",180                        r["platform_count"], r["notes"], ";".join(r["sources"])])181182    print(f"\nTotal dédupliqué : {len(out)} influenceurs")183    print(f"Avec au moins une métrique : {sum(1 for r in out if r['total_followers'])}")184    print(f"Méga (1M+) : {sum(1 for r in out if r['tier'].startswith('méga'))}")185    print(f"Macro (100K+) : {sum(1 for r in out if r['tier'].startswith('macro'))}")186187188if __name__ == "__main__":189    main()190