From 6f7beb29c47980096714ecbadf4ce677bd6c9339 Mon Sep 17 00:00:00 2001 From: theo Date: Tue, 17 Feb 2026 14:08:24 +0100 Subject: [PATCH] Initial commit --- Dockerfile | 18 +++ Dockerfile~ | 21 ++++ docker-compose.yml | 7 ++ requirements.txt | 5 + webapp.py | 277 +++++++++++++++++++++++++++++++++++++++++++++ webapp.py~ | 277 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 605 insertions(+) create mode 100644 Dockerfile create mode 100644 Dockerfile~ create mode 100644 docker-compose.yml create mode 100644 requirements.txt create mode 100644 webapp.py create mode 100644 webapp.py~ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7b238bd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +# On part d'une version légère de Python +FROM python:3.9-slim + +# On se met dans un dossier de travail +WORKDIR /app + +# On copie les requirements et on installe +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# On copie le code +COPY webapp.py . + +# On expose le port 5000 +EXPOSE 5000 + +# La commande de démarrage +CMD ["python", "webapp.py"] \ No newline at end of file diff --git a/Dockerfile~ b/Dockerfile~ new file mode 100644 index 0000000..d427097 --- /dev/null +++ b/Dockerfile~ @@ -0,0 +1,21 @@ +# On part d'une version légère de Python +FROM python:3.9-slim + +# On se met dans un dossier de travail +WORKDIR /app + +# On installe les dépendances système (nécessaire parfois pour pdfplumber/images) +RUN apt-get update && apt-get install -y libgl1-mesa-glx && rm -rf /var/lib/apt/lists/* + +# On copie les requirements et on installe +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# On copie le code +COPY webapp.py . + +# On expose le port 5000 +EXPOSE 5000 + +# La commande de démarrage +CMD ["python", "webapp.py"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5d25ec8 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,7 @@ +services: + extracteur-pdf: + build: . + container_name: extracteur-pdf + restart: unless-stopped + ports: + - "5050:5000" # J'ai mis 5050 pour éviter un conflit si le 5000 est pris \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..12d2ec2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +flask +pdfplumber +pandas +openpyxl +xlsxwriter diff --git a/webapp.py b/webapp.py new file mode 100644 index 0000000..1a5da79 --- /dev/null +++ b/webapp.py @@ -0,0 +1,277 @@ +import os +import io +import pandas as pd +import pdfplumber +from flask import Flask, request, render_template_string, Response, send_from_directory + +app = Flask(__name__) + +# Dossier temporaire pour stocker le fichier généré à l'intérieur du conteneur +TEMP_DIR = "/app/downloads" +if not os.path.exists(TEMP_DIR): + os.makedirs(TEMP_DIR) + +# --- DESIGN HTML & JAVASCRIPT --- +# Ce bloc est envoyé en premier au navigateur pour préparer l'interface de log +HTML_LAYOUT = """ + + + + + + Extracteur de formations + + + +
+

Analyse du catalogue en cours...

+

Chaque page du fichier est analysée pour extraire les fiches de formation.

+ +
+ +
+ > Initialisation du moteur d'extraction...
+
+ + +
+ + +""" + +# --- PAGE D'ACCUEIL --- +@app.route("/") +def home(): + return render_template_string(""" + + + + + + Extracteur de formations + + + +
+
📄
+

Extracteur de formations

+

Sélectionnez le catalogue de formations (PDF) pour extraire les données vers Excel.

