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-onlyfans)4# Desc: Profils OnlyFans publics (page de profil hors connexion) — extraction5# best-effort des compteurs embarqués : posts, photos, vidéos, J'aime6# (favoritedCount), prix d'abonnement, badge, bio, avatar/bannière.7# EXPÉRIMENTAL : OnlyFans change souvent son rendu ; found=False n'est8# pas une erreur du pipeline.9# ==============================================================================10from __future__ import annotations1112import asyncio13import html as htmllib14import json15import re1617from apify import Actor1819from .net import Fetcher2021PROFILE_URL = "https://onlyfans.com/{u}"2223_NUM_RES = {24 "posts_count": re.compile(r'"postsCount"\s*:\s*(\d+)'),25 "photos_count": re.compile(r'"photosCount"\s*:\s*(\d+)'),26 "videos_count": re.compile(r'"videosCount"\s*:\s*(\d+)'),27 "likes": re.compile(r'"favoritedCount"\s*:\s*(\d+)'),28 "streams_count": re.compile(r'"finishedStreamsCount"\s*:\s*(\d+)'),29 "audios_count": re.compile(r'"audiosCount"\s*:\s*(\d+)'),30 "medias_count": re.compile(r'"mediasCount"\s*:\s*(\d+)'),31}32_JOIN_RE = re.compile(r'"joinDate"\s*:\s*"((?:[^"\\]|\\.)*)"')33_PERFORMER_RE = re.compile(r'"isPerformer"\s*:\s*(true|false)')34_PRICE_RE = re.compile(r'"subscribePrice"\s*:\s*([\d.]+)')35_STR_RES = {36 "full_name": re.compile(r'"name"\s*:\s*"((?:[^"\\]|\\.)*)"'),37 "biography": re.compile(r'"rawAbout"\s*:\s*"((?:[^"\\]|\\.)*)"'),38 "location": re.compile(r'"location"\s*:\s*"((?:[^"\\]|\\.)*)"'),39 "website": re.compile(r'"website"\s*:\s*"((?:[^"\\]|\\.)*)"'),40 "avatar": re.compile(r'"avatar"\s*:\s*"((?:[^"\\]|\\.)*)"'),41 "banner": re.compile(r'"header"\s*:\s*"((?:[^"\\]|\\.)*)"'),42}43_VERIFIED_RE = re.compile(r'"isVerified"\s*:\s*(true|false)')44_OG_TITLE_RE = re.compile(r'<meta property="og:title" content="([^"]*)"')454647def _dec(raw: str) -> str:48 try:49 return htmllib.unescape(json.loads(f'"{raw}"'))50 except Exception:51 return raw525354async def main() -> None:55 async with Actor:56 inp = await Actor.get_input() or {}57 usernames = [u.strip().lstrip("@").lower()58 for u in (inp.get("usernames") or []) if u and u.strip()]59 proxy = await Actor.create_proxy_configuration(60 actor_proxy_input=inp.get("proxyConfiguration"))61 fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 2))62 sem = asyncio.Semaphore(int(inp.get("concurrency") or 2))6364 async def one(u: str) -> None:65 async with sem:66 miss = {"kind": "profile", "platform": "onlyfans",67 "username": u, "found": False}68 resp = None69 for attempt in range(3): # mur Cloudflare → nouvelle IP70 try:71 resp = await fetcher.get(PROFILE_URL.format(u=u),72 session_id=f"{u}o{attempt}")73 except Exception as exc:74 await Actor.push_data({**miss,75 "error": str(exc)[:200]})76 return77 if resp.status_code == 404:78 await Actor.push_data({**miss, "error": "http_404"})79 return80 if resp.status_code == 200 and \81 "postsCount" in (resp.text or ""):82 break83 text = resp.text or ""84 if resp.status_code != 200 or "postsCount" not in text:85 await Actor.push_data(86 {**miss, "error": f"wall_{resp.status_code}"})87 return88 rec: dict = {"kind": "profile", "platform": "onlyfans",89 "found": True, "username": u}90 for key, rx in _NUM_RES.items():91 m = rx.search(text)92 rec[key] = int(m.group(1)) if m else None93 for key, rx in _STR_RES.items():94 m = rx.search(text)95 rec[key] = _dec(m.group(1)) if m else None96 if rec.get("biography"):97 rec["biography"] = re.sub(r"<[^>]+>", " ",98 rec["biography"])[:1000].strip()99 m = _PRICE_RE.search(text)100 rec["subscribe_price_usd"] = float(m.group(1)) if m else None101 rec["is_free"] = (rec["subscribe_price_usd"] == 0.0102 if rec["subscribe_price_usd"] is not None103 else None)104 m = _VERIFIED_RE.search(text)105 rec["is_verified"] = (m.group(1) == "true") if m else None106 m = _JOIN_RE.search(text)107 rec["join_date"] = _dec(m.group(1)) if m else None108 m = _PERFORMER_RE.search(text)109 rec["is_performer"] = (m.group(1) == "true") if m else None110 if not rec.get("full_name"):111 m = _OG_TITLE_RE.search(text)112 if m:113 rec["full_name"] = htmllib.unescape(114 m.group(1)).split(" OnlyFans")[0].strip()115 # « followers » au sens crea-ka : le compteur public = J'aime116 rec["followers"] = None117 await Actor.push_data(rec)118119 await asyncio.gather(*[one(u) for u in usernames])120 Actor.log.info(f"terminé : {len(usernames)} profils")121