SPB Git

spb/crea-ka

Public

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

Couverture Apify forcée : mode APIFY_FORCE=1 (ignore fraîcheur + caps, délai élargi), tampon des miss définitifs (handle mort/mur de login → rotation hebdo au lieu d'un re-scan quotidien en pure perte), concurrence X réduite à 4 + retries 429 avec backoff et rotation d'IP dans ka-x (0.1.7)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 21, 2026) parent 8fa08f4 Browse files

2 changed files +38 −7

modified actors/ka-x/src/main.py +9 −3
@@ -112,10 +112,16 @@ async def main() -> None:
112 try: 112 try:
113 resp = await fetcher.get(SYNDICATION_URL.format(u=u), 113 resp = await fetcher.get(SYNDICATION_URL.format(u=u),
114 session_id=u, impersonate=None) 114 session_id=u, impersonate=None)
115− if resp.status_code == 429: 115+ # 429 : retries avec backoff + NOUVELLE session proxy
116+ # (nouvelle IP résidentielle) — une seule relance ne
117+ # suffisait pas (13/40 handles en 429, constat 2026-08-21)
118+ for i, imp in enumerate(("chrome", None, "chrome")):
119+ if resp.status_code != 429:
120+ break
121+ await asyncio.sleep(5 * (i + 1))
116 resp = await fetcher.get(SYNDICATION_URL.format(u=u), 122 resp = await fetcher.get(SYNDICATION_URL.format(u=u),
117− session_id=f"{u}b", 123+ session_id=f"{u}r{i}",
118− impersonate="chrome") 124+ impersonate=imp)
119 except Exception as exc: 125 except Exception as exc:
120 await Actor.push_data({**miss, "error": str(exc)[:200]}) 126 await Actor.push_data({**miss, "error": str(exc)[:200]})
121 return 127 return
modified creaka/connectors/apify_social.py +29 −4
@@ -44,6 +44,14 @@ APIFY_API = "https://api.apify.com/v2"
44 ACTOR_OWNER = "gorgeous_thistle" 44 ACTOR_OWNER = "gorgeous_thistle"
45 RESIDENTIAL = {"useApifyProxy": True, "apifyProxyGroups": ["RESIDENTIAL"]} 45 RESIDENTIAL = {"useApifyProxy": True, "apifyProxyGroups": ["RESIDENTIAL"]}
46 46
47+# erreurs DÉFINITIVES des acteurs (handle mort, profil privé, mur de login…) :
48+# on tamponne le compte quand même pour qu'il ne soit re-sondé qu'à la rotation
49+# `revisit_days`, au lieu d'être re-payé chaque jour en pure perte. Les erreurs
50+# transitoires (429, 5xx, shell, proxy) ne sont PAS tamponnées → re-tentées.
51+MISS_PREFIXES = ("user_not_found", "private", "not_found", "no_profile",
52+ "no_channel", "login_wall", "invite_invalide", "http_404",
53+ "wall_")
54+
47 # --- parallélisme : on dispose de 256 Go de RAM Apify → au lieu d'UN run à 55 # --- parallélisme : on dispose de 256 Go de RAM Apify → au lieu d'UN run à
48 # 1 Go, on éclate le lot en tranches lancées EN PARALLÈLE, chacune avec plus de 56 # 1 Go, on éclate le lot en tranches lancées EN PARALLÈLE, chacune avec plus de
49 # mémoire (donc plus de CPU/vCPU côté Apify) et une concurrence interne accrue. 57 # mémoire (donc plus de CPU/vCPU côté Apify) et une concurrence interne accrue.
@@ -57,6 +65,9 @@ RUN_CONCURRENCY = int(os.environ.get("APIFY_RUN_CONCURRENCY", "10")) # interne
57 # rafraîchissement : un compte est re-sondé s'il n'a pas été enrichi depuis 65 # rafraîchissement : un compte est re-sondé s'il n'a pas été enrichi depuis
58 # REVISIT_DAYS jours. Le watch quotidien couvre ainsi TOUT le bassin en ≤ N j. 66 # REVISIT_DAYS jours. Le watch quotidien couvre ainsi TOUT le bassin en ≤ N j.
59 REVISIT_DAYS = int(os.environ.get("APIFY_REVISIT_DAYS", "7")) 67 REVISIT_DAYS = int(os.environ.get("APIFY_REVISIT_DAYS", "7"))
68+# passage FORCÉ (APIFY_FORCE=1) : ignore fraîcheur ET caps — couvre TOUT le
69+# bassin de chaque plateforme en un seul passage (rattrapage de couverture)
70+FORCE_PASS = os.environ.get("APIFY_FORCE", "") == "1"
60 71
61 72
62 def _token() -> str: 73 def _token() -> str:
@@ -178,6 +189,8 @@ class _ApifySocialEnrich(BaseConnector):
178 return f"{self.source_id}_at" 189 return f"{self.source_id}_at"
179 190
180 def _stale(self, acc) -> bool: 191 def _stale(self, acc) -> bool:
192+ if FORCE_PASS:
193+ return True
181 stamp = (acc.metrics or {}).get(self._stamp_key) 194 stamp = (acc.metrics or {}).get(self._stamp_key)
182 if not stamp: 195 if not stamp:
183 return True 196 return True
@@ -209,7 +222,7 @@ class _ApifySocialEnrich(BaseConnector):
209 if acc is None or not self._stale(acc): 222 if acc is None or not self._stale(acc):
210 continue 223 continue
211 targets.setdefault(self.target_of(acc), []).append((cr, acc)) 224 targets.setdefault(self.target_of(acc), []).append((cr, acc))
212− if len(targets) >= self.cap: 225+ if not FORCE_PASS and len(targets) >= self.cap:
213 break 226 break
214 if not targets: 227 if not targets:
215 return [] 228 return []
@@ -218,7 +231,10 @@ class _ApifySocialEnrich(BaseConnector):
218 {"proxyConfiguration": RESIDENTIAL, 231 {"proxyConfiguration": RESIDENTIAL,
219 "concurrency": RUN_CONCURRENCY, 232 "concurrency": RUN_CONCURRENCY,
220 **self.extra_input()}, 233 **self.extra_input()},
221− sorted(targets)) 234+ sorted(targets),
235+ # passage forcé : tranches plus grosses (bassin entier / 32 runs)
236+ # → délai par run élargi en proportion
237+ timeout_s=7200 if FORCE_PASS else 2400)
222 by_handle = {str(it.get("username") or "").lower(): it 238 by_handle = {str(it.get("username") or "").lower(): it
223 for it in items if it.get("kind") == "profile"} 239 for it in items if it.get("kind") == "profile"}
224 enriched: list[Creator] = [] 240 enriched: list[Creator] = []
@@ -228,8 +244,14 @@ class _ApifySocialEnrich(BaseConnector):
228 if it is None: 244 if it is None:
229 continue 245 continue
230 if not it.get("found"): 246 if not it.get("found"):
231− if str(it.get("error", "")).startswith(("http_5", "shell")): 247+ err = str(it.get("error", ""))
248+ if err.startswith(("http_5", "shell")):
232 self.errors += 1 249 self.errors += 1
250+ elif err.startswith(MISS_PREFIXES):
251+ for cr, acc in pairs: # miss définitif → rotation douce
252+ acc.metrics[self._stamp_key] = now_iso()
253+ acc.metrics[f"{self.source_id}_miss"] = err
254+ enriched.append(cr)
233 continue 255 continue
234 for cr, acc in pairs: 256 for cr, acc in pairs:
235 self.apply(cr, acc, it) 257 self.apply(cr, acc, it)
@@ -252,6 +274,7 @@ class _ApifySocialEnrich(BaseConnector):
252 if it.get(img_key): 274 if it.get(img_key):
253 metrics[img_key] = it[img_key] 275 metrics[img_key] = it[img_key]
254 metrics[self._stamp_key] = now_iso() # tampon d'enrichissement propre 276 metrics[self._stamp_key] = now_iso() # tampon d'enrichissement propre
277+ acc.metrics.pop(f"{self.source_id}_miss", None) # ressuscité
255 acc.metrics.update(metrics) 278 acc.metrics.update(metrics)
256 # avatar/bannière de la FICHE : rafraîchis à chaque passage depuis la 279 # avatar/bannière de la FICHE : rafraîchis à chaque passage depuis la
257 # plateforme principale (les URLs CDN signées expirent — ex. Instagram) ; 280 # plateforme principale (les URLs CDN signées expirent — ex. Instagram) ;
@@ -362,7 +385,9 @@ class ApifyX(_ApifySocialEnrich):
362 "top_hashtags": "top_hashtags", "top_tweet": "top_tweet"} 385 "top_hashtags": "top_hashtags", "top_tweet": "top_tweet"}
363 386
364 def extra_input(self) -> dict: 387 def extra_input(self) -> dict:
365− return {"maxTweets": 20} 388+ # concurrence réduite : l'endpoint syndication de X rend des 429 en
389+ # rafale au-delà de ~4 requêtes simultanées par run (constat 2026-08-21)
390+ return {"maxTweets": 20, "concurrency": 4}
366 391
367 def bio_urls(self, it: dict) -> list[str]: 392 def bio_urls(self, it: dict) -> list[str]:
368 return [it.get("website") or ""] 393 return [it.get("website") or ""]
369 394
Menu

Theme