init: scripts diversos (crawlers, conversores, scrapers)
This commit is contained in:
254
scraper/extract_knowledge_batch3_reddit.py
Executable file
254
scraper/extract_knowledge_batch3_reddit.py
Executable file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
extract_knowledge_batch3_reddit.py - Extração Final Batch 3 + Reddit
|
||||
|
||||
Extrai conhecimento de:
|
||||
- 65 ficheiros triumphexp.com*.md (Batch 3)
|
||||
- 2 ficheiros reddit_*.md (Reddit scraping)
|
||||
|
||||
Modelo: google/gemini-2.5-flash-lite (económico e rápido)
|
||||
|
||||
Author: Descomplicar® Crescimento Digital
|
||||
Link: https://descomplicar.pt
|
||||
Copyright: 2025 Descomplicar®
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import requests
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
from dotenv import load_dotenv
|
||||
import time
|
||||
from datetime import datetime
|
||||
import glob
|
||||
|
||||
load_dotenv()
|
||||
|
||||
# Configurações
|
||||
INPUT_DIR = "/media/ealmeida/Dados/GDrive/Cloud/Clientes_360/CTF_Carstuff/KB/Scrapper/sites/output_md"
|
||||
OUTPUT_DIR = "/media/ealmeida/Dados/GDrive/Cloud/Clientes_360/CTF_Carstuff/KB/Scrapper/sites/knowledge_base_final"
|
||||
API_KEY = os.getenv("OPENROUTER_API_KEY")
|
||||
|
||||
class KnowledgeExtractorFinal:
|
||||
def __init__(self):
|
||||
self.api_key = API_KEY
|
||||
self.api_url = "https://openrouter.ai/api/v1/chat/completions"
|
||||
self.model = "google/gemini-2.5-flash-lite" # Modelo económico
|
||||
|
||||
# Estatísticas
|
||||
self.stats = {
|
||||
'batch3': {'total': 0, 'processados': 0, 'relevantes': 0, 'erros': 0},
|
||||
'reddit': {'total': 0, 'processados': 0, 'relevantes': 0, 'erros': 0}
|
||||
}
|
||||
|
||||
# Prompt REFORÇADO
|
||||
self.extraction_prompt = """
|
||||
És um especialista em estofamento automotivo, náutico, ferroviário e aeronáutico.
|
||||
|
||||
⚠️ CRITÉRIO CRÍTICO DE RELEVÂNCIA:
|
||||
O conteúdo SÓ É RELEVANTE se contiver o fluxo COMPLETO:
|
||||
PROBLEMA → SOLUÇÃO → RESULTADO
|
||||
|
||||
Se o texto apenas descreve problemas SEM as suas soluções, retorna: {"relevante": false}
|
||||
|
||||
IGNORAR COMPLETAMENTE:
|
||||
- Navegação de site
|
||||
- Publicidade
|
||||
- Comentários genéricos ("obrigado", "bom post")
|
||||
- Conversas off-topic
|
||||
- Problemas mencionados sem soluções correspondentes
|
||||
- Links sem contexto
|
||||
|
||||
EXTRAIR APENAS SE EXISTIR FLUXO COMPLETO:
|
||||
1. **Problema técnico específico** identificado claramente
|
||||
2. **Solução prática** aplicada ou recomendada para esse problema
|
||||
3. **Resultado obtido** ou esperado (se mencionado)
|
||||
|
||||
FORMATO JSON DE SAÍDA:
|
||||
{
|
||||
"relevante": true/false,
|
||||
"categoria_aplicacao": "automovel|automovel-classico|mobiliario|nautica|ferroviaria|aeronautica|geral",
|
||||
"tipo_conteudo": "problema-tecnico|tutorial|caso-pratico|comparacao-materiais",
|
||||
"casos_completos": [
|
||||
{
|
||||
"problema": {
|
||||
"descricao": "Problema específico identificado",
|
||||
"contexto": "Tipo de veículo/aplicação/situação",
|
||||
"severidade": "baixa|media|alta"
|
||||
},
|
||||
"solucao": {
|
||||
"material_usado": "Material específico aplicado",
|
||||
"tecnica": "Técnica ou método usado",
|
||||
"passos": "Passos principais (se mencionados)"
|
||||
},
|
||||
"resultado": {
|
||||
"obtido": "Resultado concreto alcançado",
|
||||
"qualidade": "Avaliação da solução"
|
||||
}
|
||||
}
|
||||
],
|
||||
"materiais_discutidos": {
|
||||
"principais": ["materiais eficazes mencionados"],
|
||||
"nao_recomendados": ["materiais que falharam ou são evitados"]
|
||||
},
|
||||
"keywords_tecnicas": ["termos", "tecnicos", "relevantes"],
|
||||
"aplicabilidade": ["tipos de veículos/situações"],
|
||||
"nivel_expertise": "iniciante|intermedio|avancado"
|
||||
}
|
||||
|
||||
⚠️ IMPORTANTE:
|
||||
- Se o texto só menciona problemas sem soluções: {"relevante": false}
|
||||
- Se menciona soluções sem contexto de problema: {"relevante": false}
|
||||
- Textos muito curtos (<200 caracteres): {"relevante": false}
|
||||
- Apenas CASOS COMPLETOS com problema→solução→resultado devem ser extraídos.
|
||||
"""
|
||||
|
||||
def extract_knowledge(self, content: str) -> Optional[Dict]:
|
||||
"""Extrai conhecimento via OpenRouter API."""
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"HTTP-Referer": "https://descomplicar.pt",
|
||||
"X-Title": "CTF Knowledge Base Extractor"
|
||||
}
|
||||
|
||||
payload = {
|
||||
"model": self.model,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": self.extraction_prompt
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": f"Analisa este conteúdo e extrai conhecimento técnico:\n\n{content[:12000]}"
|
||||
}
|
||||
],
|
||||
"temperature": 0.3,
|
||||
"max_tokens": 2000,
|
||||
"response_format": {"type": "json_object"}
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(self.api_url, headers=headers, json=payload, timeout=60)
|
||||
response.raise_for_status()
|
||||
|
||||
result = response.json()
|
||||
content_text = result['choices'][0]['message']['content']
|
||||
|
||||
# Parse JSON
|
||||
knowledge = json.loads(content_text)
|
||||
return knowledge
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Erro API: {e}")
|
||||
return None
|
||||
|
||||
def process_file(self, filepath: Path, batch_type: str):
|
||||
"""Processa um ficheiro MD e extrai conhecimento."""
|
||||
print(f"\n📄 A processar: {filepath.name}")
|
||||
|
||||
self.stats[batch_type]['total'] += 1
|
||||
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# Validação básica
|
||||
if len(content) < 200:
|
||||
print(f" ⏭️ Ficheiro muito pequeno (<200 chars), ignorado")
|
||||
return
|
||||
|
||||
# Extração via API
|
||||
knowledge = self.extract_knowledge(content)
|
||||
|
||||
if not knowledge:
|
||||
print(f" ❌ Erro ao extrair conhecimento")
|
||||
self.stats[batch_type]['erros'] += 1
|
||||
return
|
||||
|
||||
# Verificar relevância
|
||||
if not knowledge.get('relevante', False):
|
||||
print(f" ⏭️ Conteúdo não relevante (sem problema→solução)")
|
||||
return
|
||||
|
||||
# Verificar casos completos
|
||||
casos = knowledge.get('casos_completos', [])
|
||||
if not casos or len(casos) == 0:
|
||||
print(f" ⏭️ Sem casos completos extraídos")
|
||||
return
|
||||
|
||||
# Guardar JSON
|
||||
output_file = OUTPUT_DIR + f"/knowledge_{filepath.stem}.json"
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(knowledge, f, ensure_ascii=False, indent=2)
|
||||
|
||||
print(f" ✅ Extraído: {len(casos)} casos completos → {filepath.stem}.json")
|
||||
self.stats[batch_type]['processados'] += 1
|
||||
self.stats[batch_type]['relevantes'] += 1
|
||||
|
||||
# Rate limiting
|
||||
time.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Erro ao processar: {e}")
|
||||
self.stats[batch_type]['erros'] += 1
|
||||
|
||||
def run(self):
|
||||
"""Executa extração completa Batch 3 + Reddit."""
|
||||
print("═══════════════════════════════════════════════════════════")
|
||||
print(" EXTRAÇÃO FINAL - BATCH 3 + REDDIT")
|
||||
print(" Modelo: google/gemini-2.5-flash-lite")
|
||||
print(" Descomplicar® Crescimento Digital")
|
||||
print("═══════════════════════════════════════════════════════════")
|
||||
print()
|
||||
|
||||
# Criar output dir se não existir
|
||||
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 1. BATCH 3 - Ficheiros triumphexp
|
||||
print("🔵 BATCH 3: A processar ficheiros triumphexp...")
|
||||
print("-" * 60)
|
||||
|
||||
triumphexp_files = sorted(glob.glob(f"{INPUT_DIR}/triumphexp.com*.md"))
|
||||
print(f"📊 Encontrados {len(triumphexp_files)} ficheiros triumphexp\n")
|
||||
|
||||
for filepath in triumphexp_files:
|
||||
self.process_file(Path(filepath), 'batch3')
|
||||
|
||||
# 2. REDDIT - Ficheiros reddit
|
||||
print("\n🟠 REDDIT: A processar ficheiros Reddit...")
|
||||
print("-" * 60)
|
||||
|
||||
reddit_files = sorted(glob.glob(f"{INPUT_DIR}/reddit_*.md"))
|
||||
print(f"📊 Encontrados {len(reddit_files)} ficheiros Reddit\n")
|
||||
|
||||
for filepath in reddit_files:
|
||||
self.process_file(Path(filepath), 'reddit')
|
||||
|
||||
# Resumo final
|
||||
print("\n═══════════════════════════════════════════════════════════")
|
||||
print(" EXTRAÇÃO CONCLUÍDA")
|
||||
print("═══════════════════════════════════════════════════════════")
|
||||
print()
|
||||
print("📊 BATCH 3 (Triumphexp):")
|
||||
print(f" Total ficheiros: {self.stats['batch3']['total']}")
|
||||
print(f" Processados com sucesso: {self.stats['batch3']['processados']}")
|
||||
print(f" Relevantes: {self.stats['batch3']['relevantes']}")
|
||||
print(f" Erros: {self.stats['batch3']['erros']}")
|
||||
print()
|
||||
print("📊 REDDIT:")
|
||||
print(f" Total ficheiros: {self.stats['reddit']['total']}")
|
||||
print(f" Processados com sucesso: {self.stats['reddit']['processados']}")
|
||||
print(f" Relevantes: {self.stats['reddit']['relevantes']}")
|
||||
print(f" Erros: {self.stats['reddit']['erros']}")
|
||||
print()
|
||||
|
||||
total_novos = self.stats['batch3']['relevantes'] + self.stats['reddit']['relevantes']
|
||||
print(f"🎯 TOTAL NOVOS CASOS: {total_novos}")
|
||||
print(f"📁 Ficheiros guardados em: {OUTPUT_DIR}")
|
||||
print()
|
||||
|
||||
if __name__ == "__main__":
|
||||
extractor = KnowledgeExtractorFinal()
|
||||
extractor.run()
|
||||
Reference in New Issue
Block a user