habulaj commited on
Commit
39c4a66
·
verified ·
1 Parent(s): 532f1bb

Update routers/searchterm.py

Browse files
Files changed (1) hide show
  1. routers/searchterm.py +195 -290
routers/searchterm.py CHANGED
@@ -17,8 +17,10 @@ from newspaper import Article
17
  from threading import Timer
18
  from google import genai
19
  from google.genai import types
20
- import concurrent.futures
21
- from collections import deque
 
 
22
 
23
  router = APIRouter()
24
 
@@ -43,12 +45,16 @@ USER_AGENTS = [
43
  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
44
  ]
45
 
46
- BLOCKED_DOMAINS = {"reddit.com", "www.reddit.com", "old.reddit.com",
47
- "quora.com", "www.quora.com"}
 
 
48
 
49
  MAX_TEXT_LENGTH = 4000
50
- MAX_CONCURRENT_EXTRACTIONS = 100 # Aumentado drasticamente
51
- MAX_CONCURRENT_SEARCHES = 50 # Aumentado para pesquisas
 
 
52
 
53
  # Diretório para arquivos temporários
54
  TEMP_DIR = Path("/tmp")
@@ -57,25 +63,30 @@ TEMP_DIR.mkdir(exist_ok=True)
57
  # Dicionário para controlar arquivos temporários
58
  temp_files = {}
59
 
60
- # Pool de threads para operações CPU-intensivas
61
- THREAD_POOL = concurrent.futures.ThreadPoolExecutor(max_workers=20)
62
 
 
 
63
 
64
  def is_blocked_domain(url: str) -> bool:
65
  try:
66
  host = urlparse(url).netloc.lower()
67
- return any(host == b or host.endswith("." + b) for b in BLOCKED_DOMAINS)
 
 
 
 
 
 
 
68
  except Exception:
69
  return False
70
 
71
-
72
  def clamp_text(text: str) -> str:
73
- if not text:
74
- return ""
75
- if len(text) > MAX_TEXT_LENGTH:
76
- return text[:MAX_TEXT_LENGTH]
77
- return text
78
-
79
 
80
  def get_realistic_headers() -> Dict[str, str]:
81
  return {
@@ -84,11 +95,8 @@ def get_realistic_headers() -> Dict[str, str]:
84
  "Accept-Language": "en-US,en;q=0.7,pt-BR;q=0.6",
85
  "Connection": "keep-alive",
86
  "Accept-Encoding": "gzip, deflate, br",
87
- "Cache-Control": "no-cache",
88
- "Pragma": "no-cache",
89
  }
90
 
91
-
92
  def delete_temp_file(file_id: str, file_path: Path):
93
  """Remove arquivo temporário após expiração"""
94
  try:
@@ -99,17 +107,16 @@ def delete_temp_file(file_id: str, file_path: Path):
99
  except Exception as e:
100
  print(f"Erro ao remover arquivo temporário: {e}")
101
 
102
-
103
- def create_temp_file(data: Dict[str, Any]) -> Dict[str, str]:
104
- """Cria arquivo temporário e agenda sua remoção"""
105
  file_id = str(uuid.uuid4())
106
  file_path = TEMP_DIR / f"fontes_{file_id}.txt"
107
 
108
- # Salva o JSON no arquivo
109
- with open(file_path, 'w', encoding='utf-8') as f:
110
- json.dump(data, f, ensure_ascii=False, indent=2)
111
 
112
- # Agenda remoção em 24 horas (86400 segundos)
113
  timer = Timer(86400, delete_temp_file, args=[file_id, file_path])
114
  timer.start()
115
 
@@ -126,22 +133,6 @@ def create_temp_file(data: Dict[str, Any]) -> Dict[str, str]:
126
  "expires_in_hours": 24
127
  }
128
 
