Untitled
Anonymous
plain_text
02/10/2026 1:51 AM
9.4 KB
24
Indexable
# core/management/commands/scrape_all.py
from __future__ import annotations
import importlib
import inspect
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import date
from pathlib import Path
from django.core.management.base import BaseCommand
from django.db import close_old_connections, transaction
from core.models import Article, Source
from core.utils.text import strip_nul
class Command(BaseCommand):
help = "Scrape un scraper (--only) ou tous les scrapers de core/scrapers/."
def _resolve_fn(self, module, module_name):
module_key = module_name.lower()
callables = {}
for name in dir(module):
if name.startswith("_"):
continue
fn = getattr(module, name, None)
if callable(fn):
callables[name.lower()] = fn
preferred = [
"scrape",
f"scrape_{module_key}",
module_key,
]
for key in preferred:
if key in callables:
return callables[key]
for key, fn in callables.items():
if key.startswith("scrape"):
return fn
return None
def add_arguments(self, parser):
parser.add_argument("--all", action="store_true", help="Rouler tous les scrapers (*.py) du dossier core/scrapers.")
parser.add_argument("--only", type=str, help="Nom du scraper (ex: CBC, lapresse, Arsenal).")
parser.add_argument("--days", type=int, default=14, help="Passé seulement si le scraper accepte days.")
parser.add_argument("--threads", type=int, default=5, help="Nombre de scrapers en parallèle.")
def _run_one_scraper(self, module_name: str, days: int, existing_urls_snapshot: set[str]):
"""
Exécute 1 scraper (import + run + upsert DB).
Note: existing_urls_snapshot est un snapshot (read-only) utile aux scrapers pour filtrer.
"""
# Important en multi-threads Django:
close_old_connections()
started_all = time.perf_counter()
module = importlib.import_module(f"core.scrapers.{module_name}")
fn = self._resolve_fn(module, module_name)
if not callable(fn):
return (module_name, 0, 0, 0, f"skip (fonction introuvable)")
sig = inspect.signature(fn)
params = sig.parameters
kwargs = {}
if "existing_urls" in params:
kwargs["existing_urls"] = existing_urls_snapshot
if "days" in params:
kwargs["days"] = days
if "cursor" in params:
kwargs["cursor"] = None
started_run = time.perf_counter()
items = fn(**kwargs) or []
run_elapsed = time.perf_counter() - started_run
created = updated = skipped = 0
with transaction.atomic():
for it in items:
source_name = strip_nul(it.get("source") or it.get("Media") or "")
if not source_name:
source_name = "Source inconnue"
source, _ = Source.objects.get_or_create(nom=source_name)
d = None
raw_date = strip_nul(it.get("date_publication") or it.get("Date") or "")
if raw_date:
try:
d = date.fromisoformat(raw_date)
except ValueError:
pass
url = strip_nul(it.get("url") or it.get("Lien") or "")
if not url:
skipped += 1
continue
_, was_created = Article.objects.update_or_create(
url=url,
defaults={
"source": source,
"titre": strip_nul(it.get("titre") or it.get("Titre") or ""),
"texte": strip_nul(it.get("texte") or it.get("Article") or ""),
"date_publication": d,
},
)
if was_created:
created += 1
else:
updated += 1
total_elapsed = time.perf_counter() - started_all
msg = f"items={len(items)} run={run_elapsed:.2f}s total={total_elapsed:.2f}s"
return (module_name, created, updated, skipped, msg)
def handle(self, *args, **options):
run_all = options["all"]
only = options["only"]
days = options["days"]
threads = int(options["threads"] or 5)
if not run_all and not only:
run_all = True
self.stdout.write("Aucun argument fourni: mode --all activé par défaut.")
# Snapshot initial (anti-doublon)
existing_urls = set(Article.objects.values_list("url", flat=True))
self.stdout.write(f"Existing URLs in DB: {len(existing_urls)}")
scrapers_dir = Path("core/scrapers")
files = sorted(scrapers_dir.glob("*.py"))
files = [p for p in files if p.name != "__init__.py" and not p.name.startswith("_")]
if only:
wanted = only.lower().replace(".py", "")
files = [p for p in files if p.stem.lower() == wanted]
if not files:
self.stdout.write(self.style.ERROR(f"Scraper introuvable: {only}"))
return
if not files:
self.stdout.write(self.style.WARNING("Aucun scraper trouvé dans core/scrapers."))
return
module_names = [p.stem for p in files]
self.stdout.write(self.style.NOTICE(f"Running {len(module_names)} scrapers with threads={threads}"))
total_created = total_updated = total_skipped = 0
errors = 0
# Important: on passe un snapshot read-only aux scrapers
existing_urls_snapshot = frozenset(existing_urls)
with ThreadPoolExecutor(max_workers=threads) as ex:
futures = {
ex.submit(self._run_one_scraper, mn, days, existing_urls_snapshot): mn
for mn in module_names
}
for fut in as_completed(futures):
mn = futures[fut]
try:
module_name, created, updated, skipped, msg = fut.result()
total_created += created
total_updated += updated
total_skipped += skipped
self.stdout.write(
self.style.SUCCESS(
f"{module_name} done — created={created}, updated={updated}, skipped={skipped} ({msg})"
)
)
except Exception as exc:
errors += 1
self.stdout.write(self.style.ERROR(f"{mn} ERROR: {exc}"))
# Recompte final “vrai” des URLs en DB (plutôt que de tenter de sync un set cross-threads)
final_known_urls = Article.objects.values("url").count()
self.stdout.write(
self.style.SUCCESS(
f"\nALL done — created={total_created}, updated={total_updated}, skipped={total_skipped}, "
f"errors={errors}, urls_in_db={final_known_urls}"
)
)
Editor is loading...
Leave a Comment