""" Module principal de l'application Web Scraper et Convertisseur Markdown. """ import os import logging import time from typing import Dict, Optional, Union, List from urllib.parse import urlparse import pathlib from dotenv import load_dotenv from src.web2llm.app.scraper.scraper import WebScraper from src.web2llm.app.converter.converter import MarkdownConverter # Configuration du logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # Chargement des variables d'environnement load_dotenv() # Configuration OUTPUT_DIR = os.getenv('OUTPUT_DIR', './output') DEFAULT_FILENAME = os.getenv('DEFAULT_FILENAME', 'scraped_content') class WebToMarkdown: """Classe principale combinant le scraping et la conversion en Markdown.""" def __init__(self, output_dir: str = OUTPUT_DIR): """ Initialise l'outil. Args: output_dir: Répertoire où sauvegarder les fichiers Markdown """ self.scraper = WebScraper() self.converter = MarkdownConverter() self.output_dir = output_dir # S'assurer que le répertoire de sortie existe os.makedirs(self.output_dir, exist_ok=True) def generate_filename(self, url: str, title: Optional[str] = None, extension: str = '.md') -> str: """ Génère un nom de fichier valide à partir de l'URL ou du titre. Args: url: L'URL de la page title: Le titre de la page (optionnel) extension: L'extension du fichier (.md par défaut) Returns: Un nom de fichier valide """ if title: # Nettoyer le titre pour en faire un nom de fichier valide safe_title = "".join([c if c.isalnum() or c in [' ', '-', '_'] else "_" for c in title]) safe_title = safe_title.strip() filename = safe_title[:100] # Limiter la longueur mais permettre des noms plus longs else: # Utiliser l'URL parsed_url = urlparse(url) hostname = parsed_url.netloc path = parsed_url.path.strip('/') filename = f"{hostname}_{path}".replace('/', '_') # Remplacer les espaces par des tirets filename = filename.replace(' ', '-') # S'assurer que le nom se termine par l'extension spécifiée if not filename.endswith(extension): filename += extension return filename def save_raw_html(self, html_content: str, filepath: str) -> bool: """ Sauvegarde le contenu HTML brut dans un fichier. Args: html_content: Le contenu HTML filepath: Chemin où sauvegarder le fichier Returns: True si la sauvegarde a réussi, False sinon """ try: # S'assurer que le répertoire existe os.makedirs(os.path.dirname(os.path.abspath(filepath)), exist_ok=True) with open(filepath, 'w', encoding='utf-8') as f: f.write(html_content) logger.info(f"Contenu HTML sauvegardé avec succès dans {filepath}") return True except Exception as e: logger.error(f"Erreur lors de la sauvegarde du fichier HTML: {str(e)}") return False def process_url(self, url: str, save: bool = False, filename: Optional[str] = None) -> Dict[str, Union[str, None, bool]]: """ Traite une URL: scraping, nettoyage et conversion en Markdown. Args: url: L'URL à traiter save: Si True, sauvegarde le résultat dans un fichier filename: Nom du fichier pour la sauvegarde Returns: Dictionnaire avec les résultats et le statut """ result = { "url": url, "title": None, "markdown": None, "saved": False, "saved_path": None, "success": False, "error": None, "html_saved": False, "html_saved_path": None } try: # Définir l'URL de base pour la conversion des liens relatifs self.converter.base_url = url # Scraper l'URL logger.info(f"Scraping de l'URL: {url}") scraped_data = self.scraper.scrape(url, clean=True, extract_text=True) # Stocker le titre result["title"] = scraped_data["title"] if not scraped_data["clean_html"]: result["error"] = "Impossible de récupérer ou nettoyer le contenu HTML" return result # Conversion en Markdown logger.info("Conversion du HTML en Markdown") markdown_content = self.converter.html_to_markdown( scraped_data["clean_html"], url) # Vérifier si la conversion a produit un résultat significatif if not markdown_content or len(markdown_content) < 100: logger.warning("Conversion en Markdown insuffisante, tentative avec le texte brut") # Si le texte brut est disponible, l'utiliser comme alternative if scraped_data["text_content"]: markdown_content = scraped_data["text_content"] else: # Dernière tentative: extraire le texte à partir du HTML nettoyé from bs4 import BeautifulSoup soup = BeautifulSoup(scraped_data["clean_html"], 'html.parser') markdown_content = soup.get_text(separator='\n\n', strip=True) # Mise à jour du résultat result["markdown"] = markdown_content result["success"] = True # Sauvegarde si demandée if save: # Générer un nom de fichier si non spécifié if not filename: filename = self.generate_filename(url, result["title"]) # S'assurer que l'extension est .md elif not filename.endswith('.md'): filename += '.md' filepath = os.path.join(self.output_dir, filename) # Enregistrer le fichier Markdown saved = self.converter.save_markdown(markdown_content, filepath) result["saved"] = saved result["saved_path"] = filepath if saved else None # Si la conversion en Markdown n'est pas optimale, sauvegarder aussi le HTML if len(markdown_content) < 500 or "<" in markdown_content: html_filename = filename.replace('.md', '.html') html_filepath = os.path.join(self.output_dir, html_filename) html_saved = self.save_raw_html(scraped_data["clean_html"], html_filepath) result["html_saved"] = html_saved result["html_saved_path"] = html_filepath if html_saved else None if html_saved: logger.info(f"Le HTML a été sauvegardé en complément dans {html_filepath}") return result except Exception as e: logger.error(f"Erreur lors du traitement de l'URL {url}: {str(e)}") result["error"] = str(e) # En cas d'erreur, tenter de sauvegarder le HTML brut si disponible if save and scraped_data and "raw_html" in scraped_data and scraped_data["raw_html"]: if not filename: filename = self.generate_filename(url, result["title"], '.html') else: filename = filename.replace('.md', '.html') html_filepath = os.path.join(self.output_dir, filename) html_saved = self.save_raw_html(scraped_data["raw_html"], html_filepath) result["html_saved"] = html_saved result["html_saved_path"] = html_filepath if html_saved else None if html_saved: logger.info(f"Sauvegarde de secours du HTML brut dans {html_filepath}") return result def process_multiple_urls(self, urls: List[str], save: bool = True) -> Dict[str, List[Dict]]: """ Traite plusieurs URLs en parallèle. Args: urls: Liste d'URLs à traiter save: Si True, sauvegarde les résultats Returns: Dictionnaire contenant les résultats pour chaque URL """ results = [] for url in urls: result = self.process_url(url, save=save) results.append(result) return { "total": len(urls), "success": sum(1 for r in results if r["success"]), "results": results } # Fonction pour une utilisation rapide en ligne de commande def process_url(url: str, save: bool = False, filename: Optional[str] = None) -> Dict: """ Fonction utilitaire pour traiter rapidement une URL. Args: url: L'URL à traiter save: Si True, sauvegarde le résultat filename: Nom du fichier pour la sauvegarde Returns: Dictionnaire avec les résultats """ processor = WebToMarkdown() return processor.process_url(url, save, filename) if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description="Scraper et convertisseur Markdown") parser.add_argument("url", help="URL à scraper") parser.add_argument("--save", action="store_true", help="Sauvegarder en fichier Markdown") parser.add_argument("--output", help="Nom du fichier de sortie") parser.add_argument("--dir", help="Répertoire de sortie", default=OUTPUT_DIR) args = parser.parse_args() processor = WebToMarkdown(output_dir=args.dir) result = processor.process_url(args.url, save=args.save, filename=args.output) if result["success"]: print(f"Titre: {result['title']}") print("\nContenu Markdown:") print("-------------------") print(result["markdown"][:500] + "..." if len(result["markdown"]) > 500 else result["markdown"]) if result["saved"]: print(f"\nFichier sauvegardé: {result['saved_path']}") if result["html_saved"]: print(f"\nFichier HTML sauvegardé: {result['html_saved_path']}") else: print(f"Erreur: {result['error']}") if result["html_saved"]: print(f"\nFichier HTML de secours sauvegardé: {result['html_saved_path']}")