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-threads)4# Desc: Profils Meta Threads publics : la page @handle embarque le profil5# dans des scripts JSON Relay (follower_count, biography, badge,6# avatar) + les posts récents (caption/like_count) quand servis.7# Extraction par regex ciblées — robuste aux réorganisations du JSON.8# ==============================================================================9from __future__ import annotations1011import asyncio12import html as htmllib13import json14import re1516from apify import Actor1718from .net import Fetcher1920PROFILE_URL = "https://www.threads.com/@{u}"2122_FOLLOWERS_RE = re.compile(r'"follower_count"\s*:\s*(\d+)')23_BIO_RE = re.compile(r'"biography"\s*:\s*"((?:[^"\\]|\\.)*)"')24_NAME_RE = re.compile(r'"full_name"\s*:\s*"((?:[^"\\]|\\.)*)"')25_VERIFIED_RE = re.compile(r'"is_verified"\s*:\s*(true|false)')26_PIC_RE = re.compile(r'"profile_pic_url"\s*:\s*"((?:[^"\\]|\\.)*)"')27# liens auto-déclarés de la bio → cross-links forts côté crea-ka (§12.1)28_BIO_LINKS_RE = re.compile(r'"bio_links"\s*:\s*\[(.{0,2000}?)\]', re.S)29_LINK_URL_RE = re.compile(r'"url"\s*:\s*"(https?:(?:[^"\\]|\\.)+)"')30_PK_RE = re.compile(r'"pk"\s*:\s*"?(\d{4,})"?')31# posts : paires texte + like_count dans les payloads thread_items32_POST_RE = re.compile(33 r'"caption"\s*:\s*\{\s*"text"\s*:\s*"((?:[^"\\]|\\.)*)"[^{}]*?\}'34 r'.{0,600}?"like_count"\s*:\s*(\d+)', re.S)353637async def main() -> None:38 async with Actor:39 inp = await Actor.get_input() or {}40 usernames = [u.strip().lstrip("@").lower()41 for u in (inp.get("usernames") or []) if u and u.strip()]42 proxy = await Actor.create_proxy_configuration(43 actor_proxy_input=inp.get("proxyConfiguration"))44 fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 2))45 sem = asyncio.Semaphore(int(inp.get("concurrency") or 2))4647 async def one(u: str) -> None:48 async with sem:49 miss = {"kind": "profile", "platform": "threads",50 "username": u, "found": False}51 try:52 resp = await fetcher.get(PROFILE_URL.format(u=u),53 session_id=u)54 except Exception as exc:55 await Actor.push_data({**miss, "error": str(exc)[:200]})56 return57 text = resp.text or ""58 fol = _FOLLOWERS_RE.search(text)59 if resp.status_code != 200 or not fol:60 await Actor.push_data(61 {**miss,62 "error": f"shell_or_{resp.status_code}"})63 return64 posts = []65 for m in _POST_RE.finditer(text):66 cap = _dec(m.group(1))[:500]67 if cap and all(p["caption"] != cap for p in posts):68 posts.append({"caption": cap,69 "likes": int(m.group(2))})70 if len(posts) >= 15:71 break72 bio = _BIO_RE.search(text)73 name = _NAME_RE.search(text)74 ver = _VERIFIED_RE.search(text)75 pic = _PIC_RE.search(text)76 pk = _PK_RE.search(text)77 bio_links: list[str] = []78 bl = _BIO_LINKS_RE.search(text)79 if bl:80 for m2 in _LINK_URL_RE.finditer(bl.group(1)):81 url2 = _dec(m2.group(1))82 if url2 not in bio_links:83 bio_links.append(url2)84 likes = [p["likes"] for p in posts]85 top = max(posts, key=lambda p: p["likes"], default=None)86 await Actor.push_data({87 "kind": "profile",88 "platform": "threads",89 "found": True,90 "username": u,91 "id": pk.group(1) if pk else None,92 "full_name": _dec(name.group(1)) if name else None,93 "biography": _dec(bio.group(1)) if bio else None,94 "bio_links": bio_links[:5] or None,95 "followers": int(fol.group(1)),96 "is_verified": (ver.group(1) == "true") if ver else None,97 "avatar": _dec(pic.group(1)) if pic else None,98 "avg_likes": round(sum(likes) / len(likes))99 if likes else None,100 "top_post": top,101 "recent_posts": posts,102 })103104 await asyncio.gather(*[one(u) for u in usernames])105 Actor.log.info(f"terminé : {len(usernames)} profils")106107108def _dec(raw: str) -> str:109 """Décode une chaîne échappée JSON (\\uXXXX, \\/ …)."""110 try:111 return htmllib.unescape(json.loads(f'"{raw}"'))112 except Exception:113 return raw114