Spaces:
Running
Running
| import os | |
| import time | |
| import gradio as gr | |
| from selenium import webdriver | |
| from selenium.webdriver.chrome.service import Service | |
| from selenium.webdriver.chrome.options import Options | |
| from selenium.webdriver.common.by import By | |
| from selenium.webdriver.support.ui import WebDriverWait | |
| from selenium.webdriver.support import expected_conditions as EC | |
| from selenium.webdriver.common.action_chains import ActionChains | |
| from webdriver_manager.chrome import ChromeDriverManager | |
| def wait_for_download_to_complete(download_directory, timeout=15): | |
| """Vérifie que le téléchargement est bien terminé.""" | |
| end_time = time.time() + timeout | |
| while time.time() < end_time: | |
| files = os.listdir(download_directory) | |
| if any(f.endswith('.crdownload') or f.endswith('.tmp') for f in files): | |
| time.sleep(0.2) | |
| else: | |
| return True | |
| return False | |
| def lancer_telechargement(nom_produit, mots_cles, nomenclature_str, progress=gr.Progress()): | |
| """Fonction principale appelée par le bouton Gradio.""" | |
| download_dir = os.path.join(os.getcwd(), "FDES_Telecharges") | |
| if not os.path.exists(download_dir): | |
| os.makedirs(download_dir) | |
| chrome_options = Options() | |
| chrome_options.page_load_strategy = 'eager' | |
| chrome_options.add_argument("--headless=new") # Mode invisible | |
| chrome_options.add_argument("--disable-gpu") | |
| chrome_options.add_argument("--window-size=1920,1080") | |
| chrome_options.add_experimental_option( | |
| 'prefs', { | |
| "download.default_directory": download_dir, | |
| "download.prompt_for_download": False, | |
| "download.directory_upgrade": True, | |
| "plugins.always_open_pdf_externally": True, | |
| "profile.default_content_settings.popups": 0, | |
| "profile.managed_default_content_settings.images": 2 | |
| } | |
| ) | |
| progress(0, desc="Démarrage du navigateur...") | |
| driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options) | |
| # Autoriser les téléchargements en mode Headless | |
| driver.command_executor._commands["send_command"] = ("POST", '/session/$sessionId/chromium/send_command') | |
| params = {'cmd': 'Page.setDownloadBehavior', 'params': {'behavior': 'allow', 'downloadPath': download_dir}} | |
| driver.execute("send_command", params) | |
| wait = WebDriverWait(driver, 10) | |
| actions = ActionChains(driver) | |
| def navigate_angular_node(keyword, expand=True): | |
| progress(0, desc=f"Recherche de '{keyword}'...") | |
| target_node = None | |
| # Astuce : Gérer les exclusions via une syntaxe spéciale (ex: "Chauffage |NOT| Ventilation") | |
| exclude_keyword = None | |
| if "|NOT|" in keyword: | |
| keyword, exclude_keyword = keyword.split("|NOT|") | |
| keyword = keyword.strip() | |
| exclude_keyword = exclude_keyword.strip() | |
| for _ in range(20): | |
| wrappers = driver.find_elements(By.CSS_SELECTOR, ".node-wrapper") | |
| for w in wrappers: | |
| text_lower = w.text.lower() | |
| # Si le mot-clé est trouvé | |
| if keyword.lower() in text_lower: | |
| # On vérifie qu'il ne contient PAS le mot exclu (s'il y en a un) | |
| if exclude_keyword and exclude_keyword.lower() in text_lower: | |
| continue | |
| # On vérifie que le noeud est bien affiché à l'écran | |
| if w.is_displayed(): | |
| target_node = w | |
| break | |
| if target_node: | |
| break | |
| time.sleep(0.2) | |
| if not target_node: | |
| raise Exception(f"❌ Dossier contenant '{keyword}' introuvable à l'écran.") | |
| driver.execute_script("arguments[0].scrollIntoView({behavior: 'auto', block: 'center'});", target_node) | |
| if expand: | |
| try: | |
| expander = target_node.find_element(By.CSS_SELECTOR, ".toggle-children-wrapper-collapsed") | |
| driver.execute_script("arguments[0].click();", expander) | |
| time.sleep(0.5) # Laisser le temps au dossier de s'ouvrir | |
| except: | |
| pass # Déjà ouvert ou pas de flèche | |
| else: | |
| content = target_node.find_element(By.CSS_SELECTOR, ".node-content-wrapper") | |
| driver.execute_script("arguments[0].click();", content) | |
| try: | |
| progress(0.1, desc="Ouverture du site INIES...") | |
| driver.get("https://base-inies.fr/consultation/recherche") | |
| wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "button.nomenclature-select-btn"))) | |
| # Accepter les cookies | |
| try: | |
| cookie_btn = driver.find_element(By.XPATH, "//*[contains(text(), 'compris') or contains(text(), 'Accepter')]") | |
| driver.execute_script("arguments[0].click();", cookie_btn) | |
| except: | |
| pass | |
| # 1. Remplir le champ "Nom de produit" | |
| if nom_produit: | |
| input_nom = driver.find_element(By.CSS_SELECTOR, 'input[formcontrolname="nomProduit"]') | |
| input_nom.send_keys(nom_produit) | |
| # 2. Remplir le champ "Mots clés" | |
| if mots_cles: | |
| input_mots = driver.find_element(By.CSS_SELECTOR, 'input[formcontrolname="motsCles"]') | |
| input_mots.send_keys(mots_cles) | |
| # 3. Gérer la nomenclature (si renseignée) | |
| if nomenclature_str: | |
| btn_nomenclature = driver.find_element(By.CSS_SELECTOR, "button.nomenclature-select-btn") | |
| driver.execute_script("arguments[0].click();", btn_nomenclature) | |
| wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".node-wrapper"))) | |
| # Découper le chemin fourni par l'utilisateur (séparé par des ;) | |
| etapes = [e.strip() for e in nomenclature_str.split(';') if e.strip()] | |
| for i, etape in enumerate(etapes): | |
| est_dernier = (i == len(etapes) - 1) | |
| # Déplier tous les dossiers sauf le dernier qu'on doit sélectionner | |
| navigate_angular_node(etape, expand=not est_dernier) | |
| btn_ok = wait.until(EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'app-modal-footer')]//button[contains(text(), 'Ok')]"))) | |
| driver.execute_script("arguments[0].click();", btn_ok) | |
| time.sleep(0.5) | |
| # 4. Lancer la recherche globale | |
| progress(0.2, desc="Lancement de la recherche...") | |
| btn_rechercher = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(., 'Rechercher')]"))) | |
| driver.execute_script("arguments[0].click();", btn_rechercher) | |
| # 5. Attente des résultats | |
| try: | |
| wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".result-panel .tree-node-leaf"))) | |
| time.sleep(1) | |
| except: | |
| return "Aucun produit trouvé avec ces critères." | |
| leaf_nodes = driver.find_elements(By.CSS_SELECTOR, ".result-panel .tree-node-leaf .node-content-wrapper") | |
| num_products = len(leaf_nodes) | |
| if num_products == 0: | |
| return "Aucun produit trouvé avec ces critères." | |
| # 6. Boucle de téléchargement | |
| telecharges = 0 | |
| for i in range(num_products): | |
| progress(0.2 + (0.8 * (i / num_products)), desc=f"Téléchargement: {i + 1}/{num_products} produits...") | |
| wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".result-panel .tree-node-leaf"))) | |
| leaf_nodes = driver.find_elements(By.CSS_SELECTOR, ".result-panel .tree-node-leaf .node-content-wrapper") | |
| if i >= len(leaf_nodes): | |
| break | |
| product_to_click = leaf_nodes[i] | |
| try: | |
| driver.execute_script("arguments[0].scrollIntoView({behavior: 'auto', block: 'center'});", product_to_click) | |
| actions.move_to_element(product_to_click).click().perform() | |
| except: | |
| continue | |
| # Onglet Documents et téléchargement | |
| try: | |
| wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, ".produit-details"))) | |
| tab_docs = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[contains(@class, 'tablinks') and contains(., 'Documents')]"))) | |
| driver.execute_script("arguments[0].click();", tab_docs) | |
| xpath_pdf = "//td[contains(., 'FDES/PEP')]/following-sibling::td/a" | |
| pdf_link = wait.until(EC.presence_of_element_located((By.XPATH, xpath_pdf))) | |
| driver.execute_script("arguments[0].scrollIntoView({behavior: 'auto', block: 'center'});", pdf_link) | |
| driver.execute_script("arguments[0].click();", pdf_link) | |
| # Attendre la fin du fichier .crdownload | |
| wait_for_download_to_complete(download_dir) | |
| telecharges += 1 | |
| except: | |
| pass # Pas de FDES pour ce produit | |
| driver.back() | |
| wait.until(EC.visibility_of_element_located((By.CSS_SELECTOR, ".result-panel .tree-node-leaf"))) | |
| return f"✅ Opération terminée avec succès !\n\n{telecharges} fichier(s) FDES/PEP téléchargé(s) sur un total de {num_products} produit(s) trouvé(s).\nDossier : {download_dir}" | |
| except Exception as e: | |
| return f"❌ Une erreur est survenue : {str(e)}" | |
| finally: | |
| driver.quit() | |
| # --------------------------------------------------------- | |
| # INTERFACE GRAPHIQUE AVEC GRADIO | |
| # --------------------------------------------------------- | |
| with gr.Blocks(title="Extracteur INIES", theme=gr.themes.Soft()) as interface: | |
| gr.Markdown("# 🏢 Extracteur Automatique - Base INIES") | |
| gr.Markdown("Renseignez un ou plusieurs champs. Le robot s'occupera de piloter le site en arrière-plan pour récupérer les PDFs (FDES/PEP) correspondants.") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| nom_produit = gr.Textbox(label="Nom de produit", placeholder="Ex: AQUACIAT POWER") | |
| mots_cles = gr.Textbox(label="Mot(s) clé(s)", placeholder="Ex: Pompe à chaleur") | |
| gr.Markdown("**Famille de produit** (Nomenclature)\nSéparez chaque étape par un point-virgule (`;`).\n*Astuce : Utilisez `|NOT|` pour exclure un terme si plusieurs dossiers se ressemblent.*") | |
| nomenclature = gr.Textbox( | |
| label="", | |
| placeholder="Ex: électriques; génie climatique |NOT| électriques; Chauffage |NOT| Ventilation; Pompes à chaleur collectives; Air/eau", | |
| value="électriques; génie climatique |NOT| électriques; Chauffage |NOT| Ventilation; Pompes à chaleur collectives; Air/eau", | |
| lines=2 | |
| ) | |
| btn_lancer = gr.Button("🚀 Lancer la recherche et le téléchargement", variant="primary") | |
| with gr.Column(scale=1): | |
| resultat = gr.Textbox(label="Statut de l'opération", lines=8, interactive=False) | |
| # Lier le bouton à la fonction Selenium | |
| btn_lancer.click( | |
| fn=lancer_telechargement, | |
| inputs=[nom_produit, mots_cles, nomenclature], | |
| outputs=resultat | |
| ) | |
| if __name__ == "__main__": | |
| # Lancement de l'interface | |
| print("Démarrage de l'interface Gradio...") | |
| interface.launch() |