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-facebook)4# Desc: Pages Facebook publiques — extraction best-effort du HTML servi aux5# navigateurs non connectés : compteur d'abonnés (JSON embarqué6# follower_count / texte « N followers/abonnés »), nom (og:title),7# avatar (og:image), catégorie. EXPÉRIMENTAL : FB mure agressivement ;8# un résultat found=False n'est pas une erreur du pipeline.9# ==============================================================================10from __future__ import annotations1112import asyncio13import html as htmllib14import json15import re1617from apify import Actor1819from .net import Fetcher2021PAGE_URL = "https://www.facebook.com/{u}"2223_OG_RE = {24 "title": re.compile(r'<meta property="og:title" content="([^"]*)"'),25 "image": re.compile(r'<meta property="og:image" content="([^"]*)"'),26 "description": re.compile(27 r'<meta (?:property="og:description"|name="description") '28 r'content="([^"]*)"'),29}30_FOLLOWER_RES = (31 re.compile(r'"follower_count"\s*:\s*(\d+)'),32 re.compile(r'"global_likers_count"\s*:\s*(\d+)'),33)34# texte « 1,2 M followers » / « 12 k abonnés » / « 4 016 971 mentions J’aime »35_TEXT_COUNT_RE = re.compile(36 r'([\d][\d\s .,]*)\s*([KkMm]?)\s*'37 r'(?:followers|abonnés|mentions\s+J’aime|J’aime|likes)',38 re.I)39_CATEGORY_RE = re.compile(r'"category_name"\s*:\s*"([^"]+)"')40_VERIFIED_RE = re.compile(r'"is_verified"\s*:\s*(true|false)')41_PAGE_ID_RES = (42 re.compile(r'"page_id"\s*:\s*"?(\d{6,})"?'),43 re.compile(r'"pageID"\s*:\s*"(\d{6,})"'),44 re.compile(r'"delegate_page_id"\s*:\s*"(\d{6,})"'),45)46# site web auto-déclaré de la page → cross-link côté crea-ka47_WEBSITE_RE = re.compile(48 r'"website(?:s)?"\s*:\s*\[?\s*"((?:https?:)?(?:[^"\\]|\\.)+)"')49_RATING_RE = re.compile(r'"overall_star_rating"\s*:\s*\{[^{}]*?'50 r'"value"\s*:\s*([\d.]+)')51_OG_URL_RE = re.compile(r'<meta property="og:url" content="([^"]*)"')525354def parse_text_count(text: str) -> int | None:55 m = _TEXT_COUNT_RE.search(text or "")56 if not m:57 return None58 num = m.group(1).replace(" ", "").replace(" ", "").replace(",", ".")59 try:60 val = float(num)61 except ValueError:62 return None63 unit = (m.group(2) or "").lower()64 if unit == "k":65 val *= 1_00066 elif unit == "m":67 val *= 1_000_00068 return int(val)697071async def main() -> None:72 async with Actor:73 inp = await Actor.get_input() or {}74 usernames = []75 for raw in (inp.get("usernames") or []):76 raw = (raw or "").strip()77 if not raw:78 continue79 raw = re.sub(r"^https?://(www\.|m\.)?facebook\.com/", "", raw)80 usernames.append(raw.strip("/").split("?")[0])81 proxy = await Actor.create_proxy_configuration(82 actor_proxy_input=inp.get("proxyConfiguration"))83 fetcher = Fetcher(proxy, delay=float(inp.get("requestDelay") or 2))84 sem = asyncio.Semaphore(int(inp.get("concurrency") or 2))8586 async def one(u: str) -> None:87 async with sem:88 miss = {"kind": "profile", "platform": "facebook",89 "username": u, "found": False}90 try:91 resp = await fetcher.get(PAGE_URL.format(u=u),92 session_id=u)93 except Exception as exc:94 await Actor.push_data({**miss, "error": str(exc)[:200]})95 return96 text = resp.text or ""97 if resp.status_code != 200 or len(text) < 5000:98 await Actor.push_data(99 {**miss, "error": f"http_{resp.status_code}"})100 return101 followers = None102 for rx in _FOLLOWER_RES:103 m = rx.search(text)104 if m:105 followers = int(m.group(1))106 break107 og = {k: htmllib.unescape(m.group(1)) if (m := rx.search(text))108 else None for k, rx in _OG_RE.items()}109 if followers is None:110 followers = parse_text_count(og.get("description") or "")111 if followers is None: # texte « N followers/abonnés » du corps112 followers = parse_text_count(text)113 if followers is None and not og.get("title"):114 await Actor.push_data({**miss, "error": "login_wall"})115 return116 cat = _CATEGORY_RE.search(text)117 ver = _VERIFIED_RE.search(text)118 page_id = next((m.group(1) for rx in _PAGE_ID_RES119 if (m := rx.search(text))), None)120 site = _WEBSITE_RE.search(text)121 website = None122 if site:123 try:124 website = json.loads(f'"{site.group(1)}"').strip()125 except Exception:126 website = site.group(1)127 if website and not website.startswith("http"):128 website = f"https://{website}"129 if website and "facebook.com" in website:130 website = None # auto-référence sans valeur131 rating = _RATING_RE.search(text)132 canon = _OG_URL_RE.search(text)133 await Actor.push_data({134 "kind": "profile",135 "platform": "facebook",136 "found": True,137 "username": u,138 "page_id": page_id,139 "full_name": og.get("title"),140 "biography": og.get("description"),141 "followers": followers,142 "category": cat.group(1) if cat else None,143 "is_verified": (ver.group(1) == "true") if ver else None,144 "website": website,145 "rating": float(rating.group(1)) if rating else None,146 "canonical_url": (htmllib.unescape(canon.group(1))147 if canon else None),148 "avatar": og.get("image"),149 "og_description": og.get("description"),150 })151152 await asyncio.gather(*[one(u) for u in usernames])153 Actor.log.info(f"terminé : {len(usernames)} pages")154