129
-
130
- def extract_text_cpu_intensive(html_content: str) -> str:
131
- """Função CPU-intensiva para extrair texto (roda em thread separada)"""
132
- try:
133
- if re.search(r"(paywall|subscribe|metered|registration|captcha|access denied)", html_content, re.I):
134
- return ""
135
-
136
- extracted = trafilatura.extract(html_content) or ""
137
- extracted = extracted.strip()
138
- if extracted and len(extracted) > 100:
139
- return clamp_text(extracted)
140
- except Exception:
141
- pass
142
- return ""
143
-
144
-
145
  async def generate_search_terms(context: str) -> List[str]:
146
  """Gera termos de pesquisa usando o modelo Gemini"""
147
  try:
@@ -188,9 +179,7 @@ Retorne apenas o JSON, sem mais nenhum texto."""
188
  ]
189
 
190
  generate_content_config = types.GenerateContentConfig(
191
- thinking_config=types.ThinkingConfig(
192
- thinking_budget=0,
193
- ),
194
  )
195
 
196
  # Coletamos toda a resposta em stream
@@ -205,7 +194,6 @@ Retorne apenas o JSON, sem mais nenhum texto."""
205
 
206
  # Tenta extrair o JSON da resposta
207
  try:
208
- # Remove possíveis ```json e ``` da resposta
209
  clean_response = full_response.strip()
210
  if clean_response.startswith("```json"):
211
  clean_response = clean_response[7:]
