From 71bdc9f6eae680d7cc28709ebb4c68eb112873f3 Mon Sep 17 00:00:00 2001 From: sylvain Date: Sun, 8 Mar 2026 16:35:30 +0100 Subject: [PATCH] =?UTF-8?q?Scraping=20cibl=C3=A9=20:=20uniquement=20les=20?= =?UTF-8?q?=C3=A9v=C3=A9nements=20site=20dont=20les=20dates=20corresponden?= =?UTF-8?q?t=20aux=20concerts=20PDF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- webapp/core.py | 88 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 18 deletions(-) diff --git a/webapp/core.py b/webapp/core.py index b977c0a..6a9499e 100644 --- a/webapp/core.py +++ b/webapp/core.py @@ -160,7 +160,15 @@ def extract_events_from_pdf(pdf_path: Path) -> list: # ── Scraping site web ───────────────────────────────────────────────────────── 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_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'] 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: - 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.raise_for_status() @@ -186,43 +204,51 @@ def scrape_catalog(config: dict, cache_dir: Path, href = a['href'] if '/evenements/' in href and href.rstrip('/') != f'{site_base}/evenements': full_url = href if href.startswith('http') else site_base + href - h3 = a.find('h3') - cat_tag = a.find('p') - title = h3.get_text(strip=True) if h3 else a.get_text(strip=True) + h3 = a.find('h3') + cat_tag = a.find('p') + title = h3.get_text(strip=True) if h3 else a.get_text(strip=True) category = cat_tag.get_text(strip=True) if cat_tag else '' if title and len(title) > 3: event_links[title] = {'url': full_url, 'category': category} - catalog = {} total = len(event_links) 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()): - if log and i % 20 == 0: - log(f"Descriptions : {i}/{total}") + catalog = {} + fetched = 0 + skipped = 0 + + for title, info in event_links.items(): try: r = requests.get(info['url'], headers=headers, timeout=20) 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] = { 'url': info['url'], 'description': _extract_description(page_soup), 'category': info['category'], 'dates': _extract_dates_from_page(page_soup), } + fetched += 1 time_module.sleep(0.2) + except Exception: - catalog[title] = { - 'url': info['url'], 'description': '', 'category': info['category'], - 'dates': [], - } + pass # On ignore silencieusement les pages inaccessibles with open(cache_file, 'w') as f: json.dump(catalog, f, ensure_ascii=False, indent=2) 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 @@ -259,6 +285,29 @@ def _extract_dates_from_page(soup: BeautifulSoup) -> list[str]: 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 ─────────────────────────────────────────────────────────────────────── 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...") note_to_canonical = cluster_notes_into_series(unique_notes, config, cache_dir, log) - # ── 3. Scraping site (pour enrichissement) ───────────────────────────────── - catalog = scrape_catalog(config, cache_dir, log) + # ── 3. Scraping site ciblé sur les dates de concerts du PDF ─────────────── + 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) ───────────────────────── canonical_series = set(v for v in note_to_canonical.values() if v)