# ============================================================================== # Author: Simon-Pierre Boucher # File: src/main.py (ka-onlyfans) # Desc: Profils OnlyFans publics (page de profil hors connexion) — extraction # best-effort des compteurs embarqués : posts, photos, vidéos, J'aime # (favoritedCount), prix d'abonnement, badge, bio, avatar/bannière. # EXPÉRIMENTAL : OnlyFans change souvent son rendu ; found=False n'est # pas une erreur du pipeline. # ============================================================================== from __future__ import annotations import asyncio import html as htmllib import json import re from apify import Actor from .net import Fetcher PROFILE_URL = "https://onlyfans.com/{u}" _NUM_RES = { "posts_count": re.compile(r'"postsCount"\s*:\s*(\d+)'), "photos_count": re.compile(r'"photosCount"\s*:\s*(\d+)'), "videos_count": re.compile(r'"videosCount"\s*:\s*(\d+)'), "likes": re.compile(r'"favoritedCount"\s*:\s*(\d+)'), "streams_count": re.compile(r'"finishedStreamsCount"\s*:\s*(\d+)'), "audios_count": re.compile(r'"audiosCount"\s*:\s*(\d+)'), "medias_count": re.compile(r'"mediasCount"\s*:\s*(\d+)'), } _JOIN_RE = re.compile(r'"joinDate"\s*:\s*"((?:[^"\\]|\\.)*)"') _PERFORMER_RE = re.compile(r'"isPerformer"\s*:\s*(true|false)') _PRICE_RE = re.compile(r'"subscribePrice"\s*:\s*([\d.]+)') _STR_RES = { "full_name": re.compile(r'"name"\s*:\s*"((?:[^"\\]|\\.)*)"'), "biography": re.compile(r'"rawAbout"\s*:\s*"((?:[^"\\]|\\.)*)"'), "location": re.compile(r'"location"\s*:\s*"((?:[^"\\]|\\.)*)"'), "website": re.compile(r'"website"\s*:\s*"((?:[^"\\]|\\.)*)"'), "avatar": re.compile(r'"avatar"\s*:\s*"((?:[^"\\]|\\.)*)"'), "banner": re.compile(r'"header"\s*:\s*"((?:[^"\\]|\\.)*)"'), } _VERIFIED_RE = re.compile(r'"isVerified"\s*:\s*(true|false)') _OG_TITLE_RE = re.compile(r' str: try: return htmllib.unescape(json.loads(f'"{raw}"')) except Exception: return raw 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 2)) sem = asyncio.Semaphore(int(inp.get("concurrency") or 2)) async def one(u: str) -> None: async with sem: miss = {"kind": "profile", "platform": "onlyfans", "username": u, "found": False} resp = None for attempt in range(3): # mur Cloudflare → nouvelle IP try: resp = await fetcher.get(PROFILE_URL.format(u=u), session_id=f"{u}o{attempt}") except Exception as exc: await Actor.push_data({**miss, "error": str(exc)[:200]}) return if resp.status_code == 404: await Actor.push_data({**miss, "error": "http_404"}) return if resp.status_code == 200 and \ "postsCount" in (resp.text or ""): break text = resp.text or "" if resp.status_code != 200 or "postsCount" not in text: await Actor.push_data( {**miss, "error": f"wall_{resp.status_code}"}) return rec: dict = {"kind": "profile", "platform": "onlyfans", "found": True, "username": u} for key, rx in _NUM_RES.items(): m = rx.search(text) rec[key] = int(m.group(1)) if m else None for key, rx in _STR_RES.items(): m = rx.search(text) rec[key] = _dec(m.group(1)) if m else None if rec.get("biography"): rec["biography"] = re.sub(r"<[^>]+>", " ", rec["biography"])[:1000].strip() m = _PRICE_RE.search(text) rec["subscribe_price_usd"] = float(m.group(1)) if m else None rec["is_free"] = (rec["subscribe_price_usd"] == 0.0 if rec["subscribe_price_usd"] is not None else None) m = _VERIFIED_RE.search(text) rec["is_verified"] = (m.group(1) == "true") if m else None m = _JOIN_RE.search(text) rec["join_date"] = _dec(m.group(1)) if m else None m = _PERFORMER_RE.search(text) rec["is_performer"] = (m.group(1) == "true") if m else None if not rec.get("full_name"): m = _OG_TITLE_RE.search(text) if m: rec["full_name"] = htmllib.unescape( m.group(1)).split(" OnlyFans")[0].strip() # « followers » au sens crea-ka : le compteur public = J'aime rec["followers"] = None await Actor.push_data(rec) await asyncio.gather(*[one(u) for u in usernames]) Actor.log.info(f"terminé : {len(usernames)} profils")