230 lines
9.0 KiB
Python
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) |