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)