joel commited on
Commit
655a82b
·
1 Parent(s): 491c975

Fix: JSON syntax error and deploy all changes maj async auto serch

Browse files
Files changed (3) hide show
  1. app.py +22 -10
  2. scraper/main.py +21 -4
  3. sources.json +56 -0
app.py CHANGED
@@ -307,34 +307,46 @@ async def gradio_search(query: str, pays: str, langue: str, fuzzy: bool):
307
  titre = doc.get('titre', 'Sans titre')
308
  texte = doc.get('texte', '')[:200] + "..."
309
  pays_doc = doc.get('pays', 'Inconnu')
 
310
  score = doc.get('_score', 0)
 
311
 
312
  output += f"### {i}. {titre}\n"
313
- output += f"**Pays:** {pays_doc}\n\n"
314
  output += f"{texte}\n\n"
315
- output += f"[🔗 Source]({source})\n\n"
316
  output += "---\n\n"
317
 
318
  return output
319
 
320
- def gradio_stats():
321
  """Affiche les statistiques pour Gradio"""
322
- stats = search_engine.get_stats()
 
323
 
324
  output = "# 📊 Statistiques de la Base de Données\n\n"
325
  output += f"**Total de documents:** {stats['total_documents']}\n\n"
326
 
327
  output += "## 🌍 Répartition par Pays\n\n"
328
- for pays, count in sorted(stats['pays'].items(), key=lambda x: x[1], reverse=True):
329
- output += f"- **{pays}:** {count} documents\n"
 
 
 
330
 
331
  output += "\n## 🗣️ Répartition par Langue\n\n"
332
- for langue, count in sorted(stats['langues'].items(), key=lambda x: x[1], reverse=True):
333
- output += f"- **{langue}:** {count} documents\n"
 
 
 
334
 
335
  output += "\n## 📰 Répartition par Source\n\n"
336
- for source, count in sorted(stats['sources'].items(), key=lambda x: x[1], reverse=True)[:10]:
337
- output += f"- **{source}:** {count} documents\n"
 
 
 
338
 
339
  return output
340
 
 
307
  titre = doc.get('titre', 'Sans titre')
308
  texte = doc.get('texte', '')[:200] + "..."
309
  pays_doc = doc.get('pays', 'Inconnu')
310
+ source = doc.get('source_url', '#')
311
  score = doc.get('_score', 0)
312
+ date = doc.get('date', '').split('T')[0]
313
 
314
  output += f"### {i}. {titre}\n"
315
+ output += f"**Pays:** {pays_doc} | **Date:** {date}\n\n"
316
  output += f"{texte}\n\n"
317
+ output += f"[🔗 Lire la source]({source})\n\n"
318
  output += "---\n\n"
319
 
320
  return output
321
 
322
+ async def gradio_stats():
323
  """Affiche les statistiques pour Gradio"""
324
+ # Fix: await pour la fonction async
325
+ stats = await search_engine.get_stats()
326
 
327
  output = "# 📊 Statistiques de la Base de Données\n\n"
328
  output += f"**Total de documents:** {stats['total_documents']}\n\n"
329
 
330
  output += "## 🌍 Répartition par Pays\n\n"
331
+ if stats['pays']:
332
+ for pays, count in sorted(stats['pays'].items(), key=lambda x: x[1], reverse=True):
333
+ output += f"- **{pays}:** {count} documents\n"
334
+ else:
335
+ output += "_Aucune donnée_\n"
336
 
337
  output += "\n## 🗣️ Répartition par Langue\n\n"
338
+ if stats['langues']:
339
+ for langue, count in sorted(stats['langues'].items(), key=lambda x: x[1], reverse=True):
340
+ output += f"- **{langue}:** {count} documents\n"
341
+ else:
342
+ output += "_Aucune donnée_\n"
343
 
344
  output += "\n## 📰 Répartition par Source\n\n"
345
+ if stats['sources']:
346
+ for source, count in sorted(stats['sources'].items(), key=lambda x: x[1], reverse=True)[:10]:
347
+ output += f"- **{source}:** {count} documents\n"
348
+ else:
349
+ output += "_Aucune donnée_\n"
350
 
351
  return output
352
 
scraper/main.py CHANGED
@@ -11,7 +11,8 @@ from utils.config import STORAGE_PATH, SCRAPER_DELAY, SCRAPER_CONCURRENT_REQUEST
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
@@ -25,7 +26,7 @@ class ScrapDjiScraper:
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):
@@ -33,6 +34,16 @@ class ScrapDjiScraper:
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:
@@ -77,7 +88,7 @@ class ScrapDjiScraper:
77
  loop = asyncio.get_event_loop()
78
  links = await loop.run_in_executor(None, self._extract_links_sync, resp.content, base_url)
79
 
80
- # Filtrage rapide (peut rester dans le thread principal ou migrer si très lourd)
81
  new_links = []
82
  for link in links:
83
  if link not in self.discovered_urls:
@@ -162,7 +173,8 @@ class ScrapDjiScraper:
162
  count = 0
163
  async with self.sem:
164
  try:
165
- # 1. Scraper la page principale
 
166
  doc = await self.scrape_article(client, source, source['url'])
167
  if doc:
168
  self.buffer.append(doc)
@@ -175,6 +187,11 @@ class ScrapDjiScraper:
175
 
176
  # 3. Scraper les articles découverts (avec limite)
