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:
+70
-18
@@ -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()
|
||||||
@@ -186,43 +204,51 @@ def scrape_catalog(config: dict, cache_dir: Path,
|
|||||||
href = a['href']
|
href = a['href']
|
||||||
if '/evenements/' in href and href.rstrip('/') != f'{site_base}/evenements':
|
if '/evenements/' in href and href.rstrip('/') != f'{site_base}/evenements':
|
||||||
full_url = href if href.startswith('http') else site_base + href
|
full_url = href if href.startswith('http') else site_base + href
|
||||||
h3 = a.find('h3')
|
h3 = a.find('h3')
|
||||||
cat_tag = a.find('p')
|
cat_tag = a.find('p')
|
||||||
title = h3.get_text(strip=True) if h3 else a.get_text(strip=True)
|
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 ''
|
category = cat_tag.get_text(strip=True) if cat_tag else ''
|
||||||
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)
|
||||||
|
|||||||
Reference in New Issue
Block a user