From 27ce4df61b4db1dc150fab96763da56ebb43b043 Mon Sep 17 00:00:00 2001 From: francoisdotdev Date: Mon, 8 Dec 2025 15:21:45 +0100 Subject: [PATCH] feat: implement IMDB scraper with movie details extraction and CSV output --- README.md | 0 data/internal/imdb_movies.csv | 21 +++ src/main.py | 51 ++++++ src/scrapping/__init__.py | 1 + src/scrapping/scraper.py | 293 ++++++++++++++++++++++++++++++++++ 5 files changed, 366 insertions(+) delete mode 100644 README.md create mode 100644 data/internal/imdb_movies.csv create mode 100644 src/main.py create mode 100644 src/scrapping/__init__.py create mode 100644 src/scrapping/scraper.py diff --git a/README.md b/README.md deleted file mode 100644 index e69de29..0000000 diff --git a/data/internal/imdb_movies.csv b/data/internal/imdb_movies.csv new file mode 100644 index 0000000..60bcfd3 --- /dev/null +++ b/data/internal/imdb_movies.csv @@ -0,0 +1,21 @@ +titre,acteur_1,acteur_2,acteur_3,realisateur,genres,duree,note,url +Jay Kelly,George Clooney,Adam Sandler,Stanley Townsend,Noah Baumbach,"Drames dans le show-business, Duos comiques, Passage à l'âge adulte",2h 12min,"6,6",https://www.imdb.com/fr/title/tt30446847/ +Five Nights at Freddy's 2,Josh Hutcherson,Piper Rubio,Elizabeth Lail,Emma Tammi,"Horreur corporelle, Horreur pour adolescents, Horreur surnaturelle",1h 44min,"5,7",https://www.imdb.com/fr/title/tt30274401/ +Dhurandhar,Naveen Kaushik,Manav Gohil,Danish Pandor,Aditya Dhar,"Hindi, Action épique, Drame épique",3h 32min,"8,1",https://www.imdb.com/fr/title/tt33014583/ +Zootopie 2,Ginnifer Goodwin,Jason Bateman,Ke Huy Quan,Jared Bush,"Animation numérique, Aventure animalière, Aventure urbaine",1h 48min,"7,7",https://www.imdb.com/fr/title/tt26443597/ +"Joyeux Noël, Maman!",Michelle Pfeiffer,Denis Leary,Felicity Jones,Michael Showalter,"Comédie de Noël, Comédie, Fêtes de fin d'année",1h 47min,"5,3",https://www.imdb.com/fr/title/tt31998881/ +Une bataille après l'autre,Leonardo DiCaprio,Sean Penn,Benicio Del Toro,Paul Thomas Anderson,"Action épique, Comédie noire, Drame épique",2h 41min,"7,9",https://www.imdb.com/fr/title/tt30144839/ +Frankenstein,Oscar Isaac,Jacob Elordi,Christoph Waltz,Guillermo del Toro,"Dark Fantasy, Drame psychologique, Horreur corporelle",2h 29min,"7,5",https://www.imdb.com/fr/title/tt1312221/ +Train Dreams,Joel Edgerton,Clifton Collins Jr.,Felicity Jones,Clint Bentley,"Drame d’époque, Drame, Retour en haut de la page",1h 42min,"7,6",https://www.imdb.com/fr/title/tt29768334/ +My Secret Santa,Alexandra Breckenridge,Ryan Eggold,Tia Mowry,Mike Rohl,Retour en haut de la page,1h 30min,"5,8",https://www.imdb.com/fr/title/tt35219851/ +Bugonia,Emma Stone,Jesse Plemons,Aidan Delbis,Yorgos Lanthimos,"Comédie noire, Invasion extraterrestre, Satire",1h 58min,"7,5",https://www.imdb.com/fr/title/tt12300742/ +Mission: Impossible - The Final Reckoning,Tom Cruise,Hayley Atwell,Ving Rhames,Christopher McQuarrie,"Action épique, Aventure épique, Espionnage",2h 49min,"7,2",https://www.imdb.com/fr/title/tt9603208/ +Predator: Badlands,Elle Fanning,Dimitrius Schuster-Koloamatangi,Ravi Narayan,Dan Trachtenberg,"Invasion extraterrestre, Action, Aventure",1h 47min,"7,4",https://www.imdb.com/fr/title/tt31227572/ +Wake Up Dead Man: A Knives Out Mystery,Daniel Craig,Josh O'Connor,Glenn Close,Rian Johnson,"Comédie noire, Énigme policière, Comédie",2h 24min,"7,9",https://www.imdb.com/fr/title/tt14364480/ +Nuremberg,Michael Shannon,Colin Hanks,Russell Crowe,James Vanderbilt,"Docudrame, Drame d’époque, Drame épique",2h 28min,"7,6",https://www.imdb.com/fr/title/tt29567915/ +"Maman, j'ai raté l'avion !",Macaulay Culkin,Joe Pesci,Daniel Stern,Chris Columbus,"Burlesque, Comédie à concept, Comédie de Noël",1h 43min,"7,7",https://www.imdb.com/fr/title/tt0099785/ +Avatar: De feu et de cendres,Kate Winslet,Zoe Saldaña,Stephen Lang,James Cameron,"Action épique, Aventure épique, Drame épique",3h 15min,N/A,https://www.imdb.com/fr/title/tt1757678/ +Vive le vol d'hiver,Olivia Holt,Connor Swindells,Lucy Punch,Michael Fimognari,"Casses et Braquage, Comédie noire, Comédie romantique",1h 36min,"5,7",https://www.imdb.com/fr/title/tt24852126/ +Hamnet,Jessie Buckley,Paul Mescal,Zac Wishart,Chloé Zhao,"Drame d’époque, Drame épique, Tragédie",2h 5min,"8,2",https://www.imdb.com/fr/title/tt14905854/ +Wicked: Partie II,Cynthia Erivo,Ariana Grande,Jeff Goldblum,Jon M. Chu,"Comédie musicale pop, Conte de fées, Comédie musicale",2h 17min,"7,0",https://www.imdb.com/fr/title/tt19847976/ +Tere Ishk Mein,Dhanush,Kriti Sanon,Jaya Bhattacharya,Aanand L. Rai,"Hindi, Action, Comédie musicale",2h 47min,"7,9",https://www.imdb.com/fr/title/tt28142095/ diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..b8c5fdf --- /dev/null +++ b/src/main.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +""" +Script pour lancer le scraper IMDB. + +Usage: + python run_scraper.py [nombre_de_films] + +Exemple: + python run_scraper.py 200 +""" +import asyncio +import sys +import os + +# Ajouter le répertoire src au path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from scrapping.imdb_scraper import IMDBScraper + + +async def main(): + """Point d'entrée principal.""" + # Déterminer le nombre de films à scraper + target_count = 200 + if len(sys.argv) > 1: + try: + target_count = int(sys.argv[1]) + print(f"Scraping de {target_count} films...") + except ValueError: + print("Erreur: Le paramètre doit être un nombre entier") + sys.exit(1) + else: + print(f"Scraping de {target_count} films par défaut...") + + # Créer le scraper et lancer le scraping + scraper = IMDBScraper(target_count=target_count) + await scraper.scrape_movies() + + # Sauvegarder les résultats + output_file = "data/imdb_movies.csv" + scraper.save_to_csv(output_file) + + print(f"\n{'='*60}") + print(f"Scraping terminé!") + print(f"Nombre de films récupérés: {len(scraper.movies_data)}") + print(f"Fichier de sortie: {output_file}") + print(f"{'='*60}") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/src/scrapping/__init__.py b/src/scrapping/__init__.py new file mode 100644 index 0000000..a527468 --- /dev/null +++ b/src/scrapping/__init__.py @@ -0,0 +1 @@ +"""Module de scraping IMDB.""" diff --git a/src/scrapping/scraper.py b/src/scrapping/scraper.py new file mode 100644 index 0000000..ad734e4 --- /dev/null +++ b/src/scrapping/scraper.py @@ -0,0 +1,293 @@ +""" +Scraper pour IMDB - Récupère les informations des derniers films. +""" +import asyncio +import csv +import logging +from typing import List, Dict, Optional +from datetime import datetime +from playwright.async_api import async_playwright, Page, Browser +import time + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +class IMDBScraper: + """Scraper pour récupérer les informations des films IMDB.""" + + def __init__(self, target_count: int = 200): + """ + Initialize the IMDB scraper. + + Args: + target_count: Nombre de films à récupérer (défaut: 200) + """ + self.target_count = target_count + self.movies_data: List[Dict] = [] + + async def scrape_movie_details(self, page: Page, movie_url: str) -> Optional[Dict]: + """ + Récupère les détails d'un film spécifique. + + Args: + page: Page Playwright + movie_url: URL de la page du film + + Returns: + Dictionnaire contenant les informations du film ou None + """ + try: + await page.goto(movie_url, wait_until="domcontentloaded", timeout=45000) + await page.wait_for_timeout(2000) + + movie_data = {} + + # Titre du film + try: + title_element = await page.query_selector('h1[data-testid="hero__primary-text"]') + if title_element: + movie_data['titre'] = await title_element.inner_text() + else: + title_element = await page.query_selector('h1') + movie_data['titre'] = await title_element.inner_text() if title_element else "N/A" + except Exception as e: + logger.warning(f"Titre non trouvé: {e}") + movie_data['titre'] = "N/A" + + # Note du film + try: + rating_element = await page.query_selector('span[class*="AggregateRatingButton__RatingScore"]') + if not rating_element: + rating_element = await page.query_selector('div[data-testid="hero-rating-bar__aggregate-rating__score"] span') + if rating_element: + rating_text = await rating_element.inner_text() + movie_data['note'] = rating_text.strip().split('/')[0] + else: + movie_data['note'] = "N/A" + except Exception as e: + logger.warning(f"Note non trouvée: {e}") + movie_data['note'] = "N/A" + + # Durée du film + try: + duration_element = await page.query_selector('li[data-testid="title-techspec_runtime"]') + if not duration_element: + duration_element = await page.query_selector('ul[class*="TitleBlockMetaData"] li:has-text("min")') + if duration_element: + duration_text = await duration_element.inner_text() + import re + match = re.search(r'(\d+h\s*\d+min|\d+min|\d+h)', duration_text) + if match: + movie_data['duree'] = match.group(1).strip() + else: + movie_data['duree'] = duration_text.strip() + else: + movie_data['duree'] = "N/A" + except Exception as e: + logger.warning(f"Durée non trouvée: {e}") + movie_data['duree'] = "N/A" + + # Genres + try: + genre_elements = await page.query_selector_all('a[class*="GenresAndPlot__GenreChip"]') + if not genre_elements: + genre_elements = await page.query_selector_all('span[class*="ipc-chip__text"]') + if not genre_elements: + genre_div = await page.query_selector('div[data-testid="genres"]') + if genre_div: + genre_elements = await genre_div.query_selector_all('a, span') + + genres = [] + if genre_elements: + for genre_elem in genre_elements[:3]: + genre_text = await genre_elem.inner_text() + if genre_text and len(genre_text) > 1: + genres.append(genre_text.strip()) + movie_data['genres'] = ", ".join(genres) if genres else "N/A" + else: + movie_data['genres'] = "N/A" + except Exception as e: + logger.warning(f"Genres non trouvés: {e}") + movie_data['genres'] = "N/A" + + # Réalisateur + try: + director_element = None + # Essayer plusieurs sélecteurs (français et anglais) + director_section = await page.query_selector('li[data-testid="title-pc-principal-credit"]:has-text("Réalisation")') + if not director_section: + director_section = await page.query_selector('li[data-testid="title-pc-principal-credit"]:has-text("Director")') + if not director_section: + director_section = await page.query_selector('li[data-testid="title-pc-principal-credit"]:has-text("Réalisateur")') + + if director_section: + director_links = await director_section.query_selector_all('a') + if director_links: + director_element = director_links[0] + + if director_element: + movie_data['realisateur'] = (await director_element.inner_text()).strip() + else: + movie_data['realisateur'] = "N/A" + except Exception as e: + logger.warning(f"Réalisateur non trouvé: {e}") + movie_data['realisateur'] = "N/A" + + # Top 3 acteurs + try: + # Chercher dans la section cast + cast_section = await page.query_selector('section[data-testid="title-cast"]') + actors = [] + + if cast_section: + actor_links = await cast_section.query_selector_all('a[data-testid="title-cast-item__actor"]') + for actor_link in actor_links[:3]: + actor_name = await actor_link.inner_text() + actors.append(actor_name.strip()) + + # Si pas assez d'acteurs, chercher dans une autre section + if len(actors) < 3: + alternative_actors = await page.query_selector_all('a[href*="/name/"]') + for actor_elem in alternative_actors: + if len(actors) >= 3: + break + actor_text = await actor_elem.inner_text() + if actor_text and actor_text not in actors and len(actor_text) > 2: + actors.append(actor_text.strip()) + + movie_data['acteur_1'] = actors[0] if len(actors) > 0 else "N/A" + movie_data['acteur_2'] = actors[1] if len(actors) > 1 else "N/A" + movie_data['acteur_3'] = actors[2] if len(actors) > 2 else "N/A" + + except Exception as e: + logger.warning(f"Acteurs non trouvés: {e}") + movie_data['acteur_1'] = "N/A" + movie_data['acteur_2'] = "N/A" + movie_data['acteur_3'] = "N/A" + + movie_data['url'] = movie_url + + logger.info(f"Film récupéré: {movie_data.get('titre', 'Unknown')}") + return movie_data + + except Exception as e: + logger.error(f"Erreur lors du scraping de {movie_url}: {e}") + return None + + async def get_movie_urls(self, page: Page) -> List[str]: + """ + Récupère les URLs des films récents depuis IMDB. + + Args: + page: Page Playwright + + Returns: + Liste d'URLs de films + """ + movie_urls = [] + + try: + # Aller sur la page des films les plus populaires + logger.info("Récupération des URLs de films depuis IMDB...") + await page.goto("https://www.imdb.com/chart/moviemeter/", wait_until="domcontentloaded", timeout=45000) + await page.wait_for_timeout(3000) + + # Récupérer les liens des films + movie_links = await page.query_selector_all('a[class*="ipc-title-link-wrapper"]') + + for link in movie_links[:self.target_count]: + href = await link.get_attribute('href') + if href and '/title/' in href: + full_url = f"https://www.imdb.com{href}" if href.startswith('/') else href + # Nettoyer l'URL + full_url = full_url.split('?')[0] + if full_url not in movie_urls: + movie_urls.append(full_url) + + logger.info(f"{len(movie_urls)} URLs de films trouvées") + + except Exception as e: + logger.error(f"Erreur lors de la récupération des URLs: {e}") + + return movie_urls[:self.target_count] + + async def scrape_movies(self): + """Lance le scraping des films IMDB.""" + async with async_playwright() as p: + logger.info("Lancement du navigateur...") + browser = await p.chromium.launch(headless=True) + context = await browser.new_context( + user_agent='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36', + locale='fr-FR' + ) + page = await context.new_page() + + try: + # Récupérer les URLs des films + movie_urls = await self.get_movie_urls(page) + + logger.info(f"Début du scraping de {len(movie_urls)} films...") + + # Scraper chaque film + for idx, url in enumerate(movie_urls, 1): + logger.info(f"[{idx}/{len(movie_urls)}] Scraping: {url}") + + movie_data = await self.scrape_movie_details(page, url) + if movie_data: + self.movies_data.append(movie_data) + + # Pause pour éviter de surcharger le serveur + await page.wait_for_timeout(2000) + + if idx >= self.target_count: + break + + logger.info(f"Scraping terminé: {len(self.movies_data)} films récupérés") + + except Exception as e: + logger.error(f"Erreur générale: {e}") + finally: + await browser.close() + + def save_to_csv(self, filename: str = "imdb_movies.csv"): + """ + Sauvegarde les données dans un fichier CSV. + + Args: + filename: Nom du fichier CSV (défaut: imdb_movies.csv) + """ + if not self.movies_data: + logger.warning("Aucune donnée à sauvegarder") + return + + fieldnames = [ + 'titre', 'acteur_1', 'acteur_2', 'acteur_3', + 'realisateur', 'genres', 'duree', 'note', 'url' + ] + + try: + with open(filename, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.DictWriter(csvfile, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(self.movies_data) + + logger.info(f"Données sauvegardées dans {filename}") + logger.info(f"Nombre total de films: {len(self.movies_data)}") + + except Exception as e: + logger.error(f"Erreur lors de la sauvegarde CSV: {e}") + + +async def main(): + """Point d'entrée principal du script.""" + scraper = IMDBScraper(target_count=200) + await scraper.scrape_movies() + scraper.save_to_csv("data/imdb_movies.csv") + + +if __name__ == "__main__": + asyncio.run(main())