Spaces:
Sleeping
Sleeping
joel commited on
Commit ·
dfdddb1
1
Parent(s): f59bae2
Initial deployment: Scrap-Dji with API
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +0 -35
- .gitignore +44 -0
- DEPLOYMENT_GUIDE.md +270 -0
- FAQ.md +265 -0
- INCONSISTENCY_REPORT.md +40 -0
- INSTALL_WINDOWS.md +255 -0
- OPTIMIZATION_STRATEGIES.md +80 -0
- PROJECT_ANALYSIS.md +91 -0
- QUICKSTART.md +138 -0
- QUICK_START.md +165 -0
- README.md +142 -6
- RESOURCE_ESTIMATION.md +92 -0
- TOGO_BENIN_ESTIMATION.md +74 -0
- TROUBLESHOOTING.md +230 -0
- VALUE_PROPOSITION.md +27 -0
- api/__init__.py +1 -0
- api/main.py +74 -0
- app.py +599 -0
- config.env.example +28 -0
- datasets/ewe/README.md +18 -0
- datasets/ewe/annotated/.gitkeep +1 -0
- datasets/ewe/clean/.gitkeep +1 -0
- datasets/ewe/final/ewe_corpus.jsonl +1 -0
- datasets/ewe/final/ewe_corpus.schema.json +90 -0
- datasets/ewe/raw/.gitkeep +1 -0
- db/__init__.py +1 -0
- db/models.py +60 -0
- db/mongo_connector.py +17 -0
- db/postgres_connector.py +10 -0
- frontend/README.md +7 -0
- frontend/index.html +428 -0
- frontend_example.html +372 -0
- indexer/__init__.py +1 -0
- indexer/qdrant_indexer.py +24 -0
- indexer/typesense_indexer.py +30 -0
- parser/__init__.py +1 -0
- parser/cleaner.py +9 -0
- parser/hasher.py +7 -0
- render.yaml +37 -0
- requirements.txt +34 -0
- run_scraper.py +85 -0
- scrape_togo_massive.py +104 -0
- scraper/__init__.py +1 -0
- scraper/discovery.py +113 -0
- scraper/main.py +186 -0
- scraper/rss.py +57 -0
- scraper/rss_sources.py +16 -0
- scripts/bootstrap_typesense.py +35 -0
- setup.py +101 -0
- sources.json +108 -0
.gitattributes
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
-
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
-
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
-
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
-
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
-
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
-
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
-
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
-
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
-
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
-
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
-
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
-
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
-
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
-
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
-
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
-
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
-
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
-
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
-
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
-
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
-
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
-
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
-
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
-
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
-
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
-
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
-
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
-
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
-
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
-
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
-
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
-
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
-
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
-
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
| 5 |
+
*.so
|
| 6 |
+
.Python
|
| 7 |
+
venv/
|
| 8 |
+
env/
|
| 9 |
+
ENV/
|
| 10 |
+
|
| 11 |
+
# Environnement
|
| 12 |
+
.env
|
| 13 |
+
.env.local
|
| 14 |
+
config.env
|
| 15 |
+
|
| 16 |
+
# Données et logs
|
| 17 |
+
data/
|
| 18 |
+
logs/
|
| 19 |
+
*.log
|
| 20 |
+
storage_data/
|
| 21 |
+
|
| 22 |
+
# IDE
|
| 23 |
+
.vscode/
|
| 24 |
+
.idea/
|
| 25 |
+
*.swp
|
| 26 |
+
*.swo
|
| 27 |
+
*~
|
| 28 |
+
|
| 29 |
+
# OS
|
| 30 |
+
.DS_Store
|
| 31 |
+
Thumbs.db
|
| 32 |
+
|
| 33 |
+
# Hugging Face Spaces
|
| 34 |
+
flagged/
|
| 35 |
+
|
| 36 |
+
# Bases de données locales (si utilisées)
|
| 37 |
+
*.db
|
| 38 |
+
*.sqlite
|
| 39 |
+
*.sqlite3
|
| 40 |
+
|
| 41 |
+
# Fichiers temporaires
|
| 42 |
+
*.tmp
|
| 43 |
+
*.temp
|
| 44 |
+
.cache/
|
DEPLOYMENT_GUIDE.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Guide de Déploiement sur Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
## 📋 Prérequis
|
| 4 |
+
|
| 5 |
+
1. **Compte Hugging Face** : [Créer un compte](https://huggingface.co/join)
|
| 6 |
+
2. **Git installé** sur votre machine
|
| 7 |
+
3. **Projet Scrap-Dji** prêt avec les fichiers suivants :
|
| 8 |
+
- ✅ `app.py` (créé)
|
| 9 |
+
- ✅ `requirements.txt` (optimisé)
|
| 10 |
+
- ✅ `README.md` (avec metadata HF)
|
| 11 |
+
- ✅ `.gitignore` (configuré)
|
| 12 |
+
- ✅ `sources.json` (avec sources Togo + Bénin)
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
## 🚀 Étapes de Déploiement
|
| 17 |
+
|
| 18 |
+
### 1. Créer un Space sur Hugging Face
|
| 19 |
+
|
| 20 |
+
1. Allez sur [huggingface.co/new-space](https://huggingface.co/new-space)
|
| 21 |
+
2. Remplissez les informations :
|
| 22 |
+
- **Owner** : Votre username
|
| 23 |
+
- **Space name** : `scrap-dji` (ou autre nom)
|
| 24 |
+
- **License** : MIT
|
| 25 |
+
- **Select the Space SDK** : **Gradio**
|
| 26 |
+
- **Visibility** : **Private** ✅ (comme demandé)
|
| 27 |
+
3. Cliquez sur **Create Space**
|
| 28 |
+
|
| 29 |
+
### 2. Cloner le Space localement
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
# Cloner le Space vide
|
| 33 |
+
git clone https://huggingface.co/spaces/VOTRE_USERNAME/scrap-dji
|
| 34 |
+
cd scrap-dji
|
| 35 |
+
```
|
| 36 |
+
|
| 37 |
+
### 3. Copier les fichiers du projet
|
| 38 |
+
|
| 39 |
+
```bash
|
| 40 |
+
# Depuis le répertoire Scrap-Dji
|
| 41 |
+
# Copier tous les fichiers nécessaires vers le Space
|
| 42 |
+
|
| 43 |
+
# Fichiers principaux
|
| 44 |
+
cp app.py /path/to/scrap-dji/
|
| 45 |
+
cp requirements.txt /path/to/scrap-dji/
|
| 46 |
+
cp README.md /path/to/scrap-dji/
|
| 47 |
+
cp .gitignore /path/to/scrap-dji/
|
| 48 |
+
cp sources.json /path/to/scrap-dji/
|
| 49 |
+
|
| 50 |
+
# Copier les dossiers du code
|
| 51 |
+
cp -r scraper/ /path/to/scrap-dji/
|
| 52 |
+
cp -r parser/ /path/to/scrap-dji/
|
| 53 |
+
cp -r utils/ /path/to/scrap-dji/
|
| 54 |
+
cp -r indexer/ /path/to/scrap-dji/
|
| 55 |
+
|
| 56 |
+
# Créer le dossier data (sera persistant sur HF)
|
| 57 |
+
mkdir -p /path/to/scrap-dji/data
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
### 4. Pousser vers Hugging Face
|
| 61 |
+
|
| 62 |
+
```bash
|
| 63 |
+
cd /path/to/scrap-dji
|
| 64 |
+
|
| 65 |
+
# Ajouter tous les fichiers
|
| 66 |
+
git add .
|
| 67 |
+
|
| 68 |
+
# Commit
|
| 69 |
+
git commit -m "Initial deployment: Scrap-Dji with API endpoints"
|
| 70 |
+
|
| 71 |
+
# Pousser vers HF
|
| 72 |
+
git push
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
### 5. Vérifier le Build
|
| 76 |
+
|
| 77 |
+
1. Allez sur votre Space : `https://huggingface.co/spaces/VOTRE_USERNAME/scrap-dji`
|
| 78 |
+
2. Cliquez sur l'onglet **Logs** pour voir le build en cours
|
| 79 |
+
3. Attendez que le status passe à **Running** ✅
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
## 🔧 Configuration Post-Déploiement
|
| 84 |
+
|
| 85 |
+
### Vérifier que tout fonctionne
|
| 86 |
+
|
| 87 |
+
1. **Interface Gradio** : `https://VOTRE_USERNAME-scrap-dji.hf.space/`
|
| 88 |
+
2. **API Docs** : `https://VOTRE_USERNAME-scrap-dji.hf.space/docs`
|
| 89 |
+
3. **Health Check** : `https://VOTRE_USERNAME-scrap-dji.hf.space/api/health`
|
| 90 |
+
|
| 91 |
+
### Test de l'API depuis votre frontend
|
| 92 |
+
|
| 93 |
+
```javascript
|
| 94 |
+
// Exemple de requête depuis votre frontend
|
| 95 |
+
const API_URL = 'https://VOTRE_USERNAME-scrap-dji.hf.space';
|
| 96 |
+
|
| 97 |
+
// Recherche
|
| 98 |
+
const searchResults = await fetch(`${API_URL}/api/search`, {
|
| 99 |
+
method: 'POST',
|
| 100 |
+
headers: { 'Content-Type': 'application/json' },
|
| 101 |
+
body: JSON.stringify({
|
| 102 |
+
query: 'économie togo',
|
| 103 |
+
pays: 'Togo',
|
| 104 |
+
limit: 20,
|
| 105 |
+
fuzzy: true
|
| 106 |
+
})
|
| 107 |
+
});
|
| 108 |
+
|
| 109 |
+
const data = await searchResults.json();
|
| 110 |
+
console.log(data.results);
|
| 111 |
+
|
| 112 |
+
// Statistiques
|
| 113 |
+
const stats = await fetch(`${API_URL}/api/stats`);
|
| 114 |
+
const statsData = await stats.json();
|
| 115 |
+
console.log(statsData);
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
---
|
| 119 |
+
|
| 120 |
+
## 📊 Endpoints API Disponibles
|
| 121 |
+
|
| 122 |
+
| Endpoint | Méthode | Description |
|
| 123 |
+
|----------|---------|-------------|
|
| 124 |
+
| `/api/search` | POST/GET | Recherche avec filtres |
|
| 125 |
+
| `/api/stats` | GET | Statistiques de la base |
|
| 126 |
+
| `/api/documents` | GET | Liste paginée des documents |
|
| 127 |
+
| `/api/documents/{id}` | GET | Document par ID |
|
| 128 |
+
| `/api/reload` | POST | Recharger les documents |
|
| 129 |
+
| `/api/health` | GET | Health check |
|
| 130 |
+
| `/docs` | GET | Documentation Swagger |
|
| 131 |
+
|
| 132 |
+
---
|
| 133 |
+
|
| 134 |
+
## 🎯 Optimisations Recommandées
|
| 135 |
+
|
| 136 |
+
### 1. Pré-scraper des données avant déploiement
|
| 137 |
+
|
| 138 |
+
Pour éviter de démarrer avec une base vide :
|
| 139 |
+
|
| 140 |
+
```bash
|
| 141 |
+
# Sur votre machine locale
|
| 142 |
+
python scrape_togo_massive.py
|
| 143 |
+
|
| 144 |
+
# Copier le fichier de données généré
|
| 145 |
+
cp data/search_index.json /path/to/scrap-dji/data/documents.json
|
| 146 |
+
|
| 147 |
+
# Commit et push
|
| 148 |
+
cd /path/to/scrap-dji
|
| 149 |
+
git add data/documents.json
|
| 150 |
+
git commit -m "Add pre-scraped data"
|
| 151 |
+
git push
|
| 152 |
+
```
|
| 153 |
+
|
| 154 |
+
### 2. Limiter le scraping initial sur HF
|
| 155 |
+
|
| 156 |
+
Pour éviter les timeouts, modifiez temporairement `sources.json` :
|
| 157 |
+
|
| 158 |
+
```json
|
| 159 |
+
{
|
| 160 |
+
"settings": {
|
| 161 |
+
"max_pages_per_source": 50, // Au lieu de 500
|
| 162 |
+
"concurrent_requests": 5 // Au lieu de 10
|
| 163 |
+
}
|
| 164 |
+
}
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
### 3. Activer le cache pour les recherches
|
| 168 |
+
|
| 169 |
+
Le cache est déjà implémenté dans `app.py` pour optimiser les performances.
|
| 170 |
+
|
| 171 |
+
---
|
| 172 |
+
|
| 173 |
+
## 🐛 Dépannage
|
| 174 |
+
|
| 175 |
+
### Problème : Build échoue
|
| 176 |
+
|
| 177 |
+
**Solution** : Vérifiez les logs dans l'onglet "Logs" du Space. Souvent causé par :
|
| 178 |
+
- Dépendances manquantes dans `requirements.txt`
|
| 179 |
+
- Erreurs de syntaxe Python
|
| 180 |
+
- Imports manquants
|
| 181 |
+
|
| 182 |
+
### Problème : "Module not found"
|
| 183 |
+
|
| 184 |
+
**Solution** : Vérifiez que tous les dossiers sont bien copiés :
|
| 185 |
+
```bash
|
| 186 |
+
ls -la /path/to/scrap-dji/
|
| 187 |
+
# Doit contenir: scraper/, parser/, utils/, indexer/
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
### Problème : API retourne []
|
| 191 |
+
|
| 192 |
+
**Solution** : La base est vide. Lancez le scraping depuis l'interface Gradio ou pré-scrapez des données.
|
| 193 |
+
|
| 194 |
+
### Problème : Timeout lors du scraping
|
| 195 |
+
|
| 196 |
+
**Solution** : Réduisez le nombre de sources actives dans `sources.json` ou limitez `max_pages_per_source`.
|
| 197 |
+
|
| 198 |
+
---
|
| 199 |
+
|
| 200 |
+
## 🔒 Sécurité (Space Privé)
|
| 201 |
+
|
| 202 |
+
Votre Space est **privé**, donc :
|
| 203 |
+
- ✅ Seul vous pouvez y accéder
|
| 204 |
+
- ✅ L'API nécessite une authentification HF
|
| 205 |
+
- ⚠️ Pour partager avec votre frontend, vous devrez :
|
| 206 |
+
1. Rendre le Space public, OU
|
| 207 |
+
2. Utiliser un token HF dans votre frontend
|
| 208 |
+
|
| 209 |
+
### Option : Utiliser un token HF
|
| 210 |
+
|
| 211 |
+
```javascript
|
| 212 |
+
// Dans votre frontend
|
| 213 |
+
const response = await fetch(`${API_URL}/api/search`, {
|
| 214 |
+
method: 'POST',
|
| 215 |
+
headers: {
|
| 216 |
+
'Content-Type': 'application/json',
|
| 217 |
+
'Authorization': 'Bearer YOUR_HF_TOKEN'
|
| 218 |
+
},
|
| 219 |
+
body: JSON.stringify({ query: 'test' })
|
| 220 |
+
});
|
| 221 |
+
```
|
| 222 |
+
|
| 223 |
+
---
|
| 224 |
+
|
| 225 |
+
## 📈 Monitoring
|
| 226 |
+
|
| 227 |
+
### Voir les logs en temps réel
|
| 228 |
+
|
| 229 |
+
```bash
|
| 230 |
+
# Depuis votre terminal local
|
| 231 |
+
huggingface-cli repo logs spaces/VOTRE_USERNAME/scrap-dji
|
| 232 |
+
```
|
| 233 |
+
|
| 234 |
+
### Statistiques d'utilisation
|
| 235 |
+
|
| 236 |
+
Consultez l'onglet **Analytics** de votre Space pour voir :
|
| 237 |
+
- Nombre de requêtes
|
| 238 |
+
- Temps de réponse
|
| 239 |
+
- Erreurs
|
| 240 |
+
|
| 241 |
+
---
|
| 242 |
+
|
| 243 |
+
## 🔄 Mise à Jour du Space
|
| 244 |
+
|
| 245 |
+
Pour mettre à jour le code après modification :
|
| 246 |
+
|
| 247 |
+
```bash
|
| 248 |
+
cd /path/to/scrap-dji
|
| 249 |
+
|
| 250 |
+
# Modifier vos fichiers
|
| 251 |
+
# ...
|
| 252 |
+
|
| 253 |
+
# Commit et push
|
| 254 |
+
git add .
|
| 255 |
+
git commit -m "Update: description des changements"
|
| 256 |
+
git push
|
| 257 |
+
```
|
| 258 |
+
|
| 259 |
+
Le Space se rebuild automatiquement ! ✨
|
| 260 |
+
|
| 261 |
+
---
|
| 262 |
+
|
| 263 |
+
## 📞 Support
|
| 264 |
+
|
| 265 |
+
- **Documentation HF Spaces** : [docs.huggingface.co/hub/spaces](https://huggingface.co/docs/hub/spaces)
|
| 266 |
+
- **Forum HF** : [discuss.huggingface.co](https://discuss.huggingface.co)
|
| 267 |
+
|
| 268 |
+
---
|
| 269 |
+
|
| 270 |
+
**Votre projet est maintenant prêt pour Hugging Face ! 🎉**
|
FAQ.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# FAQ - Scrap-Dji : Questions et Réponses
|
| 2 |
+
|
| 3 |
+
## 📊 Que puis-je faire avec les données scrapées ?
|
| 4 |
+
|
| 5 |
+
### Fonctionnalités principales :
|
| 6 |
+
1. **Recherche sémantique** : Trouver du contenu par similarité de sens
|
| 7 |
+
2. **Recherche par mots-clés** : Recherche classique par termes
|
| 8 |
+
3. **Filtrage géographique** : Contenus par pays/région africaine
|
| 9 |
+
4. **Analyse temporelle** : Évolution des sujets dans le temps
|
| 10 |
+
5. **Détection de langues** : Contenus en français, anglais, langues locales
|
| 11 |
+
6. **Analyse de sentiments** : Positif/négatif/neutre
|
| 12 |
+
7. **Extraction d'entités** : Personnes, organisations, lieux
|
| 13 |
+
8. **Génération de résumés** : Synthèse automatique des articles
|
| 14 |
+
9. **Détection de fake news** : Analyse de crédibilité
|
| 15 |
+
10. **API REST** : Interface pour applications tierces
|
| 16 |
+
|
| 17 |
+
### Exemples d'utilisation :
|
| 18 |
+
- **Journaliste** : Recherche d'articles sur un sujet spécifique
|
| 19 |
+
- **Chercheur** : Analyse de tendances médiatiques africaines
|
| 20 |
+
- **Développeur** : API pour application mobile/web
|
| 21 |
+
- **Analyste** : Rapports sur l'actualité africaine
|
| 22 |
+
|
| 23 |
+
## 📋 Quelles données sont récupérées ?
|
| 24 |
+
|
| 25 |
+
### Données extraites :
|
| 26 |
+
```json
|
| 27 |
+
{
|
| 28 |
+
"titre": "Titre de l'article",
|
| 29 |
+
"texte": "Contenu complet nettoyé",
|
| 30 |
+
"source_url": "URL d'origine",
|
| 31 |
+
"pays": "Sénégal",
|
| 32 |
+
"langue": "fr",
|
| 33 |
+
"type_document": "article|social|pdf|video",
|
| 34 |
+
"auteur": "Nom de l'auteur",
|
| 35 |
+
"date_publication": "2024-01-15",
|
| 36 |
+
"tags": ["politique", "économie"],
|
| 37 |
+
"images": ["url1", "url2"],
|
| 38 |
+
"liens_externes": ["url1", "url2"],
|
| 39 |
+
"métadonnées": {
|
| 40 |
+
"longueur_texte": 1500,
|
| 41 |
+
"nombre_mots": 250,
|
| 42 |
+
"sentiment": "positif",
|
| 43 |
+
"crédibilité": 0.85
|
| 44 |
+
}
|
| 45 |
+
}
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
### Types de sources supportées :
|
| 49 |
+
- **Sites d'actualités** : Articles, éditoriaux, interviews
|
| 50 |
+
- **Réseaux sociaux** : Twitter, Facebook (via API)
|
| 51 |
+
- **Blogs** : Contenus personnels et professionnels
|
| 52 |
+
- **PDFs** : Documents officiels, rapports
|
| 53 |
+
- **Vidéos** : YouTube, Vimeo (métadonnées)
|
| 54 |
+
- **Podcasts** : Transcriptions et descriptions
|
| 55 |
+
|
| 56 |
+
## 🔄 Reprise du scraping après interruption
|
| 57 |
+
|
| 58 |
+
### ✅ Oui, le système évite les doublons !
|
| 59 |
+
|
| 60 |
+
**Mécanisme de détection :**
|
| 61 |
+
1. **Hash du contenu** : Chaque document a un hash unique
|
| 62 |
+
2. **URL de source** : Vérification par URL d'origine
|
| 63 |
+
3. **Timestamp** : Horodatage pour versioning
|
| 64 |
+
4. **Base de données** : Vérification en PostgreSQL/MongoDB
|
| 65 |
+
|
| 66 |
+
**Comportement :**
|
| 67 |
+
```python
|
| 68 |
+
# Si document existe déjà
|
| 69 |
+
if existing_document:
|
| 70 |
+
# Création d'une nouvelle version
|
| 71 |
+
new_version = DocumentVersion(
|
| 72 |
+
document_id=existing.id,
|
| 73 |
+
texte=new_content,
|
| 74 |
+
date=datetime.now()
|
| 75 |
+
)
|
| 76 |
+
# Pas de doublon, juste mise à jour
|
| 77 |
+
else:
|
| 78 |
+
# Nouveau document
|
| 79 |
+
create_new_document()
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
**Avantages :**
|
| 83 |
+
- ✅ Pas de répétition des données
|
| 84 |
+
- ✅ Mise à jour des contenus modifiés
|
| 85 |
+
- ✅ Historique des versions
|
| 86 |
+
- ✅ Reprise automatique où ça s'est arrêté
|
| 87 |
+
|
| 88 |
+
## 🗄️ Configuration des bases de données
|
| 89 |
+
|
| 90 |
+
### PostgreSQL (Obligatoire)
|
| 91 |
+
```bash
|
| 92 |
+
# 1. Télécharger depuis https://www.postgresql.org/download/windows/
|
| 93 |
+
# 2. Installer avec les paramètres par défaut
|
| 94 |
+
# 3. Créer la base de données
|
| 95 |
+
createdb scrapdji
|
| 96 |
+
|
| 97 |
+
# 4. Configurer dans .env
|
| 98 |
+
POSTGRES_URI=postgresql://postgres:votre_mot_de_passe@localhost:5432/scrapdji
|
| 99 |
+
```
|
| 100 |
+
|
| 101 |
+
### MongoDB (Obligatoire)
|
| 102 |
+
```bash
|
| 103 |
+
# 1. Télécharger depuis https://www.mongodb.com/try/download/community
|
| 104 |
+
# 2. Installer MongoDB Community Server
|
| 105 |
+
# 3. Démarrer le service
|
| 106 |
+
net start MongoDB
|
| 107 |
+
|
| 108 |
+
# 4. Configurer dans .env
|
| 109 |
+
MONGO_URI=mongodb://localhost:27017
|
| 110 |
+
MONGO_DB=scrapdji
|
| 111 |
+
```
|
| 112 |
+
|
| 113 |
+
### Typesense (Optionnel - Recherche avancée)
|
| 114 |
+
```bash
|
| 115 |
+
# Installation via Docker (recommandé)
|
| 116 |
+
docker run -p 8108:8108 -v typesense-data:/data typesense/typesense:latest --data-dir /data --api-key=xyz
|
| 117 |
+
|
| 118 |
+
# Ou télécharger l'exécutable Windows
|
| 119 |
+
# https://github.com/typesense/typesense/releases
|
| 120 |
+
```
|
| 121 |
+
|
| 122 |
+
### Qdrant (Optionnel - IA/ML)
|
| 123 |
+
```bash
|
| 124 |
+
# Installation via Docker
|
| 125 |
+
docker run -p 6333:6333 qdrant/qdrant
|
| 126 |
+
|
| 127 |
+
# Ou télécharger l'exécutable
|
| 128 |
+
# https://github.com/qdrant/qdrant/releases
|
| 129 |
+
```
|
| 130 |
+
|
| 131 |
+
## 💾 Consommation des ressources
|
| 132 |
+
|
| 133 |
+
### Estimation des ressources :
|
| 134 |
+
|
| 135 |
+
**Scraping léger (10 sources) :**
|
| 136 |
+
- CPU : 5-10% (processeur moderne)
|
| 137 |
+
- RAM : 200-500 MB
|
| 138 |
+
- Disque : 1-5 GB/jour
|
| 139 |
+
- Réseau : 10-50 MB/source
|
| 140 |
+
|
| 141 |
+
**Scraping intensif (100+ sources) :**
|
| 142 |
+
- CPU : 20-40%
|
| 143 |
+
- RAM : 1-2 GB
|
| 144 |
+
- Disque : 10-50 GB/jour
|
| 145 |
+
- Réseau : 100-500 MB/source
|
| 146 |
+
|
| 147 |
+
### Optimisations :
|
| 148 |
+
```python
|
| 149 |
+
# Configuration optimisée dans .env
|
| 150 |
+
SCRAPER_DELAY=2 # Délai entre requêtes
|
| 151 |
+
SCRAPER_CONCURRENT_REQUESTS=8 # Requêtes simultanées
|
| 152 |
+
SCRAPER_TIMEOUT=30 # Timeout des requêtes
|
| 153 |
+
SCRAPER_RETRY_TIMES=3 # Nombre de tentatives
|
| 154 |
+
```
|
| 155 |
+
|
| 156 |
+
### Recommandations :
|
| 157 |
+
- **Débutant** : 5-10 sources, scraping toutes les heures
|
| 158 |
+
- **Intermédiaire** : 20-50 sources, scraping toutes les 30 minutes
|
| 159 |
+
- **Avancé** : 100+ sources, scraping en continu avec queue
|
| 160 |
+
|
| 161 |
+
## 🔧 Configuration Git
|
| 162 |
+
|
| 163 |
+
### Fichier .gitignore
|
| 164 |
+
```gitignore
|
| 165 |
+
# Environnement virtuel
|
| 166 |
+
venv/
|
| 167 |
+
env/
|
| 168 |
+
.venv/
|
| 169 |
+
|
| 170 |
+
# Variables d'environnement
|
| 171 |
+
.env
|
| 172 |
+
.env.local
|
| 173 |
+
.env.production
|
| 174 |
+
|
| 175 |
+
# Logs
|
| 176 |
+
logs/
|
| 177 |
+
*.log
|
| 178 |
+
|
| 179 |
+
# Données scrapées
|
| 180 |
+
storage_data/
|
| 181 |
+
data/
|
| 182 |
+
temp/
|
| 183 |
+
|
| 184 |
+
# Cache
|
| 185 |
+
__pycache__/
|
| 186 |
+
*.pyc
|
| 187 |
+
*.pyo
|
| 188 |
+
*.pyd
|
| 189 |
+
.Python
|
| 190 |
+
|
| 191 |
+
# IDE
|
| 192 |
+
.vscode/
|
| 193 |
+
.idea/
|
| 194 |
+
*.swp
|
| 195 |
+
*.swo
|
| 196 |
+
|
| 197 |
+
# OS
|
| 198 |
+
.DS_Store
|
| 199 |
+
Thumbs.db
|
| 200 |
+
|
| 201 |
+
# Base de données
|
| 202 |
+
*.db
|
| 203 |
+
*.sqlite
|
| 204 |
+
|
| 205 |
+
# Fichiers temporaires
|
| 206 |
+
*.tmp
|
| 207 |
+
*.temp
|
| 208 |
+
```
|
| 209 |
+
|
| 210 |
+
### Commandes Git recommandées :
|
| 211 |
+
```bash
|
| 212 |
+
# Initialiser le repository
|
| 213 |
+
git init
|
| 214 |
+
|
| 215 |
+
# Ajouter les fichiers de configuration
|
| 216 |
+
git add .gitignore
|
| 217 |
+
git add config.env.example
|
| 218 |
+
git add sources.json.example
|
| 219 |
+
|
| 220 |
+
# Premier commit
|
| 221 |
+
git commit -m "Initial commit - Configuration Scrap-Dji"
|
| 222 |
+
|
| 223 |
+
# Ajouter le code source
|
| 224 |
+
git add .
|
| 225 |
+
git commit -m "Ajout du code source Scrap-Dji"
|
| 226 |
+
|
| 227 |
+
# Créer une branche pour le développement
|
| 228 |
+
git checkout -b develop
|
| 229 |
+
```
|
| 230 |
+
|
| 231 |
+
## 🚀 Script de configuration automatique
|
| 232 |
+
|
| 233 |
+
```bash
|
| 234 |
+
# Script pour configurer tout automatiquement
|
| 235 |
+
python setup.py --auto
|
| 236 |
+
|
| 237 |
+
# Ou configuration manuelle
|
| 238 |
+
python setup.py --interactive
|
| 239 |
+
```
|
| 240 |
+
|
| 241 |
+
## 📊 Monitoring et métriques
|
| 242 |
+
|
| 243 |
+
### Métriques disponibles :
|
| 244 |
+
- **Documents traités** : Nombre total
|
| 245 |
+
- **Nouvelles sources** : Sources découvertes
|
| 246 |
+
- **Taux de succès** : Pourcentage de succès
|
| 247 |
+
- **Temps de traitement** : Durée par source
|
| 248 |
+
- **Erreurs** : Logs d'erreurs détaillés
|
| 249 |
+
|
| 250 |
+
### Dashboard (futur) :
|
| 251 |
+
- Interface web pour visualiser les métriques
|
| 252 |
+
- Graphiques en temps réel
|
| 253 |
+
- Alertes automatiques
|
| 254 |
+
- Export des données
|
| 255 |
+
|
| 256 |
+
## 🎯 Prochaines étapes recommandées
|
| 257 |
+
|
| 258 |
+
1. **Installation** : PostgreSQL + MongoDB
|
| 259 |
+
2. **Configuration** : .env et sources.json
|
| 260 |
+
3. **Test** : Lancement avec --dry-run
|
| 261 |
+
4. **Déploiement** : Scraping en production
|
| 262 |
+
5. **Optimisation** : Ajustement des paramètres
|
| 263 |
+
6. **Monitoring** : Surveillance des performances
|
| 264 |
+
7. **API** : Déploiement de l'API FastAPI
|
| 265 |
+
8. **Frontend** : Interface utilisateur web
|
INCONSISTENCY_REPORT.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rapport de Détection des Incohérences - Scrap-Dji
|
| 2 |
+
|
| 3 |
+
Après une analyse approfondie de l'ensemble des fichiers et de la logique du projet, voici les incohérences majeures détectées, classées par catégorie.
|
| 4 |
+
|
| 5 |
+
## 1. Pipeline de Données Brisé (Grave)
|
| 6 |
+
Le plus gros problème est que les composants sont définis mais ne communiquent pas entre eux :
|
| 7 |
+
- **Scraper vers Indexeur** : Le script `scraper/main.py` sauvegarde les données uniquement dans **PostgreSQL**. Il n'appelle jamais les indexeurs `indexer/typesense_indexer.py` ou `indexer/qdrant_indexer.py`. Résultat : les données scrapées ne sont jamais indexées pour la recherche.
|
| 8 |
+
- **Scraper vers Storage** : Le `StorageManager` (gestion des fichiers locaux) est initialisé dans le scraper mais n'est jamais utilisé pour télécharger ou stocker des images/PDF, bien que le modèle `db/models.py` prévoie une table `Image`.
|
| 9 |
+
- **Scraper vers NoSQL** : Le projet prévoit d'utiliser MongoDB pour la flexibilité, mais le scraper n'envoie aucune donnée vers le `mongo_connector.py`.
|
| 10 |
+
|
| 11 |
+
## 2. Redondance de Code (Maintenabilité)
|
| 12 |
+
Il existe trois classes presque identiques pour la même tâche (découverte de sources) :
|
| 13 |
+
1. `UniversalSource` (`scraper/universal_source.py`)
|
| 14 |
+
2. `SourceDiscovery` (`scraper/discovery.py`)
|
| 15 |
+
3. `AutoSourceDiscovery` (`scraper/auto_discovery.py`)
|
| 16 |
+
Ces trois fichiers partagent ~80% de leur logique (détection de langue, détection de pays par extension de domaine, filtrage par mots-clés). Cela crée un risque de divergence de logique.
|
| 17 |
+
|
| 18 |
+
## 3. Incohérences de Dépendances (`requirements.txt`)
|
| 19 |
+
Plusieurs dépendances sont déclarées mais non utilisées, ou utilisées mais non déclarées :
|
| 20 |
+
- **Manquants** : `python-whois` est importé dans `discovery.py` mais absent de `requirements.txt`.
|
| 21 |
+
- **Inutiles** : `scrapy` est listé dans le README et les dépendances, mais **aucun** fichier dans `/scraper` ne l'utilise (tout est fait avec `requests` + `BeautifulSoup`).
|
| 22 |
+
- **Inutiles** : `meilisearch` est listé mais aucun code n'existe pour l'utiliser (seuls Typesense et Qdrant ont des connecteurs).
|
| 23 |
+
|
| 24 |
+
## 4. Incohérences Logiques et Bugs Potentiels
|
| 25 |
+
- **Filtres de Pays** : `auto_discovery.py` (ligne 181) rejette toutes les sources dont le pays est "Afrique", alors que `UniversalSource` et `SourceDiscovery` les acceptent.
|
| 26 |
+
- **Sélecteurs Hardcodés** : Dans `auto_discovery.py` (ligne 132), les sélecteurs CSS sont injectés en dur, ignorant totalement ceux qui pourraient être plus précis dans `sources.json`.
|
| 27 |
+
- **API Fantôme** : `api/main.py` est une simple coquille. Elle ne contient aucune logique de recherche réelle. Elle renvoie simplement les filtres passés en paramètre sans interroger la base de données.
|
| 28 |
+
- **Logs Inconsistants** : `utils/config.py` définit un chemin de log via variable d'environnement, mais `utils/logger.py` le force en dur dans `Path("logs") / "scrapdji.log"`.
|
| 29 |
+
|
| 30 |
+
## 5. Architecture vs. Réalité (README)
|
| 31 |
+
- **Vision vs Code** : Le README promet un "Scraping multi-sources avec Scrapy". En réalité, le code utilise une approche séquentielle simple avec `requests` qui sera beaucoup plus lente et moins robuste pour le passage à l'échelle.
|
| 32 |
+
- **Typesense/Meilisearch** : Le README mentionne les deux comme interchangeables, mais le code ne supporte que Typesense.
|
| 33 |
+
|
| 34 |
+
---
|
| 35 |
+
|
| 36 |
+
## 💡 Recommandations Prioritaires
|
| 37 |
+
1. **Centraliser la découverte** : Fusionner les 3 fichiers de découverte en un seul module robuste.
|
| 38 |
+
2. **Connecter les fils** : Modifier `scraper/main.py` pour qu'il appelle systématiquement `indexer.index_document()` après avoir sauvegardé en base de données.
|
| 39 |
+
3. **Nettoyer le `requirements.txt`** : Aligner les dépendances sur le code réel.
|
| 40 |
+
4. **Implémenter l'API** : Connecter FastAPI à PostgreSQL (pour les métadonnées) et Typesense (pour la recherche plein texte).
|
INSTALL_WINDOWS.md
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Guide d'installation Windows - Scrap-Dji
|
| 2 |
+
|
| 3 |
+
## 🪟 Installation des services sur Windows
|
| 4 |
+
|
| 5 |
+
### 1. PostgreSQL
|
| 6 |
+
|
| 7 |
+
#### Téléchargement et installation :
|
| 8 |
+
1. Aller sur https://www.postgresql.org/download/windows/
|
| 9 |
+
2. Télécharger PostgreSQL 15+ pour Windows
|
| 10 |
+
3. Exécuter l'installateur
|
| 11 |
+
4. **Important** : Noter le mot de passe de l'utilisateur `postgres`
|
| 12 |
+
5. Laisser le port par défaut (5432)
|
| 13 |
+
|
| 14 |
+
#### Configuration :
|
| 15 |
+
```bash
|
| 16 |
+
# Ouvrir Command Prompt en tant qu'administrateur
|
| 17 |
+
# Créer la base de données
|
| 18 |
+
psql -U postgres -c "CREATE DATABASE scrapdji;"
|
| 19 |
+
|
| 20 |
+
# Tester la connexion
|
| 21 |
+
psql -U postgres -d scrapdji -c "SELECT version();"
|
| 22 |
+
```
|
| 23 |
+
|
| 24 |
+
#### Configuration dans .env :
|
| 25 |
+
```env
|
| 26 |
+
POSTGRES_URI=postgresql://postgres:votre_mot_de_passe@localhost:5432/scrapdji
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
### 2. MongoDB
|
| 30 |
+
|
| 31 |
+
#### Téléchargement et installation :
|
| 32 |
+
1. Aller sur https://www.mongodb.com/try/download/community
|
| 33 |
+
2. Télécharger MongoDB Community Server pour Windows
|
| 34 |
+
3. Exécuter l'installateur
|
| 35 |
+
4. Choisir "Complete" installation
|
| 36 |
+
5. Installer MongoDB Compass (interface graphique)
|
| 37 |
+
|
| 38 |
+
#### Démarrer le service :
|
| 39 |
+
```bash
|
| 40 |
+
# Démarrer MongoDB
|
| 41 |
+
net start MongoDB
|
| 42 |
+
|
| 43 |
+
# Vérifier le statut
|
| 44 |
+
sc query MongoDB
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
#### Configuration dans .env :
|
| 48 |
+
```env
|
| 49 |
+
MONGO_URI=mongodb://localhost:27017
|
| 50 |
+
MONGO_DB=scrapdji
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
### 3. Typesense (Optionnel)
|
| 54 |
+
|
| 55 |
+
#### Option A : Docker (Recommandé)
|
| 56 |
+
```bash
|
| 57 |
+
# Installer Docker Desktop depuis https://www.docker.com/products/docker-desktop/
|
| 58 |
+
# Puis lancer :
|
| 59 |
+
docker run -p 8108:8108 -v typesense-data:/data typesense/typesense:latest --data-dir /data --api-key=xyz
|
| 60 |
+
```
|
| 61 |
+
|
| 62 |
+
#### Option B : Exécutable Windows
|
| 63 |
+
1. Aller sur https://github.com/typesense/typesense/releases
|
| 64 |
+
2. Télécharger `typesense-server-0.25.0-windows-amd64.exe`
|
| 65 |
+
3. Créer un dossier `C:\typesense`
|
| 66 |
+
4. Copier l'exécutable et créer un fichier `config.json` :
|
| 67 |
+
```json
|
| 68 |
+
{
|
| 69 |
+
"api-key": "xyz",
|
| 70 |
+
"data-dir": "C:\\typesense\\data",
|
| 71 |
+
"enable-cors": true
|
| 72 |
+
}
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
#### Démarrer Typesense :
|
| 76 |
+
```bash
|
| 77 |
+
# Créer un service Windows
|
| 78 |
+
sc create Typesense binPath= "C:\typesense\typesense-server.exe --config=C:\typesense\config.json"
|
| 79 |
+
sc start Typesense
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
### 4. Qdrant (Optionnel)
|
| 83 |
+
|
| 84 |
+
#### Option A : Docker
|
| 85 |
+
```bash
|
| 86 |
+
docker run -p 6333:6333 qdrant/qdrant
|
| 87 |
+
```
|
| 88 |
+
|
| 89 |
+
#### Option B : Exécutable Windows
|
| 90 |
+
1. Aller sur https://github.com/qdrant/qdrant/releases
|
| 91 |
+
2. Télécharger `qdrant-1.7.4-x86_64-pc-windows-msvc.zip`
|
| 92 |
+
3. Extraire dans `C:\qdrant`
|
| 93 |
+
4. Créer un service :
|
| 94 |
+
```bash
|
| 95 |
+
sc create Qdrant binPath= "C:\qdrant\qdrant.exe"
|
| 96 |
+
sc start Qdrant
|
| 97 |
+
```
|
| 98 |
+
|
| 99 |
+
## 🔧 Configuration automatique
|
| 100 |
+
|
| 101 |
+
### Script de configuration Windows :
|
| 102 |
+
```powershell
|
| 103 |
+
# Exécuter en tant qu'administrateur
|
| 104 |
+
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
| 105 |
+
|
| 106 |
+
# Lancer la configuration
|
| 107 |
+
python setup.py --windows
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
### Vérification des services :
|
| 111 |
+
```powershell
|
| 112 |
+
# Vérifier PostgreSQL
|
| 113 |
+
Get-Service -Name "postgresql*"
|
| 114 |
+
|
| 115 |
+
# Vérifier MongoDB
|
| 116 |
+
Get-Service -Name "MongoDB"
|
| 117 |
+
|
| 118 |
+
# Vérifier les ports
|
| 119 |
+
netstat -an | findstr ":5432"
|
| 120 |
+
netstat -an | findstr ":27017"
|
| 121 |
+
netstat -an | findstr ":8108"
|
| 122 |
+
netstat -an | findstr ":6333"
|
| 123 |
+
```
|
| 124 |
+
|
| 125 |
+
## 🚀 Lancement du scraping
|
| 126 |
+
|
| 127 |
+
### 1. Préparation :
|
| 128 |
+
```bash
|
| 129 |
+
# Activer l'environnement virtuel
|
| 130 |
+
venv\Scripts\activate
|
| 131 |
+
|
| 132 |
+
# Installer les dépendances
|
| 133 |
+
pip install -r requirements.txt
|
| 134 |
+
```
|
| 135 |
+
|
| 136 |
+
### 2. Configuration :
|
| 137 |
+
```bash
|
| 138 |
+
# Copier les fichiers de configuration
|
| 139 |
+
copy config.env.example .env
|
| 140 |
+
copy sources.json.example sources.json
|
| 141 |
+
|
| 142 |
+
# Éditer .env avec vos paramètres
|
| 143 |
+
notepad .env
|
| 144 |
+
```
|
| 145 |
+
|
| 146 |
+
### 3. Test :
|
| 147 |
+
```bash
|
| 148 |
+
# Test sans sauvegarde
|
| 149 |
+
python run_scraper.py --dry-run
|
| 150 |
+
|
| 151 |
+
# Lancement complet
|
| 152 |
+
python run_scraper.py
|
| 153 |
+
```
|
| 154 |
+
|
| 155 |
+
## 🐛 Dépannage Windows
|
| 156 |
+
|
| 157 |
+
### Erreur PostgreSQL :
|
| 158 |
+
```bash
|
| 159 |
+
# Vérifier le service
|
| 160 |
+
sc query postgresql-x64-15
|
| 161 |
+
|
| 162 |
+
# Redémarrer si nécessaire
|
| 163 |
+
sc stop postgresql-x64-15
|
| 164 |
+
sc start postgresql-x64-15
|
| 165 |
+
|
| 166 |
+
# Vérifier la connexion
|
| 167 |
+
psql -U postgres -c "SELECT 1;"
|
| 168 |
+
```
|
| 169 |
+
|
| 170 |
+
### Erreur MongoDB :
|
| 171 |
+
```bash
|
| 172 |
+
# Vérifier le service
|
| 173 |
+
sc query MongoDB
|
| 174 |
+
|
| 175 |
+
# Redémarrer si nécessaire
|
| 176 |
+
net stop MongoDB
|
| 177 |
+
net start MongoDB
|
| 178 |
+
|
| 179 |
+
# Vérifier la connexion
|
| 180 |
+
mongosh --eval "db.runCommand('ping')"
|
| 181 |
+
```
|
| 182 |
+
|
| 183 |
+
### Erreur de permissions :
|
| 184 |
+
```powershell
|
| 185 |
+
# Donner les permissions à l'utilisateur
|
| 186 |
+
icacls "C:\Program Files\PostgreSQL" /grant "Users":(OI)(CI)F
|
| 187 |
+
icacls "C:\ProgramData\MongoDB" /grant "Users":(OI)(CI)F
|
| 188 |
+
```
|
| 189 |
+
|
| 190 |
+
### Erreur de port déjà utilisé :
|
| 191 |
+
```bash
|
| 192 |
+
# Voir qui utilise le port
|
| 193 |
+
netstat -ano | findstr :5432
|
| 194 |
+
|
| 195 |
+
# Tuer le processus si nécessaire
|
| 196 |
+
taskkill /PID <PID> /F
|
| 197 |
+
```
|
| 198 |
+
|
| 199 |
+
## 📊 Monitoring Windows
|
| 200 |
+
|
| 201 |
+
### Services à surveiller :
|
| 202 |
+
```powershell
|
| 203 |
+
# Script de monitoring
|
| 204 |
+
Get-Service -Name "postgresql*", "MongoDB" | Select-Object Name, Status, StartType
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
### Logs Windows :
|
| 208 |
+
```powershell
|
| 209 |
+
# Voir les logs d'événements
|
| 210 |
+
Get-EventLog -LogName Application -Source "PostgreSQL*" -Newest 10
|
| 211 |
+
Get-EventLog -LogName Application -Source "MongoDB*" -Newest 10
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
### Performance :
|
| 215 |
+
```powershell
|
| 216 |
+
# Surveiller les ressources
|
| 217 |
+
Get-Counter "\Processor(_Total)\% Processor Time"
|
| 218 |
+
Get-Counter "\Memory\Available MBytes"
|
| 219 |
+
```
|
| 220 |
+
|
| 221 |
+
## 🔒 Sécurité Windows
|
| 222 |
+
|
| 223 |
+
### Firewall :
|
| 224 |
+
```powershell
|
| 225 |
+
# Autoriser PostgreSQL
|
| 226 |
+
netsh advfirewall firewall add rule name="PostgreSQL" dir=in action=allow protocol=TCP localport=5432
|
| 227 |
+
|
| 228 |
+
# Autoriser MongoDB
|
| 229 |
+
netsh advfirewall firewall add rule name="MongoDB" dir=in action=allow protocol=TCP localport=27017
|
| 230 |
+
```
|
| 231 |
+
|
| 232 |
+
### Antivirus :
|
| 233 |
+
- Ajouter les dossiers du projet aux exclusions
|
| 234 |
+
- Autoriser Python et les services dans l'antivirus
|
| 235 |
+
|
| 236 |
+
## 📋 Checklist d'installation
|
| 237 |
+
|
| 238 |
+
- [ ] PostgreSQL installé et configuré
|
| 239 |
+
- [ ] MongoDB installé et démarré
|
| 240 |
+
- [ ] Typesense installé (optionnel)
|
| 241 |
+
- [ ] Qdrant installé (optionnel)
|
| 242 |
+
- [ ] Environnement virtuel créé
|
| 243 |
+
- [ ] Dépendances installées
|
| 244 |
+
- [ ] Fichier .env configuré
|
| 245 |
+
- [ ] Fichier sources.json configuré
|
| 246 |
+
- [ ] Test --dry-run réussi
|
| 247 |
+
- [ ] Premier scraping lancé
|
| 248 |
+
|
| 249 |
+
## 🎯 Prochaines étapes
|
| 250 |
+
|
| 251 |
+
1. **Configurer vos sources** dans `sources.json`
|
| 252 |
+
2. **Optimiser les paramètres** selon vos besoins
|
| 253 |
+
3. **Automatiser le scraping** avec Task Scheduler
|
| 254 |
+
4. **Monitorer les performances** avec les outils Windows
|
| 255 |
+
5. **Sauvegarder régulièrement** les bases de données
|
OPTIMIZATION_STRATEGIES.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Stratégie d'Optimisation : "Performance Max, Coût Mini"
|
| 2 |
+
|
| 3 |
+
Vous avez raison : le passage au "Big Data" multimédia (OCR, Vidéo) explose les coûts si on reste sur une approche naïve. Voici comment diviser la facture par 10 grâce à l'ingénierie logicielle.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 🏗️ 1. Architecture "Tiered Storage" (Le Stockage Intelligent)
|
| 8 |
+
|
| 9 |
+
Le secret n'est pas de tout garder en RAM, mais de hiérarchiser la donnée.
|
| 10 |
+
|
| 11 |
+
* **Hot Data (RAM / NVMe)** : Les vecteurs de recherche et métadonnées récentes (3 derniers mois).
|
| 12 |
+
* **Warm Data (SSD Standard)** : Les textes intégraux.
|
| 13 |
+
* **Cold Data (Object Storage S3/MinIO)** : Les fichiers originaux (PDF, Images, Vidéos) et les archives vieilles.
|
| 14 |
+
|
| 15 |
+
> **L'Astuce :** Qdrant permet de garder les vecteurs sur disque (Mmap) et seulement l'index de navigation (HNSW) en RAM.
|
| 16 |
+
> **Gain** : On passe de 128 Go RAM nécessaire à **16-32 Go RAM** pour le même volume de données.
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## 📉 2. La Quantisation (Compression Vectorielle)
|
| 21 |
+
|
| 22 |
+
C'est la révolution récente (2023-2024) dans les bases vectorielles. Au lieu de stocker des nombres à virgule flottante (Float32 -> 4 octets), on les compresse.
|
| 23 |
+
|
| 24 |
+
* **Binary Quantization** : Transforme les vecteurs en 0 et 1.
|
| 25 |
+
* **Réduction** : **32x moins de RAM**.
|
| 26 |
+
* **Perte de précision** : Minime (< 5%) pour la recherche sémantique.
|
| 27 |
+
* **Vitesse** : Recherche 40x plus rapide.
|
| 28 |
+
* **Scalar Quantization (Int8)** :
|
| 29 |
+
* **Réduction** : **4x moins de RAM**.
|
| 30 |
+
|
| 31 |
+
> **Impact Coût :** Vous pouvez faire tourner un index de 10-20 Millions de documents sur un simple serveur standard (32 Go RAM) au lieu d'un monstre.
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
## 🖼️ 3. Pipeline Multimédia "Paresseux" (Lazy Processing)
|
| 36 |
+
|
| 37 |
+
Ne traitez pas tout, tout de suite. L'OCR et l'analyse vidéo coûtent cher en CPU/GPU.
|
| 38 |
+
|
| 39 |
+
### Stratégie "On-Demand" & "Tri"
|
| 40 |
+
1. **Ingestion Rapide** : Téléchargez et stockez le fichier brut (Image/Vidéo) sur S3 (frais minimes).
|
| 41 |
+
2. **Filtrage Intelligent** : N'envoyez à l'OCR/Vision AI que les fichiers pertinents (ex: images contenant du texte détecté par un algo léger, ou vidéos avec des mots-clés dans le titre/description).
|
| 42 |
+
3. **Traitement Vidéo** :
|
| 43 |
+
* Ne traitez pas la vidéo -> Extrayez 1 frame toutes les 5 secondes.
|
| 44 |
+
* N'écoutez pas tout -> Transcrivez (Whisper Small) uniquement l'audio.
|
| 45 |
+
|
| 46 |
+
---
|
| 47 |
+
|
| 48 |
+
## 🤖 4. Modèles "Small is Beautiful"
|
| 49 |
+
|
| 50 |
+
Pour l'Afrique, les modèles géants (Llama-3-70B) sont souvent overkill et lents.
|
| 51 |
+
|
| 52 |
+
* **Modèles d'Embedding** : Utilisez des modèles "distillés" (ex: `paraphrase-multilingual-MiniLM-L12-v2`). Ils sont minuscules, rapides et très performants pour la recherche multilingue.
|
| 53 |
+
* **OCR** : `PaddleOCR` (léger et rapide) vs Tesseract (vieux et lent) ou API Google (chère).
|
| 54 |
+
|
| 55 |
+
---
|
| 56 |
+
|
| 57 |
+
## ⚡ 5. L'Infrastructure Optimisée (Exemple Concret)
|
| 58 |
+
|
| 59 |
+
Voici comment on tient la charge avec un budget divisé par 5 :
|
| 60 |
+
|
| 61 |
+
### Nouvelle Config "Optimisée"
|
| 62 |
+
* **Serveur Principal** (Api + Vector DB): VPS 8 vCPU / 32 Go RAM.
|
| 63 |
+
* *Techno* : Qdrant (Binary Quantization + Mmap storage).
|
| 64 |
+
* **Serveur Workers (Scraping/OCR)** : Instances "Spot" (jetables, -60% du prix).
|
| 65 |
+
* On en lance 10 quand on en a besoin, on les tue après.
|
| 66 |
+
* **Stockage Froid** : MinIO (Self-hosted) ou S3 (Wasabi = 6$/To).
|
| 67 |
+
|
| 68 |
+
**Résultat :**
|
| 69 |
+
* **Coût Mensuel** : ~100-150$ / mois (au lieu de 600$+).
|
| 70 |
+
* **Capacité** : ~50 Millions de documents.
|
| 71 |
+
* **Multimédia** : Traitement asynchrone la nuit ou sur demande.
|
| 72 |
+
|
| 73 |
+
---
|
| 74 |
+
|
| 75 |
+
## 🎯 Conclusion
|
| 76 |
+
|
| 77 |
+
L'argument "Imbattable" évolue :
|
| 78 |
+
> **"Une intelligence souveraine frugal et agile, capable de tourner sur des infrastructures locales standards."**
|
| 79 |
+
|
| 80 |
+
C'est encore plus pertinent pour l'Afrique : on ne dépend pas non plus des datacenters géants.
|
PROJECT_ANALYSIS.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Analyse Détaillée du Projet Scrap-Dji
|
| 2 |
+
|
| 3 |
+
## 1. Vue d'ensemble de l'Architecture
|
| 4 |
+
|
| 5 |
+
Le projet **Scrap-Dji** est une plateforme modulaire conçue pour la collecte, le traitement et l'indexation de contenus panafricains. L'architecture suit une approche pipeline classique :
|
| 6 |
+
|
| 7 |
+
1. **Collecte (`/scraper`)** : Utilise divers outils (Scrapy, Newspaper3k, Snscrape, Selenium) pour extraire des données brutes de sites web, réseaux sociaux et documents.
|
| 8 |
+
2. **Traitement (`/parser`)** : Nettoyage HTML, dédoublonnage par hashage, et structuration des données.
|
| 9 |
+
3. **Stockage (`/db` & `/storage`)** : Hybride entre relationnel (PostgreSQL pour la structure) et NoSQL (MongoDB pour la flexibilité).
|
| 10 |
+
4. **Indexation (`/indexer`)** : Double indexation pour la recherche textuelle (Typesense/Meilisearch) et sémantique (Qdrant).
|
| 11 |
+
5. **Exposition (`/api` & `/frontend`)** : API FastAPI et une interface de recherche minimaliste.
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## 2. Réponses à vos Questions Spécifiques
|
| 16 |
+
|
| 17 |
+
### 🧠 Que peut-on faire concrètement avec les données ?
|
| 18 |
+
Une fois les données scrapées, les possibilités sont vastes :
|
| 19 |
+
- **Moteur de recherche souverain** : Créer un portail de recherche spécialisé sur les thématiques africaines.
|
| 20 |
+
- **Analyse de tendances (OSINT)** : Suivre les sujets brûlants sur les réseaux sociaux et dans la presse de différents pays africains.
|
| 21 |
+
- **Entraînement d'IA** : Utiliser les textes pour fine-tuner des modèles de langage (LLM) sur des contextes ou langues africaines.
|
| 22 |
+
- **Veille médiatique** : Automatiser la surveillance de sources spécifiques pour des rapports quotidiens.
|
| 23 |
+
|
| 24 |
+
### 📊 Quelles sont les données récupérées ?
|
| 25 |
+
D'après les modèles (`db/models.py`), le projet capture :
|
| 26 |
+
- **Identifiants** : UUID unique pour chaque document.
|
| 27 |
+
- **Contenu** : Titre, texte intégral, langue, tags.
|
| 28 |
+
- **Métadonnées** : Auteur, pays d'origine, date de publication, type de document (article, social, PDF, image).
|
| 29 |
+
- **Source** : URL d'origine (essentiel pour la traçabilité).
|
| 30 |
+
- **Relations** : Liens vers des versions antérieures du texte (historisation) et chemins vers les images téléchargées.
|
| 31 |
+
|
| 32 |
+
### 🔄 Reprise du scraping et doublons
|
| 33 |
+
**Oui, le script gère la reprise sans répétition.**
|
| 34 |
+
Dans `scraper/main.py` (ligne 108), la méthode `save_document` vérifie si l'URL existe déjà en base (`source_url`).
|
| 35 |
+
- Si l'URL existe : il crée une nouvelle **version** du texte (`DocumentVersion`) mais ne recrée pas le document principal.
|
| 36 |
+
- Pour **Mongo, Typesense et Qdrant**, le code utilise des opérations `upsert` (mise à jour si existe, sinon insertion), ce qui garantit l'absence de doublons identiques.
|
| 37 |
+
|
| 38 |
+
### 💻 Configuration sans installation locale (Windows)
|
| 39 |
+
Comme vous n'avez pas Mongo/Typesense/Qdrant installé, voici les alternatives :
|
| 40 |
+
1. **Docker (Recommandé)** : C'est le plus simple. Installez Docker Desktop et utilisez un fichier `docker-compose.yml` pour lancer tous les services en une commande.
|
| 41 |
+
2. **Cloud (Gratuit/Freemium)** :
|
| 42 |
+
- **MongoDB** : Utilisez [MongoDB Atlas](https://www.mongodb.com/cloud/atlas).
|
| 43 |
+
- **Typesense** : [Typesense Cloud](https://cloud.typesense.org/).
|
| 44 |
+
- **Qdrant** : [Qdrant Cloud](https://qdrant.tech/cloud/).
|
| 45 |
+
3. **Désactivation temporaire** : Vous pouvez modifier `scraper/main.py` pour commenter les appels aux indexeurs que vous ne possédez pas encore.
|
| 46 |
+
|
| 47 |
+
### ⚡ Ressources et Performance
|
| 48 |
+
Le scraper sera-t-il gourmand ?
|
| 49 |
+
- **CPU/RAM** : Le scraping de base est léger. Par contre, si vous utilisez **Selenium** pour des sites dynamiques ou **SpaCy** pour l'enrichissement NLP, la consommation grimpera significativement.
|
| 50 |
+
- **Réseau** : Dépend de `SCRAPER_CONCURRENT_REQUESTS` (actuellement 16). 16 requêtes simultanées est raisonnable pour une connexion standard.
|
| 51 |
+
- **Stockage** : Prévoyez de l'espace si vous téléchargez beaucoup d'images.
|
| 52 |
+
|
| 53 |
+
---
|
| 54 |
+
|
| 55 |
+
## 3. Configuration Git recommandée
|
| 56 |
+
|
| 57 |
+
Pour éviter de commiter vos secrets et votre environnement, assurez-vous que votre `.gitignore` contient :
|
| 58 |
+
|
| 59 |
+
```gitignore
|
| 60 |
+
# Environnement virtuel
|
| 61 |
+
venv/
|
| 62 |
+
.venv/
|
| 63 |
+
env/
|
| 64 |
+
|
| 65 |
+
# Secrets et config
|
| 66 |
+
.env
|
| 67 |
+
sources.json
|
| 68 |
+
|
| 69 |
+
# Données locales
|
| 70 |
+
storage_data/
|
| 71 |
+
logs/
|
| 72 |
+
data/
|
| 73 |
+
temp/
|
| 74 |
+
datasets/
|
| 75 |
+
db/*.sqlite
|
| 76 |
+
|
| 77 |
+
# Python
|
| 78 |
+
__pycache__/
|
| 79 |
+
*.py[cod]
|
| 80 |
+
*$py.class
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
---
|
| 84 |
+
|
| 85 |
+
## 4. Analyse de l'état actuel (Détails)
|
| 86 |
+
|
| 87 |
+
- **Force** : Architecture très propre et modulaire. Le choix d'une double base (Postgres/Mongo) et d'un double moteur de recherche est très pro.
|
| 88 |
+
- **Points à améliorer** :
|
| 89 |
+
- L'API (`api/main.py`) est encore vide (ne fait pas de vraie recherche).
|
| 90 |
+
- Le frontend est ultra-basique (uniquement JSON brut).
|
| 91 |
+
- Le mode `dry-run` (test sans sauvegarde) n'est pas encore implémenté.
|
QUICKSTART.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Guide de démarrage rapide - Scrap-Dji
|
| 2 |
+
|
| 3 |
+
## 🚀 Démarrage en 5 minutes
|
| 4 |
+
|
| 5 |
+
### 1. Installation rapide
|
| 6 |
+
```bash
|
| 7 |
+
# Activer l'environnement virtuel
|
| 8 |
+
venv\Scripts\activate # Windows
|
| 9 |
+
# ou
|
| 10 |
+
source venv/bin/activate # Linux/Mac
|
| 11 |
+
|
| 12 |
+
# Installer les dépendances
|
| 13 |
+
pip install -r requirements.txt
|
| 14 |
+
```
|
| 15 |
+
|
| 16 |
+
### 2. Configuration automatique
|
| 17 |
+
```bash
|
| 18 |
+
# Lance la configuration automatique
|
| 19 |
+
python setup.py
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
### 3. Configuration manuelle (si nécessaire)
|
| 23 |
+
```bash
|
| 24 |
+
# Copier les fichiers de configuration
|
| 25 |
+
copy config.env.example .env
|
| 26 |
+
copy sources.json.example sources.json
|
| 27 |
+
|
| 28 |
+
# Éditer .env avec vos paramètres
|
| 29 |
+
notepad .env
|
| 30 |
+
|
| 31 |
+
# Éditer sources.json avec vos sources
|
| 32 |
+
notepad sources.json
|
| 33 |
+
```
|
| 34 |
+
|
| 35 |
+
### 4. Test rapide
|
| 36 |
+
```bash
|
| 37 |
+
# Test sans sauvegarde
|
| 38 |
+
python run_scraper.py --dry-run
|
| 39 |
+
|
| 40 |
+
# Lancement complet
|
| 41 |
+
python run_scraper.py
|
| 42 |
+
```
|
| 43 |
+
|
| 44 |
+
## 📋 Configuration minimale
|
| 45 |
+
|
| 46 |
+
### Fichier .env minimal
|
| 47 |
+
```env
|
| 48 |
+
POSTGRES_URI=postgresql://user:password@localhost:5432/scrapdji
|
| 49 |
+
MONGO_URI=mongodb://localhost:27017
|
| 50 |
+
MONGO_DB=scrapdji
|
| 51 |
+
STORAGE_PATH=./storage_data
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
### Fichier sources.json minimal
|
| 55 |
+
```json
|
| 56 |
+
{
|
| 57 |
+
"sources": [
|
| 58 |
+
{
|
| 59 |
+
"name": "test_site",
|
| 60 |
+
"type": "news",
|
| 61 |
+
"url": "https://example.com",
|
| 62 |
+
"selectors": {
|
| 63 |
+
"title": "h1",
|
| 64 |
+
"content": "p"
|
| 65 |
+
},
|
| 66 |
+
"pays": "Test",
|
| 67 |
+
"langue": "fr",
|
| 68 |
+
"active": true
|
| 69 |
+
}
|
| 70 |
+
]
|
| 71 |
+
}
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
## 🔧 Services requis
|
| 75 |
+
|
| 76 |
+
### PostgreSQL
|
| 77 |
+
```bash
|
| 78 |
+
# Installation Windows
|
| 79 |
+
# Télécharger depuis https://www.postgresql.org/download/windows/
|
| 80 |
+
|
| 81 |
+
# Créer la base
|
| 82 |
+
createdb scrapdji
|
| 83 |
+
```
|
| 84 |
+
|
| 85 |
+
### MongoDB
|
| 86 |
+
```bash
|
| 87 |
+
# Installation Windows
|
| 88 |
+
# Télécharger depuis https://www.mongodb.com/try/download/community
|
| 89 |
+
|
| 90 |
+
# Démarrer le service
|
| 91 |
+
net start MongoDB
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
## 🐛 Dépannage
|
| 95 |
+
|
| 96 |
+
### Erreur de connexion PostgreSQL
|
| 97 |
+
- Vérifiez que PostgreSQL est installé et démarré
|
| 98 |
+
- Vérifiez les paramètres de connexion dans .env
|
| 99 |
+
|
| 100 |
+
### Erreur de connexion MongoDB
|
| 101 |
+
- Vérifiez que MongoDB est installé et démarré
|
| 102 |
+
- Vérifiez l'URI dans .env
|
| 103 |
+
|
| 104 |
+
### Erreur d'import
|
| 105 |
+
```bash
|
| 106 |
+
# Réinstaller les dépendances
|
| 107 |
+
pip install -r requirements.txt --force-reinstall
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
### Erreur de scraping
|
| 111 |
+
- Vérifiez que les URLs dans sources.json sont accessibles
|
| 112 |
+
- Vérifiez les sélecteurs CSS
|
| 113 |
+
- Testez avec --dry-run d'abord
|
| 114 |
+
|
| 115 |
+
## 📊 Monitoring
|
| 116 |
+
|
| 117 |
+
### Logs
|
| 118 |
+
```bash
|
| 119 |
+
# Consulter les logs
|
| 120 |
+
tail -f logs/scrapdji.log
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
### Base de données
|
| 124 |
+
```bash
|
| 125 |
+
# Connexion PostgreSQL
|
| 126 |
+
psql -d scrapdji
|
| 127 |
+
|
| 128 |
+
# Connexion MongoDB
|
| 129 |
+
mongosh scrapdji
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
## 🎯 Prochaines étapes
|
| 133 |
+
|
| 134 |
+
1. **Configurer vos sources** : Modifiez `sources.json` avec vos sites cibles
|
| 135 |
+
2. **Optimiser les sélecteurs** : Ajustez les sélecteurs CSS pour chaque site
|
| 136 |
+
3. **Configurer l'indexation** : Activez Typesense/Qdrant pour la recherche
|
| 137 |
+
4. **Déployer l'API** : Lancez l'API FastAPI pour l'interface web
|
| 138 |
+
5. **Automatiser** : Configurez des tâches cron pour le scraping régulier
|
QUICK_START.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 QUICK START - Scrap-Dji pour Hugging Face
|
| 2 |
+
|
| 3 |
+
## ✅ Votre projet est PRÊT !
|
| 4 |
+
|
| 5 |
+
Tous les fichiers nécessaires ont été créés. Voici comment procéder :
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 📋 Étape 1 : Test Local (5 minutes)
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
# 1. Installer les dépendances
|
| 13 |
+
pip install -r requirements.txt
|
| 14 |
+
|
| 15 |
+
# 2. Lancer l'application
|
| 16 |
+
python app.py
|
| 17 |
+
```
|
| 18 |
+
|
| 19 |
+
✅ Ouvrez `http://localhost:7860` dans votre navigateur
|
| 20 |
+
✅ Testez la recherche dans l'onglet "🔍 Recherche"
|
| 21 |
+
✅ Lancez un scraping de test dans l'onglet "🚀 Scraping"
|
| 22 |
+
|
| 23 |
+
### Test de l'API
|
| 24 |
+
|
| 25 |
+
Dans un autre terminal :
|
| 26 |
+
```bash
|
| 27 |
+
python test_api.py
|
| 28 |
+
```
|
| 29 |
+
|
| 30 |
+
Résultat attendu : **6/6 tests réussis** ✅
|
| 31 |
+
|
| 32 |
+
---
|
| 33 |
+
|
| 34 |
+
## 🌐 Étape 2 : Test du Frontend (2 minutes)
|
| 35 |
+
|
| 36 |
+
1. Gardez `python app.py` en cours d'exécution
|
| 37 |
+
2. Ouvrez `frontend_example.html` dans votre navigateur
|
| 38 |
+
3. Testez une recherche
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## ☁️ Étape 3 : Déploiement sur Hugging Face (10 minutes)
|
| 43 |
+
|
| 44 |
+
### 3.1 Créer un Space
|
| 45 |
+
|
| 46 |
+
1. Allez sur https://huggingface.co/new-space
|
| 47 |
+
2. Remplissez :
|
| 48 |
+
- **Space name** : `scrap-dji`
|
| 49 |
+
- **SDK** : Gradio
|
| 50 |
+
- **Visibility** : **Private** ✅
|
| 51 |
+
3. Cliquez sur **Create Space**
|
| 52 |
+
|
| 53 |
+
### 3.2 Pousser le Code
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
# Cloner le Space vide
|
| 57 |
+
git clone https://huggingface.co/spaces/VOTRE_USERNAME/scrap-dji
|
| 58 |
+
cd scrap-dji
|
| 59 |
+
|
| 60 |
+
# Copier TOUS les fichiers du projet Scrap-Dji
|
| 61 |
+
# (Remplacez /path/to/Scrap-Dji par le vrai chemin)
|
| 62 |
+
cp -r /path/to/Scrap-Dji/* .
|
| 63 |
+
|
| 64 |
+
# OU sur Windows PowerShell :
|
| 65 |
+
# Copy-Item -Path "C:\Users\MSI\Desktop\Scrap-Dji\*" -Destination "." -Recurse
|
| 66 |
+
|
| 67 |
+
# Ajouter et pousser
|
| 68 |
+
git add .
|
| 69 |
+
git commit -m "Initial deployment: Scrap-Dji with API"
|
| 70 |
+
git push
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
### 3.3 Vérifier le Déploiement
|
| 74 |
+
|
| 75 |
+
1. Attendez 2-3 minutes (build en cours)
|
| 76 |
+
2. Vérifiez les logs dans l'onglet "Logs" du Space
|
| 77 |
+
3. Une fois "Running" ✅, testez :
|
| 78 |
+
- Interface : `https://VOTRE_USERNAME-scrap-dji.hf.space/`
|
| 79 |
+
- API Docs : `https://VOTRE_USERNAME-scrap-dji.hf.space/docs`
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
## 🔌 Étape 4 : Intégrer dans Votre Frontend
|
| 84 |
+
|
| 85 |
+
Dans votre code frontend, remplacez l'URL :
|
| 86 |
+
|
| 87 |
+
```javascript
|
| 88 |
+
// Avant (test local)
|
| 89 |
+
const API_URL = 'http://localhost:7860';
|
| 90 |
+
|
| 91 |
+
// Après (production)
|
| 92 |
+
const API_URL = 'https://VOTRE_USERNAME-scrap-dji.hf.space';
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
### Exemple de Requête
|
| 96 |
+
|
| 97 |
+
```javascript
|
| 98 |
+
// Recherche
|
| 99 |
+
const response = await fetch(`${API_URL}/api/search`, {
|
| 100 |
+
method: 'POST',
|
| 101 |
+
headers: { 'Content-Type': 'application/json' },
|
| 102 |
+
body: JSON.stringify({
|
| 103 |
+
query: 'économie togo',
|
| 104 |
+
pays: 'Togo',
|
| 105 |
+
limit: 20,
|
| 106 |
+
fuzzy: true
|
| 107 |
+
})
|
| 108 |
+
});
|
| 109 |
+
|
| 110 |
+
const data = await response.json();
|
| 111 |
+
console.log(data.results);
|
| 112 |
+
|
| 113 |
+
// Statistiques
|
| 114 |
+
const stats = await fetch(`${API_URL}/api/stats`);
|
| 115 |
+
const statsData = await stats.json();
|
| 116 |
+
console.log(statsData);
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
## 📚 Documentation Complète
|
| 122 |
+
|
| 123 |
+
- **Guide de déploiement** : [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md)
|
| 124 |
+
- **Walkthrough complet** : Voir les artifacts
|
| 125 |
+
- **Exemple frontend** : [frontend_example.html](frontend_example.html)
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
## 🎯 Endpoints API Disponibles
|
| 130 |
+
|
| 131 |
+
| Endpoint | Méthode | Description |
|
| 132 |
+
|----------|---------|-------------|
|
| 133 |
+
| `/api/search` | POST/GET | Recherche avec filtres |
|
| 134 |
+
| `/api/stats` | GET | Statistiques |
|
| 135 |
+
| `/api/documents` | GET | Liste paginée |
|
| 136 |
+
| `/api/documents/{id}` | GET | Document par ID |
|
| 137 |
+
| `/api/health` | GET | Health check |
|
| 138 |
+
| `/docs` | GET | Documentation Swagger |
|
| 139 |
+
|
| 140 |
+
---
|
| 141 |
+
|
| 142 |
+
## ⚡ Fonctionnalités Clés
|
| 143 |
+
|
| 144 |
+
✅ **Recherche permissive** : Tolère les fautes de frappe
|
| 145 |
+
✅ **Filtres** : Par pays, langue
|
| 146 |
+
✅ **Scoring** : Résultats classés par pertinence
|
| 147 |
+
✅ **12 sources** : 8 Togo + 4 Bénin
|
| 148 |
+
✅ **API REST** : 7 endpoints pour votre frontend
|
| 149 |
+
✅ **Interface Gradio** : 4 onglets interactifs
|
| 150 |
+
|
| 151 |
+
---
|
| 152 |
+
|
| 153 |
+
## 🆘 Besoin d'Aide ?
|
| 154 |
+
|
| 155 |
+
- **Problème de build** : Vérifiez les logs dans l'onglet "Logs" du Space
|
| 156 |
+
- **API ne répond pas** : Vérifiez que le Space est "Running"
|
| 157 |
+
- **Base vide** : Lancez un scraping depuis l'interface Gradio
|
| 158 |
+
|
| 159 |
+
---
|
| 160 |
+
|
| 161 |
+
## 🎉 C'est Tout !
|
| 162 |
+
|
| 163 |
+
Votre projet est **production-ready**. Suivez les étapes ci-dessus et vous serez déployé en 15 minutes ! 🚀
|
| 164 |
+
|
| 165 |
+
**Bon déploiement !** 🌍
|
README.md
CHANGED
|
@@ -1,12 +1,148 @@
|
|
| 1 |
---
|
| 2 |
-
title: Scrap
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Scrap-Dji - Base de Connaissance Panafricaine
|
| 3 |
+
emoji: 🌍
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# 🌍 Scrap-Dji - Base de Connaissance Panafricaine
|
| 14 |
+
|
| 15 |
+
Système de scraping et de recherche de contenus africains (Togo, Bénin, Afrique).
|
| 16 |
+
|
| 17 |
+
## 🚀 Fonctionnalités
|
| 18 |
+
|
| 19 |
+
### 🔍 Recherche Intelligente
|
| 20 |
+
- **Recherche permissive** avec tolérance aux fautes de frappe (fuzzy matching)
|
| 21 |
+
- **Filtres avancés** par pays, langue, date
|
| 22 |
+
- **Scoring de pertinence** pour des résultats optimaux
|
| 23 |
+
- **API REST complète** pour intégration frontend
|
| 24 |
+
|
| 25 |
+
### 📰 Scraping Multi-Sources
|
| 26 |
+
- Collecte automatique depuis sources togolaises et béninoises
|
| 27 |
+
- Extraction intelligente de contenu (titre, texte, métadonnées)
|
| 28 |
+
- Déduplication automatique
|
| 29 |
+
- Stockage persistant
|
| 30 |
+
|
| 31 |
+
### 📊 Statistiques
|
| 32 |
+
- Répartition par pays, langue, source
|
| 33 |
+
- Visualisation des données collectées
|
| 34 |
+
- Métriques en temps réel
|
| 35 |
+
|
| 36 |
+
## 🔌 API Endpoints
|
| 37 |
+
|
| 38 |
+
### Recherche
|
| 39 |
+
```bash
|
| 40 |
+
# POST avec JSON
|
| 41 |
+
curl -X POST "https://YOUR_SPACE.hf.space/api/search" \
|
| 42 |
+
-H "Content-Type: application/json" \
|
| 43 |
+
-d '{"query": "économie togo", "limit": 10, "fuzzy": true}'
|
| 44 |
+
|
| 45 |
+
# GET simple
|
| 46 |
+
curl "https://YOUR_SPACE.hf.space/api/search?q=politique&pays=Togo&limit=20"
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
### Statistiques
|
| 50 |
+
```bash
|
| 51 |
+
curl "https://YOUR_SPACE.hf.space/api/stats"
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
### Documents
|
| 55 |
+
```bash
|
| 56 |
+
# Liste paginée
|
| 57 |
+
curl "https://YOUR_SPACE.hf.space/api/documents?skip=0&limit=10"
|
| 58 |
+
|
| 59 |
+
# Document par ID
|
| 60 |
+
curl "https://YOUR_SPACE.hf.space/api/documents/{id}"
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
### Health Check
|
| 64 |
+
```bash
|
| 65 |
+
curl "https://YOUR_SPACE.hf.space/api/health"
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
## 📖 Documentation Interactive
|
| 69 |
+
|
| 70 |
+
Une fois déployé, accédez à la documentation Swagger interactive :
|
| 71 |
+
- **Swagger UI** : `https://YOUR_SPACE.hf.space/docs`
|
| 72 |
+
- **ReDoc** : `https://YOUR_SPACE.hf.space/redoc`
|
| 73 |
+
|
| 74 |
+
## 🛠️ Technologies
|
| 75 |
+
|
| 76 |
+
- **Backend** : FastAPI + Gradio
|
| 77 |
+
- **Scraping** : newspaper3k, BeautifulSoup, lxml
|
| 78 |
+
- **NLP** : NLTK, langdetect
|
| 79 |
+
- **Recherche** : Moteur local avec fuzzy matching
|
| 80 |
+
|
| 81 |
+
## 📝 Utilisation
|
| 82 |
+
|
| 83 |
+
### Interface Web
|
| 84 |
+
Accédez directement à l'interface Gradio pour :
|
| 85 |
+
1. Effectuer des recherches
|
| 86 |
+
2. Lancer le scraping
|
| 87 |
+
3. Consulter les statistiques
|
| 88 |
+
|
| 89 |
+
### Intégration Frontend
|
| 90 |
+
|
| 91 |
+
```javascript
|
| 92 |
+
// Exemple de recherche depuis votre frontend
|
| 93 |
+
const response = await fetch('https://YOUR_SPACE.hf.space/api/search', {
|
| 94 |
+
method: 'POST',
|
| 95 |
+
headers: { 'Content-Type': 'application/json' },
|
| 96 |
+
body: JSON.stringify({
|
| 97 |
+
query: 'économie togo',
|
| 98 |
+
pays: 'Togo',
|
| 99 |
+
limit: 20,
|
| 100 |
+
fuzzy: true
|
| 101 |
+
})
|
| 102 |
+
});
|
| 103 |
+
|
| 104 |
+
const data = await response.json();
|
| 105 |
+
console.log(data.results);
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
## 🌍 Sources Couvertes
|
| 109 |
+
|
| 110 |
+
### Togo
|
| 111 |
+
- TogoFirst
|
| 112 |
+
- 27septembre
|
| 113 |
+
- IciLome
|
| 114 |
+
- TogoBreakingNews
|
| 115 |
+
- RepublicOfTogo
|
| 116 |
+
- TogoActualite
|
| 117 |
+
- LomeInfo
|
| 118 |
+
- TogoSite
|
| 119 |
+
|
| 120 |
+
### Bénin
|
| 121 |
+
- BeninWebTV
|
| 122 |
+
- La Nouvelle République
|
| 123 |
+
- (Plus de sources à venir)
|
| 124 |
+
|
| 125 |
+
## 📄 License
|
| 126 |
+
|
| 127 |
+
MIT License - Libre d'utilisation et de modification
|
| 128 |
+
|
| 129 |
+
## 👨💻 Développement
|
| 130 |
+
|
| 131 |
+
Pour contribuer ou déployer localement :
|
| 132 |
+
|
| 133 |
+
```bash
|
| 134 |
+
# Cloner le projet
|
| 135 |
+
git clone https://huggingface.co/spaces/YOUR_USERNAME/scrap-dji
|
| 136 |
+
|
| 137 |
+
# Installer les dépendances
|
| 138 |
+
pip install -r requirements.txt
|
| 139 |
+
|
| 140 |
+
# Lancer l'application
|
| 141 |
+
python app.py
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
L'application sera accessible sur `http://localhost:7860`
|
| 145 |
+
|
| 146 |
+
---
|
| 147 |
+
|
| 148 |
+
**Développé avec ❤️ pour l'Afrique**
|
RESOURCE_ESTIMATION.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Estimation des Ressources : Projet "Dataset Souverain & IA"
|
| 2 |
+
|
| 3 |
+
Ce document détaille les prérequis techniques pour passer d'un simple scraper à une **usine de données pour l'IA** (LLM Fine-tuning & RAG).
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 1. 💾 Stockage : Le Nerf de la Guerre
|
| 8 |
+
|
| 9 |
+
Pour un dataset "conséquent" (visant le fine-tuning d'un LLM 7B ou 13B paramètres sur des contextes africains), on ne parle plus en gigaoctets mais en téraoctets.
|
| 10 |
+
|
| 11 |
+
### Estimation pour 10 Millions d'articles/documents (High Quality) :
|
| 12 |
+
* **Données Brutes (HTML/PDF)** : ~500 Go - 1 To (pour archives et traçabilité).
|
| 13 |
+
* **Données Nettoyées (Texte brut/JSON)** : ~50 Go - 100 Go (TEXT ONLY).
|
| 14 |
+
* **Index Vectoriel (RAG)** : C'est le plus lourd.
|
| 15 |
+
* Si on découpe (chunk) chaque doc en 4 segments.
|
| 16 |
+
* 40 millions de vecteurs (768 dimensions float32).
|
| 17 |
+
* **Taille RAM/Disque Qdrant/Typesense** : ~150 Go - 200 Go uniquement pour les vecteurs.
|
| 18 |
+
|
| 19 |
+
**Recommandation Disque :** **2 To NVMe SSD** minimum. Les disques durs classiques (HDD) tueront les performances de votre moteur de recherche vectoriel.
|
| 20 |
+
|
| 21 |
+
---
|
| 22 |
+
|
| 23 |
+
## 2. 🖥️ Matériel pour le Scraper (L'Usine)
|
| 24 |
+
|
| 25 |
+
Le scraping n'est pas limité par le CPU, mais par la **bande passante** et les **IOPS** (écritures disque).
|
| 26 |
+
|
| 27 |
+
### Configuration "Scraper Industriel" (Minimum)
|
| 28 |
+
* **CPU** : 4 - 8 vCPUs (pour gérer le parsing HTML/PDF en parallèle).
|
| 29 |
+
* **RAM** : 16 Go - 32 Go (Chrome/Selenium consomme énormément si utilisé, sinon Scrapy pur est léger).
|
| 30 |
+
* **Réseau** : 1 Gbps stable.
|
| 31 |
+
* **Proxies** : **CRITIQUE**. Pour scraper massivement sans être banni, il faut un pool de proxies rotatifs (frais mensuels à prévoir : 50$-500$/mois selon le volume).
|
| 32 |
+
|
| 33 |
+
---
|
| 34 |
+
|
| 35 |
+
## 3. ⏱️ Estimation du Temps (La Variable "Politesse")
|
| 36 |
+
|
| 37 |
+
Si vous êtes "poli" (1 requête/seconde par domaine) pour ne pas casser les sites cibles :
|
| 38 |
+
|
| 39 |
+
* **Vitesse Moyenne** : 1 page / 2 secondes (avec parsing).
|
| 40 |
+
* **Avec 1 instance** : ~43 000 pages / jour.
|
| 41 |
+
* **Pour atteindre 1 Million de pages** : ~23 jours avec 1 machine.
|
| 42 |
+
* **Accélération** : Parallélisme. Avec 10 workers en parallèle sur des domaines différents -> **2 à 3 jours pour 1 Million de pages**.
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
## 4. 🧠 Structure des Données pour l'IA (LLM & RAG)
|
| 47 |
+
|
| 48 |
+
Pour que la donnée soit utile (et pas juste du bruit), elle doit suivre ce pipeline :
|
| 49 |
+
|
| 50 |
+
### A. Pour le RAG (Recherche Sémantique)
|
| 51 |
+
Les données doivent être "Chunkées" (découpées) intelligemment.
|
| 52 |
+
* **Format** : JSON
|
| 53 |
+
* **Structure** :
|
| 54 |
+
```json
|
| 55 |
+
{
|
| 56 |
+
"id": "uuid",
|
| 57 |
+
"text": "Extrait de texte de 512 tokens...",
|
| 58 |
+
"metadata": {
|
| 59 |
+
"source": "url",
|
| 60 |
+
"date": "2024-01-01",
|
| 61 |
+
"pays": "Sénégal",
|
| 62 |
+
"langue": "wol"
|
| 63 |
+
},
|
| 64 |
+
"vector": [0.12, -0.45, ...] // Généré par modèle d'embedding (ex: Camembert, NLLB)
|
| 65 |
+
}
|
| 66 |
+
```
|
| 67 |
+
|
| 68 |
+
### B. Pour le Fine-Tuning (LLM)
|
| 69 |
+
Les données doivent être nettoyées et formatées en paires "Instruction/Input/Output" ou en texte brut continu de haute qualité.
|
| 70 |
+
* **Format** : JSONL (JSON Lines)
|
| 71 |
+
* **Nettoyage** : Suppression des pubs, menus, bas de pages, scripts, etc. (C'est 80% du travail).
|
| 72 |
+
|
| 73 |
+
---
|
| 74 |
+
|
| 75 |
+
## 5. ⚡ Performance du Moteur de Recherche
|
| 76 |
+
|
| 77 |
+
Pour que la recherche soit "instantanée" (< 100ms) sur des millions de documents :
|
| 78 |
+
|
| 79 |
+
1. **RAM is King** : Qdrant et Typesense doivent tenir l'index en mémoire vive (RAM).
|
| 80 |
+
* Pour 10M vecteurs, prévoyez **64 Go à 128 Go de RAM** sur le serveur de recherche.
|
| 81 |
+
2. **Disque NVMe** : Indispensable pour charger les données à froid.
|
| 82 |
+
3. **HNSW Index** : C'est l'algorithme utilisé par Qdrant. Il est rapide mais gourmand en RAM.
|
| 83 |
+
|
| 84 |
+
---
|
| 85 |
+
|
| 86 |
+
## 📝 Résumé du "Kit de Démarrage Sérieux"
|
| 87 |
+
|
| 88 |
+
Si vous voulez commencer demain un projet pertinent et performant :
|
| 89 |
+
|
| 90 |
+
1. **Serveur Scraper** : VPS 8 vCPU / 16 Go RAM / 1 Gbps (ex: Hetzner, OVH) + Budget Proxies.
|
| 91 |
+
2. **Serveur Base de Données & Recherche** : Serveur Dédié 16 vCPU / 128 Go RAM / 2x1.92 To NVMe (C'est là qu'est la valeur).
|
| 92 |
+
3. **Pipeline** : Scrapy (Collecte) -> Traitement Text (Spacy/Langchain) -> Vectorisation (HuggingFace) -> Qdrant (Index).
|
TOGO_BENIN_ESTIMATION.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Estimation Ciblée : Togo 🇹🇬 & Bénin 🇧🇯
|
| 2 |
+
|
| 3 |
+
Voici une projection réaliste pour "scraper tout le web pertinent" de ces deux pays.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## 📊 1. Estimation du Volume (La Cible)
|
| 8 |
+
|
| 9 |
+
Le web togolais et béninois est moins vaste que le web mondial, mais très dense sur les réseaux sociaux.
|
| 10 |
+
|
| 11 |
+
* **Sites d'Actualités & Blogs** : ~150 sources majeures (x 10 ans d'archives) = **~2 Millions d'articles**.
|
| 12 |
+
* **Documents Officiels (JO, Lois, Rapports)** : **~50 000 PDF**.
|
| 13 |
+
* **Réseaux Sociaux (Facebook, X, LinkedIn, Commentaires)** : C'est le plus gros morceau. Discussions politiques, sociales, buzz. = **~30 à 50 Millions de posts/commentaires pertinents**.
|
| 14 |
+
|
| 15 |
+
**TOTAL CIBLE : ~50 Millions d'unités de données.**
|
| 16 |
+
|
| 17 |
+
---
|
| 18 |
+
|
| 19 |
+
## 💾 2. Besoins de Stockage (Par usage)
|
| 20 |
+
|
| 21 |
+
Voici ce qu'il vous faut selon vos trois objectifs :
|
| 22 |
+
|
| 23 |
+
### A. Base de Données "Brute Structurée" (💰 Pour la Vente)
|
| 24 |
+
*C'est la donnée "Patrimoniale". On garde tout (HTML source, métadonnées, JSON complet).*
|
| 25 |
+
* **Volume** : 50 Millions de fichiers JSON + Métadonnées.
|
| 26 |
+
* **Estimation** : **1.5 To à 2 To**.
|
| 27 |
+
* **Stockage recommandé** : Disque Dur (HDD) ou S3 (Pas cher).
|
| 28 |
+
|
| 29 |
+
### B. Dataset "Fine-Tuning LLM" (🧠 Pour l'Entraînement)
|
| 30 |
+
*C'est la donnée "Raffinée". Texte pur, nettoyé, format JSONL (Instruction/Response).*
|
| 31 |
+
* **Volume** : Le nettoyage supprime 90% du bruit (HTML, pubs).
|
| 32 |
+
* **Estimation** : **~100 Go à 150 Go** de texte pur haute qualité.
|
| 33 |
+
* **Stockage recommandé** : SSD standard (rapide à lire pour l'entraînement).
|
| 34 |
+
|
| 35 |
+
### C. Base Vectorielle (🔎 Pour RAG & Recherche Avancée)
|
| 36 |
+
*C'est la donnée "Intelligente". Vecteurs mathématiques.*
|
| 37 |
+
* **Calcul** : 50M docs x 3 chunks (moyenne) = 150 Millions de vecteurs.
|
| 38 |
+
* **Estimation (Standard Float32)** : ~450 Go (Trop lourd/cher).
|
| 39 |
+
* **Estimation (Optimisée/Quantized)** : **~30 Go à 50 Go** (Tient sur un serveur standard !).
|
| 40 |
+
* **Stockage recommandé** : NVMe + RAM (32Go).
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## ⏱️ 3. Estimation du Temps de Scraping
|
| 45 |
+
|
| 46 |
+
Combien de temps pour récupérer ces 50 millions d'items ?
|
| 47 |
+
|
| 48 |
+
### Scénario 1 : Le "Loup Solitaire" (1 Serveur, Prudent)
|
| 49 |
+
* *Vitesse* : 1 requête / 2 secondes (pour ne pas se faire bannir).
|
| 50 |
+
* **Temps estimé** : **~3 ans**. (Inenvisageable).
|
| 51 |
+
|
| 52 |
+
### Scénario 2 : L'Approche "Commando" (20 Workers en Parallèle)
|
| 53 |
+
* *Architecture* : 20 processus tournant en même temps sur des cibles différentes (Facebook, TogoWeb, BeninSoir, etc.).
|
| 54 |
+
* *Vitesse cumulée* : ~10 pages / seconde.
|
| 55 |
+
* **Temps estimé** : **~60 jours (2 mois)** pour tout l'historique.
|
| 56 |
+
|
| 57 |
+
### Scénario 3 : L'Approche "Industrielle" (Cloud Scaling)
|
| 58 |
+
* *Architecture* : 50 à 100 workers lancés massivement sur 1 semaine.
|
| 59 |
+
* **Temps estimé** : **~7 à 10 jours**.
|
| 60 |
+
* *Risque* : Fort risque de bannissement (IP blocking). Nécessite un gros budget Proxies.
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
## 💡 Ma Recommandation Stratégique
|
| 65 |
+
|
| 66 |
+
Pour le Togo et le Bénin, ne visez pas la force brute immédiate.
|
| 67 |
+
1. **Lancez une flotte "Moyenne" (10-15 workers)** stable.
|
| 68 |
+
2. Laissez tourner en fond pendant **3 mois**.
|
| 69 |
+
3. Priorisez les **Archives de Presse** (facile et rapide) le premier mois, puis les **Réseaux Sociaux** (lent et complexe) ensuite.
|
| 70 |
+
|
| 71 |
+
**Infrastructure requise pour ce plan :**
|
| 72 |
+
* **Stockage** : 1 Disque 4 To (couvre tout pour ~100€).
|
| 73 |
+
* **Serveur** : 1 VPS Costaud (8 vCPU / 32 Go RAM) pour la BDD + Scraping.
|
| 74 |
+
* **Total Matériel** : Investissement minime (~60-80€/mois) pour une valorisation de données énorme.
|
TROUBLESHOOTING.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔧 Guide de Résolution des Problèmes
|
| 2 |
+
|
| 3 |
+
## ❌ Problème 1 : Authentification Hugging Face Échouée
|
| 4 |
+
|
| 5 |
+
### Erreur
|
| 6 |
+
```
|
| 7 |
+
remote: Invalid username or password.
|
| 8 |
+
fatal: Authentication failed for 'https://huggingface.co/spaces/jojonocode/Scrap-Dji/'
|
| 9 |
+
```
|
| 10 |
+
|
| 11 |
+
### ✅ Solution : Utiliser un Token d'Accès
|
| 12 |
+
|
| 13 |
+
Hugging Face nécessite un **token d'accès** au lieu d'un mot de passe pour Git.
|
| 14 |
+
|
| 15 |
+
#### Méthode 1 : Via Hugging Face CLI (RECOMMANDÉ)
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
# 1. Installer Hugging Face CLI
|
| 19 |
+
pip install huggingface_hub
|
| 20 |
+
|
| 21 |
+
# 2. Se connecter avec votre token
|
| 22 |
+
huggingface-cli login
|
| 23 |
+
# Collez votre token quand demandé : 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
|
| 24 |
+
|
| 25 |
+
# 3. Cloner le Space
|
| 26 |
+
huggingface-cli repo clone spaces/jojonocode/Scrap-Dji
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
#### Méthode 2 : Via Git avec Token dans l'URL
|
| 30 |
+
|
| 31 |
+
```bash
|
| 32 |
+
# Utiliser le token directement dans l'URL
|
| 33 |
+
git clone https://jojonocode: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@huggingface.co/spaces/jojonocode/Scrap-Dji
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
#### Méthode 3 : Via l'Interface Web (PLUS SIMPLE)
|
| 37 |
+
|
| 38 |
+
**C'est la méthode la plus simple si Git pose problème !**
|
| 39 |
+
|
| 40 |
+
1. **Allez sur votre Space** : https://huggingface.co/spaces/jojonocode/Scrap-Dji
|
| 41 |
+
2. **Cliquez sur "Files"** en haut
|
| 42 |
+
3. **Cliquez sur "Add file" → "Upload files"**
|
| 43 |
+
4. **Glissez-déposez** tous vos fichiers depuis `C:\Users\MSI\Desktop\Scrap-Dji\`
|
| 44 |
+
5. **Commit** : Écrivez "Initial deployment" et cliquez sur "Commit"
|
| 45 |
+
|
| 46 |
+
**Fichiers à uploader** :
|
| 47 |
+
- ✅ `app.py`
|
| 48 |
+
- ✅ `requirements.txt`
|
| 49 |
+
- ✅ `README.md`
|
| 50 |
+
- ✅ `.gitignore`
|
| 51 |
+
- ✅ `sources.json`
|
| 52 |
+
- ✅ Dossiers : `scraper/`, `parser/`, `utils/`, `indexer/`
|
| 53 |
+
|
| 54 |
+
---
|
| 55 |
+
|
| 56 |
+
## ❌ Problème 2 : Module 'gradio' Non Trouvé
|
| 57 |
+
|
| 58 |
+
### Erreur
|
| 59 |
+
```
|
| 60 |
+
ModuleNotFoundError: No module named 'gradio'
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
### ✅ Solution : Installer les Dépendances
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
# Dans votre environnement virtuel (venv activé)
|
| 67 |
+
pip install -r requirements.txt
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
**Vérification** :
|
| 71 |
+
```bash
|
| 72 |
+
# Vérifier que gradio est installé
|
| 73 |
+
pip list | findstr gradio
|
| 74 |
+
```
|
| 75 |
+
|
| 76 |
+
Résultat attendu :
|
| 77 |
+
```
|
| 78 |
+
gradio 4.44.0
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
---
|
| 82 |
+
|
| 83 |
+
## 🚀 Déploiement Simplifié (Sans Git)
|
| 84 |
+
|
| 85 |
+
### Option A : Via l'Interface Web HF (RECOMMANDÉ)
|
| 86 |
+
|
| 87 |
+
**Étapes** :
|
| 88 |
+
|
| 89 |
+
1. **Créer le Space** (si pas déjà fait)
|
| 90 |
+
- Allez sur https://huggingface.co/new-space
|
| 91 |
+
- Name: `Scrap-Dji`
|
| 92 |
+
- SDK: Gradio
|
| 93 |
+
- Visibility: Private
|
| 94 |
+
- Cliquez "Create Space"
|
| 95 |
+
|
| 96 |
+
2. **Uploader les fichiers**
|
| 97 |
+
- Cliquez sur "Files" → "Add file" → "Upload files"
|
| 98 |
+
- Sélectionnez TOUS les fichiers de votre projet :
|
| 99 |
+
```
|
| 100 |
+
app.py
|
| 101 |
+
requirements.txt
|
| 102 |
+
README.md
|
| 103 |
+
.gitignore
|
| 104 |
+
sources.json
|
| 105 |
+
scraper/ (tout le dossier)
|
| 106 |
+
parser/ (tout le dossier)
|
| 107 |
+
utils/ (tout le dossier)
|
| 108 |
+
indexer/ (tout le dossier)
|
| 109 |
+
```
|
| 110 |
+
- Commit avec le message : "Initial deployment"
|
| 111 |
+
|
| 112 |
+
3. **Attendre le Build**
|
| 113 |
+
- Allez dans l'onglet "Logs"
|
| 114 |
+
- Attendez que le status passe à "Running" (2-3 minutes)
|
| 115 |
+
|
| 116 |
+
4. **Tester**
|
| 117 |
+
- Ouvrez votre Space : https://huggingface.co/spaces/jojonocode/Scrap-Dji
|
| 118 |
+
- L'interface Gradio devrait s'afficher !
|
| 119 |
+
|
| 120 |
+
### Option B : Via Git avec Token
|
| 121 |
+
|
| 122 |
+
Si vous voulez absolument utiliser Git :
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
# 1. Installer HF CLI
|
| 126 |
+
pip install huggingface_hub
|
| 127 |
+
|
| 128 |
+
# 2. Login
|
| 129 |
+
huggingface-cli login
|
| 130 |
+
# Token : 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
|
| 131 |
+
|
| 132 |
+
# 3. Cloner
|
| 133 |
+
huggingface-cli repo clone spaces/jojonocode/Scrap-Dji Scrap-Dji-HF
|
| 134 |
+
cd Scrap-Dji-HF
|
| 135 |
+
|
| 136 |
+
# 4. Copier les fichiers
|
| 137 |
+
Copy-Item -Path "C:\Users\MSI\Desktop\Scrap-Dji\*" -Destination "." -Recurse -Exclude @("venv", ".git", "data", "logs", "__pycache__")
|
| 138 |
+
|
| 139 |
+
# 5. Push
|
| 140 |
+
git add .
|
| 141 |
+
git commit -m "Initial deployment"
|
| 142 |
+
git push
|
| 143 |
+
```
|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
## 🧪 Test Local (Avant Déploiement)
|
| 148 |
+
|
| 149 |
+
Une fois les dépendances installées :
|
| 150 |
+
|
| 151 |
+
```bash
|
| 152 |
+
# 1. Activer venv (si pas déjà fait)
|
| 153 |
+
.\venv\Scripts\Activate.ps1
|
| 154 |
+
|
| 155 |
+
# 2. Installer les dépendances
|
| 156 |
+
pip install -r requirements.txt
|
| 157 |
+
|
| 158 |
+
# 3. Lancer l'app
|
| 159 |
+
python app.py
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
**Résultat attendu** :
|
| 163 |
+
```
|
| 164 |
+
Running on local URL: http://127.0.0.1:7860
|
| 165 |
+
```
|
| 166 |
+
|
| 167 |
+
Ouvrez http://localhost:7860 dans votre navigateur.
|
| 168 |
+
|
| 169 |
+
---
|
| 170 |
+
|
| 171 |
+
## 📋 Checklist de Déploiement
|
| 172 |
+
|
| 173 |
+
### Avant de Déployer
|
| 174 |
+
|
| 175 |
+
- [ ] Dépendances installées localement (`pip install -r requirements.txt`)
|
| 176 |
+
- [ ] App testée localement (`python app.py` fonctionne)
|
| 177 |
+
- [ ] Interface Gradio accessible sur http://localhost:7860
|
| 178 |
+
- [ ] Space créé sur Hugging Face
|
| 179 |
+
|
| 180 |
+
### Déploiement
|
| 181 |
+
|
| 182 |
+
**Méthode Simple (Interface Web)** :
|
| 183 |
+
- [ ] Aller sur https://huggingface.co/spaces/jojonocode/Scrap-Dji
|
| 184 |
+
- [ ] Cliquer "Files" → "Add file" → "Upload files"
|
| 185 |
+
- [ ] Uploader tous les fichiers du projet
|
| 186 |
+
- [ ] Commit
|
| 187 |
+
|
| 188 |
+
**OU Méthode Git** :
|
| 189 |
+
- [ ] Installer `huggingface_hub` : `pip install huggingface_hub`
|
| 190 |
+
- [ ] Login : `huggingface-cli login`
|
| 191 |
+
- [ ] Cloner : `huggingface-cli repo clone spaces/jojonocode/Scrap-Dji`
|
| 192 |
+
- [ ] Copier les fichiers
|
| 193 |
+
- [ ] Push
|
| 194 |
+
|
| 195 |
+
### Après Déploiement
|
| 196 |
+
|
| 197 |
+
- [ ] Vérifier les logs dans l'onglet "Logs"
|
| 198 |
+
- [ ] Attendre le status "Running"
|
| 199 |
+
- [ ] Tester l'interface : https://huggingface.co/spaces/jojonocode/Scrap-Dji
|
| 200 |
+
- [ ] Tester l'API : https://jojonocode-scrap-dji.hf.space/api/health
|
| 201 |
+
|
| 202 |
+
---
|
| 203 |
+
|
| 204 |
+
## 🆘 Problèmes Courants
|
| 205 |
+
|
| 206 |
+
### "Space not found"
|
| 207 |
+
→ Vérifiez que le Space existe : https://huggingface.co/spaces/jojonocode/Scrap-Dji
|
| 208 |
+
|
| 209 |
+
### "Build failed"
|
| 210 |
+
→ Vérifiez les logs dans l'onglet "Logs" du Space
|
| 211 |
+
|
| 212 |
+
### "Module not found" sur HF
|
| 213 |
+
→ Vérifiez que `requirements.txt` est bien uploadé
|
| 214 |
+
|
| 215 |
+
### "Permission denied"
|
| 216 |
+
→ Vérifiez que le Space est bien sous votre compte `jojonocode`
|
| 217 |
+
|
| 218 |
+
---
|
| 219 |
+
|
| 220 |
+
## 💡 Recommandation Finale
|
| 221 |
+
|
| 222 |
+
**Utilisez l'interface web Hugging Face** pour uploader les fichiers, c'est :
|
| 223 |
+
- ✅ Plus simple
|
| 224 |
+
- ✅ Pas de problème d'authentification
|
| 225 |
+
- ✅ Pas besoin de Git
|
| 226 |
+
- ✅ Drag & drop des fichiers
|
| 227 |
+
|
| 228 |
+
**Lien direct** : https://huggingface.co/spaces/jojonocode/Scrap-Dji/tree/main
|
| 229 |
+
|
| 230 |
+
Cliquez sur "Add file" et uploadez tout ! 🚀
|
VALUE_PROPOSITION.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Scrap-Dji : Proposition de Valeur Stratégique
|
| 2 |
+
|
| 3 |
+
## 🎯 Pertinence du Cas (Le Problème Africain)
|
| 4 |
+
|
| 5 |
+
**Le "Data Gap" Structurel :**
|
| 6 |
+
Dans l'écosystème numérique mondial, l'Afrique souffre d'un déficit chronique de données structurées.
|
| 7 |
+
* **Fragmentation :** Les informations locales (actualités, tendances sociales, documents officiels) sont éparpillées sur des milliers de sites web, forums et réseaux sociaux, souvent mal référencés par les moteurs de recherche géants (Google/Bing).
|
| 8 |
+
* **Invisibilité pour l'IA :** Les grands modèles d'IA (LLMs) sont entraînés majoritairement sur des données occidentales. Ils "hallucinent" ou manquent de contexte sur les réalités africaines car la donnée brute n'est pas accessible ou normalisée.
|
| 9 |
+
|
| 10 |
+
**Scrap-Dji** ne se contente pas de collecter la donnée ; il **structure le chaos numérique local** pour le rendre exploitable. Il répond au besoin critique de disposer d'une donnée propre, traçable et centralisée pour la prise de décision en Afrique.
|
| 11 |
+
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
## 🚀 Le Point Central d'Argument "Imbattable"
|
| 15 |
+
|
| 16 |
+
**"L'Infrastructure de Souveraineté Numérique pour l'IA Africaine"**
|
| 17 |
+
|
| 18 |
+
L'argument qui tue n'est pas technologique, il est **stratégique** :
|
| 19 |
+
|
| 20 |
+
> **Scrap-Dji n'est pas un simple outil de scraping, c'est le "Système Nerveux" de la future intelligence artificielle africaine.**
|
| 21 |
+
|
| 22 |
+
**Pourquoi c'est imbattable :**
|
| 23 |
+
1. **Souveraineté de la Donnée :** Contrairement à l'utilisation d'API tierces (qui peuvent couper l'accès ou augmenter les prix), Scrap-Dji vous rend **propriétaire** de votre lac de données (Data Lake). Vous ne dépendez de personne pour accéder à l'information stratégique du continent.
|
| 24 |
+
2. **Prêt pour le Futur (AI-Ready) :** Grâce à son architecture intégrant nativement les **bases de données vectorielles (Qdrant/Typesense)**, Scrap-Dji transforme instantanément le contenu collecté en vecteurs sémantiques.
|
| 25 |
+
* *Conséquence :* Vous pouvez brancher directement un LLM (comme Llama 3 ou Mistral) dessus pour créer un "ChatGPT Africain" qui connaît *réellement* l'actualité locale, les lois sénégalaises ou les tendances togolaises en temps réel.
|
| 26 |
+
|
| 27 |
+
**En résumé :** C'est la brique fondamentale qui manque à tout projet technologique sérieux en Afrique : **La maîtrise de la donnée source.** Sans Scrap-Dji, vous construisez des châteaux sur du sable ; avec Scrap-Dji, vous construisez sur des fondations de béton armé.
|
api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Backend API : FastAPI
|
api/main.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Query
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from typing import List, Optional
|
| 4 |
+
import os
|
| 5 |
+
import typesense
|
| 6 |
+
from utils.config import TYPESENSE_HOST, TYPESENSE_PORT, TYPESENSE_API_KEY
|
| 7 |
+
from db.postgres_connector import SessionLocal
|
| 8 |
+
from db.models import Document
|
| 9 |
+
from functools import lru_cache
|
| 10 |
+
|
| 11 |
+
app = FastAPI(title="Scrap-Dji Turbo API")
|
| 12 |
+
|
| 13 |
+
# Configuration CORS pour permettre les requêtes depuis le frontend
|
| 14 |
+
app.add_middleware(
|
| 15 |
+
CORSMiddleware,
|
| 16 |
+
allow_origins=["*"], # En production, spécifier les domaines autorisés
|
| 17 |
+
allow_credentials=True,
|
| 18 |
+
allow_methods=["*"],
|
| 19 |
+
allow_headers=["*"],
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
# Cache LRU pour les requêtes ultra-fréquentes (Zéro latence)
|
| 23 |
+
@lru_cache(maxsize=100)
|
| 24 |
+
def get_cached_search(q: str, pays: Optional[str]):
|
| 25 |
+
# Cette fonction est appelée par le endpoint async
|
| 26 |
+
return None # Placeholder pour la logique interne
|
| 27 |
+
|
| 28 |
+
ts_client = typesense.Client({
|
| 29 |
+
'nodes': [{'host': TYPESENSE_HOST, 'port': TYPESENSE_PORT, 'protocol': 'http'}],
|
| 30 |
+
'api_key': TYPESENSE_API_KEY,
|
| 31 |
+
'connection_timeout_seconds': 1
|
| 32 |
+
})
|
| 33 |
+
|
| 34 |
+
SEARCH_CACHE = {}
|
| 35 |
+
|
| 36 |
+
@app.get("/search")
|
| 37 |
+
async def search(q: str = Query(...), pays: Optional[str] = None):
|
| 38 |
+
# Check simple cache manuel (plus rapide pour l'async)
|
| 39 |
+
cache_key = f"{q}_{pays}"
|
| 40 |
+
if cache_key in SEARCH_CACHE:
|
| 41 |
+
return SEARCH_CACHE[cache_key]
|
| 42 |
+
|
| 43 |
+
try:
|
| 44 |
+
res = ts_client.collections['documents'].documents.search({
|
| 45 |
+
'q': q,
|
| 46 |
+
'query_by': 'titre,texte',
|
| 47 |
+
'filter_by': f'pays:={pays}' if pays else ''
|
| 48 |
+
})
|
| 49 |
+
hits = res['hits']
|
| 50 |
+
SEARCH_CACHE[cache_key] = hits # Mise en cache
|
| 51 |
+
return hits
|
| 52 |
+
except:
|
| 53 |
+
# Fallback final : Recherche dans le fichier JSON local (Mode Test)
|
| 54 |
+
local_file = "data/search_index.json"
|
| 55 |
+
if os.path.exists(local_file):
|
| 56 |
+
import json
|
| 57 |
+
with open(local_file, "r", encoding="utf-8") as f:
|
| 58 |
+
data = json.load(f)
|
| 59 |
+
results = [d for d in data if q.lower() in d['titre'].lower() or q.lower() in d['texte'].lower()]
|
| 60 |
+
if pays:
|
| 61 |
+
results = [d for d in results if d.get('pays') == pays]
|
| 62 |
+
return results[:10]
|
| 63 |
+
|
| 64 |
+
# Fallback SQL Optimisé (Indexé) - essayera quand même si Postgres est là
|
| 65 |
+
try:
|
| 66 |
+
session = SessionLocal()
|
| 67 |
+
results = session.query(Document).filter(Document.titre.ilike(f"%{q}%")).limit(5).all()
|
| 68 |
+
session.close()
|
| 69 |
+
return results
|
| 70 |
+
except:
|
| 71 |
+
return []
|
| 72 |
+
|
| 73 |
+
@app.get("/health")
|
| 74 |
+
def health(): return {"status": "turbo-charged"}
|
app.py
CHANGED
|
@@ -0,0 +1,599 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Scrap-Dji - Application Hugging Face Spaces
|
| 4 |
+
Combine FastAPI (endpoints pour frontend) + Gradio (interface web)
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
import asyncio
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from typing import List, Optional, Dict, Any
|
| 12 |
+
from pathlib import Path
|
| 13 |
+
|
| 14 |
+
# FastAPI
|
| 15 |
+
from fastapi import FastAPI, Query, HTTPException
|
| 16 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 17 |
+
from pydantic import BaseModel
|
| 18 |
+
|
| 19 |
+
# Gradio
|
| 20 |
+
import gradio as gr
|
| 21 |
+
|
| 22 |
+
# Scraper
|
| 23 |
+
from scraper.main import ScrapDjiScraper
|
| 24 |
+
from utils.logger import setup_logger
|
| 25 |
+
|
| 26 |
+
logger = setup_logger(__name__)
|
| 27 |
+
|
| 28 |
+
# ============================================================================
|
| 29 |
+
# CONFIGURATION
|
| 30 |
+
# ============================================================================
|
| 31 |
+
|
| 32 |
+
DATA_DIR = Path("/data") if os.path.exists("/data") else Path("./data")
|
| 33 |
+
DATA_DIR.mkdir(exist_ok=True)
|
| 34 |
+
DOCUMENTS_FILE = DATA_DIR / "documents.json"
|
| 35 |
+
SOURCES_FILE = Path("sources.json")
|
| 36 |
+
|
| 37 |
+
# ============================================================================
|
| 38 |
+
# FASTAPI APP - Endpoints pour Frontend
|
| 39 |
+
# ============================================================================
|
| 40 |
+
|
| 41 |
+
app = FastAPI(
|
| 42 |
+
title="Scrap-Dji API",
|
| 43 |
+
description="API de recherche et scraping de contenus africains",
|
| 44 |
+
version="2.0.0"
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
# CORS pour permettre les requêtes depuis n'importe quel frontend
|
| 48 |
+
app.add_middleware(
|
| 49 |
+
CORSMiddleware,
|
| 50 |
+
allow_origins=["*"],
|
| 51 |
+
allow_credentials=True,
|
| 52 |
+
allow_methods=["*"],
|
| 53 |
+
allow_headers=["*"],
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
# ============================================================================
|
| 57 |
+
# MODELS
|
| 58 |
+
# ============================================================================
|
| 59 |
+
|
| 60 |
+
class SearchRequest(BaseModel):
|
| 61 |
+
query: str
|
| 62 |
+
pays: Optional[str] = None
|
| 63 |
+
langue: Optional[str] = None
|
| 64 |
+
limit: int = 10
|
| 65 |
+
fuzzy: bool = True
|
| 66 |
+
|
| 67 |
+
class SearchResponse(BaseModel):
|
| 68 |
+
total: int
|
| 69 |
+
results: List[Dict[str, Any]]
|
| 70 |
+
query: str
|
| 71 |
+
execution_time_ms: float
|
| 72 |
+
|
| 73 |
+
class StatsResponse(BaseModel):
|
| 74 |
+
total_documents: int
|
| 75 |
+
pays: Dict[str, int]
|
| 76 |
+
langues: Dict[str, int]
|
| 77 |
+
sources: Dict[str, int]
|
| 78 |
+
derniere_mise_a_jour: Optional[str]
|
| 79 |
+
|
| 80 |
+
# ============================================================================
|
| 81 |
+
# SEARCH ENGINE - Recherche locale optimisée
|
| 82 |
+
# ============================================================================
|
| 83 |
+
|
| 84 |
+
class LocalSearchEngine:
|
| 85 |
+
"""Moteur de recherche local ultra-rapide avec fuzzy matching"""
|
| 86 |
+
|
| 87 |
+
def __init__(self, documents_file: Path):
|
| 88 |
+
self.documents_file = documents_file
|
| 89 |
+
self.documents = []
|
| 90 |
+
self.load_documents()
|
| 91 |
+
|
| 92 |
+
def load_documents(self):
|
| 93 |
+
"""Charge les documents depuis le fichier JSON"""
|
| 94 |
+
if self.documents_file.exists():
|
| 95 |
+
try:
|
| 96 |
+
with open(self.documents_file, 'r', encoding='utf-8') as f:
|
| 97 |
+
self.documents = json.load(f)
|
| 98 |
+
logger.info(f"✅ {len(self.documents)} documents chargés")
|
| 99 |
+
except Exception as e:
|
| 100 |
+
logger.error(f"Erreur chargement documents: {e}")
|
| 101 |
+
self.documents = []
|
| 102 |
+
else:
|
| 103 |
+
self.documents = []
|
| 104 |
+
|
| 105 |
+
def reload(self):
|
| 106 |
+
"""Recharge les documents (après scraping)"""
|
| 107 |
+
self.load_documents()
|
| 108 |
+
|
| 109 |
+
def fuzzy_match(self, text: str, query: str, threshold: float = 0.6) -> bool:
|
| 110 |
+
"""Fuzzy matching simple basé sur la distance de Levenshtein"""
|
| 111 |
+
text = text.lower()
|
| 112 |
+
query = query.lower()
|
| 113 |
+
|
| 114 |
+
# Recherche exacte d'abord
|
| 115 |
+
if query in text:
|
| 116 |
+
return True
|
| 117 |
+
|
| 118 |
+
# Fuzzy matching pour les mots individuels
|
| 119 |
+
query_words = query.split()
|
| 120 |
+
text_words = text.split()
|
| 121 |
+
|
| 122 |
+
for q_word in query_words:
|
| 123 |
+
for t_word in text_words:
|
| 124 |
+
# Calcul de similarité simple
|
| 125 |
+
if len(q_word) < 3:
|
| 126 |
+
if q_word == t_word:
|
| 127 |
+
return True
|
| 128 |
+
else:
|
| 129 |
+
# Tolérance aux fautes pour mots > 3 caractères
|
| 130 |
+
if self._similarity(q_word, t_word) >= threshold:
|
| 131 |
+
return True
|
| 132 |
+
|
| 133 |
+
return False
|
| 134 |
+
|
| 135 |
+
def _similarity(self, s1: str, s2: str) -> float:
|
| 136 |
+
"""Calcule la similarité entre deux chaînes (0-1)"""
|
| 137 |
+
if s1 == s2:
|
| 138 |
+
return 1.0
|
| 139 |
+
|
| 140 |
+
# Distance de Levenshtein simplifiée
|
| 141 |
+
len_s1, len_s2 = len(s1), len(s2)
|
| 142 |
+
if abs(len_s1 - len_s2) > 2:
|
| 143 |
+
return 0.0
|
| 144 |
+
|
| 145 |
+
# Compte les caractères communs
|
| 146 |
+
common = sum(1 for a, b in zip(s1, s2) if a == b)
|
| 147 |
+
max_len = max(len_s1, len_s2)
|
| 148 |
+
|
| 149 |
+
return common / max_len if max_len > 0 else 0.0
|
| 150 |
+
|
| 151 |
+
def search(
|
| 152 |
+
self,
|
| 153 |
+
query: str,
|
| 154 |
+
pays: Optional[str] = None,
|
| 155 |
+
langue: Optional[str] = None,
|
| 156 |
+
limit: int = 10,
|
| 157 |
+
fuzzy: bool = True
|
| 158 |
+
) -> List[Dict[str, Any]]:
|
| 159 |
+
"""Recherche dans les documents avec scoring"""
|
| 160 |
+
|
| 161 |
+
results = []
|
| 162 |
+
query_lower = query.lower()
|
| 163 |
+
|
| 164 |
+
for doc in self.documents:
|
| 165 |
+
score = 0.0
|
| 166 |
+
|
| 167 |
+
# Filtres
|
| 168 |
+
if pays and doc.get('pays') != pays:
|
| 169 |
+
continue
|
| 170 |
+
if langue and doc.get('langue') != langue:
|
| 171 |
+
continue
|
| 172 |
+
|
| 173 |
+
# Scoring
|
| 174 |
+
titre = doc.get('titre', '')
|
| 175 |
+
texte = doc.get('texte', '')
|
| 176 |
+
|
| 177 |
+
if fuzzy:
|
| 178 |
+
# Recherche permissive
|
| 179 |
+
if self.fuzzy_match(titre, query):
|
| 180 |
+
score += 10.0 # Boost titre
|
| 181 |
+
if self.fuzzy_match(texte, query):
|
| 182 |
+
score += 5.0
|
| 183 |
+
else:
|
| 184 |
+
# Recherche exacte
|
| 185 |
+
if query_lower in titre.lower():
|
| 186 |
+
score += 10.0
|
| 187 |
+
if query_lower in texte.lower():
|
| 188 |
+
score += 5.0
|
| 189 |
+
|
| 190 |
+
# Boost par pertinence
|
| 191 |
+
if 'tags' in doc:
|
| 192 |
+
for tag in doc.get('tags', []):
|
| 193 |
+
if query_lower in tag.lower():
|
| 194 |
+
score += 3.0
|
| 195 |
+
|
| 196 |
+
if score > 0:
|
| 197 |
+
results.append({
|
| 198 |
+
**doc,
|
| 199 |
+
'_score': score
|
| 200 |
+
})
|
| 201 |
+
|
| 202 |
+
# Tri par score décroissant
|
| 203 |
+
results.sort(key=lambda x: x['_score'], reverse=True)
|
| 204 |
+
|
| 205 |
+
return results[:limit]
|
| 206 |
+
|
| 207 |
+
def get_stats(self) -> Dict[str, Any]:
|
| 208 |
+
"""Retourne les statistiques de la base"""
|
| 209 |
+
pays_count = {}
|
| 210 |
+
langues_count = {}
|
| 211 |
+
sources_count = {}
|
| 212 |
+
|
| 213 |
+
for doc in self.documents:
|
| 214 |
+
# Pays
|
| 215 |
+
pays = doc.get('pays', 'Inconnu')
|
| 216 |
+
pays_count[pays] = pays_count.get(pays, 0) + 1
|
| 217 |
+
|
| 218 |
+
# Langues
|
| 219 |
+
langue = doc.get('langue', 'Inconnu')
|
| 220 |
+
langues_count[langue] = langues_count.get(langue, 0) + 1
|
| 221 |
+
|
| 222 |
+
# Sources
|
| 223 |
+
source_url = doc.get('source_url', '')
|
| 224 |
+
if source_url:
|
| 225 |
+
domain = source_url.split('/')[2] if len(source_url.split('/')) > 2 else 'Inconnu'
|
| 226 |
+
sources_count[domain] = sources_count.get(domain, 0) + 1
|
| 227 |
+
|
| 228 |
+
return {
|
| 229 |
+
'total_documents': len(self.documents),
|
| 230 |
+
'pays': pays_count,
|
| 231 |
+
'langues': langues_count,
|
| 232 |
+
'sources': sources_count,
|
| 233 |
+
'derniere_mise_a_jour': datetime.now().isoformat()
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
# Instance globale du moteur de recherche
|
| 237 |
+
search_engine = LocalSearchEngine(DOCUMENTS_FILE)
|
| 238 |
+
|
| 239 |
+
# ============================================================================
|
| 240 |
+
# API ENDPOINTS
|
| 241 |
+
# ============================================================================
|
| 242 |
+
|
| 243 |
+
@app.get("/")
|
| 244 |
+
async def root():
|
| 245 |
+
"""Endpoint racine"""
|
| 246 |
+
return {
|
| 247 |
+
"name": "Scrap-Dji API",
|
| 248 |
+
"version": "2.0.0",
|
| 249 |
+
"description": "API de recherche et scraping de contenus africains",
|
| 250 |
+
"endpoints": {
|
| 251 |
+
"search": "/api/search",
|
| 252 |
+
"stats": "/api/stats",
|
| 253 |
+
"documents": "/api/documents",
|
| 254 |
+
"health": "/api/health"
|
| 255 |
+
}
|
| 256 |
+
}
|
| 257 |
+
|
| 258 |
+
@app.get("/api/health")
|
| 259 |
+
async def health():
|
| 260 |
+
"""Health check"""
|
| 261 |
+
return {
|
| 262 |
+
"status": "healthy",
|
| 263 |
+
"documents_loaded": len(search_engine.documents),
|
| 264 |
+
"timestamp": datetime.now().isoformat()
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
@app.post("/api/search", response_model=SearchResponse)
|
| 268 |
+
async def api_search(request: SearchRequest):
|
| 269 |
+
"""
|
| 270 |
+
Endpoint de recherche principal
|
| 271 |
+
|
| 272 |
+
**Paramètres:**
|
| 273 |
+
- query: Texte à rechercher
|
| 274 |
+
- pays: Filtrer par pays (optionnel)
|
| 275 |
+
- langue: Filtrer par langue (optionnel)
|
| 276 |
+
- limit: Nombre de résultats (défaut: 10)
|
| 277 |
+
- fuzzy: Recherche permissive avec tolérance aux fautes (défaut: true)
|
| 278 |
+
|
| 279 |
+
**Exemple:**
|
| 280 |
+
```json
|
| 281 |
+
{
|
| 282 |
+
"query": "économie togo",
|
| 283 |
+
"pays": "Togo",
|
| 284 |
+
"limit": 20,
|
| 285 |
+
"fuzzy": true
|
| 286 |
+
}
|
| 287 |
+
```
|
| 288 |
+
"""
|
| 289 |
+
start_time = datetime.now()
|
| 290 |
+
|
| 291 |
+
results = search_engine.search(
|
| 292 |
+
query=request.query,
|
| 293 |
+
pays=request.pays,
|
| 294 |
+
langue=request.langue,
|
| 295 |
+
limit=request.limit,
|
| 296 |
+
fuzzy=request.fuzzy
|
| 297 |
+
)
|
| 298 |
+
|
| 299 |
+
execution_time = (datetime.now() - start_time).total_seconds() * 1000
|
| 300 |
+
|
| 301 |
+
return SearchResponse(
|
| 302 |
+
total=len(results),
|
| 303 |
+
results=results,
|
| 304 |
+
query=request.query,
|
| 305 |
+
execution_time_ms=round(execution_time, 2)
|
| 306 |
+
)
|
| 307 |
+
|
| 308 |
+
@app.get("/api/search", response_model=SearchResponse)
|
| 309 |
+
async def api_search_get(
|
| 310 |
+
q: str = Query(..., description="Texte à rechercher"),
|
| 311 |
+
pays: Optional[str] = Query(None, description="Filtrer par pays"),
|
| 312 |
+
langue: Optional[str] = Query(None, description="Filtrer par langue"),
|
| 313 |
+
limit: int = Query(10, ge=1, le=100, description="Nombre de résultats"),
|
| 314 |
+
fuzzy: bool = Query(True, description="Recherche permissive")
|
| 315 |
+
):
|
| 316 |
+
"""
|
| 317 |
+
Endpoint de recherche (GET)
|
| 318 |
+
|
| 319 |
+
**Exemple:** `/api/search?q=économie&pays=Togo&limit=20`
|
| 320 |
+
"""
|
| 321 |
+
request = SearchRequest(
|
| 322 |
+
query=q,
|
| 323 |
+
pays=pays,
|
| 324 |
+
langue=langue,
|
| 325 |
+
limit=limit,
|
| 326 |
+
fuzzy=fuzzy
|
| 327 |
+
)
|
| 328 |
+
return await api_search(request)
|
| 329 |
+
|
| 330 |
+
@app.get("/api/stats", response_model=StatsResponse)
|
| 331 |
+
async def api_stats():
|
| 332 |
+
"""
|
| 333 |
+
Retourne les statistiques de la base de données
|
| 334 |
+
|
| 335 |
+
**Retourne:**
|
| 336 |
+
- total_documents: Nombre total de documents
|
| 337 |
+
- pays: Répartition par pays
|
| 338 |
+
- langues: Répartition par langue
|
| 339 |
+
- sources: Répartition par source
|
| 340 |
+
"""
|
| 341 |
+
stats = search_engine.get_stats()
|
| 342 |
+
return StatsResponse(**stats)
|
| 343 |
+
|
| 344 |
+
@app.get("/api/documents")
|
| 345 |
+
async def api_documents(
|
| 346 |
+
skip: int = Query(0, ge=0),
|
| 347 |
+
limit: int = Query(10, ge=1, le=100)
|
| 348 |
+
):
|
| 349 |
+
"""
|
| 350 |
+
Retourne la liste des documents (paginée)
|
| 351 |
+
|
| 352 |
+
**Paramètres:**
|
| 353 |
+
- skip: Nombre de documents à sauter
|
| 354 |
+
- limit: Nombre de documents à retourner
|
| 355 |
+
"""
|
| 356 |
+
documents = search_engine.documents[skip:skip+limit]
|
| 357 |
+
return {
|
| 358 |
+
"total": len(search_engine.documents),
|
| 359 |
+
"skip": skip,
|
| 360 |
+
"limit": limit,
|
| 361 |
+
"documents": documents
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
@app.get("/api/documents/{doc_id}")
|
| 365 |
+
async def api_document_by_id(doc_id: str):
|
| 366 |
+
"""Retourne un document par son ID"""
|
| 367 |
+
for doc in search_engine.documents:
|
| 368 |
+
if doc.get('id') == doc_id:
|
| 369 |
+
return doc
|
| 370 |
+
raise HTTPException(status_code=404, detail="Document non trouvé")
|
| 371 |
+
|
| 372 |
+
@app.post("/api/reload")
|
| 373 |
+
async def api_reload():
|
| 374 |
+
"""Recharge les documents depuis le fichier (après scraping)"""
|
| 375 |
+
search_engine.reload()
|
| 376 |
+
return {
|
| 377 |
+
"status": "success",
|
| 378 |
+
"documents_loaded": len(search_engine.documents)
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
# ============================================================================
|
| 382 |
+
# GRADIO INTERFACE
|
| 383 |
+
# ============================================================================
|
| 384 |
+
|
| 385 |
+
def gradio_search(query: str, pays: str, langue: str, fuzzy: bool):
|
| 386 |
+
"""Fonction de recherche pour Gradio"""
|
| 387 |
+
if not query:
|
| 388 |
+
return "⚠️ Veuillez entrer une requête de recherche"
|
| 389 |
+
|
| 390 |
+
results = search_engine.search(
|
| 391 |
+
query=query,
|
| 392 |
+
pays=pays if pays != "Tous" else None,
|
| 393 |
+
langue=langue if langue != "Toutes" else None,
|
| 394 |
+
limit=20,
|
| 395 |
+
fuzzy=fuzzy
|
| 396 |
+
)
|
| 397 |
+
|
| 398 |
+
if not results:
|
| 399 |
+
return f"❌ Aucun résultat pour '{query}'"
|
| 400 |
+
|
| 401 |
+
# Formatage des résultats
|
| 402 |
+
output = f"## 🔍 Résultats pour '{query}' ({len(results)} trouvés)\n\n"
|
| 403 |
+
|
| 404 |
+
for i, doc in enumerate(results, 1):
|
| 405 |
+
titre = doc.get('titre', 'Sans titre')
|
| 406 |
+
texte = doc.get('texte', '')[:200] + "..."
|
| 407 |
+
pays_doc = doc.get('pays', 'Inconnu')
|
| 408 |
+
source = doc.get('source_url', '')
|
| 409 |
+
score = doc.get('_score', 0)
|
| 410 |
+
|
| 411 |
+
output += f"### {i}. {titre}\n"
|
| 412 |
+
output += f"**Pays:** {pays_doc} | **Score:** {score:.1f}\n\n"
|
| 413 |
+
output += f"{texte}\n\n"
|
| 414 |
+
output += f"[🔗 Source]({source})\n\n"
|
| 415 |
+
output += "---\n\n"
|
| 416 |
+
|
| 417 |
+
return output
|
| 418 |
+
|
| 419 |
+
def gradio_stats():
|
| 420 |
+
"""Affiche les statistiques pour Gradio"""
|
| 421 |
+
stats = search_engine.get_stats()
|
| 422 |
+
|
| 423 |
+
output = "# 📊 Statistiques de la Base de Données\n\n"
|
| 424 |
+
output += f"**Total de documents:** {stats['total_documents']}\n\n"
|
| 425 |
+
|
| 426 |
+
output += "## 🌍 Répartition par Pays\n\n"
|
| 427 |
+
for pays, count in sorted(stats['pays'].items(), key=lambda x: x[1], reverse=True):
|
| 428 |
+
output += f"- **{pays}:** {count} documents\n"
|
| 429 |
+
|
| 430 |
+
output += "\n## 🗣️ Répartition par Langue\n\n"
|
| 431 |
+
for langue, count in sorted(stats['langues'].items(), key=lambda x: x[1], reverse=True):
|
| 432 |
+
output += f"- **{langue}:** {count} documents\n"
|
| 433 |
+
|
| 434 |
+
output += "\n## 📰 Répartition par Source\n\n"
|
| 435 |
+
for source, count in sorted(stats['sources'].items(), key=lambda x: x[1], reverse=True)[:10]:
|
| 436 |
+
output += f"- **{source}:** {count} documents\n"
|
| 437 |
+
|
| 438 |
+
return output
|
| 439 |
+
|
| 440 |
+
async def gradio_scrape(progress=gr.Progress()):
|
| 441 |
+
"""Lance le scraping pour Gradio"""
|
| 442 |
+
progress(0, desc="Initialisation du scraping...")
|
| 443 |
+
|
| 444 |
+
try:
|
| 445 |
+
scraper = ScrapDjiScraper(str(SOURCES_FILE))
|
| 446 |
+
|
| 447 |
+
progress(0.3, desc="Scraping en cours...")
|
| 448 |
+
await scraper.run()
|
| 449 |
+
|
| 450 |
+
progress(0.8, desc="Rechargement des documents...")
|
| 451 |
+
search_engine.reload()
|
| 452 |
+
|
| 453 |
+
progress(1.0, desc="Terminé!")
|
| 454 |
+
|
| 455 |
+
stats = search_engine.get_stats()
|
| 456 |
+
return f"✅ Scraping terminé!\n\n**{stats['total_documents']} documents** dans la base"
|
| 457 |
+
|
| 458 |
+
except Exception as e:
|
| 459 |
+
logger.error(f"Erreur scraping: {e}")
|
| 460 |
+
return f"❌ Erreur lors du scraping: {str(e)}"
|
| 461 |
+
|
| 462 |
+
# Interface Gradio
|
| 463 |
+
with gr.Blocks(title="Scrap-Dji - Base de Connaissance Panafricaine", theme=gr.themes.Soft()) as gradio_app:
|
| 464 |
+
|
| 465 |
+
gr.Markdown("""
|
| 466 |
+
# 🌍 Scrap-Dji - Base de Connaissance Panafricaine
|
| 467 |
+
|
| 468 |
+
Système de scraping et de recherche de contenus africains (Togo, Bénin, Afrique)
|
| 469 |
+
""")
|
| 470 |
+
|
| 471 |
+
with gr.Tabs():
|
| 472 |
+
|
| 473 |
+
# ONGLET RECHERCHE
|
| 474 |
+
with gr.Tab("🔍 Recherche"):
|
| 475 |
+
gr.Markdown("### Recherchez dans la base de données")
|
| 476 |
+
|
| 477 |
+
with gr.Row():
|
| 478 |
+
search_query = gr.Textbox(
|
| 479 |
+
label="Requête de recherche",
|
| 480 |
+
placeholder="Ex: économie togo, politique bénin...",
|
| 481 |
+
scale=3
|
| 482 |
+
)
|
| 483 |
+
search_btn = gr.Button("🔍 Rechercher", variant="primary", scale=1)
|
| 484 |
+
|
| 485 |
+
with gr.Row():
|
| 486 |
+
search_pays = gr.Dropdown(
|
| 487 |
+
choices=["Tous", "Togo", "Bénin", "Afrique"],
|
| 488 |
+
value="Tous",
|
| 489 |
+
label="Pays"
|
| 490 |
+
)
|
| 491 |
+
search_langue = gr.Dropdown(
|
| 492 |
+
choices=["Toutes", "fr", "en"],
|
| 493 |
+
value="Toutes",
|
| 494 |
+
label="Langue"
|
| 495 |
+
)
|
| 496 |
+
search_fuzzy = gr.Checkbox(
|
| 497 |
+
value=True,
|
| 498 |
+
label="Recherche permissive (tolérance aux fautes)"
|
| 499 |
+
)
|
| 500 |
+
|
| 501 |
+
search_output = gr.Markdown()
|
| 502 |
+
|
| 503 |
+
search_btn.click(
|
| 504 |
+
fn=gradio_search,
|
| 505 |
+
inputs=[search_query, search_pays, search_langue, search_fuzzy],
|
| 506 |
+
outputs=search_output
|
| 507 |
+
)
|
| 508 |
+
|
| 509 |
+
# ONGLET SCRAPING
|
| 510 |
+
with gr.Tab("🚀 Scraping"):
|
| 511 |
+
gr.Markdown("### Lancer le scraping des sources")
|
| 512 |
+
|
| 513 |
+
scrape_btn = gr.Button("🚀 Lancer le Scraping", variant="primary", size="lg")
|
| 514 |
+
scrape_output = gr.Markdown()
|
| 515 |
+
|
| 516 |
+
scrape_btn.click(
|
| 517 |
+
fn=gradio_scrape,
|
| 518 |
+
outputs=scrape_output
|
| 519 |
+
)
|
| 520 |
+
|
| 521 |
+
# ONGLET STATISTIQUES
|
| 522 |
+
with gr.Tab("📊 Statistiques"):
|
| 523 |
+
gr.Markdown("### Statistiques de la base de données")
|
| 524 |
+
|
| 525 |
+
stats_btn = gr.Button("📊 Actualiser les Statistiques", variant="primary")
|
| 526 |
+
stats_output = gr.Markdown()
|
| 527 |
+
|
| 528 |
+
stats_btn.click(
|
| 529 |
+
fn=gradio_stats,
|
| 530 |
+
outputs=stats_output
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
+
# ONGLET API
|
| 534 |
+
with gr.Tab("🔌 API"):
|
| 535 |
+
gr.Markdown("""
|
| 536 |
+
### Endpoints API disponibles
|
| 537 |
+
|
| 538 |
+
L'API REST est accessible pour intégration dans votre frontend:
|
| 539 |
+
|
| 540 |
+
#### 🔍 Recherche
|
| 541 |
+
```
|
| 542 |
+
POST /api/search
|
| 543 |
+
GET /api/search?q=query&pays=Togo&limit=20
|
| 544 |
+
```
|
| 545 |
+
|
| 546 |
+
#### 📊 Statistiques
|
| 547 |
+
```
|
| 548 |
+
GET /api/stats
|
| 549 |
+
```
|
| 550 |
+
|
| 551 |
+
#### 📄 Documents
|
| 552 |
+
```
|
| 553 |
+
GET /api/documents?skip=0&limit=10
|
| 554 |
+
GET /api/documents/{id}
|
| 555 |
+
```
|
| 556 |
+
|
| 557 |
+
#### 🔄 Rechargement
|
| 558 |
+
```
|
| 559 |
+
POST /api/reload
|
| 560 |
+
```
|
| 561 |
+
|
| 562 |
+
#### ❤️ Health Check
|
| 563 |
+
```
|
| 564 |
+
GET /api/health
|
| 565 |
+
```
|
| 566 |
+
|
| 567 |
+
---
|
| 568 |
+
|
| 569 |
+
**Documentation interactive:** [/docs](/docs)
|
| 570 |
+
|
| 571 |
+
**Exemple de requête:**
|
| 572 |
+
```bash
|
| 573 |
+
curl -X POST "https://YOUR_SPACE.hf.space/api/search" \\
|
| 574 |
+
-H "Content-Type: application/json" \\
|
| 575 |
+
-d '{"query": "économie togo", "limit": 10, "fuzzy": true}'
|
| 576 |
+
```
|
| 577 |
+
""")
|
| 578 |
+
|
| 579 |
+
# ============================================================================
|
| 580 |
+
# MONTAGE GRADIO DANS FASTAPI
|
| 581 |
+
# ============================================================================
|
| 582 |
+
|
| 583 |
+
# Monter l'interface Gradio dans FastAPI
|
| 584 |
+
app = gr.mount_gradio_app(app, gradio_app, path="/")
|
| 585 |
+
|
| 586 |
+
# ============================================================================
|
| 587 |
+
# MAIN
|
| 588 |
+
# ============================================================================
|
| 589 |
+
|
| 590 |
+
if __name__ == "__main__":
|
| 591 |
+
import uvicorn
|
| 592 |
+
|
| 593 |
+
# Lancement de l'application
|
| 594 |
+
uvicorn.run(
|
| 595 |
+
app,
|
| 596 |
+
host="0.0.0.0",
|
| 597 |
+
port=7860, # Port par défaut pour Hugging Face Spaces
|
| 598 |
+
log_level="info"
|
| 599 |
+
)
|
config.env.example
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# === Config Optimisée pour Faible Machine ===
|
| 2 |
+
|
| 3 |
+
# PostgreSQL (Requis pour la structure)
|
| 4 |
+
POSTGRES_URI=postgresql://user:password@localhost:5432/scrapdji
|
| 5 |
+
|
| 6 |
+
# MongoDB (Optionnel si vous n'avez pas assez de RAM)
|
| 7 |
+
MONGO_URI=mongodb://localhost:27017
|
| 8 |
+
MONGO_DB=scrapdji
|
| 9 |
+
|
| 10 |
+
# Stockage (Optimisation : local suffisant)
|
| 11 |
+
STORAGE_PATH=./storage_data
|
| 12 |
+
|
| 13 |
+
# Recherche (Typesense est TRÈS léger par rapport à ElasticSearch)
|
| 14 |
+
TYPESENSE_HOST=localhost
|
| 15 |
+
TYPESENSE_PORT=8108
|
| 16 |
+
TYPESENSE_API_KEY=xyz
|
| 17 |
+
|
| 18 |
+
# Qdrant (Désactivez si vous n'avez pas de GPU ou de RAM)
|
| 19 |
+
QDRANT_HOST=localhost
|
| 20 |
+
QDRANT_PORT=6333
|
| 21 |
+
|
| 22 |
+
# --- PARAMÈTRES DE PERFORMANCE ---
|
| 23 |
+
# Délai entre requêtes (1s pour ne pas saturer le CPU)
|
| 24 |
+
SCRAPER_DELAY=1
|
| 25 |
+
# Nombre de requêtes simultanées (Réduisez à 4-8 si la RAM est < 2GB)
|
| 26 |
+
SCRAPER_CONCURRENT_REQUESTS=8
|
| 27 |
+
# Niveau de log (WARNING réduit les écritures disque)
|
| 28 |
+
LOG_LEVEL=WARNING
|
datasets/ewe/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Dataset Ewe (Végbé)
|
| 2 |
+
|
| 3 |
+
Ce dossier contient les données pour l'entraînement et le finetune de modèles LLM/SLM sur la langue Ewe (Togo, Ghana, Bénin).
|
| 4 |
+
|
| 5 |
+
## Structure des sous-dossiers
|
| 6 |
+
- `raw/` : données brutes collectées (tous formats)
|
| 7 |
+
- `clean/` : données nettoyées, formatées
|
| 8 |
+
- `annotated/` : données enrichies (tags, traductions, métadonnées)
|
| 9 |
+
- `final/` : corpus prêt à l'entraînement (JSONL, Parquet...)
|
| 10 |
+
|
| 11 |
+
## Bonnes pratiques
|
| 12 |
+
- Diversité des sources (oral, écrit, médias, réseaux sociaux...)
|
| 13 |
+
- Respect de la vie privée, anonymisation
|
| 14 |
+
- Annotation thématique et contextuelle
|
| 15 |
+
- Traductions si possible (français, anglais)
|
| 16 |
+
|
| 17 |
+
## Exemple de schéma JSONL
|
| 18 |
+
Voir le fichier `final/ewe_corpus.schema.json` pour la structure détaillée.
|
datasets/ewe/annotated/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
datasets/ewe/clean/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
datasets/ewe/final/ewe_corpus.jsonl
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
datasets/ewe/final/ewe_corpus.schema.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"title": "EweCorpusEntry",
|
| 4 |
+
"type": "object",
|
| 5 |
+
"properties": {
|
| 6 |
+
"id": {
|
| 7 |
+
"type": "string",
|
| 8 |
+
"description": "UUID unique"
|
| 9 |
+
},
|
| 10 |
+
"source": {
|
| 11 |
+
"type": "string",
|
| 12 |
+
"description": "Type/source du contenu"
|
| 13 |
+
},
|
| 14 |
+
"titre": {
|
| 15 |
+
"type": [
|
| 16 |
+
"string",
|
| 17 |
+
"null"
|
| 18 |
+
],
|
| 19 |
+
"description": "Titre ou contexte"
|
| 20 |
+
},
|
| 21 |
+
"texte": {
|
| 22 |
+
"type": "string",
|
| 23 |
+
"description": "Contenu principal en Ewe"
|
| 24 |
+
},
|
| 25 |
+
"langue": {
|
| 26 |
+
"type": "string",
|
| 27 |
+
"enum": [
|
| 28 |
+
"ewe"
|
| 29 |
+
]
|
| 30 |
+
},
|
| 31 |
+
"pays": {
|
| 32 |
+
"type": "string",
|
| 33 |
+
"enum": [
|
| 34 |
+
"Togo",
|
| 35 |
+
"Ghana",
|
| 36 |
+
"Bénin"
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
"type": {
|
| 40 |
+
"type": "string",
|
| 41 |
+
"description": "Catégorie du texte"
|
| 42 |
+
},
|
| 43 |
+
"auteur": {
|
| 44 |
+
"type": [
|
| 45 |
+
"string",
|
| 46 |
+
"null"
|
| 47 |
+
],
|
| 48 |
+
"description": "Auteur ou pseudo"
|
| 49 |
+
},
|
| 50 |
+
"date": {
|
| 51 |
+
"type": [
|
| 52 |
+
"string",
|
| 53 |
+
"null"
|
| 54 |
+
],
|
| 55 |
+
"description": "Date ISO"
|
| 56 |
+
},
|
| 57 |
+
"tags": {
|
| 58 |
+
"type": "array",
|
| 59 |
+
"items": {
|
| 60 |
+
"type": "string"
|
| 61 |
+
}
|
| 62 |
+
},
|
| 63 |
+
"traduction": {
|
| 64 |
+
"type": [
|
| 65 |
+
"string",
|
| 66 |
+
"null"
|
| 67 |
+
],
|
| 68 |
+
"description": "Traduction FR/EN"
|
| 69 |
+
},
|
| 70 |
+
"metadonnees": {
|
| 71 |
+
"type": [
|
| 72 |
+
"object",
|
| 73 |
+
"null"
|
| 74 |
+
]
|
| 75 |
+
},
|
| 76 |
+
"hash": {
|
| 77 |
+
"type": "string",
|
| 78 |
+
"description": "SHA256 du texte"
|
| 79 |
+
}
|
| 80 |
+
},
|
| 81 |
+
"required": [
|
| 82 |
+
"id",
|
| 83 |
+
"source",
|
| 84 |
+
"texte",
|
| 85 |
+
"langue",
|
| 86 |
+
"pays",
|
| 87 |
+
"type",
|
| 88 |
+
"hash"
|
| 89 |
+
]
|
| 90 |
+
}
|
datasets/ewe/raw/.gitkeep
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
|
db/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Module de base de données : connecteurs PostgreSQL & MongoDB
|
db/models.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Modèles de base de données pour Scrap-Dji
|
| 2 |
+
from sqlalchemy import Column, String, Text, DateTime, ForeignKey, Table
|
| 3 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 4 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 5 |
+
import datetime
|
| 6 |
+
|
| 7 |
+
Base = declarative_base()
|
| 8 |
+
|
| 9 |
+
class Document(Base):
|
| 10 |
+
__tablename__ = 'documents'
|
| 11 |
+
id = Column(UUID(as_uuid=True), primary_key=True)
|
| 12 |
+
titre = Column(String)
|
| 13 |
+
texte = Column(Text)
|
| 14 |
+
langue = Column(String)
|
| 15 |
+
type_document = Column(String)
|
| 16 |
+
auteur = Column(String)
|
| 17 |
+
source_url = Column(String)
|
| 18 |
+
tags = Column(String)
|
| 19 |
+
pays = Column(String)
|
| 20 |
+
date = Column(DateTime, default=datetime.datetime.utcnow)
|
| 21 |
+
|
| 22 |
+
class DocumentVersion(Base):
|
| 23 |
+
__tablename__ = 'document_versions'
|
| 24 |
+
id = Column(UUID(as_uuid=True), primary_key=True)
|
| 25 |
+
document_id = Column(UUID(as_uuid=True), ForeignKey('documents.id'))
|
| 26 |
+
texte = Column(Text)
|
| 27 |
+
date = Column(DateTime, default=datetime.datetime.utcnow)
|
| 28 |
+
|
| 29 |
+
class Image(Base):
|
| 30 |
+
__tablename__ = 'images'
|
| 31 |
+
id = Column(UUID(as_uuid=True), primary_key=True)
|
| 32 |
+
document_id = Column(UUID(as_uuid=True), ForeignKey('documents.id'))
|
| 33 |
+
image_hash = Column(String)
|
| 34 |
+
chemin = Column(String)
|
| 35 |
+
|
| 36 |
+
class LienExterne(Base):
|
| 37 |
+
__tablename__ = 'liens_externes'
|
| 38 |
+
id = Column(UUID(as_uuid=True), primary_key=True)
|
| 39 |
+
document_id = Column(UUID(as_uuid=True), ForeignKey('documents.id'))
|
| 40 |
+
url = Column(String)
|
| 41 |
+
|
| 42 |
+
# Schémas Pydantic pour MongoDB (NoSQL)
|
| 43 |
+
from pydantic import BaseModel, Field
|
| 44 |
+
from typing import List, Optional
|
| 45 |
+
|
| 46 |
+
class MongoDocument(BaseModel):
|
| 47 |
+
uuid: str
|
| 48 |
+
titre: str
|
| 49 |
+
texte: str
|
| 50 |
+
langue: str
|
| 51 |
+
type_document: str
|
| 52 |
+
auteur: Optional[str]
|
| 53 |
+
source_url: Optional[str]
|
| 54 |
+
tags: Optional[List[str]]
|
| 55 |
+
pays: Optional[str]
|
| 56 |
+
date: Optional[str]
|
| 57 |
+
images: Optional[List[str]]
|
| 58 |
+
liens_externes: Optional[List[str]]
|
| 59 |
+
versions: Optional[List[dict]]
|
| 60 |
+
metadonnees: Optional[dict]
|
db/mongo_connector.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from motor.motor_asyncio import AsyncIOMotorClient
|
| 2 |
+
from utils.config import MONGO_URI, MONGO_DB
|
| 3 |
+
|
| 4 |
+
client = AsyncIOMotorClient(MONGO_URI)
|
| 5 |
+
db = client[MONGO_DB]
|
| 6 |
+
|
| 7 |
+
async def save_to_mongo(collection: str, document: dict):
|
| 8 |
+
"""Sauvegarde un document de manière asynchrone dans MongoDB"""
|
| 9 |
+
try:
|
| 10 |
+
await db[collection].update_one(
|
| 11 |
+
{"source_url": document["source_url"]},
|
| 12 |
+
{"$set": document},
|
| 13 |
+
upsert=True
|
| 14 |
+
)
|
| 15 |
+
return True
|
| 16 |
+
except Exception:
|
| 17 |
+
return False
|
db/postgres_connector.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine
|
| 2 |
+
from sqlalchemy.orm import sessionmaker
|
| 3 |
+
from .models import Base
|
| 4 |
+
from utils.config import POSTGRES_URI
|
| 5 |
+
|
| 6 |
+
engine = create_engine(POSTGRES_URI)
|
| 7 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 8 |
+
|
| 9 |
+
def init_db():
|
| 10 |
+
Base.metadata.create_all(bind=engine)
|
frontend/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Frontend
|
| 2 |
+
|
| 3 |
+
Interface utilisateur pour la recherche et la visualisation des résultats.
|
| 4 |
+
|
| 5 |
+
- Champ de recherche
|
| 6 |
+
- Filtres (pays, date, source, type)
|
| 7 |
+
- Affichage des résultats + images
|
frontend/index.html
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fr" class="dark">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Scrap-Dji</title>
|
| 8 |
+
<style>
|
| 9 |
+
:root {
|
| 10 |
+
/* OpenAI / Apple Dark Mode Palette */
|
| 11 |
+
--bg-primary: #000000;
|
| 12 |
+
--bg-secondary: #121212;
|
| 13 |
+
--text-primary: #ffffff;
|
| 14 |
+
--text-secondary: #8e8e8e;
|
| 15 |
+
--accent: #3b82f6;
|
| 16 |
+
/* Subtle Blue Like Apple */
|
| 17 |
+
--border: #333333;
|
| 18 |
+
--hover: #1f1f1f;
|
| 19 |
+
--font-main: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
| 20 |
+
--transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
* {
|
| 24 |
+
box-sizing: border-box;
|
| 25 |
+
margin: 0;
|
| 26 |
+
padding: 0;
|
| 27 |
+
outline: none;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
body {
|
| 31 |
+
background-color: var(--bg-primary);
|
| 32 |
+
color: var(--text-primary);
|
| 33 |
+
font-family: var(--font-main);
|
| 34 |
+
min-height: 100vh;
|
| 35 |
+
display: flex;
|
| 36 |
+
flex-direction: column;
|
| 37 |
+
overflow-x: hidden;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
/* --- Header / Navigation --- */
|
| 41 |
+
nav {
|
| 42 |
+
position: fixed;
|
| 43 |
+
top: 0;
|
| 44 |
+
width: 100%;
|
| 45 |
+
padding: 20px 40px;
|
| 46 |
+
display: flex;
|
| 47 |
+
justify-content: space-between;
|
| 48 |
+
align-items: center;
|
| 49 |
+
z-index: 100;
|
| 50 |
+
background: rgba(0, 0, 0, 0.8);
|
| 51 |
+
backdrop-filter: blur(20px);
|
| 52 |
+
transition: var(--transition);
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.logo {
|
| 56 |
+
font-size: 1.2rem;
|
| 57 |
+
font-weight: 700;
|
| 58 |
+
letter-spacing: -0.5px;
|
| 59 |
+
color: var(--text-primary);
|
| 60 |
+
text-decoration: none;
|
| 61 |
+
opacity: 0.9;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/* --- Main Layout --- */
|
| 65 |
+
main {
|
| 66 |
+
flex: 1;
|
| 67 |
+
display: flex;
|
| 68 |
+
flex-direction: column;
|
| 69 |
+
justify-content: center;
|
| 70 |
+
align-items: center;
|
| 71 |
+
padding: 20px;
|
| 72 |
+
transition: var(--transition);
|
| 73 |
+
min-height: 100vh;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* Transition state: Results Mode */
|
| 77 |
+
body.has-results main {
|
| 78 |
+
justify-content: flex-start;
|
| 79 |
+
padding-top: 100px;
|
| 80 |
+
min-height: auto;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
/* --- Search Container --- */
|
| 84 |
+
.search-container {
|
| 85 |
+
width: 100%;
|
| 86 |
+
max-width: 680px;
|
| 87 |
+
text-align: center;
|
| 88 |
+
transition: var(--transition);
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
body.has-results .search-container {
|
| 92 |
+
position: fixed;
|
| 93 |
+
top: 15px;
|
| 94 |
+
left: 50%;
|
| 95 |
+
transform: translateX(-50%);
|
| 96 |
+
width: 50%;
|
| 97 |
+
max-width: 600px;
|
| 98 |
+
z-index: 101;
|
| 99 |
+
margin: 0;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
h1 {
|
| 103 |
+
font-size: 3rem;
|
| 104 |
+
font-weight: 600;
|
| 105 |
+
letter-spacing: -1.5px;
|
| 106 |
+
margin-bottom: 40px;
|
| 107 |
+
transition: var(--transition);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
body.has-results h1 {
|
| 111 |
+
opacity: 0;
|
| 112 |
+
pointer-events: none;
|
| 113 |
+
position: absolute;
|
| 114 |
+
transform: scale(0.9);
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/* --- Search Input --- */
|
| 118 |
+
.input-wrapper {
|
| 119 |
+
position: relative;
|
| 120 |
+
background: var(--bg-secondary);
|
| 121 |
+
border: 1px solid var(--border);
|
| 122 |
+
border-radius: 16px;
|
| 123 |
+
/* Apple rounded corners */
|
| 124 |
+
padding: 6px;
|
| 125 |
+
display: flex;
|
| 126 |
+
align-items: center;
|
| 127 |
+
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
|
| 128 |
+
transition: var(--transition);
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.input-wrapper:focus-within {
|
| 132 |
+
border-color: #555;
|
| 133 |
+
background: #1a1a1a;
|
| 134 |
+
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.6);
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
.search-icon {
|
| 138 |
+
padding: 0 15px;
|
| 139 |
+
color: var(--text-secondary);
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
input[type="text"] {
|
| 143 |
+
flex: 1;
|
| 144 |
+
background: transparent;
|
| 145 |
+
border: none;
|
| 146 |
+
padding: 14px 0;
|
| 147 |
+
font-size: 1.1rem;
|
| 148 |
+
color: var(--text-primary);
|
| 149 |
+
font-weight: 400;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
input[type="text"]::placeholder {
|
| 153 |
+
color: #555;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
/* Filter Select - Minimalist */
|
| 157 |
+
select {
|
| 158 |
+
background: transparent;
|
| 159 |
+
border: none;
|
| 160 |
+
color: var(--text-secondary);
|
| 161 |
+
font-size: 0.9rem;
|
| 162 |
+
padding: 0 15px;
|
| 163 |
+
cursor: pointer;
|
| 164 |
+
transition: color 0.2s;
|
| 165 |
+
margin-right: 10px;
|
| 166 |
+
border-left: 1px solid var(--border);
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
select:hover {
|
| 170 |
+
color: var(--text-primary);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
/* --- Key Visual Hints --- */
|
| 174 |
+
.shortcuts {
|
| 175 |
+
margin-top: 20px;
|
| 176 |
+
font-size: 0.85rem;
|
| 177 |
+
color: #444;
|
| 178 |
+
transition: var(--transition);
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
body.has-results .shortcuts {
|
| 182 |
+
opacity: 0;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
/* --- Results Area --- */
|
| 186 |
+
#results {
|
| 187 |
+
width: 100%;
|
| 188 |
+
max-width: 680px;
|
| 189 |
+
margin-top: 40px;
|
| 190 |
+
opacity: 0;
|
| 191 |
+
transform: translateY(20px);
|
| 192 |
+
transition: opacity 0.4s ease 0.2s, transform 0.4s ease 0.2s;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
body.has-results #results {
|
| 196 |
+
opacity: 1;
|
| 197 |
+
transform: translateY(0);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
/* --- Result Card (Apple-style) --- */
|
| 201 |
+
.result-card {
|
| 202 |
+
padding: 20px 0;
|
| 203 |
+
border-bottom: 1px solid #1a1a1a;
|
| 204 |
+
transition: var(--transition);
|
| 205 |
+
animation: fadeIn 0.5s ease forwards;
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
.result-card:last-child {
|
| 209 |
+
border-bottom: none;
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
/* Source URL */
|
| 213 |
+
.source-pill {
|
| 214 |
+
display: flex;
|
| 215 |
+
align-items: center;
|
| 216 |
+
gap: 8px;
|
| 217 |
+
font-size: 0.85rem;
|
| 218 |
+
color: var(--text-secondary);
|
| 219 |
+
margin-bottom: 8px;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.source-icon {
|
| 223 |
+
width: 16px;
|
| 224 |
+
height: 16px;
|
| 225 |
+
background: #333;
|
| 226 |
+
border-radius: 50%;
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
.result-card h3 {
|
| 230 |
+
font-size: 1.25rem;
|
| 231 |
+
font-weight: 500;
|
| 232 |
+
color: #3b82f6;
|
| 233 |
+
/* Modern Blue Link */
|
| 234 |
+
margin-bottom: 8px;
|
| 235 |
+
letter-spacing: -0.01em;
|
| 236 |
+
cursor: pointer;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.result-card h3:hover {
|
| 240 |
+
text-decoration: underline;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.result-card p {
|
| 244 |
+
font-size: 0.95rem;
|
| 245 |
+
line-height: 1.6;
|
| 246 |
+
color: #b0b0b0;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.meta-tag {
|
| 250 |
+
display: inline-block;
|
| 251 |
+
margin-top: 10px;
|
| 252 |
+
font-size: 0.75rem;
|
| 253 |
+
padding: 2px 8px;
|
| 254 |
+
border-radius: 4px;
|
| 255 |
+
background: #1a1a1a;
|
| 256 |
+
color: #666;
|
| 257 |
+
border: 1px solid #222;
|
| 258 |
+
}
|
| 259 |
+
|
| 260 |
+
@keyframes fadeIn {
|
| 261 |
+
from {
|
| 262 |
+
opacity: 0;
|
| 263 |
+
transform: translateY(10px);
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
to {
|
| 267 |
+
opacity: 1;
|
| 268 |
+
transform: translateY(0);
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/* --- Mobile --- */
|
| 273 |
+
@media (max-width: 768px) {
|
| 274 |
+
h1 {
|
| 275 |
+
font-size: 2rem;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
body.has-results .search-container {
|
| 279 |
+
width: 90%;
|
| 280 |
+
max-width: none;
|
| 281 |
+
top: 10px;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
body.has-results main {
|
| 285 |
+
padding-top: 90px;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.input-wrapper {
|
| 289 |
+
padding: 4px;
|
| 290 |
+
}
|
| 291 |
+
}
|
| 292 |
+
</style>
|
| 293 |
+
</head>
|
| 294 |
+
|
| 295 |
+
<body>
|
| 296 |
+
|
| 297 |
+
<nav>
|
| 298 |
+
<a href="#" class="logo" onclick="resetApp()">Scrap-Dji</a>
|
| 299 |
+
</nav>
|
| 300 |
+
|
| 301 |
+
<main>
|
| 302 |
+
<div class="search-container">
|
| 303 |
+
<h1>Que cherchez-vous ?</h1>
|
| 304 |
+
|
| 305 |
+
<form id="searchForm">
|
| 306 |
+
<div class="input-wrapper">
|
| 307 |
+
<!-- Scalable Vector Graphic Search Icon -->
|
| 308 |
+
<div class="search-icon">
|
| 309 |
+
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 310 |
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
| 311 |
+
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
|
| 312 |
+
</svg>
|
| 313 |
+
</div>
|
| 314 |
+
|
| 315 |
+
<input type="text" id="query" placeholder="Rechercher sur le Togo..." autocomplete="off" required />
|
| 316 |
+
|
| 317 |
+
<select id="pays">
|
| 318 |
+
<option value="">Tout</option>
|
| 319 |
+
<option value="Togo" selected>Togo</option>
|
| 320 |
+
<option value="Sénégal">Sénégal</option>
|
| 321 |
+
<option value="Mali">Mali</option>
|
| 322 |
+
</select>
|
| 323 |
+
</div>
|
| 324 |
+
</form>
|
| 325 |
+
|
| 326 |
+
<div class="shortcuts">
|
| 327 |
+
Appuyez sur <strong>Entrée</strong> pour rechercher
|
| 328 |
+
</div>
|
| 329 |
+
</div>
|
| 330 |
+
|
| 331 |
+
<div id="results"></div>
|
| 332 |
+
</main>
|
| 333 |
+
|
| 334 |
+
<script>
|
| 335 |
+
const form = document.getElementById('searchForm');
|
| 336 |
+
const queryInput = document.getElementById('query');
|
| 337 |
+
const resultsDiv = document.getElementById('results');
|
| 338 |
+
|
| 339 |
+
// Focus input on load
|
| 340 |
+
window.addEventListener('load', () => queryInput.focus());
|
| 341 |
+
|
| 342 |
+
// Reset App State
|
| 343 |
+
function resetApp() {
|
| 344 |
+
document.body.classList.remove('has-results');
|
| 345 |
+
resultsDiv.innerHTML = '';
|
| 346 |
+
queryInput.value = '';
|
| 347 |
+
queryInput.focus();
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
form.onsubmit = async function (e) {
|
| 351 |
+
e.preventDefault();
|
| 352 |
+
const q = queryInput.value.trim();
|
| 353 |
+
const pays = document.getElementById('pays').value;
|
| 354 |
+
|
| 355 |
+
if (!q) return;
|
| 356 |
+
|
| 357 |
+
// Transition UI
|
| 358 |
+
document.body.classList.add('has-results');
|
| 359 |
+
|
| 360 |
+
// Loading State (Skeleton or Spinner equivalent)
|
| 361 |
+
resultsDiv.innerHTML = `
|
| 362 |
+
<div style="text-align:center; padding: 40px; color: #444;">
|
| 363 |
+
<svg width="30" height="30" viewBox="0 0 50 50" style="animation: spin 1s linear infinite;">
|
| 364 |
+
<circle cx="25" cy="25" r="20" fill="none" stroke="#333" stroke-width="4"></circle>
|
| 365 |
+
<circle cx="25" cy="25" r="20" fill="none" stroke="#fff" stroke-width="4" stroke-dasharray="80" stroke-dashoffset="60"></circle>
|
| 366 |
+
</svg>
|
| 367 |
+
<style>@keyframes spin { 100% { transform: rotate(360deg); } }</style>
|
| 368 |
+
</div>
|
| 369 |
+
`;
|
| 370 |
+
|
| 371 |
+
let url = `http://localhost:8000/search?q=${encodeURIComponent(q)}`;
|
| 372 |
+
if (pays) url += `&pays=${encodeURIComponent(pays)}`;
|
| 373 |
+
|
| 374 |
+
try {
|
| 375 |
+
const res = await fetch(url);
|
| 376 |
+
const data = await res.json();
|
| 377 |
+
|
| 378 |
+
// Clear loading
|
| 379 |
+
resultsDiv.innerHTML = "";
|
| 380 |
+
|
| 381 |
+
if (data.length === 0) {
|
| 382 |
+
resultsDiv.innerHTML = `
|
| 383 |
+
<div style="text-align:center; color:#555; padding-top:40px;">
|
| 384 |
+
<p>Aucun résultat trouvé pour "${q}"</p>
|
| 385 |
+
</div>`;
|
| 386 |
+
return;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
// Render Results with Animation Delay
|
| 390 |
+
data.forEach((hit, index) => {
|
| 391 |
+
const doc = hit.document || hit;
|
| 392 |
+
const domain = new URL(doc.source_url).hostname.replace('www.', '');
|
| 393 |
+
|
| 394 |
+
const card = document.createElement('div');
|
| 395 |
+
card.className = 'result-card';
|
| 396 |
+
card.style.animationDelay = `${index * 0.05}s`;
|
| 397 |
+
|
| 398 |
+
// Truncate text intelligently
|
| 399 |
+
const snippet = doc.texte.length > 250 ? doc.texte.substring(0, 250) + "..." : doc.texte;
|
| 400 |
+
|
| 401 |
+
card.innerHTML = `
|
| 402 |
+
<div class="source-pill">
|
| 403 |
+
<span class="source-icon"></span>
|
| 404 |
+
<span>${domain}</span>
|
| 405 |
+
</div>
|
| 406 |
+
<a href="${doc.source_url}" target="_blank" style="text-decoration:none;">
|
| 407 |
+
<h3>${doc.titre}</h3>
|
| 408 |
+
</a>
|
| 409 |
+
<p>${snippet}</p>
|
| 410 |
+
<div class="meta-tag">
|
| 411 |
+
${doc.pays} • ${new Date(doc.date).toLocaleDateString()}
|
| 412 |
+
</div>
|
| 413 |
+
`;
|
| 414 |
+
resultsDiv.appendChild(card);
|
| 415 |
+
});
|
| 416 |
+
|
| 417 |
+
} catch (err) {
|
| 418 |
+
console.error(err);
|
| 419 |
+
resultsDiv.innerHTML = `
|
| 420 |
+
<div style="color:#ef4444; text-align:center; padding:20px;">
|
| 421 |
+
Erreur de connexion au serveur.
|
| 422 |
+
</div>`;
|
| 423 |
+
}
|
| 424 |
+
}
|
| 425 |
+
</script>
|
| 426 |
+
</body>
|
| 427 |
+
|
| 428 |
+
</html>
|
frontend_example.html
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fr">
|
| 3 |
+
|
| 4 |
+
<head>
|
| 5 |
+
<meta charset="UTF-8">
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 7 |
+
<title>Scrap-Dji - Exemple Frontend</title>
|
| 8 |
+
<style>
|
| 9 |
+
* {
|
| 10 |
+
margin: 0;
|
| 11 |
+
padding: 0;
|
| 12 |
+
box-sizing: border-box;
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
body {
|
| 16 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 17 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 18 |
+
min-height: 100vh;
|
| 19 |
+
padding: 20px;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.container {
|
| 23 |
+
max-width: 1200px;
|
| 24 |
+
margin: 0 auto;
|
| 25 |
+
background: white;
|
| 26 |
+
border-radius: 20px;
|
| 27 |
+
padding: 40px;
|
| 28 |
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
h1 {
|
| 32 |
+
color: #333;
|
| 33 |
+
margin-bottom: 10px;
|
| 34 |
+
font-size: 2.5em;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
.subtitle {
|
| 38 |
+
color: #666;
|
| 39 |
+
margin-bottom: 30px;
|
| 40 |
+
font-size: 1.1em;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.search-box {
|
| 44 |
+
display: flex;
|
| 45 |
+
gap: 10px;
|
| 46 |
+
margin-bottom: 20px;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
input[type="text"] {
|
| 50 |
+
flex: 1;
|
| 51 |
+
padding: 15px 20px;
|
| 52 |
+
border: 2px solid #ddd;
|
| 53 |
+
border-radius: 10px;
|
| 54 |
+
font-size: 16px;
|
| 55 |
+
transition: border-color 0.3s;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
input[type="text"]:focus {
|
| 59 |
+
outline: none;
|
| 60 |
+
border-color: #667eea;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
button {
|
| 64 |
+
padding: 15px 30px;
|
| 65 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 66 |
+
color: white;
|
| 67 |
+
border: none;
|
| 68 |
+
border-radius: 10px;
|
| 69 |
+
font-size: 16px;
|
| 70 |
+
font-weight: bold;
|
| 71 |
+
cursor: pointer;
|
| 72 |
+
transition: transform 0.2s;
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
button:hover {
|
| 76 |
+
transform: translateY(-2px);
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
button:active {
|
| 80 |
+
transform: translateY(0);
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.filters {
|
| 84 |
+
display: flex;
|
| 85 |
+
gap: 15px;
|
| 86 |
+
margin-bottom: 30px;
|
| 87 |
+
flex-wrap: wrap;
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
select,
|
| 91 |
+
.checkbox-label {
|
| 92 |
+
padding: 10px 15px;
|
| 93 |
+
border: 2px solid #ddd;
|
| 94 |
+
border-radius: 8px;
|
| 95 |
+
font-size: 14px;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.checkbox-label {
|
| 99 |
+
display: flex;
|
| 100 |
+
align-items: center;
|
| 101 |
+
gap: 8px;
|
| 102 |
+
cursor: pointer;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.stats {
|
| 106 |
+
display: grid;
|
| 107 |
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
| 108 |
+
gap: 20px;
|
| 109 |
+
margin-bottom: 30px;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
.stat-card {
|
| 113 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 114 |
+
color: white;
|
| 115 |
+
padding: 20px;
|
| 116 |
+
border-radius: 10px;
|
| 117 |
+
text-align: center;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.stat-value {
|
| 121 |
+
font-size: 2em;
|
| 122 |
+
font-weight: bold;
|
| 123 |
+
margin-bottom: 5px;
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
.stat-label {
|
| 127 |
+
font-size: 0.9em;
|
| 128 |
+
opacity: 0.9;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.results {
|
| 132 |
+
margin-top: 30px;
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
.result-item {
|
| 136 |
+
background: #f8f9fa;
|
| 137 |
+
padding: 20px;
|
| 138 |
+
border-radius: 10px;
|
| 139 |
+
margin-bottom: 15px;
|
| 140 |
+
border-left: 4px solid #667eea;
|
| 141 |
+
transition: transform 0.2s;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.result-item:hover {
|
| 145 |
+
transform: translateX(5px);
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.result-title {
|
| 149 |
+
font-size: 1.3em;
|
| 150 |
+
color: #333;
|
| 151 |
+
margin-bottom: 10px;
|
| 152 |
+
font-weight: bold;
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.result-meta {
|
| 156 |
+
color: #666;
|
| 157 |
+
font-size: 0.9em;
|
| 158 |
+
margin-bottom: 10px;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
.result-excerpt {
|
| 162 |
+
color: #444;
|
| 163 |
+
line-height: 1.6;
|
| 164 |
+
margin-bottom: 10px;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
.result-link {
|
| 168 |
+
color: #667eea;
|
| 169 |
+
text-decoration: none;
|
| 170 |
+
font-weight: bold;
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
.result-link:hover {
|
| 174 |
+
text-decoration: underline;
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
.loading {
|
| 178 |
+
text-align: center;
|
| 179 |
+
padding: 40px;
|
| 180 |
+
color: #666;
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
.error {
|
| 184 |
+
background: #fee;
|
| 185 |
+
color: #c33;
|
| 186 |
+
padding: 15px;
|
| 187 |
+
border-radius: 8px;
|
| 188 |
+
margin-bottom: 20px;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.badge {
|
| 192 |
+
display: inline-block;
|
| 193 |
+
padding: 4px 10px;
|
| 194 |
+
background: #667eea;
|
| 195 |
+
color: white;
|
| 196 |
+
border-radius: 12px;
|
| 197 |
+
font-size: 0.8em;
|
| 198 |
+
margin-right: 5px;
|
| 199 |
+
}
|
| 200 |
+
</style>
|
| 201 |
+
</head>
|
| 202 |
+
|
| 203 |
+
<body>
|
| 204 |
+
<div class="container">
|
| 205 |
+
<h1>🌍 Scrap-Dji</h1>
|
| 206 |
+
<p class="subtitle">Base de Connaissance Panafricaine - Recherche Intelligente</p>
|
| 207 |
+
|
| 208 |
+
<!-- Statistiques -->
|
| 209 |
+
<div class="stats" id="stats">
|
| 210 |
+
<div class="stat-card">
|
| 211 |
+
<div class="stat-value" id="total-docs">-</div>
|
| 212 |
+
<div class="stat-label">Documents</div>
|
| 213 |
+
</div>
|
| 214 |
+
<div class="stat-card">
|
| 215 |
+
<div class="stat-value" id="total-pays">-</div>
|
| 216 |
+
<div class="stat-label">Pays</div>
|
| 217 |
+
</div>
|
| 218 |
+
<div class="stat-card">
|
| 219 |
+
<div class="stat-value" id="total-sources">-</div>
|
| 220 |
+
<div class="stat-label">Sources</div>
|
| 221 |
+
</div>
|
| 222 |
+
</div>
|
| 223 |
+
|
| 224 |
+
<!-- Recherche -->
|
| 225 |
+
<div class="search-box">
|
| 226 |
+
<input type="text" id="search-input"
|
| 227 |
+
placeholder="Recherchez des articles (ex: économie togo, politique bénin...)"
|
| 228 |
+
onkeypress="if(event.key==='Enter') search()">
|
| 229 |
+
<button onclick="search()">🔍 Rechercher</button>
|
| 230 |
+
</div>
|
| 231 |
+
|
| 232 |
+
<!-- Filtres -->
|
| 233 |
+
<div class="filters">
|
| 234 |
+
<select id="pays-filter">
|
| 235 |
+
<option value="">Tous les pays</option>
|
| 236 |
+
<option value="Togo">Togo</option>
|
| 237 |
+
<option value="Bénin">Bénin</option>
|
| 238 |
+
<option value="Afrique">Afrique</option>
|
| 239 |
+
</select>
|
| 240 |
+
|
| 241 |
+
<select id="langue-filter">
|
| 242 |
+
<option value="">Toutes les langues</option>
|
| 243 |
+
<option value="fr">Français</option>
|
| 244 |
+
<option value="en">English</option>
|
| 245 |
+
</select>
|
| 246 |
+
|
| 247 |
+
<label class="checkbox-label">
|
| 248 |
+
<input type="checkbox" id="fuzzy-checkbox" checked>
|
| 249 |
+
Recherche permissive (tolérance aux fautes)
|
| 250 |
+
</label>
|
| 251 |
+
</div>
|
| 252 |
+
|
| 253 |
+
<!-- Résultats -->
|
| 254 |
+
<div class="results" id="results"></div>
|
| 255 |
+
</div>
|
| 256 |
+
|
| 257 |
+
<script>
|
| 258 |
+
// ⚠️ REMPLACEZ PAR L'URL DE VOTRE SPACE HUGGING FACE
|
| 259 |
+
const API_URL = 'http://localhost:7860'; // Pour test local
|
| 260 |
+
// const API_URL = 'https://VOTRE_USERNAME-scrap-dji.hf.space'; // Pour production
|
| 261 |
+
|
| 262 |
+
// Charger les statistiques au démarrage
|
| 263 |
+
loadStats();
|
| 264 |
+
|
| 265 |
+
async function loadStats() {
|
| 266 |
+
try {
|
| 267 |
+
const response = await fetch(`${API_URL}/api/stats`);
|
| 268 |
+
const data = await response.json();
|
| 269 |
+
|
| 270 |
+
document.getElementById('total-docs').textContent = data.total_documents;
|
| 271 |
+
document.getElementById('total-pays').textContent = Object.keys(data.pays).length;
|
| 272 |
+
document.getElementById('total-sources').textContent = Object.keys(data.sources).length;
|
| 273 |
+
} catch (error) {
|
| 274 |
+
console.error('Erreur chargement stats:', error);
|
| 275 |
+
}
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
async function search() {
|
| 279 |
+
const query = document.getElementById('search-input').value.trim();
|
| 280 |
+
if (!query) {
|
| 281 |
+
alert('Veuillez entrer une requête de recherche');
|
| 282 |
+
return;
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
const pays = document.getElementById('pays-filter').value;
|
| 286 |
+
const langue = document.getElementById('langue-filter').value;
|
| 287 |
+
const fuzzy = document.getElementById('fuzzy-checkbox').checked;
|
| 288 |
+
|
| 289 |
+
const resultsDiv = document.getElementById('results');
|
| 290 |
+
resultsDiv.innerHTML = '<div class="loading">🔍 Recherche en cours...</div>';
|
| 291 |
+
|
| 292 |
+
try {
|
| 293 |
+
const response = await fetch(`${API_URL}/api/search`, {
|
| 294 |
+
method: 'POST',
|
| 295 |
+
headers: {
|
| 296 |
+
'Content-Type': 'application/json',
|
| 297 |
+
},
|
| 298 |
+
body: JSON.stringify({
|
| 299 |
+
query: query,
|
| 300 |
+
pays: pays || null,
|
| 301 |
+
langue: langue || null,
|
| 302 |
+
limit: 20,
|
| 303 |
+
fuzzy: fuzzy
|
| 304 |
+
})
|
| 305 |
+
});
|
| 306 |
+
|
| 307 |
+
if (!response.ok) {
|
| 308 |
+
throw new Error(`Erreur HTTP: ${response.status}`);
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
const data = await response.json();
|
| 312 |
+
displayResults(data);
|
| 313 |
+
|
| 314 |
+
} catch (error) {
|
| 315 |
+
resultsDiv.innerHTML = `
|
| 316 |
+
<div class="error">
|
| 317 |
+
❌ Erreur lors de la recherche: ${error.message}
|
| 318 |
+
<br><br>
|
| 319 |
+
Vérifiez que l'API est accessible à l'adresse: ${API_URL}
|
| 320 |
+
</div>
|
| 321 |
+
`;
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
function displayResults(data) {
|
| 326 |
+
const resultsDiv = document.getElementById('results');
|
| 327 |
+
|
| 328 |
+
if (data.total === 0) {
|
| 329 |
+
resultsDiv.innerHTML = `
|
| 330 |
+
<div class="loading">
|
| 331 |
+
❌ Aucun résultat trouvé pour "${data.query}"
|
| 332 |
+
<br><br>
|
| 333 |
+
Essayez avec d'autres mots-clés ou activez la recherche permissive
|
| 334 |
+
</div>
|
| 335 |
+
`;
|
| 336 |
+
return;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
let html = `
|
| 340 |
+
<h2>📊 ${data.total} résultat(s) trouvé(s) en ${data.execution_time_ms}ms</h2>
|
| 341 |
+
<p style="color: #666; margin-bottom: 20px;">Requête: "${data.query}"</p>
|
| 342 |
+
`;
|
| 343 |
+
|
| 344 |
+
data.results.forEach((result, index) => {
|
| 345 |
+
const titre = result.titre || 'Sans titre';
|
| 346 |
+
const texte = result.texte || '';
|
| 347 |
+
const excerpt = texte.substring(0, 200) + (texte.length > 200 ? '...' : '');
|
| 348 |
+
const pays = result.pays || 'Inconnu';
|
| 349 |
+
const langue = result.langue || 'fr';
|
| 350 |
+
const source = result.source_url || '#';
|
| 351 |
+
const score = result._score || 0;
|
| 352 |
+
|
| 353 |
+
html += `
|
| 354 |
+
<div class="result-item">
|
| 355 |
+
<div class="result-title">${index + 1}. ${titre}</div>
|
| 356 |
+
<div class="result-meta">
|
| 357 |
+
<span class="badge">${pays}</span>
|
| 358 |
+
<span class="badge">${langue.toUpperCase()}</span>
|
| 359 |
+
<span class="badge">Score: ${score.toFixed(1)}</span>
|
| 360 |
+
</div>
|
| 361 |
+
<div class="result-excerpt">${excerpt}</div>
|
| 362 |
+
<a href="${source}" target="_blank" class="result-link">🔗 Lire l'article complet</a>
|
| 363 |
+
</div>
|
| 364 |
+
`;
|
| 365 |
+
});
|
| 366 |
+
|
| 367 |
+
resultsDiv.innerHTML = html;
|
| 368 |
+
}
|
| 369 |
+
</script>
|
| 370 |
+
</body>
|
| 371 |
+
|
| 372 |
+
</html>
|
indexer/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Module d'indexation : Typesense, Qdrant, FAISS
|
indexer/qdrant_indexer.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from qdrant_client import QdrantClient
|
| 2 |
+
from utils.config import QDRANT_HOST, QDRANT_PORT
|
| 3 |
+
|
| 4 |
+
client = QdrantClient(host=QDRANT_HOST, port=int(QDRANT_PORT))
|
| 5 |
+
|
| 6 |
+
def create_collection_if_not_exists(collection_name, vector_size):
|
| 7 |
+
try:
|
| 8 |
+
client.create_collection(
|
| 9 |
+
collection_name=collection_name,
|
| 10 |
+
vectors_config={"size": vector_size, "distance": "Cosine"}
|
| 11 |
+
)
|
| 12 |
+
except Exception as e:
|
| 13 |
+
if 'already exists' not in str(e):
|
| 14 |
+
raise
|
| 15 |
+
|
| 16 |
+
def index_embedding(collection_name, embedding, payload, id=None):
|
| 17 |
+
return client.upsert(
|
| 18 |
+
collection_name=collection_name,
|
| 19 |
+
points=[{
|
| 20 |
+
"id": id,
|
| 21 |
+
"vector": embedding,
|
| 22 |
+
"payload": payload
|
| 23 |
+
}]
|
| 24 |
+
)
|
indexer/typesense_indexer.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import typesense
|
| 2 |
+
from utils.config import TYPESENSE_HOST, TYPESENSE_PORT, TYPESENSE_API_KEY
|
| 3 |
+
|
| 4 |
+
client = typesense.Client({
|
| 5 |
+
'nodes': [{
|
| 6 |
+
'host': TYPESENSE_HOST,
|
| 7 |
+
'port': int(TYPESENSE_PORT),
|
| 8 |
+
'protocol': 'http'
|
| 9 |
+
}],
|
| 10 |
+
'api_key': TYPESENSE_API_KEY,
|
| 11 |
+
'connection_timeout_seconds': 2
|
| 12 |
+
})
|
| 13 |
+
|
| 14 |
+
def create_collection_if_not_exists(schema):
|
| 15 |
+
try:
|
| 16 |
+
# Check existence first to avoid exception cost
|
| 17 |
+
name = schema.get('name')
|
| 18 |
+
if name:
|
| 19 |
+
try:
|
| 20 |
+
client.collections[name].retrieve()
|
| 21 |
+
return
|
| 22 |
+
except Exception:
|
| 23 |
+
pass
|
| 24 |
+
client.collections.create(schema)
|
| 25 |
+
except Exception as e:
|
| 26 |
+
if 'already exists' not in str(e):
|
| 27 |
+
raise
|
| 28 |
+
|
| 29 |
+
def index_document(collection, document):
|
| 30 |
+
return client.collections[collection].documents.upsert(document)
|
parser/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Module de parsing : nettoyage, enrichissement, hashing
|
parser/cleaner.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import re
|
| 2 |
+
from bs4 import BeautifulSoup
|
| 3 |
+
|
| 4 |
+
def clean_html(raw_html: str) -> str:
|
| 5 |
+
"""Supprime les balises HTML et normalise le texte."""
|
| 6 |
+
soup = BeautifulSoup(raw_html, "html.parser")
|
| 7 |
+
text = soup.get_text(separator=" ")
|
| 8 |
+
text = re.sub(r"\s+", " ", text).strip()
|
| 9 |
+
return text
|
parser/hasher.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
|
| 3 |
+
def hash_text(text: str) -> str:
|
| 4 |
+
return hashlib.sha256(text.encode('utf-8')).hexdigest()
|
| 5 |
+
|
| 6 |
+
def hash_bytes(data: bytes) -> str:
|
| 7 |
+
return hashlib.sha256(data).hexdigest()
|
render.yaml
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
- type: web
|
| 3 |
+
name: scrap-dji-api
|
| 4 |
+
env: python
|
| 5 |
+
plan: free
|
| 6 |
+
buildCommand: pip install -r requirements.txt
|
| 7 |
+
startCommand: uvicorn api.main:app --host 0.0.0.0 --port $PORT
|
| 8 |
+
envVars:
|
| 9 |
+
- key: TYPESENSE_HOST
|
| 10 |
+
value: your-typesense-host
|
| 11 |
+
- key: TYPESENSE_PORT
|
| 12 |
+
value: 8108
|
| 13 |
+
- key: TYPESENSE_API_KEY
|
| 14 |
+
value: xyz
|
| 15 |
+
- key: STORAGE_PATH
|
| 16 |
+
value: /tmp/storage_data
|
| 17 |
+
|
| 18 |
+
- type: worker
|
| 19 |
+
name: scrap-dji-worker
|
| 20 |
+
env: python
|
| 21 |
+
plan: free
|
| 22 |
+
buildCommand: pip install -r requirements.txt
|
| 23 |
+
startCommand: python workers/scraper_worker.py
|
| 24 |
+
envVars:
|
| 25 |
+
- key: TYPESENSE_HOST
|
| 26 |
+
value: your-typesense-host
|
| 27 |
+
- key: TYPESENSE_PORT
|
| 28 |
+
value: 8108
|
| 29 |
+
- key: TYPESENSE_API_KEY
|
| 30 |
+
value: xyz
|
| 31 |
+
- key: SCRAPER_INTERVAL
|
| 32 |
+
value: 300
|
| 33 |
+
|
| 34 |
+
staticSites:
|
| 35 |
+
- name: scrap-dji-frontend
|
| 36 |
+
buildCommand: ''
|
| 37 |
+
publishPath: frontend
|
requirements.txt
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================================
|
| 2 |
+
# DÉPENDANCES OPTIMISÉES POUR HUGGING FACE SPACES
|
| 3 |
+
# ============================================================================
|
| 4 |
+
|
| 5 |
+
# Web Framework
|
| 6 |
+
fastapi==0.109.0
|
| 7 |
+
uvicorn[standard]==0.27.0
|
| 8 |
+
gradio==4.44.0
|
| 9 |
+
|
| 10 |
+
# HTTP Client
|
| 11 |
+
httpx==0.26.0
|
| 12 |
+
|
| 13 |
+
# Scraping
|
| 14 |
+
newspaper3k==0.2.8
|
| 15 |
+
beautifulsoup4==4.12.3
|
| 16 |
+
lxml==5.1.0
|
| 17 |
+
lxml_html_clean==0.1.1
|
| 18 |
+
feedparser==6.0.11
|
| 19 |
+
|
| 20 |
+
# NLP & Text Processing
|
| 21 |
+
nltk==3.8.1
|
| 22 |
+
langdetect==1.0.9
|
| 23 |
+
textblob==0.18.0
|
| 24 |
+
|
| 25 |
+
# Utilities
|
| 26 |
+
python-dotenv==1.0.1
|
| 27 |
+
pydantic==2.5.3
|
| 28 |
+
aiofiles==23.2.1
|
| 29 |
+
|
| 30 |
+
# Data Processing
|
| 31 |
+
python-multipart==0.0.9
|
| 32 |
+
|
| 33 |
+
# Logging (optionnel, Python a déjà logging)
|
| 34 |
+
# colorlog==6.8.2
|
run_scraper.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script de lancement pour le scraping Scrap-Dji
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
import argparse
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
# Ajout du répertoire racine au path
|
| 12 |
+
sys.path.append(str(Path(__file__).parent))
|
| 13 |
+
|
| 14 |
+
def check_environment():
|
| 15 |
+
"""Vérifie l'environnement avant lancement"""
|
| 16 |
+
print("🔍 Vérification de l'environnement...")
|
| 17 |
+
|
| 18 |
+
# Vérification du fichier .env
|
| 19 |
+
if not os.path.exists(".env"):
|
| 20 |
+
print("❌ Fichier .env manquant")
|
| 21 |
+
print(" Copiez config.env.example vers .env et configurez-le")
|
| 22 |
+
return False
|
| 23 |
+
|
| 24 |
+
# Vérification des sources
|
| 25 |
+
if not os.path.exists("sources.json"):
|
| 26 |
+
print("❌ Fichier sources.json manquant")
|
| 27 |
+
print(" Copiez sources.json.example vers sources.json et configurez vos sources")
|
| 28 |
+
return False
|
| 29 |
+
|
| 30 |
+
# Vérification des répertoires
|
| 31 |
+
directories = ["storage_data", "logs"]
|
| 32 |
+
for directory in directories:
|
| 33 |
+
Path(directory).mkdir(exist_ok=True)
|
| 34 |
+
|
| 35 |
+
print("✅ Environnement OK")
|
| 36 |
+
return True
|
| 37 |
+
|
| 38 |
+
def run_scraping(sources_file: str = "sources.json", dry_run: bool = False):
|
| 39 |
+
"""Lance le scraping"""
|
| 40 |
+
try:
|
| 41 |
+
from scraper.main import ScrapDjiScraper
|
| 42 |
+
|
| 43 |
+
if dry_run:
|
| 44 |
+
print("🧪 Mode test - aucun document ne sera sauvegardé")
|
| 45 |
+
# TODO: Implémenter le mode dry_run
|
| 46 |
+
return
|
| 47 |
+
|
| 48 |
+
scraper = ScrapDjiScraper(sources_file)
|
| 49 |
+
total_docs = scraper.run_scraping()
|
| 50 |
+
|
| 51 |
+
print(f"\n🎉 Scraping terminé avec succès!")
|
| 52 |
+
print(f"📊 {total_docs} documents traités")
|
| 53 |
+
|
| 54 |
+
except ImportError as e:
|
| 55 |
+
print(f"❌ Erreur d'import: {e}")
|
| 56 |
+
print(" Vérifiez que toutes les dépendances sont installées")
|
| 57 |
+
except Exception as e:
|
| 58 |
+
print(f"❌ Erreur lors du scraping: {e}")
|
| 59 |
+
|
| 60 |
+
def main():
|
| 61 |
+
parser = argparse.ArgumentParser(description="Scrap-Dji - Lancement du scraping")
|
| 62 |
+
parser.add_argument("--sources", "-s", default="sources.json",
|
| 63 |
+
help="Fichier de configuration des sources")
|
| 64 |
+
parser.add_argument("--dry-run", "-d", action="store_true",
|
| 65 |
+
help="Mode test sans sauvegarde")
|
| 66 |
+
parser.add_argument("--setup", action="store_true",
|
| 67 |
+
help="Lance la configuration initiale")
|
| 68 |
+
|
| 69 |
+
args = parser.parse_args()
|
| 70 |
+
|
| 71 |
+
if args.setup:
|
| 72 |
+
print("🔧 Lancement de la configuration...")
|
| 73 |
+
os.system("python setup.py")
|
| 74 |
+
return
|
| 75 |
+
|
| 76 |
+
print("🚀 Scrap-Dji - Lancement du scraping")
|
| 77 |
+
print("=" * 50)
|
| 78 |
+
|
| 79 |
+
if not check_environment():
|
| 80 |
+
sys.exit(1)
|
| 81 |
+
|
| 82 |
+
run_scraping(args.sources, args.dry_run)
|
| 83 |
+
|
| 84 |
+
if __name__ == "__main__":
|
| 85 |
+
main()
|
scrape_togo_massive.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script de scraping massif pour le Togo
|
| 4 |
+
Collecte des données depuis toutes les sources togolaises configurées
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
import sys
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
# Ajout du répertoire racine au path
|
| 12 |
+
sys.path.append(str(Path(__file__).parent))
|
| 13 |
+
|
| 14 |
+
from scraper.main import ScrapDjiScraper
|
| 15 |
+
from utils.logger import setup_logger
|
| 16 |
+
import json
|
| 17 |
+
|
| 18 |
+
logger = setup_logger(__name__)
|
| 19 |
+
|
| 20 |
+
def print_banner():
|
| 21 |
+
print("=" * 60)
|
| 22 |
+
print("🇹🇬 SCRAPING MASSIF - DONNÉES TOGO 🇹🇬")
|
| 23 |
+
print("=" * 60)
|
| 24 |
+
print()
|
| 25 |
+
|
| 26 |
+
def print_stats(data_file: str = "data/search_index.json"):
|
| 27 |
+
"""Affiche les statistiques de scraping"""
|
| 28 |
+
try:
|
| 29 |
+
with open(data_file, 'r', encoding='utf-8') as f:
|
| 30 |
+
data = json.load(f)
|
| 31 |
+
|
| 32 |
+
total = len(data)
|
| 33 |
+
togo_docs = [d for d in data if d.get('pays') == 'Togo']
|
| 34 |
+
|
| 35 |
+
print("\n" + "=" * 60)
|
| 36 |
+
print("📊 STATISTIQUES DE SCRAPING")
|
| 37 |
+
print("=" * 60)
|
| 38 |
+
print(f"📄 Total de documents collectés: {total}")
|
| 39 |
+
print(f"🇹🇬 Documents sur le Togo: {len(togo_docs)}")
|
| 40 |
+
|
| 41 |
+
if togo_docs:
|
| 42 |
+
# Statistiques par source
|
| 43 |
+
sources = {}
|
| 44 |
+
for doc in togo_docs:
|
| 45 |
+
url = doc.get('source_url', '')
|
| 46 |
+
domain = url.split('/')[2] if len(url.split('/')) > 2 else 'unknown'
|
| 47 |
+
sources[domain] = sources.get(domain, 0) + 1
|
| 48 |
+
|
| 49 |
+
print("\n📰 Répartition par source:")
|
| 50 |
+
for source, count in sorted(sources.items(), key=lambda x: x[1], reverse=True):
|
| 51 |
+
print(f" • {source}: {count} articles")
|
| 52 |
+
|
| 53 |
+
print("=" * 60)
|
| 54 |
+
print()
|
| 55 |
+
|
| 56 |
+
except FileNotFoundError:
|
| 57 |
+
print("⚠️ Aucune donnée trouvée. Le scraping va commencer...")
|
| 58 |
+
except Exception as e:
|
| 59 |
+
logger.error(f"Erreur lecture stats: {e}")
|
| 60 |
+
|
| 61 |
+
async def main():
|
| 62 |
+
print_banner()
|
| 63 |
+
|
| 64 |
+
# Vérifier la configuration
|
| 65 |
+
if not Path("sources.json").exists():
|
| 66 |
+
print("❌ Fichier sources.json manquant!")
|
| 67 |
+
return
|
| 68 |
+
|
| 69 |
+
# Afficher les sources configurées
|
| 70 |
+
with open("sources.json", 'r', encoding='utf-8') as f:
|
| 71 |
+
config = json.load(f)
|
| 72 |
+
|
| 73 |
+
sources = config.get('sources', [])
|
| 74 |
+
active_sources = [s for s in sources if s.get('active', True)]
|
| 75 |
+
togo_sources = [s for s in active_sources if s.get('pays') == 'Togo']
|
| 76 |
+
|
| 77 |
+
print(f"🎯 Sources togolaises actives: {len(togo_sources)}")
|
| 78 |
+
for source in togo_sources:
|
| 79 |
+
print(f" • {source['name']} - {source['url']}")
|
| 80 |
+
print()
|
| 81 |
+
|
| 82 |
+
# Stats avant scraping
|
| 83 |
+
print_stats()
|
| 84 |
+
|
| 85 |
+
# Lancer le scraping
|
| 86 |
+
print("🚀 Démarrage du scraping massif...")
|
| 87 |
+
print("⏳ Cela peut prendre plusieurs minutes...\n")
|
| 88 |
+
|
| 89 |
+
scraper = ScrapDjiScraper("sources.json")
|
| 90 |
+
await scraper.run()
|
| 91 |
+
|
| 92 |
+
# Stats après scraping
|
| 93 |
+
print("\n✅ Scraping terminé!")
|
| 94 |
+
print_stats()
|
| 95 |
+
|
| 96 |
+
if __name__ == "__main__":
|
| 97 |
+
try:
|
| 98 |
+
asyncio.run(main())
|
| 99 |
+
except KeyboardInterrupt:
|
| 100 |
+
print("\n\n⚠️ Scraping interrompu par l'utilisateur")
|
| 101 |
+
print_stats()
|
| 102 |
+
except Exception as e:
|
| 103 |
+
logger.error(f"Erreur fatale: {e}")
|
| 104 |
+
print(f"\n❌ Erreur: {e}")
|
scraper/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# Module de scraping : extraction web, réseaux sociaux, documents
|
scraper/discovery.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Module de découverte unifié et optimisé pour Scrap-Dji
|
| 4 |
+
Conçu pour être rapide et économe en ressources (Async/Httpx)
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
import httpx
|
| 9 |
+
import json
|
| 10 |
+
import re
|
| 11 |
+
import os
|
| 12 |
+
from urllib.parse import urljoin, urlparse
|
| 13 |
+
from typing import List, Dict, Set
|
| 14 |
+
from bs4 import BeautifulSoup
|
| 15 |
+
from datetime import datetime
|
| 16 |
+
|
| 17 |
+
from utils.logger import setup_logger
|
| 18 |
+
from utils.config import SCRAPER_DELAY, SCRAPER_USER_AGENT
|
| 19 |
+
|
| 20 |
+
logger = setup_logger(__name__)
|
| 21 |
+
|
| 22 |
+
class UnifiedDiscovery:
|
| 23 |
+
def __init__(self):
|
| 24 |
+
self.headers = {'User-Agent': SCRAPER_USER_AGENT}
|
| 25 |
+
self.african_domains = {'.sn', '.ml', '.ci', '.ng', '.gh', '.ke', '.ma', '.tn', '.dz', '.cm', '.cd', '.ga', '.bj', '.tg'}
|
| 26 |
+
self.seed_sources = [
|
| 27 |
+
"https://www.allafrica.com",
|
| 28 |
+
"https://www.africanews.com",
|
| 29 |
+
"https://www.bbc.com/africa",
|
| 30 |
+
"https://www.rfi.fr/fr/afrique"
|
| 31 |
+
]
|
| 32 |
+
self.african_keywords = ['afrique', 'africa', 'actualités', 'news', 'journal', 'presse']
|
| 33 |
+
|
| 34 |
+
async def is_african_site(self, client: httpx.AsyncClient, url: str) -> bool:
|
| 35 |
+
"""Vérification rapide (Domaine) puis analyse de contenu si nécessaire"""
|
| 36 |
+
domain = urlparse(url).netloc.lower()
|
| 37 |
+
if any(domain.endswith(ext) for ext in self.african_domains):
|
| 38 |
+
return True
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
resp = await client.get(url, timeout=5.0)
|
| 42 |
+
if resp.status_code == 200:
|
| 43 |
+
text = resp.text.lower()
|
| 44 |
+
return sum(text.count(kw) for kw in self.african_keywords) > 3
|
| 45 |
+
except Exception:
|
| 46 |
+
pass
|
| 47 |
+
return False
|
| 48 |
+
|
| 49 |
+
async def explore_source(self, client: httpx.AsyncClient, seed_url: str) -> List[Dict]:
|
| 50 |
+
"""Explore une source de départ asynchronement"""
|
| 51 |
+
discovered = []
|
| 52 |
+
try:
|
| 53 |
+
logger.info(f"🔍 Exploration de {seed_url}...")
|
| 54 |
+
resp = await client.get(seed_url, timeout=10.0)
|
| 55 |
+
soup = BeautifulSoup(resp.content, 'lxml')
|
| 56 |
+
|
| 57 |
+
links = soup.find_all('a', href=True)
|
| 58 |
+
for link in links:
|
| 59 |
+
href = link.get('href')
|
| 60 |
+
if not href or not href.startswith('http'): continue
|
| 61 |
+
|
| 62 |
+
full_url = urljoin(seed_url, href)
|
| 63 |
+
domain = urlparse(full_url).netloc
|
| 64 |
+
|
| 65 |
+
if any(ext in domain for ext in self.african_domains):
|
| 66 |
+
discovered.append({
|
| 67 |
+
'name': domain.replace('.', '_'),
|
| 68 |
+
'url': f"{urlparse(full_url).scheme}://{domain}",
|
| 69 |
+
'type': 'news',
|
| 70 |
+
'active': True,
|
| 71 |
+
'discovered_at': datetime.now().isoformat()
|
| 72 |
+
})
|
| 73 |
+
except Exception as e:
|
| 74 |
+
logger.error(f"Erreur seed {seed_url}: {e}")
|
| 75 |
+
return discovered
|
| 76 |
+
|
| 77 |
+
async def run_discovery(self):
|
| 78 |
+
async with httpx.AsyncClient(headers=self.headers, follow_redirects=True) as client:
|
| 79 |
+
tasks = [self.explore_source(client, seed) for seed in self.seed_sources]
|
| 80 |
+
results = await asyncio.gather(*tasks)
|
| 81 |
+
|
| 82 |
+
all_sources = []
|
| 83 |
+
seen = set()
|
| 84 |
+
for batch in results:
|
| 85 |
+
for s in batch:
|
| 86 |
+
if s['url'] not in seen:
|
| 87 |
+
all_sources.append(s)
|
| 88 |
+
seen.add(s['url'])
|
| 89 |
+
|
| 90 |
+
logger.info(f"✅ {len(all_sources)} sources uniques découvertes.")
|
| 91 |
+
self.save_sources(all_sources)
|
| 92 |
+
return all_sources
|
| 93 |
+
|
| 94 |
+
def save_sources(self, sources: List[Dict], filename: str = "sources.json"):
|
| 95 |
+
existing = {"sources": []}
|
| 96 |
+
if os.path.exists(filename):
|
| 97 |
+
try:
|
| 98 |
+
with open(filename, 'r', encoding='utf-8') as f:
|
| 99 |
+
existing = json.load(f)
|
| 100 |
+
except: pass
|
| 101 |
+
|
| 102 |
+
existing_urls = {s['url'] for s in existing.get('sources', [])}
|
| 103 |
+
new_sources = [s for s in sources if s['url'] not in existing_urls]
|
| 104 |
+
|
| 105 |
+
existing['sources'].extend(new_sources)
|
| 106 |
+
|
| 107 |
+
with open(filename, 'w', encoding='utf-8') as f:
|
| 108 |
+
json.dump(existing, f, indent=2, ensure_ascii=False)
|
| 109 |
+
logger.info(f"💾 {len(new_sources)} nouvelles sources ajoutées à {filename}")
|
| 110 |
+
|
| 111 |
+
if __name__ == "__main__":
|
| 112 |
+
discovery = UnifiedDiscovery()
|
| 113 |
+
asyncio.run(discovery.run_discovery())
|
scraper/main.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import httpx
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
from typing import Dict, List, Optional
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from lxml import html
|
| 8 |
+
from newspaper import Article
|
| 9 |
+
|
| 10 |
+
from utils.config import STORAGE_PATH, SCRAPER_DELAY, SCRAPER_CONCURRENT_REQUESTS
|
| 11 |
+
from utils.logger import setup_logger
|
| 12 |
+
from db.postgres_connector import SessionLocal
|
| 13 |
+
from db.models import Document, DocumentVersion
|
| 14 |
+
from db.mongo_connector import save_to_mongo
|
| 15 |
+
from parser.cleaner import clean_html
|
| 16 |
+
from indexer.typesense_indexer import index_document as index_typesense
|
| 17 |
+
from utils.uuid_gen import generate_uuid
|
| 18 |
+
|
| 19 |
+
logger = setup_logger(__name__)
|
| 20 |
+
|
| 21 |
+
class ScrapDjiScraper:
|
| 22 |
+
def __init__(self, sources_file: str = "sources.json"):
|
| 23 |
+
self.sources_file = sources_file
|
| 24 |
+
self.sources = self.load_sources()
|
| 25 |
+
self.sem = asyncio.Semaphore(10) # Augmenté pour scraping massif
|
| 26 |
+
self.buffer = []
|
| 27 |
+
self.buffer_size = 50 # Buffer augmenté pour scraping massif
|
| 28 |
+
self.discovered_urls = set() # Pour éviter les doublons
|
| 29 |
+
|
| 30 |
+
def load_sources(self) -> Dict:
|
| 31 |
+
if not os.path.exists(self.sources_file):
|
| 32 |
+
return {"sources": []}
|
| 33 |
+
with open(self.sources_file, 'r', encoding='utf-8') as f:
|
| 34 |
+
return json.load(f)
|
| 35 |
+
|
| 36 |
+
async def scrape_article(self, client: httpx.AsyncClient, source: Dict, url: str) -> Optional[Dict]:
|
| 37 |
+
"""Scrape ultra-rapide avec lxml (C-level parsing)"""
|
| 38 |
+
try:
|
| 39 |
+
resp = await client.get(url, timeout=7.0)
|
| 40 |
+
if resp.status_code != 200: return None
|
| 41 |
+
|
| 42 |
+
# Utilisation de lxml directement pour la vitesse pure
|
| 43 |
+
tree = html.fromstring(resp.content)
|
| 44 |
+
title = tree.xpath('//h1/text()')
|
| 45 |
+
title = title[0].strip() if title else "Sans titre"
|
| 46 |
+
|
| 47 |
+
# Extraction brute via newspaper (très optimisée)
|
| 48 |
+
article = Article(url)
|
| 49 |
+
article.set_html(resp.text)
|
| 50 |
+
article.parse()
|
| 51 |
+
content = article.text
|
| 52 |
+
|
| 53 |
+
if len(content) < 100: return None
|
| 54 |
+
|
| 55 |
+
return {
|
| 56 |
+
'id': generate_uuid(),
|
| 57 |
+
'titre': title,
|
| 58 |
+
'texte': content,
|
| 59 |
+
'source_url': url,
|
| 60 |
+
'pays': source.get('pays', 'Afrique'),
|
| 61 |
+
'langue': source.get('langue', 'fr'),
|
| 62 |
+
'type_document': 'article',
|
| 63 |
+
'date': datetime.now().isoformat()
|
| 64 |
+
}
|
| 65 |
+
except Exception as e:
|
| 66 |
+
logger.debug(f"Erreur scraping {url}: {e}")
|
| 67 |
+
return None
|
| 68 |
+
|
| 69 |
+
async def discover_links(self, client: httpx.AsyncClient, base_url: str) -> List[str]:
|
| 70 |
+
"""Découvre les liens d'articles sur une page"""
|
| 71 |
+
try:
|
| 72 |
+
resp = await client.get(base_url, timeout=10.0)
|
| 73 |
+
if resp.status_code != 200:
|
| 74 |
+
return []
|
| 75 |
+
|
| 76 |
+
tree = html.fromstring(resp.content)
|
| 77 |
+
# Extraction de tous les liens d'articles
|
| 78 |
+
links = tree.xpath('//a/@href')
|
| 79 |
+
|
| 80 |
+
# Filtrage et normalisation des URLs
|
| 81 |
+
article_urls = []
|
| 82 |
+
for link in links:
|
| 83 |
+
if not link:
|
| 84 |
+
continue
|
| 85 |
+
# Construire l'URL complète
|
| 86 |
+
if link.startswith('/'):
|
| 87 |
+
from urllib.parse import urljoin
|
| 88 |
+
link = urljoin(base_url, link)
|
| 89 |
+
elif not link.startswith('http'):
|
| 90 |
+
continue
|
| 91 |
+
|
| 92 |
+
# Filtrer les URLs qui ressemblent à des articles
|
| 93 |
+
if any(x in link.lower() for x in ['article', 'actualite', 'news', '/20', '-20']):
|
| 94 |
+
if link not in self.discovered_urls:
|
| 95 |
+
article_urls.append(link)
|
| 96 |
+
self.discovered_urls.add(link)
|
| 97 |
+
|
| 98 |
+
return article_urls[:100] # Limiter à 100 liens par page
|
| 99 |
+
except Exception as e:
|
| 100 |
+
logger.debug(f"Erreur découverte liens {base_url}: {e}")
|
| 101 |
+
return []
|
| 102 |
+
|
| 103 |
+
async def flush_buffer(self):
|
| 104 |
+
"""Sauvegarde groupée pour réduire les accès disque/réseau"""
|
| 105 |
+
if not self.buffer: return
|
| 106 |
+
logger.info(f"💾 Flush buffer: sauvegarde de {len(self.buffer)} documents...")
|
| 107 |
+
tasks = [self.save_everywhere(doc) for doc in self.buffer]
|
| 108 |
+
await asyncio.gather(*tasks)
|
| 109 |
+
self.buffer = []
|
| 110 |
+
|
| 111 |
+
async def save_everywhere(self, doc: Dict):
|
| 112 |
+
# Fallback de secours : Toujours sauver en local JSON pour le test
|
| 113 |
+
try:
|
| 114 |
+
os.makedirs("data", exist_ok=True)
|
| 115 |
+
local_file = "data/search_index.json"
|
| 116 |
+
data = []
|
| 117 |
+
if os.path.exists(local_file):
|
| 118 |
+
with open(local_file, "r", encoding="utf-8") as f:
|
| 119 |
+
data = json.load(f)
|
| 120 |
+
data.append(doc)
|
| 121 |
+
# Garder tous les documents pour scraping massif
|
| 122 |
+
with open(local_file, "w", encoding="utf-8") as f:
|
| 123 |
+
json.dump(data, f, indent=2, ensure_ascii=False)
|
| 124 |
+
except Exception as e:
|
| 125 |
+
logger.error(f"Erreur sauvegarde JSON locale: {e}")
|
| 126 |
+
|
| 127 |
+
# Les autres bases (échoueront silencieusement si non installées)
|
| 128 |
+
try:
|
| 129 |
+
session = SessionLocal()
|
| 130 |
+
new_doc = Document(**{k: v for k, v in doc.items() if k in Document.__table__.columns})
|
| 131 |
+
session.add(new_doc)
|
| 132 |
+
session.commit()
|
| 133 |
+
session.close()
|
| 134 |
+
except: pass
|
| 135 |
+
|
| 136 |
+
try: await save_to_mongo("documents", doc)
|
| 137 |
+
except: pass
|
| 138 |
+
try: index_typesense("documents", doc)
|
| 139 |
+
except: pass
|
| 140 |
+
|
| 141 |
+
async def process_source(self, client: httpx.AsyncClient, source: Dict):
|
| 142 |
+
"""Traite une source avec découverte de liens"""
|
| 143 |
+
count = 0
|
| 144 |
+
async with self.sem:
|
| 145 |
+
try:
|
| 146 |
+
# 1. Scraper la page principale
|
| 147 |
+
doc = await self.scrape_article(client, source, source['url'])
|
| 148 |
+
if doc:
|
| 149 |
+
self.buffer.append(doc)
|
| 150 |
+
count += 1
|
| 151 |
+
|
| 152 |
+
# 2. Découvrir et scraper les articles liés
|
| 153 |
+
logger.info(f"🔍 Découverte de liens sur {source['name']}...")
|
| 154 |
+
article_urls = await self.discover_links(client, source['url'])
|
| 155 |
+
logger.info(f"📰 {len(article_urls)} articles découverts sur {source['name']}")
|
| 156 |
+
|
| 157 |
+
# 3. Scraper les articles découverts (avec limite)
|
| 158 |
+
for url in article_urls[:50]: # Limiter à 50 articles par source pour commencer
|
| 159 |
+
doc = await self.scrape_article(client, source, url)
|
| 160 |
+
if doc:
|
| 161 |
+
self.buffer.append(doc)
|
| 162 |
+
count += 1
|
| 163 |
+
if len(self.buffer) >= self.buffer_size:
|
| 164 |
+
await self.flush_buffer()
|
| 165 |
+
await asyncio.sleep(0.5) # Petit délai pour ne pas surcharger
|
| 166 |
+
|
| 167 |
+
return count
|
| 168 |
+
except Exception as e:
|
| 169 |
+
logger.error(f"Erreur traitement source {source.get('name')}: {e}")
|
| 170 |
+
return count
|
| 171 |
+
|
| 172 |
+
async def run(self):
|
| 173 |
+
active_sources = [s for s in self.sources.get('sources', []) if s.get('active', True)]
|
| 174 |
+
# Utilisation de HTTP/2 pour plus de vitesse si supporté par le site
|
| 175 |
+
async with httpx.AsyncClient(timeout=10.0, follow_redirects=True, http2=True) as client:
|
| 176 |
+
tasks = [self.process_source(client, s) for s in active_sources]
|
| 177 |
+
counts = await asyncio.gather(*tasks)
|
| 178 |
+
await self.flush_buffer() # Dernier flush
|
| 179 |
+
logger.info(f"⚡ Turbo Scraping terminé. {sum(counts)} documents traités.")
|
| 180 |
+
|
| 181 |
+
import os
|
| 182 |
+
from datetime import datetime
|
| 183 |
+
|
| 184 |
+
if __name__ == "__main__":
|
| 185 |
+
scraper = ScrapDjiScraper()
|
| 186 |
+
asyncio.run(scraper.run())
|
scraper/rss.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import feedparser
|
| 2 |
+
from datetime import datetime
|
| 3 |
+
from typing import List, Dict
|
| 4 |
+
from parser.cleaner import clean_html
|
| 5 |
+
from parser.hasher import hash_text
|
| 6 |
+
from indexer.typesense_indexer import create_collection_if_not_exists, index_document
|
| 7 |
+
|
| 8 |
+
SCHEMA = {
|
| 9 |
+
"name": "documents",
|
| 10 |
+
"fields": [
|
| 11 |
+
{"name": "id", "type": "string"},
|
| 12 |
+
{"name": "titre", "type": "string"},
|
| 13 |
+
{"name": "texte", "type": "string"},
|
| 14 |
+
{"name": "langue", "type": "string", "facet": True},
|
| 15 |
+
{"name": "type_document", "type": "string", "facet": True},
|
| 16 |
+
{"name": "pays", "type": "string", "facet": True},
|
| 17 |
+
{"name": "source_url", "type": "string"},
|
| 18 |
+
{"name": "date", "type": "string"}
|
| 19 |
+
]
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
def fetch_and_index_feeds(feed_urls: List[str]) -> Dict[str, int]:
|
| 23 |
+
create_collection_if_not_exists(SCHEMA)
|
| 24 |
+
total, ok = 0, 0
|
| 25 |
+
for url in feed_urls:
|
| 26 |
+
parsed = feedparser.parse(url)
|
| 27 |
+
for entry in parsed.entries:
|
| 28 |
+
total += 1
|
| 29 |
+
title = entry.get('title', '')
|
| 30 |
+
link = entry.get('link', '')
|
| 31 |
+
summary = entry.get('summary', '') or entry.get('description', '')
|
| 32 |
+
content = summary
|
| 33 |
+
text = clean_html(content)
|
| 34 |
+
if not text and title:
|
| 35 |
+
text = title
|
| 36 |
+
if not text:
|
| 37 |
+
continue
|
| 38 |
+
# Déduplication stable
|
| 39 |
+
doc_id = hash_text((link or title) + '|' + text)[:16]
|
| 40 |
+
doc = {
|
| 41 |
+
"id": doc_id,
|
| 42 |
+
"titre": title or link,
|
| 43 |
+
"texte": text[:8000],
|
| 44 |
+
"langue": "fr", # best effort; détection langue à ajouter
|
| 45 |
+
"type_document": "rss",
|
| 46 |
+
"pays": "", # enrichissement géo à ajouter
|
| 47 |
+
"source_url": link,
|
| 48 |
+
"date": datetime.utcnow().isoformat()
|
| 49 |
+
}
|
| 50 |
+
try:
|
| 51 |
+
index_document("documents", doc)
|
| 52 |
+
ok += 1
|
| 53 |
+
except Exception:
|
| 54 |
+
# on ignore silencieusement pour la robustesse POC
|
| 55 |
+
pass
|
| 56 |
+
return {"processed": total, "indexed": ok}
|
| 57 |
+
|
scraper/rss_sources.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FEEDS = [
|
| 2 |
+
# Afrique - actualités
|
| 3 |
+
"https://www.rfi.fr/fr/afrique/rss",
|
| 4 |
+
"https://www.aljazeera.com/xml/rss/all.xml", # contient Afrique, filtrage côté contenu
|
| 5 |
+
"https://feeds.bbci.co.uk/news/world/africa/rss.xml",
|
| 6 |
+
|
| 7 |
+
# Science générale
|
| 8 |
+
"https://www.nature.com/subjects/artificial-intelligence/rss",
|
| 9 |
+
"https://www.sciencedaily.com/rss/computers_math/artificial_intelligence.xml",
|
| 10 |
+
|
| 11 |
+
# ArXiv - math, IA
|
| 12 |
+
"https://export.arxiv.org/rss/cs.AI",
|
| 13 |
+
"https://export.arxiv.org/rss/cs.LG",
|
| 14 |
+
"https://export.arxiv.org/rss/math",
|
| 15 |
+
]
|
| 16 |
+
|
scripts/bootstrap_typesense.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import os
|
| 3 |
+
from indexer.typesense_indexer import create_collection_if_not_exists, index_document
|
| 4 |
+
|
| 5 |
+
SCHEMA = {
|
| 6 |
+
"name": "documents",
|
| 7 |
+
"fields": [
|
| 8 |
+
{"name": "id", "type": "string"},
|
| 9 |
+
{"name": "titre", "type": "string"},
|
| 10 |
+
{"name": "texte", "type": "string"},
|
| 11 |
+
{"name": "langue", "type": "string", "facet": True},
|
| 12 |
+
{"name": "type_document", "type": "string", "facet": True},
|
| 13 |
+
{"name": "pays", "type": "string", "facet": True},
|
| 14 |
+
{"name": "source_url", "type": "string"},
|
| 15 |
+
{"name": "date", "type": "string"}
|
| 16 |
+
]
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
def main():
|
| 20 |
+
create_collection_if_not_exists(SCHEMA)
|
| 21 |
+
path = os.environ.get("SEED_JSONL", "datasets/ewe/final/ewe_corpus.jsonl")
|
| 22 |
+
if os.path.exists(path):
|
| 23 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 24 |
+
for line in f:
|
| 25 |
+
doc = json.loads(line)
|
| 26 |
+
doc_id = doc.get("uuid") or doc.get("id") or os.urandom(8).hex()
|
| 27 |
+
doc["id"] = doc_id
|
| 28 |
+
index_document("documents", doc)
|
| 29 |
+
print("Seed terminé.")
|
| 30 |
+
else:
|
| 31 |
+
print("Aucun fichier de seed trouvé, collection créée sans documents.")
|
| 32 |
+
|
| 33 |
+
if __name__ == "__main__":
|
| 34 |
+
main()
|
| 35 |
+
|
setup.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Script de configuration pour Scrap-Dji
|
| 4 |
+
Initialise les bases de données et les services nécessaires
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import sys
|
| 9 |
+
import subprocess
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
def check_python_version():
|
| 13 |
+
"""Vérifie la version de Python"""
|
| 14 |
+
if sys.version_info < (3, 10):
|
| 15 |
+
print("❌ Python 3.10+ requis")
|
| 16 |
+
sys.exit(1)
|
| 17 |
+
print("✅ Version Python OK")
|
| 18 |
+
|
| 19 |
+
def install_dependencies():
|
| 20 |
+
"""Installe les dépendances"""
|
| 21 |
+
print("📦 Installation des dépendances...")
|
| 22 |
+
try:
|
| 23 |
+
subprocess.run([sys.executable, "-m", "pip", "install", "-r", "requirements.txt"], check=True)
|
| 24 |
+
print("✅ Dépendances installées")
|
| 25 |
+
except subprocess.CalledProcessError:
|
| 26 |
+
print("❌ Erreur lors de l'installation des dépendances")
|
| 27 |
+
sys.exit(1)
|
| 28 |
+
|
| 29 |
+
def create_directories():
|
| 30 |
+
"""Crée les répertoires nécessaires"""
|
| 31 |
+
directories = [
|
| 32 |
+
"storage_data",
|
| 33 |
+
"logs",
|
| 34 |
+
"data",
|
| 35 |
+
"temp"
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
for directory in directories:
|
| 39 |
+
Path(directory).mkdir(exist_ok=True)
|
| 40 |
+
print(f"✅ Répertoire créé: {directory}")
|
| 41 |
+
|
| 42 |
+
def setup_env_file():
|
| 43 |
+
"""Configure le fichier .env"""
|
| 44 |
+
if not os.path.exists(".env"):
|
| 45 |
+
if os.path.exists("config.env.example"):
|
| 46 |
+
os.system("copy config.env.example .env")
|
| 47 |
+
print("✅ Fichier .env créé (à configurer)")
|
| 48 |
+
else:
|
| 49 |
+
print("⚠️ Créez un fichier .env basé sur config.env.example")
|
| 50 |
+
else:
|
| 51 |
+
print("✅ Fichier .env existe déjà")
|
| 52 |
+
|
| 53 |
+
def init_databases():
|
| 54 |
+
"""Initialise les bases de données"""
|
| 55 |
+
print("🗄️ Initialisation des bases de données...")
|
| 56 |
+
try:
|
| 57 |
+
from db.postgres_connector import init_db
|
| 58 |
+
init_db()
|
| 59 |
+
print("✅ Base PostgreSQL initialisée")
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"⚠️ Erreur PostgreSQL: {e}")
|
| 62 |
+
print(" Assurez-vous que PostgreSQL est installé et configuré")
|
| 63 |
+
|
| 64 |
+
def check_services():
|
| 65 |
+
"""Vérifie les services requis"""
|
| 66 |
+
services = {
|
| 67 |
+
"PostgreSQL": "postgresql://localhost:5432",
|
| 68 |
+
"MongoDB": "mongodb://localhost:27017",
|
| 69 |
+
"Typesense": "http://localhost:8108",
|
| 70 |
+
"Qdrant": "http://localhost:6333"
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
print("🔍 Vérification des services...")
|
| 74 |
+
for service, url in services.items():
|
| 75 |
+
try:
|
| 76 |
+
# Vérification basique
|
| 77 |
+
print(f" {service}: {'✅' if 'localhost' in url else '⚠️'}")
|
| 78 |
+
except:
|
| 79 |
+
print(f" {service}: ❌")
|
| 80 |
+
|
| 81 |
+
def main():
|
| 82 |
+
print("🚀 Configuration de Scrap-Dji")
|
| 83 |
+
print("=" * 40)
|
| 84 |
+
|
| 85 |
+
check_python_version()
|
| 86 |
+
install_dependencies()
|
| 87 |
+
create_directories()
|
| 88 |
+
setup_env_file()
|
| 89 |
+
init_databases()
|
| 90 |
+
check_services()
|
| 91 |
+
|
| 92 |
+
print("\n" + "=" * 40)
|
| 93 |
+
print("✅ Configuration terminée!")
|
| 94 |
+
print("\n📋 Prochaines étapes:")
|
| 95 |
+
print("1. Configurez le fichier .env avec vos paramètres")
|
| 96 |
+
print("2. Installez et démarrez PostgreSQL, MongoDB, Typesense, Qdrant")
|
| 97 |
+
print("3. Configurez vos sources dans sources.json")
|
| 98 |
+
print("4. Lancez le scraping avec: python -m scraper.main")
|
| 99 |
+
|
| 100 |
+
if __name__ == "__main__":
|
| 101 |
+
main()
|
sources.json
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"sources": [
|
| 3 |
+
{
|
| 4 |
+
"name": "TogoFirst",
|
| 5 |
+
"type": "news",
|
| 6 |
+
"url": "https://www.togofirst.com",
|
| 7 |
+
"pays": "Togo",
|
| 8 |
+
"langue": "fr",
|
| 9 |
+
"active": true
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"name": "27septembre",
|
| 13 |
+
"type": "news",
|
| 14 |
+
"url": "https://www.27septembre.com",
|
| 15 |
+
"pays": "Togo",
|
| 16 |
+
"langue": "fr",
|
| 17 |
+
"active": true
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"name": "IciLome",
|
| 21 |
+
"type": "news",
|
| 22 |
+
"url": "https://icilome.com",
|
| 23 |
+
"pays": "Togo",
|
| 24 |
+
"langue": "fr",
|
| 25 |
+
"active": true
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"name": "TogoBreakingNews",
|
| 29 |
+
"type": "news",
|
| 30 |
+
"url": "https://www.togobreakingnews.info",
|
| 31 |
+
"pays": "Togo",
|
| 32 |
+
"langue": "fr",
|
| 33 |
+
"active": true
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"name": "RepublicOfTogo",
|
| 37 |
+
"type": "news",
|
| 38 |
+
"url": "https://www.republicoftogo.com",
|
| 39 |
+
"pays": "Togo",
|
| 40 |
+
"langue": "fr",
|
| 41 |
+
"active": true
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"name": "TogoActualite",
|
| 45 |
+
"type": "news",
|
| 46 |
+
"url": "https://togoactualite.com",
|
| 47 |
+
"pays": "Togo",
|
| 48 |
+
"langue": "fr",
|
| 49 |
+
"active": true
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"name": "LomeInfo",
|
| 53 |
+
"type": "news",
|
| 54 |
+
"url": "https://www.lomeinfo.net",
|
| 55 |
+
"pays": "Togo",
|
| 56 |
+
"langue": "fr",
|
| 57 |
+
"active": true
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"name": "TogoSite",
|
| 61 |
+
"type": "news",
|
| 62 |
+
"url": "https://www.togosite.com",
|
| 63 |
+
"pays": "Togo",
|
| 64 |
+
"langue": "fr",
|
| 65 |
+
"active": true
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"name": "BeninWebTV",
|
| 69 |
+
"type": "news",
|
| 70 |
+
"url": "https://beninwebtv.com",
|
| 71 |
+
"pays": "Bénin",
|
| 72 |
+
"langue": "fr",
|
| 73 |
+
"active": true
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"name": "LaNouvelleRepublique",
|
| 77 |
+
"type": "news",
|
| 78 |
+
"url": "https://lanouvellerepublique.bj",
|
| 79 |
+
"pays": "Bénin",
|
| 80 |
+
"langue": "fr",
|
| 81 |
+
"active": true
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"name": "BeninActu",
|
| 85 |
+
"type": "news",
|
| 86 |
+
"url": "https://beninactu.com",
|
| 87 |
+
"pays": "Bénin",
|
| 88 |
+
"langue": "fr",
|
| 89 |
+
"active": true
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"name": "Matin Libre",
|
| 93 |
+
"type": "news",
|
| 94 |
+
"url": "https://www.matinlibre.com",
|
| 95 |
+
"pays": "Bénin",
|
| 96 |
+
"langue": "fr",
|
| 97 |
+
"active": true
|
| 98 |
+
}
|
| 99 |
+
],
|
| 100 |
+
"settings": {
|
| 101 |
+
"delay_between_requests": 0.5,
|
| 102 |
+
"max_pages_per_source": 500,
|
| 103 |
+
"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",
|
| 104 |
+
"concurrent_requests": 10,
|
| 105 |
+
"buffer_size": 50,
|
| 106 |
+
"deep_crawl": true
|
| 107 |
+
}
|
| 108 |
+
}
|