init: scripts diversos (crawlers, conversores, scrapers)
This commit is contained in:
412
scraper/structure_content_ctf.py
Executable file
412
scraper/structure_content_ctf.py
Executable file
@@ -0,0 +1,412 @@
|
||||
"""
|
||||
structure_content_ctf.py - Estruturação inteligente de conteúdo em formato problema → solução → resultado
|
||||
|
||||
Author: Descomplicar® Crescimento Digital
|
||||
Link: https://descomplicar.pt
|
||||
Copyright: 2025 Descomplicar®
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Dict, Optional, List
|
||||
from dotenv import load_dotenv
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
# Carregar variáveis de ambiente
|
||||
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/formatted"
|
||||
API_KEY = os.getenv("OPENROUTER_API_KEY")
|
||||
|
||||
# Configurar logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ContentStructurer:
|
||||
def __init__(self):
|
||||
self.api_key = API_KEY
|
||||
self.api_url = "https://openrouter.ai/api/v1/chat/completions"
|
||||
# Usar Gemini 2.5 Flash - modelo 2025 com "thinking" capabilities (~40x mais barato que Claude)
|
||||
self.model = "google/gemini-2.5-flash"
|
||||
|
||||
# Prompt especializado para estofamento automotivo
|
||||
self.structure_prompt = """
|
||||
És um especialista em estofamento automotivo e documentação técnica.
|
||||
|
||||
Analisa o texto fornecido e extrai informação estruturada no seguinte formato JSON:
|
||||
|
||||
{
|
||||
"metadata": {
|
||||
"titulo": "Título principal do conteúdo",
|
||||
"categoria": "tipo de conteúdo (tutorial, problema-tecnico, showcase, dica, recurso)",
|
||||
"topicos": ["lista", "de", "topicos", "principais"],
|
||||
"fonte": "nome do site original"
|
||||
},
|
||||
"conteudo": [
|
||||
{
|
||||
"tipo": "problema|solucao|resultado|info",
|
||||
"titulo": "Título desta secção",
|
||||
"descricao": "Descrição detalhada em português PT-PT",
|
||||
"detalhes": [
|
||||
"Lista de pontos-chave",
|
||||
"Passos específicos",
|
||||
"Materiais ou ferramentas mencionados"
|
||||
],
|
||||
"relevancia": "alta|media|baixa"
|
||||
}
|
||||
],
|
||||
"keywords": ["palavras-chave", "tecnicas", "relevantes"],
|
||||
"aplicabilidade": ["tipos de veículos ou situações onde se aplica"]
|
||||
}
|
||||
|
||||
REGRAS IMPORTANTES:
|
||||
1. Se o conteúdo for um artigo sobre problema técnico:
|
||||
- Identifica claramente: problema → causa → solução → resultado
|
||||
2. Se for showcase/galeria:
|
||||
- Extrai: projeto → técnicas usadas → materiais → resultado visual
|
||||
3. Se for tutorial:
|
||||
- Extrai: objetivo → passos → dicas → precauções
|
||||
4. Se for recurso/ferramenta:
|
||||
- Extrai: propósito → características → vantagens → aplicação
|
||||
|
||||
5. IGNORA completamente:
|
||||
- Navegação do site
|
||||
- Comentários genéricos
|
||||
- Publicidade
|
||||
- Links de rodapé sem contexto
|
||||
|
||||
6. FOCA em:
|
||||
- Técnicas de estofamento
|
||||
- Materiais e ferramentas
|
||||
- Processos e métodos
|
||||
- Problemas comuns e soluções
|
||||
- Casos práticos
|
||||
|
||||
7. Usa PORTUGUÊS DE PORTUGAL (PT-PT):
|
||||
- "estofamento" (não "estofado")
|
||||
- "automóvel" (não "carro")
|
||||
- "tecido" (não "tecido")
|
||||
|
||||
Responde APENAS com o JSON, sem texto adicional.
|
||||
"""
|
||||
|
||||
def chunk_content(self, content: str, max_chars: int = 15000) -> List[str]:
|
||||
"""Divide conteúdo em chunks gerenciáveis."""
|
||||
if len(content) <= max_chars:
|
||||
return [content]
|
||||
|
||||
chunks = []
|
||||
lines = content.split('\n')
|
||||
current_chunk = []
|
||||
current_size = 0
|
||||
|
||||
for line in lines:
|
||||
line_size = len(line) + 1 # +1 para \n
|
||||
|
||||
if current_size + line_size > max_chars and current_chunk:
|
||||
chunks.append('\n'.join(current_chunk))
|
||||
current_chunk = [line]
|
||||
current_size = line_size
|
||||
else:
|
||||
current_chunk.append(line)
|
||||
current_size += line_size
|
||||
|
||||
if current_chunk:
|
||||
chunks.append('\n'.join(current_chunk))
|
||||
|
||||
return chunks
|
||||
|
||||
def structure_content(self, content: str, source_file: str, retries: int = 3) -> Optional[Dict]:
|
||||
"""Estrutura conteúdo usando AI."""
|
||||
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 Carstuff Knowledge Base"
|
||||
}
|
||||
|
||||
data = {
|
||||
"model": self.model,
|
||||
"messages": [
|
||||
{"role": "system", "content": self.structure_prompt},
|
||||
{"role": "user", "content": f"Ficheiro: {source_file}\n\n{content}"}
|
||||
],
|
||||
"temperature": 0.3, # Baixa temperatura para respostas mais consistentes
|
||||
"max_tokens": 4000
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
self.api_url,
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=90
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
if 'choices' in result and len(result['choices']) > 0:
|
||||
content_text = result['choices'][0]['message']['content']
|
||||
|
||||
# Tentar extrair JSON do texto
|
||||
try:
|
||||
# Remover markdown code blocks se existir
|
||||
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]
|
||||
|
||||
structured_data = json.loads(content_text.strip())
|
||||
return structured_data
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Erro ao parsear JSON: {e}")
|
||||
logger.debug(f"Conteúdo recebido: {content_text[:500]}")
|
||||
if attempt < retries - 1:
|
||||
time.sleep(2 ** attempt)
|
||||
continue
|
||||
return None
|
||||
else:
|
||||
logger.error(f"Resposta sem choices")
|
||||
|
||||
elif response.status_code == 429: # Rate limit
|
||||
wait_time = int(response.headers.get('Retry-After', 20))
|
||||
logger.warning(f"Rate limit. Aguardando {wait_time}s...")
|
||||
time.sleep(wait_time)
|
||||
continue
|
||||
else:
|
||||
logger.error(f"Erro API: {response.status_code} - {response.text}")
|
||||
if attempt < retries - 1:
|
||||
time.sleep(2 ** attempt)
|
||||
continue
|
||||
return None
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.warning(f"Timeout (tentativa {attempt + 1}/{retries})")
|
||||
if attempt < retries - 1:
|
||||
time.sleep(2 ** attempt)
|
||||
continue
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao estruturar conteúdo: {str(e)}")
|
||||
if attempt < retries - 1:
|
||||
time.sleep(2 ** attempt)
|
||||
continue
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def format_structured_md(self, structured_data: Dict, original_file: str) -> str:
|
||||
"""Converte dados estruturados em Markdown formatado."""
|
||||
md_lines = []
|
||||
|
||||
# Metadata
|
||||
meta = structured_data.get('metadata', {})
|
||||
md_lines.append(f"# {meta.get('titulo', 'Sem Título')}")
|
||||
md_lines.append("")
|
||||
md_lines.append(f"**Categoria**: {meta.get('categoria', 'Geral')}")
|
||||
md_lines.append(f"**Fonte**: {meta.get('fonte', original_file)}")
|
||||
|
||||
if meta.get('topicos'):
|
||||
md_lines.append(f"**Tópicos**: {', '.join(meta['topicos'])}")
|
||||
|
||||
md_lines.append("")
|
||||
md_lines.append("---")
|
||||
md_lines.append("")
|
||||
|
||||
# Conteúdo estruturado
|
||||
conteudo = structured_data.get('conteudo', [])
|
||||
|
||||
# Agrupar por tipo
|
||||
problemas = [c for c in conteudo if c.get('tipo') == 'problema']
|
||||
solucoes = [c for c in conteudo if c.get('tipo') == 'solucao']
|
||||
resultados = [c for c in conteudo if c.get('tipo') == 'resultado']
|
||||
info = [c for c in conteudo if c.get('tipo') == 'info']
|
||||
|
||||
# Problemas
|
||||
if problemas:
|
||||
md_lines.append("## 🔍 Problemas Identificados")
|
||||
md_lines.append("")
|
||||
for p in problemas:
|
||||
md_lines.append(f"### {p.get('titulo', 'Problema')}")
|
||||
md_lines.append("")
|
||||
md_lines.append(p.get('descricao', ''))
|
||||
md_lines.append("")
|
||||
if p.get('detalhes'):
|
||||
for detalhe in p['detalhes']:
|
||||
md_lines.append(f"- {detalhe}")
|
||||
md_lines.append("")
|
||||
|
||||
# Soluções
|
||||
if solucoes:
|
||||
md_lines.append("## 💡 Soluções")
|
||||
md_lines.append("")
|
||||
for s in solucoes:
|
||||
md_lines.append(f"### {s.get('titulo', 'Solução')}")
|
||||
md_lines.append("")
|
||||
md_lines.append(s.get('descricao', ''))
|
||||
md_lines.append("")
|
||||
if s.get('detalhes'):
|
||||
for i, detalhe in enumerate(s['detalhes'], 1):
|
||||
md_lines.append(f"{i}. {detalhe}")
|
||||
md_lines.append("")
|
||||
|
||||
# Resultados
|
||||
if resultados:
|
||||
md_lines.append("## ✅ Resultados")
|
||||
md_lines.append("")
|
||||
for r in resultados:
|
||||
md_lines.append(f"### {r.get('titulo', 'Resultado')}")
|
||||
md_lines.append("")
|
||||
md_lines.append(r.get('descricao', ''))
|
||||
md_lines.append("")
|
||||
if r.get('detalhes'):
|
||||
for detalhe in r['detalhes']:
|
||||
md_lines.append(f"- {detalhe}")
|
||||
md_lines.append("")
|
||||
|
||||
# Informação adicional
|
||||
if info:
|
||||
md_lines.append("## 📋 Informação Adicional")
|
||||
md_lines.append("")
|
||||
for inf in info:
|
||||
md_lines.append(f"### {inf.get('titulo', 'Info')}")
|
||||
md_lines.append("")
|
||||
md_lines.append(inf.get('descricao', ''))
|
||||
md_lines.append("")
|
||||
if inf.get('detalhes'):
|
||||
for detalhe in inf['detalhes']:
|
||||
md_lines.append(f"- {detalhe}")
|
||||
md_lines.append("")
|
||||
|
||||
# Keywords e aplicabilidade
|
||||
md_lines.append("---")
|
||||
md_lines.append("")
|
||||
|
||||
if structured_data.get('keywords'):
|
||||
md_lines.append(f"**Palavras-chave**: {', '.join(structured_data['keywords'])}")
|
||||
md_lines.append("")
|
||||
|
||||
if structured_data.get('aplicabilidade'):
|
||||
md_lines.append("**Aplicabilidade**:")
|
||||
for app in structured_data['aplicabilidade']:
|
||||
md_lines.append(f"- {app}")
|
||||
md_lines.append("")
|
||||
|
||||
return '\n'.join(md_lines)
|
||||
|
||||
def process_file(self, input_file: Path, output_dir: Path) -> bool:
|
||||
"""Processa um ficheiro."""
|
||||
try:
|
||||
output_file = output_dir / f"structured_{input_file.name}"
|
||||
|
||||
# Skip se já existe
|
||||
if output_file.exists():
|
||||
logger.info(f"Já existe: {output_file.name}")
|
||||
return True
|
||||
|
||||
# Verificar tamanho
|
||||
file_size = input_file.stat().st_size
|
||||
if file_size < 500: # Muito pequeno
|
||||
logger.warning(f"Ficheiro muito pequeno ({file_size}B): {input_file.name}")
|
||||
return False
|
||||
|
||||
if file_size > 100000: # >100KB
|
||||
logger.warning(f"Ficheiro grande ({file_size/1024:.1f}KB): {input_file.name}")
|
||||
|
||||
# Ler conteúdo
|
||||
with open(input_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
logger.info(f"Processando: {input_file.name} ({file_size/1024:.1f}KB)")
|
||||
|
||||
# Estruturar com AI
|
||||
structured_data = self.structure_content(content, input_file.name)
|
||||
|
||||
if not structured_data:
|
||||
logger.error(f"Falha ao estruturar: {input_file.name}")
|
||||
return False
|
||||
|
||||
# Converter para MD formatado
|
||||
formatted_md = self.format_structured_md(structured_data, input_file.name)
|
||||
|
||||
# Guardar
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(formatted_md)
|
||||
|
||||
# Guardar JSON também
|
||||
json_file = output_dir / f"structured_{input_file.stem}.json"
|
||||
with open(json_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(structured_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
logger.info(f"✓ Guardado: {output_file.name}")
|
||||
|
||||
# Pausa para evitar rate limits
|
||||
time.sleep(3)
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Erro ao processar {input_file.name}: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Função principal."""
|
||||
if not API_KEY:
|
||||
logger.error("OPENROUTER_API_KEY não configurada no .env")
|
||||
return
|
||||
|
||||
input_path = Path(INPUT_DIR)
|
||||
output_path = Path(OUTPUT_DIR)
|
||||
|
||||
if not input_path.exists():
|
||||
logger.error(f"Diretório não existe: {INPUT_DIR}")
|
||||
return
|
||||
|
||||
output_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Encontrar ficheiros (focar nos sites completos primeiro)
|
||||
priority_sites = ['thehogring.com', 'sailrite.com', 'relicate.com', 'thesamba.com']
|
||||
|
||||
all_files = []
|
||||
for site in priority_sites:
|
||||
pattern = f"{site}_*.md"
|
||||
files = list(input_path.glob(pattern))
|
||||
all_files.extend(files)
|
||||
|
||||
logger.info(f"Encontrados {len(all_files)} ficheiros para processar")
|
||||
|
||||
# Processar
|
||||
structurer = ContentStructurer()
|
||||
successful = 0
|
||||
|
||||
for md_file in all_files:
|
||||
if structurer.process_file(md_file, output_path):
|
||||
successful += 1
|
||||
|
||||
logger.info(f"Concluído: {successful}/{len(all_files)} ficheiros processados")
|
||||
|
||||
# Estatísticas
|
||||
json_files = list(output_path.glob('*.json'))
|
||||
logger.info(f"Total ficheiros JSON criados: {len(json_files)}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("═══════════════════════════════════════════════════════════")
|
||||
print(" CTF CARSTUFF - ESTRUTURAÇÃO INTELIGENTE DE CONTEÚDO")
|
||||
print(" Formato: Problema → Solução → Resultado")
|
||||
print("═══════════════════════════════════════════════════════════")
|
||||
print("")
|
||||
main()
|
||||
print("")
|
||||
print("✓ Processo concluído!")
|
||||
Reference in New Issue
Block a user