mmpSearch/scripts/handler/crawler.py

230 lines
9.0 KiB
Python

import os
import time
import threading
import requests
import csv
import psutil
import re
import unicodedata
from datetime import datetime, timedelta
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse, parse_qs
from concurrent.futures import ThreadPoolExecutor, as_completed
from utils import (
SRC_MMPSEARCH,
MMP_FOLDER,
)
# ================= CONFIGURAÇÕES PADRONIZADAS =================
BASE_DIR = SRC_MMPSEARCH
PASTA_DESTINO = MMP_FOLDER # Pasta de entrada do pipeline
LOG_DIR = os.path.join(BASE_DIR, "logs/crawler")
ARQUIVO_AUDITORIA = os.path.join(LOG_DIR, "audit_crawler.csv")
# Segurança de Hardware
MIN_RAM_FREE_MB = 500 # Pausa se tiver menos que 500MB livre
MAX_WORKERS_SAFE = 4 # I/O Bound não precisa de tantos workers quanto CPU, mas 4 é seguro
HEADERS = {
'User-Agent': 'Mozilla/5.0 (Compatible; MMPResearchBot/1.0; +http://seu-site.com)'
}
# Cria pastas
for p in [PASTA_DESTINO, LOG_DIR]:
os.makedirs(p, exist_ok=True)
# ================= FUNÇÕES AUXILIARES =================
def slugify(value):
"""Sanitização robusta de nomes de arquivo (Padrão do Pipeline)"""
value = str(value)
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
value = value.lower()
value = re.sub(r'[^\w\s-]', '', value)
value = re.sub(r'[-\s_]+', '-', value)
return value.strip('-_')
def verificar_memoria_segura():
"""Retorna True se é seguro continuar"""
try:
mem = psutil.virtual_memory()
return (mem.available / (1024 * 1024)) > MIN_RAM_FREE_MB
except:
return True
# ================= CLASSE DE AUDITORIA =================
class AuditoriaCrawler:
def __init__(self, arquivo_csv):
self.arquivo_csv = arquivo_csv
self.inicio_global = time.time()
self.lock = threading.Lock()
# Cria CSV se não existir
if not os.path.exists(self.arquivo_csv):
with open(self.arquivo_csv, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow([
"Timestamp", "Nome_Original", "Slug_Final",
"Tamanho_MB", "Tempo_DL_s", "Velocidade_KBps",
"Status", "URL_Origem"
])
def registrar_download(self, nome_orig, slug, tamanho_bytes, tempo_s, url, status="SUCESSO"):
with self.lock:
try:
tamanho_mb = tamanho_bytes / (1024 * 1024)
velocidade = (tamanho_bytes / 1024) / tempo_s if tempo_s > 0 else 0
ts = datetime.now().strftime("%H:%M:%S")
with open(self.arquivo_csv, 'a', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow([
ts, nome_orig, slug,
f"{tamanho_mb:.2f}", f"{tempo_s:.2f}",
f"{velocidade:.2f}", status, url
])
# Log Console Simplificado
print(f"[{ts}] {status}: {slug} ({tamanho_mb:.2f}MB @ {velocidade:.0f}KB/s)")
except Exception as e:
print(f"Erro ao auditar: {e}")
# ================= WORKER =================
def baixar_projeto_worker(args):
url_detalhes, auditor = args
# Check de Segurança (Throttle)
if not verificar_memoria_segura():
time.sleep(5) # Espera RAM liberar
if not verificar_memoria_segura():
return (False, "RAM Crítica - Download Abortado")
try:
# 1. Pega página de detalhes
resp = requests.get(url_detalhes, headers=HEADERS, timeout=15)
if resp.status_code != 200:
auditor.registrar_download("N/A", "N/A", 0, 0, url_detalhes, f"HTTP_{resp.status_code}")
return (False, f"HTTP {resp.status_code}")
soup = BeautifulSoup(resp.text, 'html.parser')
# Lógica de encontrar link de download (Manteve a sua, que é boa)
link_tag = soup.find('a', href=lambda h: h and 'download_file.php' in h)
if not link_tag:
link_tag = soup.find('a', string='Download')
if link_tag:
url_download = urljoin(url_detalhes, link_tag['href'])
inicio_dl = time.time()
# Request do Arquivo
with requests.get(url_download, headers=HEADERS, stream=True, timeout=60) as r_file:
r_file.raise_for_status()
# --- DESCIFRAR NOME (COM SANITIZAÇÃO) ---
nome_bruto = ""
if "Content-Disposition" in r_file.headers:
cd = r_file.headers["Content-Disposition"]
if "filename=" in cd:
nome_bruto = cd.split("filename=")[1].strip('"')
if not nome_bruto:
query = parse_qs(urlparse(url_download).query)
nome_bruto = query['name'][0] if 'name' in query else f"proj_{int(time.time())}.mmpz"
# Aplica Slugify (Padrão do Pipeline)
nome_base, ext = os.path.splitext(nome_bruto)
if not ext: ext = ".mmpz" # Default seguro
slug = slugify(nome_base)
nome_final = f"{slug}{ext}"
caminho_final = os.path.join(PASTA_DESTINO, nome_final)
# Skip se já existe
if os.path.exists(caminho_final):
auditor.registrar_download(nome_bruto, slug, os.path.getsize(caminho_final), 0, url_detalhes, "SKIPPED_EXISTS")
return (True, "Já existe")
# Gravação em chunks (Seguro para RAM)
bytes_downloaded = 0
with open(caminho_final, 'wb') as f:
for chunk in r_file.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
bytes_downloaded += len(chunk)
tempo_total = time.time() - inicio_dl
# Auditoria de Sucesso
auditor.registrar_download(nome_bruto, slug, bytes_downloaded, tempo_total, url_detalhes, "SUCESSO")
return (True, "OK")
else:
auditor.registrar_download("N/A", "N/A", 0, 0, url_detalhes, "LINK_NOT_FOUND")
return (False, "Link não encontrado")
except Exception as e:
auditor.registrar_download("Erro", "Erro", 0, 0, url_detalhes, f"EXCEPT_{str(e)[:20]}")
return (False, str(e))
# ================= MAIN =================
def crawler_manager(url_base, max_paginas=2):
print(f"\n=== INICIANDO CRAWLER PADRONIZADO ===")
print(f"Destino: {PASTA_DESTINO}")
print(f"Auditoria: {ARQUIVO_AUDITORIA}")
auditor = AuditoriaCrawler(ARQUIVO_AUDITORIA)
pagina_atual = 1
total_sucesso = 0
# Executor seguro
with ThreadPoolExecutor(max_workers=MAX_WORKERS_SAFE) as executor:
while pagina_atual <= max_paginas:
print(f"\n--- Varrendo Página {pagina_atual} ---")
operador = '&' if '?' in url_base else '?'
url_pag = f"{url_base}{operador}page={pagina_atual}"
try:
# Pega a lista de projetos da página
r = requests.get(url_pag, headers=HEADERS)
soup = BeautifulSoup(r.text, 'html.parser')
# Filtro específico do LMMS Sharing Platform
links = soup.find_all('a', href=lambda h: h and '?action=show&file=' in h)
urls_unicas = sorted(list(set([l['href'] for l in links])))
if not urls_unicas:
print("Nenhum projeto encontrado nesta página. Encerrando.")
break
# Submete tarefas
futures = []
for u in urls_unicas:
url_completa = urljoin(url_base, u)
futures.append(executor.submit(baixar_projeto_worker, (url_completa, auditor)))
# Processa resultados conforme chegam
for future in as_completed(futures):
ok, msg = future.result()
if ok: total_sucesso += 1
except Exception as e:
print(f"Erro na paginação: {e}")
pagina_atual += 1
time.sleep(1) # Respeito ao servidor
tempo_total = time.time() - auditor.inicio_global
print(f"\n=== CRAWLER FINALIZADO ===")
print(f"Tempo: {timedelta(seconds=int(tempo_total))}")
print(f"Downloads com Sucesso: {total_sucesso}")
if __name__ == "__main__":
# URL de exemplo (Projetos ordenados por data)
URL_ALVO = 'https://lmms.io/lsp/?action=browse&category=Projects&sort=date'
crawler_manager(URL_ALVO, max_paginas=499)