SPB Git forge

spb/crea-ka

Public

Créa·Ka — annuaire public cross-plateforme des créateurs de contenu québécois (crea-ka.com)

52commits 1branches 0releases
11.3 MBsize
maindefault branch
19 days agolast push
Python 73.6% HTML 13.2% TypeScript 6% JavaScript 4.5% CSS 1.7% Dockerfile 0.6%
6.4 KB · 154 lines python
Raw Blame History
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