@@ -213,234 +201,170 @@ Retorne apenas o JSON, sem mais nenhum texto."""
213
  clean_response = clean_response[:-3]
214
  clean_response = clean_response.strip()
215
 
216
- # Parse do JSON
217
- response_data = json.loads(clean_response)
218
  terms = response_data.get("terms", [])
219
 
220
- # Validação básica
221
  if not isinstance(terms, list):
222
  raise ValueError("Terms deve ser uma lista")
223
 
224
- return terms[:20] # Garante máximo de 20 termos
225
 
226
- except (json.JSONDecodeError, ValueError) as e:
227
  print(f"Erro ao parsear resposta do Gemini: {e}")
228
- print(f"Resposta recebida: {full_response}")
229
- # Retorna uma lista vazia em caso de erro
230
  return []
231
 
232
  except Exception as e:
233
  print(f"Erro ao gerar termos de pesquisa: {str(e)}")
234
  return []
235
 
236
-
237
  async def search_brave_batch(client: httpx.AsyncClient, terms: List[str]) -> List[Tuple[str, List[Dict[str, str]]]]:
238
- """Realiza múltiplas pesquisas em paralelo com batch otimizado"""
 
239
 
240
  async def search_single_term(term: str) -> Tuple[str, List[Dict[str, str]]]:
241
- params = {"q": term, "count": 10, "safesearch": "off", "summary": "false"}
242
-
243
- try:
244
- resp = await client.get(BRAVE_SEARCH_URL, headers=BRAVE_HEADERS, params=params)
245
- if resp.status_code != 200:
246
- return term, []
247
-
248
- data = resp.json()
249
- results = []
250
 
251
- if "web" in data and "results" in data["web"]:
252
- for item in data["web"]["results"]:
253
- url = item.get("url")
254
- age = item.get("age", "Unknown")
255
-
256
- if url and not is_blocked_domain(url):
257
- results.append({"url": url, "age": age, "term": term})
258
 
259
- return term, results
260
- except Exception:
261
- return term, []
262
-
263
- # Executa todas as pesquisas em paralelo
264
- search_tasks = [search_single_term(term) for term in terms]
265
- results = await asyncio.gather(*search_tasks, return_exceptions=True)
266
-
267
- # Filtra apenas resultados válidos
268
- valid_results = [r for r in results if isinstance(r, tuple)]
269
- return valid_results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
 
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
- async def extract_content_ultra_fast(session: aiohttp.ClientSession, url_data: Dict[str, str]) -> Optional[Dict[str, Any]]:
273
- """Extração de conteúdo ultra-rápida com fallbacks otimizados"""
274
- url = url_data["url"]
275
- term = url_data["term"]
276
- age = url_data["age"]
277
 
278
- # Primeira tentativa: Newspaper3k (mais rápido para muitos sites)
279
  try:
280
- loop = asyncio.get_event_loop()
281
-
282
- # Executa newspaper em thread separada
283
- def newspaper_extract():
284
- try:
285
- art = Article(url)
286
- art.config.browser_user_agent = random.choice(USER_AGENTS)
287
- art.config.request_timeout = 5 # Reduzido para 5s
288
- art.config.number_threads = 1
289
- art.download()
290
- art.parse()
291
- text = (art.text or "").strip()
292
- return text if text and len(text) > 100 else None
293
- except Exception:
294
- return None
295
-
296
- # Tenta newspaper em paralelo com download HTTP
297
- newspaper_task = loop.run_in_executor(THREAD_POOL, newspaper_extract)
298
-
299
- # Download HTTP em paralelo
300
  headers = get_realistic_headers()
301
-
302
- try:
303
- async with session.get(url, headers=headers, timeout=8) as resp: # Timeout reduzido
304
- if resp.status != 200:
305
- # Se HTTP falhar, espera newspaper
306
- newspaper_result = await newspaper_task
307
- if newspaper_result:
308
- return {
309
- "term": term,
310
- "age": age,
311
- "url": url,
312
- "text": clamp_text(newspaper_result),
313
- "method": "newspaper"
314
- }
315
- return None
316
-
317
  html = await resp.text()
318
 
319
- # Executa extração de texto em thread separada
320
- text_extraction_task = loop.run_in_executor(
321
- THREAD_POOL,
322
- extract_text_cpu_intensive,
323
- html
324
- )
325
-
326
- # Aguarda tanto newspaper quanto trafilatura, pega o primeiro que terminar
327
- done, pending = await asyncio.wait(
328
- [newspaper_task, text_extraction_task],
329
- return_when=asyncio.FIRST_COMPLETED,
330
- timeout=10
331
- )
332
-
333
- # Cancela tarefas pendentes
334
- for task in pending:
335
- task.cancel()
336
-
337
- # Processa resultados
338
- results = []
339
- for task in done:
340
  try:
341
- result = await task
342
- if result:
343
- results.append(result)
344
- except Exception:
345
- continue
346
-
347
- # Retorna o melhor resultado
348
- if results:
349
- # Prioriza o texto mais longo
350
- best_text = max(results, key=len)
351
- return {
352
- "term": term,
353
- "age": age,
354
- "url": url,
355
- "text": clamp_text(best_text),
356
- "method": "hybrid"
357
- }
358
 
359
- except asyncio.TimeoutError:
360
- # Se HTTP der timeout, ainda tenta newspaper
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
361
  try:
362
- newspaper_result = await asyncio.wait_for(newspaper_task, timeout=5)
363
- if newspaper_result:
 
 
 
364
  return {
365
  "term": term,
366
  "age": age,
367
  "url": url,
368
- "text": clamp_text(newspaper_result),
369
- "method": "newspaper_fallback"
370
  }
371
- except asyncio.TimeoutError:
372
- pass
373
-
374
- except Exception:
375
- pass
376
-
377
- return None
378
-
379
-
380
- async def process_urls_ultra_parallel(session: aiohttp.ClientSession, all_urls: List[Dict[str, str]], used_urls: Set[str]) -> List[Dict[str, Any]]:
381
- """Processa URLs com máximo paralelismo"""
382
-
383
- # Remove URLs duplicadas imediatamente
384
- unique_urls = []
385
- local_used = set()
386
-
387
- for url_data in all_urls:
388
- url = url_data["url"]
389
- if url not in used_urls and url not in local_used:
390
- unique_urls.append(url_data)
391
- local_used.add(url)
392
- used_urls.add(url) # Adiciona ao set global imediatamente
393
-
394
- if not unique_urls:
395
- return []
396
 
397
- print(f"Processando {len(unique_urls)} URLs únicas em paralelo...")
 
 
 
398
 
399
- # Cria semáforo com limite alto
400
- semaphore = asyncio.Semaphore(MAX_CONCURRENT_EXTRACTIONS)
401
 
402
- async def extract_with_semaphore(url_data):
403
- async with semaphore:
404
- return await extract_content_ultra_fast(session, url_data)
405
 
406
- # Executa TODAS as extrações em paralelo
407
- tasks = [extract_with_semaphore(url_data) for url_data in unique_urls]
408
 
409
- # Aguarda todas as tarefas com timeout global
410
- try:
411
- results = await asyncio.wait_for(
412
- asyncio.gather(*tasks, return_exceptions=True),
413
- timeout=30 # 30 segundos para todas as extrações
414
- )
415
-
416
- # Filtra resultados válidos
417
- valid_results = [
418
- r for r in results
419
- if r is not None and not isinstance(r, Exception) and isinstance(r, dict)
420
- ]
421
-
422
- print(f"Extraídos {len(valid_results)} artigos de {len(unique_urls)} URLs")
423
- return valid_results
424
-
425
- except asyncio.TimeoutError:
426
- print("Timeout global atingido, retornando resultados parciais...")
427
- # Em caso de timeout, pega os resultados que já terminaram
428
- completed_tasks = [task for task in tasks if task.done()]
429
- valid_results = []
430
-
431
- for task in completed_tasks:
432
- try:
433
- result = task.result()
434
- if result is not None and isinstance(result, dict):
435
- valid_results.append(result)
436
- except Exception:
437
- continue
438
-
439
- return valid_results
440
-
441
 
442
  @router.post("/search-terms")
443
  async def search_terms(payload: Dict[str, str] = Body(...)) -> Dict[str, Any]:
 
 
444
  context = payload.get("context")
445
  if not context or not isinstance(context, str):
446
  raise HTTPException(status_code=400, detail="Campo 'context' é obrigatório e deve ser uma string.")
@@ -448,92 +372,73 @@ async def search_terms(payload: Dict[str, str] = Body(...)) -> Dict[str, Any]:
448
  if len(context.strip()) == 0:
449
  raise HTTPException(status_code=400, detail="Campo 'context' não pode estar vazio.")
450
 
451
- start_time = time.time()
452
- print(f"Iniciando busca para contexto: {context[:100]}...")
453
-
454
  # Gera os termos de pesquisa usando o Gemini
455
  terms = await generate_search_terms(context)
456
 
457
  if not terms:
458
  raise HTTPException(status_code=500, detail="Não foi possível gerar termos de pesquisa válidos.")
 
 
459
 
460
- print(f"Gerados {len(terms)} termos em {time.time() - start_time:.2f}s")
461
-
462
- used_urls: Set[str] = set()
463
-
464
- # Configurações otimizadas para máxima velocidade
465
  connector = aiohttp.TCPConnector(
466
- limit=200, # Dobrou o limite total
467
- limit_per_host=50, # Aumentou limite por host
468
- keepalive_timeout=30,
469
- enable_cleanup_closed=True,
470
- force_close=False,
471
- ttl_dns_cache=300, # Cache DNS por 5 minutos
472
  )
473
-
474
- timeout = aiohttp.ClientTimeout(
475
- total=25, # Timeout total reduzido
476
- connect=8, # Timeout de conexão reduzido
477
- sock_read=8 # Timeout de leitura reduzido
478
- )
479
-
480
- # Configurações HTTPX otimizadas
481
- http_limits = httpx.Limits(
482
- max_connections=MAX_CONCURRENT_SEARCHES,
483
- max_keepalive_connections=40
484
  )
485
 
486
- async with httpx.AsyncClient(timeout=12.0, limits=http_limits) as http_client:
487
  async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
488
-
489
- # Fase 1: Busca em lote (todas as pesquisas em paralelo)
490
- print("Fase 1: Executando pesquisas em paralelo...")
491
- search_start = time.time()
492
-
493
  search_results = await search_brave_batch(http_client, terms)
 
494
 
495
- print(f"Pesquisas concluídas em {time.time() - search_start:.2f}s")
496
-
497
- # Fase 2: Coleta e organiza todas as URLs
498
- all_urls = []
499
  for term, results in search_results:
500
  for result in results:
501
- all_urls.append({
502
- "url": result["url"],
503
- "age": result["age"],
504
- "term": term
505
- })
506
 
507
- print(f"Total de URLs coletadas: {len(all_urls)}")
508
 
509
- # Fase 3: Extração ultra-paralela
510
- print("Fase 2: Extraindo conteúdo em máximo paralelismo...")
511
- extraction_start = time.time()
512
 
513
- final_results = await process_urls_ultra_parallel(session, all_urls, used_urls)
514
-
515
- print(f"Extração concluída em {time.time() - extraction_start:.2f}s")
516
-
517
- total_time = time.time() - start_time
518
- print(f"Processo completo em {total_time:.2f}s - {len(final_results)} artigos extraídos")
519
 
520
- # Cria o JSON final
 
 
 
521
  result_data = {"results": final_results}
 
522
 
523
- # Cria arquivo temporário
524
- temp_file_info = create_temp_file(result_data)
525
 
526
  return {
527
  "message": "Dados salvos em arquivo temporário",
528
  "total_results": len(final_results),
529
  "context": context,
530
  "generated_terms": terms,
531
- "processing_time_seconds": round(total_time, 2),
532
- "urls_processed": len(all_urls),
533
- "file_info": temp_file_info
534
  }
535
 
536
-
537
  @router.get("/download-temp/{file_id}")
538
  async def download_temp_file(file_id: str):
539
  """Endpoint para download do arquivo temporário"""
 
17
  from threading import Timer
18
  from google import genai
19
  from google.genai import types
20
+ from asyncio import Queue, create_task, gather
21
+ from concurrent.futures import ThreadPoolExecutor
22
+ import aiofiles
23
+ import ujson # JSON mais rápido
24
 
25
  router = APIRouter()
26
 
 
45
  "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
46
  ]
47
 
48
+ BLOCKED_DOMAINS = frozenset({ # frozenset é mais rápido para lookup
49
+ "reddit.com", "www.reddit.com", "old.reddit.com",
50
+ "quora.com", "www.quora.com"
51
+ })
52
 
53
  MAX_TEXT_LENGTH = 4000
54
+ MAX_CONCURRENT_SEARCHES = 30 # Aumentado
55
+ MAX_CONCURRENT_EXTRACTIONS = 80 # Aumentado significativamente
56
+ EXTRACTION_TIMEOUT = 8 # Reduzido
57
+ HTTP_TIMEOUT = 10 # Reduzido
58
 
59
  # Diretório para arquivos temporários
60
  TEMP_DIR = Path("/tmp")
 
63
  # Dicionário para controlar arquivos temporários
64
  temp_files = {}
65
 
66
+ # Pool de threads para operações CPU-intensive
67
+ thread_pool = ThreadPoolExecutor(max_workers=20)
68
 
69
+ # Cache de domínios bloqueados para evitar verificações repetidas
70
+ domain_cache = {}
71
 
72
  def is_blocked_domain(url: str) -> bool:
73
  try:
74
  host = urlparse(url).netloc.lower()
75
+
76
+ # Cache lookup
77
+ if host in domain_cache:
78
+ return domain_cache[host]
79
+
80
+ is_blocked = any(host == b or host.endswith("." + b) for b in BLOCKED_DOMAINS)
81
+ domain_cache[host] = is_blocked
82
+ return is_blocked
83
  except Exception:
84
  return False
85
 
 
86
  def clamp_text(text: str) -> str:
87
+ if not text or len(text) <= MAX_TEXT_LENGTH:
88
+ return text
89
+ return text[:MAX_TEXT_LENGTH]
 
 
 
90
 
91
  def get_realistic_headers() -> Dict[str, str]:
92
  return {
 
95
  "Accept-Language": "en-US,en;q=0.7,pt-BR;q=0.6",
96
  "Connection": "keep-alive",
97
  "Accept-Encoding": "gzip, deflate, br",
 
 
98
  }
99
 
 
100
  def delete_temp_file(file_id: str, file_path: Path):
101
  """Remove arquivo temporário após expiração"""
102
  try:
 
107
  except Exception as e:
108
  print(f"Erro ao remover arquivo temporário: {e}")
109
 
110
+ async def create_temp_file(data: Dict[str, Any]) -> Dict[str, str]:
111
+ """Cria arquivo temporário assíncrono e agenda sua remoção"""
 
112
  file_id = str(uuid.uuid4())
113
  file_path = TEMP_DIR / f"fontes_{file_id}.txt"
114
 
115
+ # Salva o JSON no arquivo de forma assíncrona
116
+ async with aiofiles.open(file_path, 'w', encoding='utf-8') as f:
117
+ await f.write(ujson.dumps(data, ensure_ascii=False, indent=2))
118
 
119
+ # Agenda remoção em 24 horas
120
  timer = Timer(86400, delete_temp_file, args=[file_id, file_path])
121
  timer.start()
122
 
 
133
  "expires_in_hours": 24
134
  }
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  async def generate_search_terms(context: str) -> List[str]:
137
  """Gera termos de pesquisa usando o modelo Gemini"""
138
  try:
 
179
  ]
180
 
181
  generate_content_config = types.GenerateContentConfig(
182
+ thinking_config=types.ThinkingConfig(thinking_budget=0),
 
 
183
  )
184
 
185
  # Coletamos toda a resposta em stream
 
194
 
195
  # Tenta extrair o JSON da resposta
196
  try:
 
197
  clean_response = full_response.strip()
198
  if clean_response.startswith("```json"):
199
  clean_response = clean_response[7:]
 
201
  clean_response = clean_response[:-3]
202
  clean_response = clean_response.strip()
203
 
204
+ response_data = ujson.loads(clean_response)
 
205
  terms = response_data.get("terms", [])
206
 
 
207
  if not isinstance(terms, list):
208
  raise ValueError("Terms deve ser uma lista")
209
 
210
+ return terms[:20]
211
 
212
+ except (ujson.JSONDecodeError, ValueError) as e:
213
  print(f"Erro ao parsear resposta do Gemini: {e}")
 
 
214
  return []
215
 
216
  except Exception as e:
217
  print(f"Erro ao gerar termos de pesquisa: {str(e)}")
218
  return []
219
 
 
220
  async def search_brave_batch(client: httpx.AsyncClient, terms: List[str]) -> List[Tuple[str, List[Dict[str, str]]]]:
221
+ """Busca múltiplos termos em paralelo com otimizações"""
222
+ semaphore = asyncio.Semaphore(MAX_CONCURRENT_SEARCHES)
223
 
224
  async def search_single_term(term: str) -> Tuple[str, List[Dict[str, str]]]:
225
+ async with semaphore:
226
+ params = {"q": term, "count": 10, "safesearch": "off", "summary": "false"}
 
 
 
 
 
 
 
227
 
228
+ try:
229
+ resp = await client.get(BRAVE_SEARCH_URL, headers=BRAVE_HEADERS, params=params)
230
+ if resp.status_code != 200:
231
+ return (term, [])
 
 
 
232
 
233
+ data = resp.json()
234
+ results = []
235
+
236
+ if "web" in data and "results" in data["web"]:
237
+ for item in data["web"]["results"]:
238
+ url = item.get("url")
239
+ age = item.get("age", "Unknown")
240
+
241
+ if url and not is_blocked_domain(url):
242
+ results.append({"url": url, "age": age})
243
+
244
+ return (term, results)
245
+ except Exception as e:
246
+ print(f"Erro na busca do termo '{term}': {e}")
247
+ return (term, [])
248
+
249
+ # Executa todas as buscas em paralelo
250
+ tasks = [search_single_term(term) for term in terms]
251
+ return await gather(*tasks, return_exceptions=False)
252
+
253
+ def extract_with_trafilatura(html: str) -> str:
254
+ """Extração CPU-intensive executada em thread pool"""
255
+ try:
256
+ extracted = trafilatura.extract(html)
257
+ return extracted.strip() if extracted else ""
258
+ except Exception:
259
+ return ""
260
 
261
+ def extract_with_newspaper(url: str) -> str:
262
+ """Extração com newspaper executada em thread pool"""
263
+ try:
264
+ art = Article(url)
265
+ art.config.browser_user_agent = random.choice(USER_AGENTS)
266
+ art.config.request_timeout = 6
267
+ art.config.number_threads = 1
268
+ art.download()
269
+ art.parse()
270
+ return (art.text or "").strip()
271
+ except Exception:
272
+ return ""
273
 
274
+ async def extract_article_text_optimized(url: str, session: aiohttp.ClientSession) -> str:
275
+ """Extração de artigo otimizada com fallback robusto"""
 
 
 
276
 
277
+ # Método 1: Tentar com trafilatura primeiro (mais rápido)
278
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
279
  headers = get_realistic_headers()
280
+ async with session.get(url, headers=headers, timeout=EXTRACTION_TIMEOUT) as resp:
281
+ if resp.status == 200:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  html = await resp.text()
283
 
284
+ # Verifica paywall rapidamente
285
+ if not re.search(r"(paywall|subscribe|metered|registration|captcha|access denied)",
286
+ html[:2000], re.I):
287
+
288
+ # Extração com trafilatura em thread pool
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  try:
290
+ trafilatura_result = await asyncio.get_event_loop().run_in_executor(
291
+ thread_pool, extract_with_trafilatura, html
292
+ )
293
+
294
+ if trafilatura_result and len(trafilatura_result.strip()) > 100:
295
+ return clamp_text(trafilatura_result.strip())
296
+ except Exception as e:
297
+ print(f"Erro trafilatura para {url}: {e}")
298
+
299
+ except Exception as e:
300
+ print(f"Erro HTTP para {url}: {e}")
301
+
302
+ # Método 2: Fallback para newspaper
303
+ try:
304
+ newspaper_result = await asyncio.get_event_loop().run_in_executor(
305
+ thread_pool, extract_with_newspaper, url
306
+ )
307
 
308
+ if newspaper_result and len(newspaper_result.strip()) > 100:
309
+ return clamp_text(newspaper_result.strip())
310
+
311
+ except Exception as e:
312
+ print(f"Erro newspaper para {url}: {e}")
313
+
314
+ return ""
315
+
316
+ async def process_urls_batch(session: aiohttp.ClientSession, urls_data: List[Tuple[str, str, str]]) -> List[Dict[str, Any]]:
317
+ """Processa URLs em lotes otimizados com logging detalhado"""
318
+ semaphore = asyncio.Semaphore(MAX_CONCURRENT_EXTRACTIONS)
319
+ results = []
320
+ used_urls: Set[str] = set()
321
+ success_count = 0
322
+
323
+ async def process_single_url(term: str, url: str, age: str) -> Optional[Dict[str, Any]]:
324
+ nonlocal success_count
325
+ async with semaphore:
326
+ if url in used_urls:
327
+ return None
328
+
329
  try:
330
+ text = await extract_article_text_optimized(url, session)
331
+ if text:
332
+ used_urls.add(url)
333
+ success_count += 1
334
+ print(f"✓ Extraído: {url[:60]}... ({len(text)} chars)")
335
  return {
336
  "term": term,
337
  "age": age,
338
  "url": url,
339
+ "text": text
 
340
  }
341
+ else:
342
+ print(f"✗ Falhou: {url[:60]}... (sem conteúdo)")
343
+ except Exception as e:
344
+ print(f"✗ Erro: {url[:60]}... - {str(e)[:50]}")
345
+
346
+ return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
 
348
+ # Cria todas as tasks de uma vez
349
+ tasks = []
350
+ for term, url, age in urls_data:
351
+ tasks.append(process_single_url(term, url, age))
352
 
353
+ print(f"Processando {len(tasks)} URLs com semáforo de {MAX_CONCURRENT_EXTRACTIONS}...")
 
354
 
355
+ # Processa tudo em paralelo
356
+ processed_results = await gather(*tasks, return_exceptions=True)
 
357
 
358
+ # Filtra resultados válidos
359
+ valid_results = [r for r in processed_results if r is not None and not isinstance(r, Exception)]
360
 
361
+ print(f"Sucesso: {success_count}/{len(urls_data)} URLs extraídas")
362
+ return valid_results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
 
364
  @router.post("/search-terms")
365
  async def search_terms(payload: Dict[str, str] = Body(...)) -> Dict[str, Any]:
366
+ start_time = time.time()
367
+
368
  context = payload.get("context")
369
  if not context or not isinstance(context, str):
370
  raise HTTPException(status_code=400, detail="Campo 'context' é obrigatório e deve ser uma string.")
 
372
  if len(context.strip()) == 0:
373
  raise HTTPException(status_code=400, detail="Campo 'context' não pode estar vazio.")
374
 
375
+ print(f"Iniciando geração de termos...")
 
 
376
  # Gera os termos de pesquisa usando o Gemini
377
  terms = await generate_search_terms(context)
378
 
379
  if not terms:
380
  raise HTTPException(status_code=500, detail="Não foi possível gerar termos de pesquisa válidos.")
381
+
382
+ print(f"Termos gerados em {time.time() - start_time:.2f}s. Iniciando buscas...")
383
 
384
+ # Configurações otimizadas para conexões
 
 
 
 
385
  connector = aiohttp.TCPConnector(
386
+ limit=200, # Aumentado
387
+ limit_per_host=30, # Aumentado
388
+ ttl_dns_cache=300,
389
+ use_dns_cache=True,
390
+ enable_cleanup_closed=True
 
391
  )
392
+ timeout = aiohttp.ClientTimeout(total=HTTP_TIMEOUT, connect=5)
393
+
394
+ # Cliente HTTP otimizado
395
+ http_client = httpx.AsyncClient(
396
+ timeout=HTTP_TIMEOUT,
397
+ limits=httpx.Limits(
398
+ max_connections=200, # Aumentado
399
+ max_keepalive_connections=50 # Aumentado
400
+ ),
401
+ http2=True # Ativa HTTP/2 para melhor performance
 
402
  )
403
 
404
+ try:
405
  async with aiohttp.ClientSession(connector=connector, timeout=timeout) as session:
406
+ # Fase 1: Busca todos os termos em paralelo
 
 
 
 
407
  search_results = await search_brave_batch(http_client, terms)
408
+ print(f"Buscas concluídas em {time.time() - start_time:.2f}s. Iniciando extrações...")
409
 
410
+ # Fase 2: Prepara dados para extração em lote
411
+ urls_data = []
 
 
412
  for term, results in search_results:
413
  for result in results:
414
+ urls_data.append((term, result["url"], result["age"]))
 
 
 
 
415
 
416
+ print(f"Processando {len(urls_data)} URLs...")
417
 
418
+ # Fase 3: Processa todas as URLs em paralelo
419
+ final_results = await process_urls_batch(session, urls_data)
 
420
 
421
+ print(f"Extração concluída em {time.time() - start_time:.2f}s. Salvando arquivo...")
 
 
 
 
 
422
 
423
+ finally:
424
+ await http_client.aclose()
425
+
426
+ # Fase 4: Cria arquivo temporário assíncrono
427
  result_data = {"results": final_results}
428
+ temp_file_info = await create_temp_file(result_data)
429
 
430
+ total_time = time.time() - start_time
431
+ print(f"Processo completo em {total_time:.2f}s")
432
 
433
  return {
434
  "message": "Dados salvos em arquivo temporário",
435
  "total_results": len(final_results),
436
  "context": context,
437
  "generated_terms": terms,
438
+ "file_info": temp_file_info,
439
+ "processing_time": f"{total_time:.2f}s"
 
440
  }
441
 
 
442
  @router.get("/download-temp/{file_id}")
443
  async def download_temp_file(file_id: str):
444
  """Endpoint para download do arquivo temporário"""