# ============================================================================== # Author: Simon-Pierre Boucher # File: src/main.py (ka-facebook) # Desc: Pages Facebook publiques — extraction best-effort du HTML servi aux # navigateurs non connectés : compteur d'abonnés (JSON embarqué # follower_count / texte « N followers/abonnés »), nom (og:title), # avatar (og:image), catégorie. EXPÉRIMENTAL : FB mure agressivement ; # un résultat 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 PAGE_URL = "https://www.facebook.com/{u}" _OG_RE = { "title": re.compile(r' int | None: m = _TEXT_COUNT_RE.search(text or "") if not m: return None num = m.group(1).replace(" ", "").replace(" ", "").replace(",", ".") try: val = float(num) except ValueError: return None unit = (m.group(2) or "").lower() if unit == "k": val *= 1_000 elif unit == "m": val *= 1_000_000 return int(val) async def main() -> None: async with Actor: inp = await Actor.get_input() or {} usernames = [] for raw in (inp.get("usernames") or []): raw = (raw or "").strip() if not raw: continue raw = re.sub(r"^https?://(www\.|m\.)?facebook\.com/", "", raw) usernames.append(raw.strip("/").split("?")[0]) 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": "facebook", "username": u, "found": False} try: resp = await fetcher.get(PAGE_URL.format(u=u), session_id=u) except Exception as exc: await Actor.push_data({**miss, "error": str(exc)[:200]}) return text = resp.text or "" if resp.status_code != 200 or len(text) < 5000: await Actor.push_data( {**miss, "error": f"http_{resp.status_code}"}) return followers = None for rx in _FOLLOWER_RES: m = rx.search(text) if m: followers = int(m.group(1)) break og = {k: htmllib.unescape(m.group(1)) if (m := rx.search(text)) else None for k, rx in _OG_RE.items()} if followers is None: followers = parse_text_count(og.get("description") or "") if followers is None: # texte « N followers/abonnés » du corps followers = parse_text_count(text) if followers is None and not og.get("title"): await Actor.push_data({**miss, "error": "login_wall"}) return cat = _CATEGORY_RE.search(text) ver = _VERIFIED_RE.search(text) page_id = next((m.group(1) for rx in _PAGE_ID_RES if (m := rx.search(text))), None) site = _WEBSITE_RE.search(text) website = None if site: try: website = json.loads(f'"{site.group(1)}"').strip() except Exception: website = site.group(1) if website and not website.startswith("http"): website = f"https://{website}" if website and "facebook.com" in website: website = None # auto-référence sans valeur rating = _RATING_RE.search(text) canon = _OG_URL_RE.search(text) await Actor.push_data({ "kind": "profile", "platform": "facebook", "found": True, "username": u, "page_id": page_id, "full_name": og.get("title"), "biography": og.get("description"), "followers": followers, "category": cat.group(1) if cat else None, "is_verified": (ver.group(1) == "true") if ver else None, "website": website, "rating": float(rating.group(1)) if rating else None, "canonical_url": (htmllib.unescape(canon.group(1)) if canon else None), "avatar": og.get("image"), "og_description": og.get("description"), }) await asyncio.gather(*[one(u) for u in usernames]) Actor.log.info(f"terminé : {len(usernames)} pages")