Este documento describe en detalle el pipeline de scraping y crawling para extraer datos de Sputnikmusic.
- Arquitectura General
- Scraper CLI
- Crawler Masivo
- Flujo Escalonado de Ingestión
- Monitoreo y Seguimiento
- Mantenimiento de la Base de Datos
- Esquema de Datos
- Consideraciones Éticas
El sistema de extracción se compone de dos capas principales:
Módulos de bajo nivel para parsing de HTML y comunicación HTTP:
| Módulo | Descripción |
|---|---|
charts.py |
Extrae rankings anuales de álbumes |
soundoffs.py |
Parsea comentarios y ratings de usuarios en releases |
tracklist.py |
Extrae listados de canciones |
users.py |
Obtiene perfiles públicos de usuarios |
user_ratings.py |
Extrae historial de calificaciones de usuarios |
discography.py |
Parsea discografías completas de artistas |
http.py |
Cliente HTTP con rate limiting y reintentos |
Orquestadores de alto nivel que coordinan el scraping y persisten en SQLite:
| Módulo | Descripción |
|---|---|
runner.py |
Crawler principal: charts → releases → soundoffs |
discography.py |
Expande discografías de artistas encolados |
user_expander.py |
Trae ratings completos de usuarios encolados |
Para obtener datos puntuales sin persistir en base de datos:
# Top anual en JSON
python -m scraper --year 2024 --pretty > data/best_albums_2024.json
# Con más detalle
python -m scraper --year 2024 --pretty --verbose| Flag | Descripción |
|---|---|
--year |
Año del chart a extraer |
--pretty |
Formato JSON indentado |
--verbose |
Logging detallado |
Ver examples/fetch_latest.py para un ejemplo de cómo usar el scraper programáticamente:
from scraper import charts
# Obtener chart del año
albums = charts.fetch_year_chart(2024)
for album in albums:
print(f"{album['artist']} - {album['title']} ({album['rating']})")El crawler principal ingiere datos de forma sistemática y los persiste en SQLite.
python -m crawler \
--start-year 1960 \
--end-year 2025 \
--db data/sputnik.db \
--schema data/schema.sql \
--log-level INFO| Parámetro | Descripción | Default |
|---|---|---|
--start-year |
Año inicial del crawl | - |
--end-year |
Año final del crawl | - |
--db |
Ruta a la base SQLite | data/sputnik.db |
--schema |
Ruta al esquema SQL | data/schema.sql |
--log-level |
Nivel de logging | INFO |
| Parámetro | Descripción |
|---|---|
--skip-tracklists |
Omite extracción de tracklists |
--skip-soundoffs |
Omite extracción de soundoffs |
--skip-user-profiles |
No trae perfiles durante el crawl |
--max-soundoffs N |
Limita soundoffs por álbum |
--dry-run |
Valida sin escribir en la base |
--no-queue-users |
No encola usuarios detectados |
--user-queue-priority N |
Prioridad para usuarios encolados |
- Rate limiting configurable: Respeta
min_intervalentre requests - Idempotente: Usa
ON CONFLICTpara evitar duplicados - Reanudable: Mantiene estado en
crawl_state(status por año) - Enriquecimiento: Detecta roles de usuarios (EMERITUS, STAFF, etc.)
Para reanudar un crawl interrumpido, simplemente relanzar el comando:
# Los años completados quedan marcados como DONE en crawl_state
python -m crawler --start-year 1960 --end-year 2025 --db data/sputnik.dbPara poblar la base de forma eficiente, se recomienda separar la ingesta en tres etapas:
Captura los rankings anuales y las interacciones visibles en cada álbum.
python -m crawler \
--start-year 2000 \
--end-year 2024 \
--db data/sputnik.db \
--schema data/schema.sql \
--skip-tracklists \
--skip-user-profiles \
--user-queue-priority 5 \
--log-level INFOScript alternativo: scripts/seed_charts.sh
Resultado:
- Artistas y releases del top anual
- Interacciones de soundoffs
- Usuarios encolados en
crawl_users - Artistas encolados en
crawl_artists
Expande la discografía completa de cada artista detectado.
python -m crawler.discography \
--db data/sputnik.db \
--schema data/schema.sql \
--batch-size 25 \
--max-soundoffs 100 \
--log-level INFOScript alternativo: scripts/expand_discographies.sh
Parámetros:
| Parámetro | Descripción | Default |
|---|---|---|
--batch-size |
Artistas por batch | 25 |
--max-soundoffs |
Soundoffs por release | 100 |
--skip-tracklists |
Omite tracklists | false |
--skip-soundoffs |
Omite soundoffs | false |
Recomendación: Ejecutar antes de expandir usuarios para que las interacciones futuras apunten a releases ya poblados.
Trae el historial completo de ratings de cada usuario encolado.
python -m crawler.user_expander \
--db data/sputnik.db \
--schema data/schema.sql \
--batch-size 25 \
--max-rating-pages none \
--log-level INFOScript alternativo: scripts/expand_users.sh
Parámetros:
| Parámetro | Descripción | Default |
|---|---|---|
--batch-size |
Usuarios por batch | 25 |
--max-rating-pages |
Páginas de ratings por usuario | none (todas) |
--priority-min |
Prioridad mínima para procesar | 0 |
Nota: El endpoint uservote.php no expone la fecha exacta del voto, solo el rating.
- Batches pequeños: Facilita el control de rate limiting
- Monitorear colas: Revisar
crawl_users,crawl_artists,crawl_releases - Relanzar sin miedo: Las tablas usan
ON CONFLICTpara evitar duplicados - Variables de entorno: Los scripts respetan
DB_PATH,SCHEMA_PATH, etc.
scripts/monitor_crawler.sh data/sputnik.db logs/crawler-full.logFunciones:
- Tail del log en tiempo real
- Procesos activos del crawler
- Estado por año (
crawl_state) - Estadísticas de la base (releases, users, interactions)
- Distribución de ratings
- Top releases por cantidad de votos
- Estado de colas y últimos errores
-- Estado del crawl por año
SELECT year, status, last_album, note FROM crawl_state ORDER BY year;
-- Usuarios pendientes
SELECT COUNT(*) FROM crawl_users WHERE status = 'pending';
-- Artistas con errores
SELECT id_artist, last_error FROM crawl_artists WHERE status = 'error' LIMIT 10;
-- Distribución de ratings
SELECT ROUND(rating, 1) as rating, COUNT(*) as count
FROM interactions
WHERE rating > 0
GROUP BY ROUND(rating, 1)
ORDER BY rating;Detecta y repara problemas comunes:
# Diagnóstico
./scripts/db_health.sh
# Ver en JSON
python maintenance/db_health.py --db data/sputnik.db --format json
# Reparar categoría específica
./scripts/db_health.sh --fix users.error.timeout --apply
# Reparar todo
./scripts/db_health.sh --fix-all --applyCategorías de problemas detectados:
| Categoría | Descripción |
|---|---|
users.error.* |
Usuarios con errores (404, timeout, conexión) |
users.incomplete |
Perfiles sin role o join_date |
users.ratings_mismatch |
ratings_count inconsistente |
releases.error.* |
Releases con errores |
releases.incomplete |
Metadata incompleta |
artists.no_genres |
Artistas sin géneros asignados |
Después de ingestas grandes o reparaciones:
# VACUUM para desfragmentar
sqlite3 data/sputnik.db "VACUUM;"
# ANALYZE para actualizar estadísticas del planner
sqlite3 data/sputnik.db "ANALYZE;"
# Script completo de análisis y optimización
python maintenance/analyze_and_vacuum.py📖 Ver maintenance/README.md para más detalles.
users # Perfiles de usuarios
artists # Artistas
releases # Álbumes, EPs, Singles, Compilations
interactions # Ratings y soundoffs de usuarios
crawl_state # Estado del crawl por año
crawl_users # Cola de usuarios a expandir
crawl_artists # Cola de artistas a expandir
crawl_releases # Cola de releases a procesar
genres # Catálogo de géneros
artist_genres # Géneros por artista
release_genres # Géneros por release
release_tracks # Tracklists
artist_similars # Artistas similares
release_recommendations # Recomendaciones pre-calculadas
release_pairs # Co-ocurrencias para recomendación
user_embeddings # Embeddings NMF de usuarios
release_embeddings # Embeddings NMF de releases
user_embeddings_dl # Embeddings Two Towers de usuarios
release_embeddings_dl # Embeddings Two Towers de releases
artists_enriched # Artistas con géneros y similares en JSON
releases_enriched # Releases con recomendaciones y tracklist en JSON
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ artists │────<│ releases │────<│interactions │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│artist_genres│ │release_genres│ │ users │
└─────────────┘ └──────────────┘ └─────────────┘
El sistema implementa rate limiting para no sobrecargar Sputnikmusic:
- Intervalo mínimo: Configurable vía
min_interval - Burst control: Límite de requests consecutivos
- Backoff exponencial: En caso de errores o rate limiting
El scraper respeta las directivas del sitio y evita endpoints protegidos.
- Los datos son solo para análisis personal
- No se redistribuyen datasets
- El proyecto es exclusivamente educativo
- Usar batches pequeños: Reduce la carga en el servidor
- Monitorear errores: Detectar y respetar rate limiting
- Horarios de bajo tráfico: Preferir crawling en horarios nocturnos
- Cachear resultados: Evitar re-crawlear datos ya obtenidos
SQLite no soporta escrituras concurrentes. Soluciones:
- Usar WAL mode:
PRAGMA journal_mode=WAL; - Reducir concurrencia en el crawler
- Esperar y reintentar automáticamente
El servidor está limitando requests:
- Aumentar
min_interval - Reducir
batch_size - Esperar antes de reintentar
Usar el script de salud para diagnosticar:
./scripts/db_health.sh --format json | jq '.users.errors'Reparar según el tipo de error:
- 404: Usuario/release eliminado → eliminar de cola
- timeout: Problema temporal → reencolar
- connection: Problema de red → reintentar
Verificar con:
-- Releases sin año
SELECT COUNT(*) FROM releases WHERE release_year IS NULL;
-- Usuarios sin role
SELECT COUNT(*) FROM users WHERE role IS NULL;Reencolar para completar:
./scripts/db_health.sh --fix releases.incomplete --apply- Módulos de scraping:
scraper/ - Módulos de crawling:
crawler/ - Scripts de utilidad:
scripts/ - Mantenimiento:
maintenance/ - Esquema SQL:
data/schema.sql