Spaces:
Sleeping
Sleeping
Create extract_codex.py
Browse files- src/extract_codex.py +171 -0
src/extract_codex.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# extract_codex.py
|
| 2 |
+
import requests
|
| 3 |
+
from bs4 import BeautifulSoup
|
| 4 |
+
import json
|
| 5 |
+
import time
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
import os
|
| 8 |
+
|
| 9 |
+
# URLs des sections du Codex Alimentarius
|
| 10 |
+
URLS = {
|
| 11 |
+
"guidelines": "https://www.fao.org/fao-who-codexalimentarius/codex-texts/guidelines/fr/",
|
| 12 |
+
"standards": "https://www.fao.org/fao-who-codexalimentarius/codex-texts/list-standards/fr/",
|
| 13 |
+
"codes_of_practice": "https://www.fao.org/fao-who-codexalimentarius/codex-texts/codes-of-practice/fr/",
|
| 14 |
+
"miscellaneous": "https://www.fao.org/fao-who-codexalimentarius/codex-texts/miscellaneous/fr/"
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
# Mapping des catégories pour les noms de fichiers ou l'identification
|
| 18 |
+
CATEGORY_MAP = {
|
| 19 |
+
"guidelines": "Directives (CXG)",
|
| 20 |
+
"standards": "Normes (CXS)",
|
| 21 |
+
"codes_of_practice": "Codes de Pratique (CXC)",
|
| 22 |
+
"miscellaneous": "Divers (CXM)"
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
HEADERS = {
|
| 26 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36'
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
def extract_table_data(url, category_key):
|
| 30 |
+
"""
|
| 31 |
+
Extrait les données d'un tableau spécifique d'une page Codex.
|
| 32 |
+
"""
|
| 33 |
+
print(f"Extraction depuis: {url}")
|
| 34 |
+
try:
|
| 35 |
+
response = requests.get(url, headers=HEADERS, timeout=15) # Ajout d'un timeout
|
| 36 |
+
response.raise_for_status()
|
| 37 |
+
soup = BeautifulSoup(response.content, 'html.parser')
|
| 38 |
+
|
| 39 |
+
data_lines = []
|
| 40 |
+
# Trouver tous les tableaux
|
| 41 |
+
tables = soup.find_all('table')
|
| 42 |
+
|
| 43 |
+
target_table = None
|
| 44 |
+
# Heuristique : trouver le tableau avec le plus de lignes significatives
|
| 45 |
+
max_rows = 0
|
| 46 |
+
for table in tables:
|
| 47 |
+
rows = table.find_all('tr')
|
| 48 |
+
# Compter les lignes avec des cellules de données (td), pas seulement des en-têtes (th)
|
| 49 |
+
data_rows_count = sum(1 for row in rows if row.find('td'))
|
| 50 |
+
if data_rows_count > max_rows:
|
| 51 |
+
max_rows = data_rows_count
|
| 52 |
+
target_table = table
|
| 53 |
+
|
| 54 |
+
if not target_table:
|
| 55 |
+
print(f" Avertissement: Aucun tableau principal trouvé sur {url}")
|
| 56 |
+
return []
|
| 57 |
+
|
| 58 |
+
# Extraire les lignes du tableau trouvé
|
| 59 |
+
rows = target_table.find_all('tr')
|
| 60 |
+
# Identifier les en-têtes si nécessaire (optionnel ici car la structure est connue)
|
| 61 |
+
# Nous supposons que la première ligne est l'en-tête et les suivantes sont les données
|
| 62 |
+
# Parcourir les lignes de données
|
| 63 |
+
for row in rows:
|
| 64 |
+
cells = row.find_all(['td', 'th']) # Inclure th au cas où l'en-tête serait dans le tbody
|
| 65 |
+
if len(cells) >= 4: # S'assurer qu'il y a suffisamment de colonnes
|
| 66 |
+
# Extraire le texte de chaque cellule et le nettoyer
|
| 67 |
+
# La structure semble être: Code | Titre | Comité | Année | Status
|
| 68 |
+
code_raw = cells[0].get_text(strip=True) if cells[0] else ""
|
| 69 |
+
title_raw = cells[1].get_text(strip=True) if cells[1] else ""
|
| 70 |
+
committee_raw = cells[2].get_text(strip=True) if cells[2] else ""
|
| 71 |
+
year_raw = cells[3].get_text(strip=True) if cells[3] else ""
|
| 72 |
+
# status_raw = cells[4].get_text(strip=True) if len(cells) > 4 else "" # Optionnel
|
| 73 |
+
|
| 74 |
+
# Nettoyer les données extraites
|
| 75 |
+
# Parfois, le code contient aussi le titre, essayons de les séparer si c'est le cas
|
| 76 |
+
# Mais souvent, ils sont dans des cellules séparées. Vérifions simplement.
|
| 77 |
+
code = code_raw
|
| 78 |
+
title = title_raw
|
| 79 |
+
committee = committee_raw
|
| 80 |
+
year = year_raw
|
| 81 |
+
|
| 82 |
+
# Vérifier si les données de base sont présentes
|
| 83 |
+
if code and title and committee and year:
|
| 84 |
+
# Déterminer le préfixe pour la catégorie (CXG, CXS, etc.)
|
| 85 |
+
prefix_map = {
|
| 86 |
+
"guidelines": "CXG",
|
| 87 |
+
"standards": "CXS",
|
| 88 |
+
"codes_of_practice": "CXC",
|
| 89 |
+
"miscellaneous": "CXM"
|
| 90 |
+
}
|
| 91 |
+
expected_prefix = prefix_map.get(category_key, "CX")
|
| 92 |
+
# Vérifier si le code commence par le bon préfixe (optionnel mais bon filtre)
|
| 93 |
+
# Parfois les pages peuvent avoir des données supplémentaires, donc on filtre
|
| 94 |
+
if code.startswith(expected_prefix) or category_key == "miscellaneous": # Divers peut avoir différents codes
|
| 95 |
+
data_lines.append({
|
| 96 |
+
"code": code,
|
| 97 |
+
"title": title,
|
| 98 |
+
"committee": committee,
|
| 99 |
+
"year": year,
|
| 100 |
+
# "status": status_raw # Ajouter si pertinent
|
| 101 |
+
})
|
| 102 |
+
# else:
|
| 103 |
+
# print(f" Debug: Code '{code}' ne commence pas par '{expected_prefix}', ignoré sur {url}.") # Pour debug
|
| 104 |
+
# else:
|
| 105 |
+
# print(f" Debug: Ligne ignorée en raison de données manquantes: {code_raw}, {title_raw}, {committee_raw}, {year_raw} sur {url}") # Pour debug
|
| 106 |
+
|
| 107 |
+
print(f" {len(data_lines)} documents extraits de {category_key}.")
|
| 108 |
+
return data_lines
|
| 109 |
+
|
| 110 |
+
except requests.exceptions.Timeout:
|
| 111 |
+
print(f" Erreur: Timeout lors de la requête HTTP pour {url}")
|
| 112 |
+
return []
|
| 113 |
+
except requests.exceptions.RequestException as e:
|
| 114 |
+
print(f" Erreur lors de la requête HTTP pour {url}: {e}")
|
| 115 |
+
return []
|
| 116 |
+
except Exception as e:
|
| 117 |
+
print(f" Erreur inattendue lors du parsing de {url}: {e}")
|
| 118 |
+
import traceback
|
| 119 |
+
traceback.print_exc() # Affiche la pile d'appel pour le débogage
|
| 120 |
+
return []
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def run_extraction(output_dir="data"):
|
| 124 |
+
"""
|
| 125 |
+
Fonction principale pour orchestrer l'extraction et sauvegarder les données.
|
| 126 |
+
"""
|
| 127 |
+
print("Démarrage de l'extraction des données du Codex Alimentarius...")
|
| 128 |
+
|
| 129 |
+
# Créer le dossier de sortie s'il n'existe pas
|
| 130 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 131 |
+
|
| 132 |
+
all_data = {}
|
| 133 |
+
total_documents = 0
|
| 134 |
+
|
| 135 |
+
for key, url in URLS.items():
|
| 136 |
+
print(f"\n--- Extraction de la catégorie: {CATEGORY_MAP[key]} ---")
|
| 137 |
+
documents = extract_table_data(url, key)
|
| 138 |
+
all_data[key] = documents
|
| 139 |
+
total_documents += len(documents)
|
| 140 |
+
# Pause courte pour être gentil avec le serveur
|
| 141 |
+
time.sleep(1)
|
| 142 |
+
|
| 143 |
+
# Sauvegarder les données dans un fichier JSON structuré
|
| 144 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 145 |
+
output_filename = f"codex_data_{timestamp}.json"
|
| 146 |
+
output_path = os.path.join(output_dir, output_filename)
|
| 147 |
+
|
| 148 |
+
final_output = {
|
| 149 |
+
"extraction_date": datetime.now().isoformat(),
|
| 150 |
+
"total_documents": total_documents,
|
| 151 |
+
"categories": all_data
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
try:
|
| 155 |
+
with open(output_path, 'w', encoding='utf-8') as f:
|
| 156 |
+
json.dump(final_output, f, ensure_ascii=False, indent=4)
|
| 157 |
+
print(f"\n✅ Extraction terminée avec succès!")
|
| 158 |
+
print(f" {total_documents} documents ont été extraits et sauvegardés dans '{output_path}'.")
|
| 159 |
+
return output_path # Retourner le chemin du fichier créé
|
| 160 |
+
except Exception as e:
|
| 161 |
+
print(f"\n❌ Erreur lors de la sauvegarde du fichier JSON: {e}")
|
| 162 |
+
return None
|
| 163 |
+
|
| 164 |
+
# Optionnel: Afficher un résumé
|
| 165 |
+
# print("\n--- Résumé de l'extraction ---")
|
| 166 |
+
# for key, docs in all_data.items():
|
| 167 |
+
# print(f" - {CATEGORY_MAP[key]}: {len(docs)} documents")
|
| 168 |
+
|
| 169 |
+
if __name__ == "__main__":
|
| 170 |
+
# Si le script est exécuté directement
|
| 171 |
+
run_extraction()
|