init: scripts diversos (crawlers, conversores, scrapers)

This commit is contained in:
2026-03-05 20:38:36 +00:00
commit 6ac6f4be2a
925 changed files with 850330 additions and 0 deletions

View File

@@ -0,0 +1,357 @@
"""
extract_knowledge_production.py - Extração COMPLETA com Validação Problema→Solução
Objetivo: Processar TODOS os 3,285 ficheiros MD com critério rigoroso
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
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 KnowledgeExtractor:
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"
# Estatísticas
self.stats = {
'total': 0,
'processados': 0,
'relevantes': 0,
'rejeitados_pequenos': 0,
'rejeitados_incompletos': 0,
'erros': 0
}
# Prompt REFORÇADO - Exige Problemas + Soluções
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}}
- Só marca relevante se tiver AMBOS problema E solução claramente relacionados
TEXTO PARA ANALISAR:
---
{content}
---
Responde APENAS com o JSON, sem texto adicional.
"""
def extract_knowledge(self, content: str, source_file: str, retries: int = 3) -> Optional[Dict]:
"""Extrai conhecimento útil usando AI."""
# Pré-filtro
if len(content) < 1000:
self.stats['rejeitados_pequenos'] += 1
return {"relevante": False, "motivo": "conteudo_pequeno"}
if len(content) > 50000:
content = content[:50000]
for attempt in range(retries):
try:
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
"HTTP-Referer": "https://descomplicar.pt",
"X-Title": "CTF Knowledge Production"
}
prompt = self.extraction_prompt.format(content=content)
data = {
"model": self.model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.2,
"max_tokens": 3500
}
response = requests.post(
self.api_url,
headers=headers,
json=data,
timeout=60
)
if response.status_code == 200:
result = response.json()
if 'choices' in result and len(result['choices']) > 0:
content_text = result['choices'][0]['message']['content']
# Parsing robusto
try:
# Remover blocos markdown
if '```json' in content_text:
content_text = content_text.split('```json')[1].split('```')[0]
elif '```' in content_text:
content_text = content_text.split('```')[1].split('```')[0]
# Limpeza
content_text = content_text.strip()
# Parse
knowledge = json.loads(content_text)
# ✅ VALIDAÇÃO CRÍTICA: Verificar completude
if knowledge.get('relevante', False):
# Verificar se tem casos completos (problema + solução)
casos = knowledge.get('casos_completos', [])
if not casos or len(casos) == 0:
knowledge['relevante'] = False
knowledge['motivo_rejeicao'] = 'sem_casos_completos'
self.stats['rejeitados_incompletos'] += 1
return knowledge
# Verificar se cada caso tem problema E solução
casos_validos = []
for caso in casos:
tem_problema = bool(caso.get('problema', {}).get('descricao'))
tem_solucao = bool(caso.get('solucao', {}).get('material_usado') or
caso.get('solucao', {}).get('tecnica'))
if tem_problema and tem_solucao:
casos_validos.append(caso)
if not casos_validos:
knowledge['relevante'] = False
knowledge['motivo_rejeicao'] = 'problemas_sem_solucoes'
self.stats['rejeitados_incompletos'] += 1
return knowledge
# Atualizar com apenas casos válidos
knowledge['casos_completos'] = casos_validos
return knowledge
except json.JSONDecodeError as e:
# Fallback: extrair { ... } manualmente
try:
start = content_text.find('{')
end = content_text.rfind('}') + 1
if start != -1 and end > start:
clean_json = content_text[start:end]
knowledge = json.loads(clean_json)
return knowledge
except:
self.stats['erros'] += 1
return None
return None
elif response.status_code == 429: # Rate limit
time.sleep(20)
continue
except Exception as e:
if attempt < retries - 1:
time.sleep(5)
continue
else:
self.stats['erros'] += 1
return None
def process_file(self, input_file: Path, output_dir: Path) -> bool:
"""Processa um ficheiro e extrai conhecimento."""
try:
with open(input_file, 'r', encoding='utf-8') as f:
content = f.read()
knowledge = self.extract_knowledge(content, input_file.name)
if not knowledge:
return False
if not knowledge.get('relevante', False):
return False
# RELEVANTE - Guardar!
output_file = output_dir / f"knowledge_{input_file.stem}.json"
with open(output_file, 'w', encoding='utf-8') as f:
json.dump(knowledge, f, indent=2, ensure_ascii=False)
self.stats['relevantes'] += 1
# Rate limiting
time.sleep(2)
return True
except Exception as e:
self.stats['erros'] += 1
return False
def print_progress(self, current: int, total: int):
"""Imprime progresso."""
percent = (current / total * 100) if total > 0 else 0
relevance_rate = (self.stats['relevantes'] / current * 100) if current > 0 else 0
print(f"\r⏳ Progresso: {current}/{total} ({percent:.1f}%) | "
f"✅ Relevantes: {self.stats['relevantes']} ({relevance_rate:.1f}%) | "
f"❌ Rejeitados: {self.stats['rejeitados_incompletos']} | "
f"⚠️ Erros: {self.stats['erros']}", end='', flush=True)
def main():
"""Função principal."""
print("═══════════════════════════════════════════════════════════")
print(" CTF CARSTUFF - EXTRAÇÃO PRODUÇÃO (PROBLEMA→SOLUÇÃO)")
print(" PROCESSAMENTO COMPLETO: ~3,285 ficheiros")
print("═══════════════════════════════════════════════════════════")
print()
if not API_KEY:
print("❌ OPENROUTER_API_KEY não configurada no .env")
return
input_path = Path(INPUT_DIR)
output_path = Path(OUTPUT_DIR)
if not input_path.exists():
print(f"❌ Diretório não existe: {INPUT_DIR}")
return
output_path.mkdir(parents=True, exist_ok=True)
# Sites prioritários (ordenados por volume de ficheiros)
priority_sites = [
# Batch 1 - Já processados (118 casos extraídos)
'thehogring.com', # 264 ficheiros → 105 casos
'forums.pelicanparts.com', # 1636 ficheiros → 11 casos
'thesamba.com', # 158 ficheiros → 2 casos
'sailrite.com', # 41 ficheiros → 0 casos
# Batch 2 - Novos sites (1186 ficheiros a processar)
'relicate.com', # 359 ficheiros - PRIORIDADE ALTA
'trawlerforum.com', # 193 ficheiros
'alfabb.com', # 165 ficheiros
'vansairforce.net', # 132 ficheiros
'mgexp.com', # 91 ficheiros
'cruisersforum.com', # 78 ficheiros
'ultrafabricsinc.com', # 51 ficheiros
'sunbrella.com', # 46 ficheiros
'camirafabrics.com', # 44 ficheiros
'keystonbros.com' # 27 ficheiros
]
all_files = []
for site in priority_sites:
pattern = f"{site}_*.md"
files = list(input_path.glob(pattern))
all_files.extend(files)
total_files = len(all_files)
print(f"📂 Encontrados {total_files} ficheiros")
print(f"🎯 Iniciando extração completa...")
print()
extractor = KnowledgeExtractor()
extractor.stats['total'] = total_files
start_time = datetime.now()
for idx, md_file in enumerate(all_files, 1):
extractor.process_file(md_file, output_path)
extractor.stats['processados'] = idx
# Atualizar progresso a cada 10 ficheiros
if idx % 10 == 0 or idx == total_files:
extractor.print_progress(idx, total_files)
end_time = datetime.now()
duration = end_time - start_time
print("\n")
print("═══════════════════════════════════════════════════════════")
print(" EXTRAÇÃO CONCLUÍDA")
print("═══════════════════════════════════════════════════════════")
print(f"📊 Total processado: {extractor.stats['processados']}/{total_files}")
print(f"✅ Conhecimento COMPLETO: {extractor.stats['relevantes']} ficheiros")
print(f"❌ Rejeitados (pequenos): {extractor.stats['rejeitados_pequenos']}")
print(f"❌ Rejeitados (incompletos): {extractor.stats['rejeitados_incompletos']}")
print(f"⚠️ Erros: {extractor.stats['erros']}")
print()
relevance_rate = (extractor.stats['relevantes'] / extractor.stats['processados'] * 100) if extractor.stats['processados'] > 0 else 0
print(f"📈 Taxa de relevância: {relevance_rate:.1f}%")
print(f"⏱️ Duração: {duration}")
print(f"📁 Output: {OUTPUT_DIR}")
print()
print("CRITÉRIO: Apenas Problema + Solução + Resultado")
print("═══════════════════════════════════════════════════════════")
if __name__ == '__main__':
main()