+ +
+
+ + +
+ + +
+
+ + + + + """) + +# --- MOTEUR D'EXTRACTION --- +@app.route("/process", methods=["POST"]) +def process(): + file = request.files['pdf_file'] + if not file: return "Fichier manquant" + + file_content = file.read() # On garde le fichier en mémoire vive + + def generate(): + yield HTML_LAYOUT # Envoie l'interface au navigateur + + all_formations = [] + COLONNES_ADMISES = ["Référence", "Durée", "Type", "Publics éligibles", "Niveau", "Sessions", "Tarifs"] + + try: + with pdfplumber.open(io.BytesIO(file_content)) as pdf: + total_pages = len(pdf.pages) + yield f"" + + for i, page in enumerate(pdf.pages): + page_num = i + 1 + percent = int((page_num / total_pages) * 100) + + try: + # Zone de détection (haut de page) + check_zone = page.crop((page.width * 0.5, 0, page.width, 400)) + text = check_zone.extract_text() or "" + + if "Référence" in text: + # --- EXTRACTION DE LA FICHE --- + header = page.crop((0, 0, page.width, 100)) + titre_brut = header.extract_text().split('\n')[0].strip() + # Sécurisation du titre pour JS (on enlève les quotes) + titre_clean = titre_brut.replace("'", "").replace('"', '').strip()[:60] + + data = {'Titre': titre_brut, 'Page_Source': page_num} + + # Extraction du tableau à droite + right_side = page.crop((page.width * 0.6, 180, page.width, 600)) + table = right_side.extract_table() + if table: + for row in table: + if len(row) >= 2 and row[0]: + cle = row[0].replace('\n', ' ').strip() + for col in COLONNES_ADMISES: + if col.lower() in cle.lower(): + data[col] = row[1].replace('\n', ' ').strip() + break + + all_formations.append(data) + yield f"" + + else: + # --- PAGE IGNORÉE --- + yield f"" + + except Exception as page_err: + yield f"" + + # --- FIN DE BOUCLE : SAUVEGARDE EXCEL --- + yield f"" + + if all_formations: + df = pd.DataFrame(all_formations) + output_path = os.path.join(TEMP_DIR, "resultat.xlsx") + # On utilise xlsxwriter pour la stabilité + df.to_excel(output_path, index=False, engine='xlsxwriter') + yield f"" + yield "" + else: + yield f"" + + except Exception as global_err: + yield f"" + + yield "" + + return Response(generate(), mimetype='text/html') + +# --- TÉLÉCHARGEMENT --- +@app.route("/download") +def download(): + return send_from_directory(TEMP_DIR, "resultat.xlsx", as_attachment=True) + +if __name__ == "__main__": + # Écoute sur 0.0.0.0 pour Docker + app.run(host='0.0.0.0', port=5000, debug=False) diff --git a/webapp.py~ b/webapp.py~ new file mode 100644 index 0000000..0d36071 --- /dev/null +++ b/webapp.py~ @@ -0,0 +1,277 @@ +import os +import io +import pandas as pd +import pdfplumber +from flask import Flask, request, render_template_string, Response, send_from_directory + +app = Flask(__name__) + +# Dossier temporaire pour stocker le fichier généré à l'intérieur du conteneur +TEMP_DIR = "/app/downloads" +if not os.path.exists(TEMP_DIR): + os.makedirs(TEMP_DIR) + +# --- DESIGN HTML & JAVASCRIPT --- +# Ce bloc est envoyé en premier au navigateur pour préparer l'interface de log +HTML_LAYOUT = """ + + + + + + Extracteur PDF - Progression en direct + + + +
+

Analyse du catalogue en cours...

+

Chaque page du fichier est analysée pour extraire les fiches de formation.

+ +
+ +
+ > Initialisation du moteur d'extraction...
+
+ + +
+ + +""" + +# --- PAGE D'ACCUEIL --- +@app.route("/") +def home(): + return render_template_string(""" + + + + + + Extracteur de formations + + + +
+
📄
+

Extracteur PDF

+

Sélectionnez le catalogue de formations (PDF) pour extraire les données vers Excel.

+ +
+
+ + +
+ + +
+
+ + + + + """) + +# --- MOTEUR D'EXTRACTION --- +@app.route("/process", methods=["POST"]) +def process(): + file = request.files['pdf_file'] + if not file: return "Fichier manquant" + + file_content = file.read() # On garde le fichier en mémoire vive + + def generate(): + yield HTML_LAYOUT # Envoie l'interface au navigateur + + all_formations = [] + COLONNES_ADMISES = ["Référence", "Durée", "Type", "Publics éligibles", "Niveau", "Sessions", "Tarifs"] + + try: + with pdfplumber.open(io.BytesIO(file_content)) as pdf: + total_pages = len(pdf.pages) + yield f"" + + for i, page in enumerate(pdf.pages): + page_num = i + 1 + percent = int((page_num / total_pages) * 100) + + try: + # Zone de détection (haut de page) + check_zone = page.crop((page.width * 0.5, 0, page.width, 400)) + text = check_zone.extract_text() or "" + + if "Référence" in text: + # --- EXTRACTION DE LA FICHE --- + header = page.crop((0, 0, page.width, 100)) + titre_brut = header.extract_text().split('\n')[0].strip() + # Sécurisation du titre pour JS (on enlève les quotes) + titre_clean = titre_brut.replace("'", "").replace('"', '').strip()[:60] + + data = {'Titre': titre_brut, 'Page_Source': page_num} + + # Extraction du tableau à droite + right_side = page.crop((page.width * 0.6, 180, page.width, 600)) + table = right_side.extract_table() + if table: + for row in table: + if len(row) >= 2 and row[0]: + cle = row[0].replace('\n', ' ').strip() + for col in COLONNES_ADMISES: + if col.lower() in cle.lower(): + data[col] = row[1].replace('\n', ' ').strip() + break + + all_formations.append(data) + yield f"" + + else: + # --- PAGE IGNORÉE --- + yield f"" + + except Exception as page_err: + yield f"" + + # --- FIN DE BOUCLE : SAUVEGARDE EXCEL --- + yield f"" + + if all_formations: + df = pd.DataFrame(all_formations) + output_path = os.path.join(TEMP_DIR, "resultat.xlsx") + # On utilise xlsxwriter pour la stabilité + df.to_excel(output_path, index=False, engine='xlsxwriter') + yield f"" + yield "" + else: + yield f"" + + except Exception as global_err: + yield f"" + + yield "" + + return Response(generate(), mimetype='text/html') + +# --- TÉLÉCHARGEMENT --- +@app.route("/download") +def download(): + return send_from_directory(TEMP_DIR, "resultat.xlsx", as_attachment=True) + +if __name__ == "__main__": + # Écoute sur 0.0.0.0 pour Docker + app.run(host='0.0.0.0', port=5000, debug=False)