# ============================================================================== # Author: Simon-Pierre Boucher # File: src/main.py (ka-snapchat) # Desc: Profils Snapchat publics via le JSON __NEXT_DATA__ de la page # snapchat.com/add/ : publicProfileInfo (abonnés, bio, badge, # catégorie, avatar) + présence story/spotlight + comptes reliés. # Structure validée le 2026-08-18 (pageProps.userProfile.$case). # ============================================================================== from __future__ import annotations import asyncio import json import re from apify import Actor from .net import Fetcher PROFILE_URL = "https://www.snapchat.com/add/{u}" _NEXT_DATA_RE = re.compile( r'', re.S) def _num(v): try: return int(v) except (TypeError, ValueError): return None async def main() -> None: async with Actor: inp = await Actor.get_input() or {} usernames = [u.strip().lstrip("@").lower() for u in (inp.get("usernames") or []) if u and u.strip()] proxy = await Actor.create_proxy_configuration( actor_proxy_input=inp.get("proxyConfiguration")) fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 1)) sem = asyncio.Semaphore(int(inp.get("concurrency") or 3)) async def one(u: str) -> None: async with sem: miss = {"kind": "profile", "platform": "snapchat", "username": u, "found": False} try: resp = await fetcher.get(PROFILE_URL.format(u=u), session_id=u) except Exception as exc: await Actor.push_data({**miss, "error": str(exc)[:200]}) return m = _NEXT_DATA_RE.search(resp.text or "") if resp.status_code != 200 or not m: await Actor.push_data( {**miss, "error": f"http_{resp.status_code}"}) return try: props = (json.loads(m.group(1)).get("props") or {}) \ .get("pageProps") or {} except Exception: await Actor.push_data({**miss, "error": "bad_json"}) return up = props.get("userProfile") or {} info = up.get("publicProfileInfo") or {} story = (props.get("story") or {}) snaps = story.get("snapList") or [] lenses = props.get("lenses") or [] spotlight = props.get("spotlightHighlights") or [] if not info.get("username"): await Actor.push_data({**miss, "error": "no_profile"}) return related = [ {"username": r.get("username"), "title": r.get("title")} for r in ((info.get("relatedAccountsInfo") or []) if isinstance(info.get("relatedAccountsInfo"), list) else []) if isinstance(r, dict) and r.get("username")] story_previews = [] for s in snaps[:6]: if not isinstance(s, dict): continue urls = s.get("snapUrls") or {} prev = (urls.get("mediaPreviewUrl") or {}) \ if isinstance(urls.get("mediaPreviewUrl"), dict) \ else {"value": urls.get("mediaPreviewUrl")} url = prev.get("value") or urls.get("mediaUrl") if url: story_previews.append(url) spotlight_previews = [ h.get("thumbnailUrl") for h in spotlight[:6] if isinstance(h, dict) and h.get("thumbnailUrl")] subs = _num(info.get("subscriberCount")) await Actor.push_data({ "kind": "profile", "platform": "snapchat", "found": True, "username": info.get("username"), "full_name": info.get("title"), "biography": info.get("bio"), "followers": subs if subs else None, # 0 = masqué "is_verified": str(info.get("badge")) == "1", "category": info.get("categoryStringId"), "website": info.get("websiteUrl"), "address": info.get("address"), "avatar": info.get("profilePictureUrl"), "banner": (info.get("squareHeroImageUrl") or info.get("heroImageUrl") or None), "snapcode": info.get("snapcodeImageUrl") or None, "has_story": bool(info.get("hasStory")), "has_spotlight": bool(info.get("hasSpotlightHighlights")), "has_curated_highlights": bool( info.get("hasCuratedHighlights")), "story_snaps_count": len(snaps) or None, "story_previews": story_previews or None, "lenses_count": len(lenses) or None, "spotlight_highlights_count": len(spotlight) or None, "spotlight_previews": spotlight_previews or None, "publisher_type": info.get("publisherType") or None, "subcategory": info.get("subcategoryStringId") or None, "related_accounts": related, }) await asyncio.gather(*[one(u) for u in usernames]) Actor.log.info(f"terminé : {len(usernames)} profils")