Scraping ciblé : uniquement les événements site dont les dates correspondent aux concerts PDF

Au lieu de scraper toutes les pages du site, on :
1. Extrait les dates de concerts/représentations depuis le PDF
2. Scrape le listing du site (1 requête)
3. Pour chaque page événement, extrait ses dates et vérifie si au moins
   une date correspond à celles du PDF
4. Ignore silencieusement les événements sans correspondance de date

Avantages :
- Beaucoup moins de requêtes HTTP (seuls les événements pertinents)
- Correspondances plus fiables (validées par les dates)
- _parse_french_dates_from_page : convertit les dates texte en objets date
  pour la comparaison avec les dates PDF

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sylvain
2026-03-08 16:35:30 +01:00
parent da14137bd9
commit 71bdc9f6ea
+66 -14
View File
@@ -160,7 +160,15 @@ def extract_events_from_pdf(pdf_path: Path) -> list:
# ── Scraping site web ───────────────────────────────────────────────────────── # ── Scraping site web ─────────────────────────────────────────────────────────
def scrape_catalog(config: dict, cache_dir: Path, def scrape_catalog(config: dict, cache_dir: Path,
log: Callable = None, force: bool = False) -> dict: log: Callable = None, force: bool = False,
target_dates: set = None) -> dict:
"""
Scrape les événements du site en se limitant aux dates de concerts du PDF.
target_dates : set de date objects issus du PDF (concerts/représentations).
Seules les pages site dont au moins une date correspond sont conservées.
Si target_dates est None ou vide, toutes les pages sont récupérées (mode exhaustif).
"""
cache_file = cache_dir / "website_catalog.json" cache_file = cache_dir / "website_catalog.json"
cache_dir.mkdir(parents=True, exist_ok=True) cache_dir.mkdir(parents=True, exist_ok=True)
@@ -174,8 +182,18 @@ def scrape_catalog(config: dict, cache_dir: Path,
calendar_url = config['site']['calendar_url'] calendar_url = config['site']['calendar_url']
site_base = config['site']['base_url'] site_base = config['site']['base_url']
# Année de référence : la plus représentée dans les dates PDF
if target_dates:
years = [d.year for d in target_dates]
reference_year = max(set(years), key=years.count)
else:
reference_year = datetime.now().year
if log: if log:
log("Scraping du site web de l'opéra...") if target_dates:
log(f"Scraping ciblé : {len(target_dates)} dates de concerts à croiser avec le site...")
else:
log("Scraping du site web de l'opéra (mode exhaustif)...")
resp = requests.get(calendar_url, headers=headers, timeout=30) resp = requests.get(calendar_url, headers=headers, timeout=30)
resp.raise_for_status() resp.raise_for_status()
@@ -193,36 +211,44 @@ def scrape_catalog(config: dict, cache_dir: Path,
if title and len(title) > 3: if title and len(title) > 3:
event_links[title] = {'url': full_url, 'category': category} event_links[title] = {'url': full_url, 'category': category}
catalog = {}
total = len(event_links) total = len(event_links)
if log: if log:
log(f"{total} événements trouvés sur le site, récupération des descriptions...") log(f"{total} événements trouvés sur le site, filtrage par dates...")
for i, (title, info) in enumerate(event_links.items()): catalog = {}
if log and i % 20 == 0: fetched = 0
log(f"Descriptions : {i}/{total}") skipped = 0
for title, info in event_links.items():
try: try:
r = requests.get(info['url'], headers=headers, timeout=20) r = requests.get(info['url'], headers=headers, timeout=20)
r.raise_for_status() r.raise_for_status()
page_soup = BeautifulSoup(r.text, 'html.parser') page_soup = BeautifulSoup(r.text, 'html.parser')
page_dates = _parse_french_dates_from_page(page_soup, reference_year)
# Filtrage par correspondance de dates
if target_dates and not (page_dates & target_dates):
skipped += 1
time_module.sleep(0.1)
continue
catalog[title] = { catalog[title] = {
'url': info['url'], 'url': info['url'],
'description': _extract_description(page_soup), 'description': _extract_description(page_soup),
'category': info['category'], 'category': info['category'],
'dates': _extract_dates_from_page(page_soup), 'dates': _extract_dates_from_page(page_soup),
} }
fetched += 1
time_module.sleep(0.2) time_module.sleep(0.2)
except Exception: except Exception:
catalog[title] = { pass # On ignore silencieusement les pages inaccessibles
'url': info['url'], 'description': '', 'category': info['category'],
'dates': [],
}
with open(cache_file, 'w') as f: with open(cache_file, 'w') as f:
json.dump(catalog, f, ensure_ascii=False, indent=2) json.dump(catalog, f, ensure_ascii=False, indent=2)
if log: if log:
log(f"Catalogue mis en cache : {len(catalog)} événements") log(f"Site : {fetched} événements retenus, {skipped} ignorés (dates hors planning)")
return catalog return catalog
@@ -259,6 +285,29 @@ def _extract_dates_from_page(soup: BeautifulSoup) -> list[str]:
return unique[:15] return unique[:15]
def _parse_french_dates_from_page(soup: BeautifulSoup, reference_year: int) -> set:
"""
Extrait et convertit les dates de représentation en objets date.
Utilisé pour filtrer les pages site par correspondance avec les dates PDF.
"""
months = '|'.join(FRENCH_MONTHS.keys())
pattern = rf'\b(\d{{1,2}})\s+({months})(?:\s+(\d{{4}}))?\b'
text = soup.get_text(' ')
result = set()
for m in re.finditer(pattern, text, re.IGNORECASE):
day = int(m.group(1))
month_name = m.group(2).lower()
year = int(m.group(3)) if m.group(3) else reference_year
month = FRENCH_MONTHS.get(month_name)
if not month:
continue
try:
result.add(date(year, month, day))
except ValueError:
pass
return result
# ── LLM ─────────────────────────────────────────────────────────────────────── # ── LLM ───────────────────────────────────────────────────────────────────────
def _llm_call(prompt: str, ollama_url: str, model: str) -> str: def _llm_call(prompt: str, ollama_url: str, model: str) -> str:
@@ -550,8 +599,11 @@ def process_pdfs(pdf_paths: list, config: dict, data_dir: Path,
log(f"{len(unique_notes)} notes uniques à analyser...") log(f"{len(unique_notes)} notes uniques à analyser...")
note_to_canonical = cluster_notes_into_series(unique_notes, config, cache_dir, log) note_to_canonical = cluster_notes_into_series(unique_notes, config, cache_dir, log)
# ── 3. Scraping site (pour enrichissement) ───────────────────────────────── # ── 3. Scraping site ciblé sur les dates de concerts du PDF ───────────────
catalog = scrape_catalog(config, cache_dir, log) concert_dates = {e['date'] for e in all_events if is_public_event(e['titre'])}
if log and concert_dates:
log(f"{len(concert_dates)} dates de concerts identifiées dans le PDF")
catalog = scrape_catalog(config, cache_dir, log, target_dates=concert_dates or None)
# ── 4. Correspondance canonique → site (optionnel) ───────────────────────── # ── 4. Correspondance canonique → site (optionnel) ─────────────────────────
canonical_series = set(v for v in note_to_canonical.values() if v) canonical_series = set(v for v in note_to_canonical.values() if v)