Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)
Python 73.6%
HTML 13.2%
TypeScript 6%
JavaScript 4.5%
CSS 1.7%
Dockerfile 0.6%
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File: src/main.py (ka-instagram)4# Desc: Profils Instagram publics ULTRA DÉTAILLÉS via l'endpoint web officiel5# web_profile_info (JSON du web public) : identité complète, compteurs,6# 12 derniers posts (likes/commentaires/vues/légende/musique/lieu/7# co-auteurs/épinglés), agrégats d'engagement et de cadence, profils8# reliés — + recherche d'utilisateurs (topsearch) en découverte.9# ==============================================================================10from __future__ import annotations1112import asyncio13import json14import urllib.parse15from datetime import datetime, timezone1617from apify import Actor1819from .net import Fetcher2021PROFILE_API = ("https://i.instagram.com/api/v1/users/web_profile_info/"22 "?username={u}")23SEARCH_API = ("https://www.instagram.com/web/search/topsearch/"24 "?context=blended&query={q}&count=30")25SEARCH_API_ALT = ("https://i.instagram.com/api/v1/web/search/topsearch/"26 "?context=blended&query={q}&count=30")27HDRS = {"x-ig-app-id": "936619743392459", "Accept": "application/json"}282930def _iso(ts) -> str | None:31 try:32 return datetime.fromtimestamp(int(ts), tz=timezone.utc) \33 .strftime("%Y-%m-%dT%H:%M:%SZ")34 except (TypeError, ValueError, OSError):35 return None363738def _avg(vals: list) -> int | None:39 vals = [v for v in vals if isinstance(v, (int, float))]40 return round(sum(vals) / len(vals)) if vals else None414243def parse_post(node: dict) -> dict:44 """Nœud média du fil → post détaillé (métriques, musique, lieu, épinglé)."""45 cap = (node.get("edge_media_to_caption") or {}).get("edges") or []46 music = node.get("clips_music_attribution_info") or {}47 dims = node.get("dimensions") or {}48 return {49 "id": node.get("id"),50 "shortcode": node.get("shortcode"),51 "url": f"https://www.instagram.com/p/{node.get('shortcode')}/",52 "type": ("reel" if node.get("product_type") == "clips"53 else "video" if node.get("is_video") else54 "carousel" if node.get("edge_sidecar_to_children")55 else "image"),56 "caption": ((cap[0].get("node") or {}).get("text", "")57 if cap else "")[:500],58 "likes": (node.get("edge_liked_by") or {}).get("count"),59 "comments": (node.get("edge_media_to_comment") or {}).get("count"),60 "video_views": node.get("video_view_count"),61 "video_duration_s": node.get("video_duration"),62 "timestamp": node.get("taken_at_timestamp"),63 "posted_at": _iso(node.get("taken_at_timestamp")),64 "display_url": node.get("display_url"),65 "thumbnail_url": node.get("thumbnail_src"),66 "width": dims.get("width"),67 "height": dims.get("height"),68 "is_pinned": bool(node.get("pinned_for_users")),69 "comments_disabled": node.get("comments_disabled"),70 "location": (node.get("location") or {}).get("name"),71 "coauthors": [c.get("username")72 for c in (node.get("coauthor_producers") or [])73 if c.get("username")],74 "music": ({"artist": music.get("artist_name"),75 "song": music.get("song_name"),76 "uses_original_audio": music.get("uses_original_audio")}77 if music.get("song_name") or music.get("artist_name")78 else None),79 "accessibility_caption": (node.get("accessibility_caption")80 or "")[:200] or None,81 "tagged_users": [((t.get("node") or {}).get("user") or {})82 .get("username")83 for t in ((node.get("edge_media_to_tagged_user")84 or {}).get("edges") or [])][:10],85 }868788def parse_user(user: dict) -> dict:89 """Objet user web_profile_info → enregistrement riche standardisé."""90 posts = [parse_post((e.get("node") or {}))91 for e in ((user.get("edge_owner_to_timeline_media") or {})92 .get("edges") or [])]93 bio_ent = user.get("biography_with_entities") or {}94 bio_mentions, bio_hashtags = [], []95 for ent in (bio_ent.get("entities") or []):96 eu = ((ent or {}).get("user") or {}).get("username")97 eh = ((ent or {}).get("hashtag") or {}).get("name")98 if eu:99 bio_mentions.append(eu)100 if eh:101 bio_hashtags.append(eh)102 followers = (user.get("edge_followed_by") or {}).get("count")103 avg_likes = _avg([p["likes"] for p in posts])104 avg_comments = _avg([p["comments"] for p in posts])105 videos = [p for p in posts if p["type"] in ("video", "reel")]106 avg_video_views = _avg([p["video_views"] for p in videos])107 engagement = None108 if followers and avg_likes is not None:109 engagement = round(110 (avg_likes + (avg_comments or 0)) / followers * 100, 3)111 timestamps = sorted(p["timestamp"] for p in posts112 if isinstance(p["timestamp"], int))113 posts_per_week = None114 if len(timestamps) >= 3 and timestamps[-1] > timestamps[0]:115 span_weeks = (timestamps[-1] - timestamps[0]) / 604_800116 posts_per_week = round((len(timestamps) - 1) / span_weeks, 2)117 top_post = max((p for p in posts if isinstance(p["likes"], int)),118 key=lambda p: p["likes"], default=None)119 related = []120 for edge in ((user.get("edge_related_profiles") or {}).get("edges") or []):121 n = edge.get("node") or {}122 if n.get("username"):123 related.append({"username": n["username"],124 "full_name": n.get("full_name"),125 "is_verified": n.get("is_verified"),126 "is_private": n.get("is_private")})127 return {128 "kind": "profile",129 "platform": "instagram",130 "found": True,131 "id": user.get("id"),132 "username": user.get("username"),133 "full_name": user.get("full_name"),134 "biography": user.get("biography"),135 "bio_mentions": bio_mentions[:10], # @comptes cités dans la bio136 "bio_hashtags": bio_hashtags[:10],137 "pronouns": user.get("pronouns") or None,138 "external_url": user.get("external_url"),139 "bio_links": [b.get("url") for b in (user.get("bio_links") or [])140 if b.get("url")],141 "followers": followers,142 "following": (user.get("edge_follow") or {}).get("count"),143 "posts_count": (user.get("edge_owner_to_timeline_media")144 or {}).get("count"),145 "igtv_videos_count": (user.get("edge_felix_video_timeline")146 or {}).get("count"),147 "mutual_followers": (user.get("edge_mutual_followed_by")148 or {}).get("count"),149 "highlight_reels": user.get("highlight_reel_count"),150 "is_verified": user.get("is_verified"),151 "is_private": user.get("is_private"),152 "is_business": user.get("is_business_account"),153 "is_professional": user.get("is_professional_account"),154 "category": user.get("category_name"),155 "business_category": user.get("business_category_name"),156 "business_address": user.get("business_address_json"),157 # courriel PRO affiché publiquement par le créateur (§15 : public only)158 "business_email": user.get("business_email") or None,159 # présent = compte Threads relié (cross-link fort, même handle)160 "has_threads": (bool(user.get("has_onboarded_to_text_post_app"))161 if "has_onboarded_to_text_post_app" in user162 else None),163 "has_clips": user.get("has_clips"),164 "has_channel": user.get("has_channel"),165 "has_ar_effects": user.get("has_ar_effects"),166 "recently_joined": user.get("is_joined_recently"),167 "avatar": user.get("profile_pic_url_hd")168 or user.get("profile_pic_url"),169 # agrégats calculés sur les 12 derniers posts170 "avg_likes": avg_likes,171 "avg_comments": avg_comments,172 "avg_video_views": avg_video_views,173 "engagement_rate_pct": engagement,174 "posts_per_week": posts_per_week,175 "last_post_at": _iso(timestamps[-1]) if timestamps else None,176 "video_share_pct": (round(len(videos) / len(posts) * 100)177 if posts else None),178 "top_post": ({"url": top_post["url"], "likes": top_post["likes"],179 "comments": top_post["comments"],180 "type": top_post["type"]} if top_post else None),181 "recent_posts": posts,182 "related_profiles": related,183 }184185186async def main() -> None:187 async with Actor:188 inp = await Actor.get_input() or {}189 usernames = [u.strip().lstrip("@").lower()190 for u in (inp.get("usernames") or []) if u and u.strip()]191 queries = [q.strip() for q in (inp.get("queries") or [])192 if q and q.strip()]193 proxy = await Actor.create_proxy_configuration(194 actor_proxy_input=inp.get("proxyConfiguration"))195 fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 1))196 sem = asyncio.Semaphore(int(inp.get("concurrency") or 3))197198 async def one_profile(u: str) -> None:199 async with sem:200 miss = {"kind": "profile", "platform": "instagram",201 "username": u, "found": False}202 try:203 resp = await fetcher.get(PROFILE_API.format(u=u),204 headers=HDRS, session_id=u)205 except Exception as exc:206 await Actor.push_data({**miss, "error": str(exc)[:200]})207 return208 if resp.status_code == 404:209 await Actor.push_data({**miss, "error": "http_404"})210 return211 user = None212 if resp.status_code == 200:213 try:214 user = ((json.loads(resp.text).get("data") or {})215 .get("user"))216 except Exception:217 user = None218 if not user:219 await Actor.push_data(220 {**miss, "error": f"http_{resp.status_code}"})221 return222 await Actor.push_data(parse_user(user))223224 async def one_search(q: str) -> None:225 async with sem:226 data = {}227 for endpoint in (SEARCH_API, SEARCH_API_ALT):228 u2 = endpoint.format(q=urllib.parse.quote(q))229 try:230 resp = await fetcher.get(u2, headers=HDRS,231 session_id=f"srch{q[:4]}")232 except Exception as exc:233 Actor.log.warning(f"recherche '{q}' : {exc}")234 continue235 if resp.status_code == 200:236 try:237 data = json.loads(resp.text)238 except Exception:239 data = {}240 if data.get("users"):241 break242 if not data.get("users"):243 Actor.log.warning(244 f"recherche '{q}' : 0 user "245 f"(http {resp.status_code}) {resp.text[:200]!r}")246 for item in data.get("users") or []:247 u = item.get("user") or {}248 if not u.get("username") or u.get("is_private"):249 continue250 await Actor.push_data({251 "kind": "search_user",252 "platform": "instagram",253 "query": q,254 "username": u.get("username"),255 "full_name": u.get("full_name"),256 "is_verified": u.get("is_verified"),257 "followers": u.get("follower_count"),258 "avatar": u.get("profile_pic_url"),259 })260261 await asyncio.gather(*[one_profile(u) for u in usernames],262 *[one_search(q) for q in queries])263 Actor.log.info(f"terminé : {len(usernames)} profils, "264 f"{len(queries)} recherches")265