att base de dados
This commit is contained in:
1 parent
ceb7908da4
commit
73693e5da9
14 files changed
+3010
-1
No files matched your search
@@ -0,0 +1,230 @@
|
||||
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)
|
||||
Reference in new issue
Block a user