177
  for url in article_urls[:50]: # Limiter à 50 articles par source pour commencer
 
 
 
 
 
178
  doc = await self.scrape_article(client, source, url)
179
  if doc:
180
  self.buffer.append(doc)
 
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, db
15
+
16
  from parser.cleaner import clean_html
17
  from indexer.typesense_indexer import index_document as index_typesense
18
  from utils.uuid_gen import generate_uuid
 
26
  self.sem = asyncio.Semaphore(10) # Augmenté pour scraping massif
27
  self.buffer = []
28
  self.buffer_size = 50 # Buffer augmenté pour scraping massif
29
+ self.discovered_urls = set() # Pour éviter les doublons (mémoire session)
30
 
31
  def load_sources(self) -> Dict:
32
  if not os.path.exists(self.sources_file):
 
34
  with open(self.sources_file, 'r', encoding='utf-8') as f:
35
  return json.load(f)
36
 
37
+ async def is_url_scraped(self, url: str) -> bool:
38
+ """Vérifie si l'URL existe déjà dans MongoDB"""
39
+ try:
40
+ # Vérification rapide sur l'ID (source_url est unique ?)
41
+ # Ou count_documents
42
+ doc = await db["documents"].find_one({"source_url": url}, {"_id": 1})
43
+ return doc is not None
44
+ except Exception:
45
+ return False
46
+
47
  async def scrape_article(self, client: httpx.AsyncClient, source: Dict, url: str) -> Optional[Dict]:
48
  """Scrape ultra-rapide avec lxml (C-level parsing)"""
49
  try:
 
88
  loop = asyncio.get_event_loop()
89
  links = await loop.run_in_executor(None, self._extract_links_sync, resp.content, base_url)
90
 
91
+ # Filtrage rapide
92
  new_links = []
93
  for link in links:
94
  if link not in self.discovered_urls:
 
173
  count = 0
174
  async with self.sem:
175
  try:
176
+ # 1. Scraper la page principale (si non scrapée récemment?)
177
+ # On ne vérifie pas is_url_scraped pour la home car elle change
178
  doc = await self.scrape_article(client, source, source['url'])
179
  if doc:
180
  self.buffer.append(doc)
 
187
 
188
  # 3. Scraper les articles découverts (avec limite)
189
  for url in article_urls[:50]: # Limiter à 50 articles par source pour commencer
190
+ # Vérification anti-doublon MongoDB
191
+ if await self.is_url_scraped(url):
192
+ # logger.debug(f"⏭️ Déjà scrapé: {url}")
193
+ continue
194
+
195
  doc = await self.scrape_article(client, source, url)
196
  if doc:
197
  self.buffer.append(doc)
sources.json CHANGED
@@ -16,6 +16,14 @@
16
  "langue": "fr",
17
  "active": true
18
  },
 
 
 
 
 
 
 
 
19
  {
20
  "name": "IciLome",
21
  "type": "news",
@@ -40,6 +48,14 @@
40
  "langue": "fr",
41
  "active": true
42
  },
 
 
 
 
 
 
 
 
43
  {
44
  "name": "TogoActualite",
45
  "type": "news",
@@ -64,6 +80,46 @@
64
  "langue": "fr",
65
  "active": true
66
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  {
68
  "name": "BeninWebTV",
69
  "type": "news",
 
16
  "langue": "fr",
17
  "active": true
18
  },
19
+ {
20
+ "name": "27avril",
21
+ "type": "news",
22
+ "url": "https://www.27avril.com",
23
+ "pays": "Togo",
24
+ "langue": "fr",
25
+ "active": true
26
+ },
27
  {
28
  "name": "IciLome",
29
  "type": "news",
 
48
  "langue": "fr",
49
  "active": true
50
  },
51
+ {
52
+ "name": "RepubliqueTogolaise",
53
+ "type": "news",
54
+ "url": "https://www.republiquetogolaise.com",
55
+ "pays": "Togo",
56
+ "langue": "fr",
57
+ "active": true
58
+ },
59
  {
60
  "name": "TogoActualite",
61
  "type": "news",
 
80
  "langue": "fr",
81
  "active": true
82
  },
83
+ {
84
+ "name": "ActuLome",
85
+ "type": "news",
86
+ "url": "https://actulome.com",
87
+ "pays": "Togo",
88
+ "langue": "fr",
89
+ "active": true
90
+ },
91
+ {
92
+ "name": "24HeureInfo",
93
+ "type": "news",
94
+ "url": "https://24heureinfo.com",
95
+ "pays": "Togo",
96
+ "langue": "fr",
97
+ "active": true
98
+ },
99
+ {
100
+ "name": "RadioLome",
101
+ "type": "news",
102
+ "url": "https://www.radiolome.tg",
103
+ "pays": "Togo",
104
+ "langue": "fr",
105
+ "active": true
106
+ },
107
+ {
108
+ "name": "aLome",
109
+ "type": "news",
110
+ "url": "http://www.alome.com",
111
+ "pays": "Togo",
112
+ "langue": "fr",
113
+ "active": true
114
+ },
115
+ {
116
+ "name": "TogoTopNews",
117
+ "type": "news",
118
+ "url": "https://togotopnews.tg",
119
+ "pays": "Togo",
120
+ "langue": "fr",
121
+ "active": true
122
+ },
123
  {
124
  "name": "BeninWebTV",
125
  "type": "news",