Pocas cosas desmotivan tanto como escribir 30 líneas de Python, ejecutar tu scraper de Goodreads y ver que devuelve []. Una lista vacía. Nada. Solo tú y el cursor parpadeando.
He visto esto decenas de veces: en nuestras propias pruebas internas en , en foros de desarrolladores y en issues de GitHub que se acumulan en repositorios de scrapers abandonados. Las quejas suelen ser las mismas: "la sección de reseñas destacadas está en blanco, solo me muestra []", "da igual el número de página que pase, siempre extrae la primera", "mi código funcionaba el año pasado, ahora se rompió". Y para complicarlo más, la API de Goodreads quedó obsoleta en diciembre de 2020, así que el consejo de "usa la API" que verás en tutoriales antiguos ya no sirve.
Si hoy quieres obtener datos estructurados de libros desde Goodreads —títulos, autores, valoraciones, reseñas, géneros, ISBN—, la extracción de datos es la vía principal. Esta guía te llevará paso a paso por un enfoque completo y funcional para extraer Goodreads con Python, incluyendo contenido renderizado por JS, paginación, prevención de bloqueos y exportación. Y si Python no es lo tuyo, también te mostraré una alternativa sin código que lo resuelve en apenas dos clics.
¿Qué es extraer datos de Goodreads y por qué hacerlo con Python?
Extraer datos de Goodreads significa obtener automáticamente información de libros —títulos, autores, valoraciones, número de reseñas, géneros, ISBN, número de páginas, fecha de publicación y más— directamente de las páginas web de Goodreads, usando código en lugar de copiar y pegar manualmente.
Goodreads es una de las bases de datos de libros más grandes del mundo, con más de y unos . Cada mes, más de 18 millones de libros llegan a estanterías de "Want to Read". Ese tipo de datos estructurados y actualizados constantemente es justo lo que buscan editores, científicos de datos, libreros e investigadores.
Python es el lenguaje favorito para este tipo de trabajo: impulsa aproximadamente el de todos los proyectos de scraping. Sus bibliotecas están muy maduras (requests, BeautifulSoup, Selenium, Playwright, pandas), la sintaxis es amigable para principiantes y la comunidad es enorme.
Si nunca has extraído datos de una web, Python es un gran punto de partida.
¿Por qué extraer Goodreads con Python? Casos de uso reales
Antes de entrar en el código, vale la pena preguntarse: ¿quién necesita realmente estos datos y para qué los usa?
| Caso de uso | Quién se beneficia | Qué se extrae |
|---|---|---|
| Investigación de mercado editorial | Editoriales, agentes literarios | Géneros en tendencia, títulos mejor valorados, autores emergentes, valoraciones de la competencia |
| Sistemas de recomendación de libros | Científicos de datos, aficionados, desarrolladores de apps | Valoraciones, géneros, estanterías de usuarios, sentimiento de reseñas |
| Control de precios e inventario | Librerías online | Títulos populares, volumen de reseñas, número de "Want to Read" |
| Investigación académica | Investigadores, estudiantes | Texto de reseñas, distribución de valoraciones, clasificación por géneros |
| Analítica de lectura | Blogueros literarios, proyectos personales | Datos de estanterías personales, historial de lectura, estadísticas anuales |
Algunos ejemplos concretos: UCSD Book Graph —uno de los conjuntos de datos académicos más citados en investigación sobre recomendación— contiene , todo ello recopilado de estanterías públicas de Goodreads. Varios datasets de Kaggle (goodbooks-10k, Best Books Ever, etc.) nacieron de extracción de datos de Goodreads. Y un estudio de 2025 en Big Data and Society recopiló de forma computacional para analizar cómo las reseñas patrocinadas afectan a la plataforma.
En el lado comercial, Bright Data vende datasets de Goodreads ya extraídos por tan solo $0,50 por cada 1.000 registros, una prueba clara de que estos datos tienen valor real en el mercado.
La API de Goodreads ya no está disponible — esto es lo que la sustituyó
Si has buscado "Goodreads API" últimamente, seguramente te has topado con un tutorial desactualizado. El 8 de diciembre de 2020, Goodreads dejó de emitir nuevas claves de desarrollador de forma silenciosa. No hubo post en el blog ni correo masivo; solo un pequeño aviso en la página de documentación y muchos desarrolladores confundidos.

El impacto fue inmediato. Un desarrollador, Kyle K, creó un bot de Discord para compartir recomendaciones de libros y de repente "¡zas! dejó de funcionar". Otro, Matthew Jones, perdió acceso a la API una semana antes de la votación de los Reddit r/Fantasy Stabby Awards, y tuvo que volver a Google Forms. Una estudiante de posgrado llamada Elena Neacsu vio cómo su proyecto de tesis de máster se desmoronaba a mitad de desarrollo.
Entonces, ¿qué queda? El panorama actual es este:
| Enfoque | Datos disponibles | Facilidad de uso | Límites de uso | Estado |
|---|---|---|---|---|
| API de Goodreads | Metadatos completos, reseñas | Fácil (lo era) | 1 req/seg | Obsoleta (dic. 2020) — no hay nuevas claves |
| Open Library API | Títulos, autores, ISBN, portadas (~30M títulos) | Fácil | 1-3 req/seg | Activa, gratuita, sin autenticación |
| Google Books API | Metadatos, vistas previas | Fácil | 1.000/día gratis | Activa (faltan ISBN en algunos idiomas) |
| Scraping con Python (requests + BS4) | Todo lo que esté en el HTML inicial | Moderado | Gestionado por ti | Funciona con contenido estático |
| Scraping con Python (Selenium/Playwright) | También contenido renderizado por JS | Más difícil | Gestionado por ti | Necesario para reseñas y algunas listas |
| Thunderbit (extensión Chrome sin código) | Cualquier dato visible de la página | Muy fácil (2 clics) | Basado en créditos | Activo — sin necesidad de Python |
Open Library es un buen complemento, sobre todo para búsquedas de ISBN y metadatos básicos. Pero si necesitas valoraciones, reseñas, etiquetas de géneros o conteos de "Want to Read", tendrás que extraer Goodreads directamente —ya sea con Python o con una herramienta como Thunderbit, que puede extraer páginas de Goodreads (incluyendo subpáginas con detalles del libro) con campos sugeridos por IA y exportación directa a Google Sheets, Notion o Airtable.
Por qué tu scraper de Goodreads en Python devuelve resultados vacíos (y cómo arreglarlo)
Esta es la sección que me habría gustado tener cuando empecé a trabajar con datos de Goodreads. El problema de los "resultados vacíos" es la queja más común en foros de desarrolladores, y tiene varias causas distintas —cada una con su solución.
| Síntoma | Causa raíz | Solución |
|---|---|---|
Reseñas/valoraciones devuelven [] | Contenido renderizado por JS (React/carga diferida) | Usa Selenium o Playwright en lugar de requests |
| Siempre extrae solo la página 1 | Se ignoran parámetros de paginación o la navegación depende de JS | Pasa correctamente el parámetro ?page=N; usa automatización del navegador para scroll infinito |
| El código funcionaba el año pasado y ahora falla | Goodreads cambió los nombres de las clases HTML | Usa selectores más robustos (JSON-LD, atributos data-testid) |
| Errores 403/bloqueo tras pocas peticiones | Faltan headers / las solicitudes son demasiado rápidas | Añade User-Agent, usa time.sleep(), rota proxies |
| Pared de login en páginas de estanterías/listas | Se requiere cookie/sesión | Usa requests.Session() con cookies o scraping desde navegador |
Contenido renderizado por JS: reseñas y valoraciones aparecen vacías
Goodreads utiliza una interfaz basada en React. Cuando haces requests.get() a una página de libro, recibes el HTML inicial; pero las reseñas, la distribución de valoraciones y muchas secciones de "más información" se cargan asíncronamente vía JavaScript. Tu scraper literalmente no puede verlas.
La solución: para cualquier página en la que necesites contenido renderizado por JS, cambia a Selenium o Playwright. Mi recomendación para proyectos nuevos es Playwright: es gracias a su protocolo basado en WebSocket, y además ofrece mejor soporte nativo para stealth y async.

Paginación que solo devuelve la página 1
Esta es traicionera. Escribes un bucle, incrementas ?page=N y aun así obtienes siempre los mismos resultados. En Goodreads, las páginas de estanterías devuelven en silencio el contenido de la página 1 independientemente del parámetro ?page= si no estás autenticado. Sin error, sin redirección: solo la misma primera página una y otra vez.
La solución: incluye una cookie de sesión autenticada (en concreto _session_id2). Más abajo, en la sección de paginación, te explico cómo.
El código que funcionaba el año pasado ahora falla
Goodreads cambia periódicamente los nombres de las clases HTML y la estructura de las páginas. El popular repositorio maria-antoniak/goodreads-scraper en GitHub ahora muestra un aviso permanente: "This project is unmaintained and no longer functioning." La solución es usar selectores más resistentes —datos estructurados JSON-LD (que siguen los estándares schema.org y casi nunca cambian) o atributos data-testid en lugar de clases frágiles.
Errores 403 o bloqueos
La biblioteca requests de Python tiene una huella TLS distinta a la de Chrome. Incluso con un User-Agent de Chrome, sistemas de detección de bots como AWS WAF (que Goodreads usa al ser una subsidiaria de Amazon) pueden detectar la discrepancia. La solución: añade headers de navegador realistas, introduce pausas de time.sleep() de 3 a 8 segundos entre peticiones y, si haces scraping a gran escala, considera curl_cffi para imitar la huella TLS.
Muros de login en estanterías y páginas de listas
Algunas páginas de estanterías y listas de Goodreads requieren autenticación para mostrar el contenido completo, especialmente a partir de la página 5. Usa requests.Session() con cookies exportadas desde tu navegador, o bien Selenium/Playwright con un perfil ya autenticado. Thunderbit lo gestiona de forma natural porque trabaja dentro de tu propio navegador Chrome con sesión iniciada.
Antes de empezar
- Dificultad: Intermedia (se asumen conocimientos básicos de Python)
- Tiempo estimado: ~20-30 minutos para el tutorial completo
- Lo que necesitas:
- Python 3.8+
- Navegador Chrome (para inspección con DevTools y Selenium/Playwright)
- Bibliotecas:
requests,beautifulsoup4,seleniumoplaywright,pandas - (Opcional)
gspreadpara exportar a Google Sheets - (Opcional) como alternativa sin código

Paso 1: Configura tu entorno de Python
Instala las bibliotecas necesarias. Abre tu terminal y ejecuta:
1pip install requests beautifulsoup4 selenium pandas lxml
Si prefieres Playwright (recomendado para proyectos nuevos):
1pip install playwright
2playwright install chromium
Para exportar a Google Sheets (opcional):
1pip install gspread oauth2client
Asegúrate de usar Python 3.8 o superior. Puedes comprobarlo con python --version.
Después de la instalación, deberías poder importar todas las bibliotecas sin errores. Prueba con python -c "import requests, bs4, pandas; print('Ready')" para confirmar.
Paso 2: Envía tu primera petición con headers correctos
Abre en tu navegador una página de Goodreads de género o lista —por ejemplo, https://www.goodreads.com/list/show/1.Best_Books_Ever. Ahora vamos a obtener esa página con Python.
1import requests
2from bs4 import BeautifulSoup
3headers = {
4 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
5 "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
6 "Accept-Language": "en-US,en;q=0.9",
7 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
8}
9url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
10response = requests.get(url, headers=headers, timeout=15)
11print(f"Status: {response.status_code}")
Deberías ver Status: 200. Si recibes 403, revisa los headers: el AWS WAF de Goodreads comprueba que el User-Agent sea realista y rechaza solicitudes demasiado básicas. Los headers anteriores imitan una sesión real de Chrome.
Paso 3: Inspecciona la página e identifica los selectores correctos
Abre las DevTools de Chrome (F12) en la página de la lista de Goodreads. Haz clic derecho sobre el título de un libro y selecciona "Inspect". Verás la estructura DOM de cada entrada.
En las páginas de listas, cada libro suele estar envuelto en un elemento <tr> con itemtype="http://schema.org/Book". Dentro encontrarás:
- Título:
a.bookTitle(el texto del enlace es el título,hrefda la URL del libro) - Autor:
a.authorName - Valoración:
span.minirating(contiene la media y el número de valoraciones) - Imagen de portada:
imgdentro de la fila del libro
En las páginas individuales de detalle del libro, conviene saltarse los selectores CSS y usar directamente JSON-LD. Goodreads incrusta datos estructurados en una etiqueta <script type="application/ld+json"> que sigue el formato Book de schema.org. Es mucho más estable que los nombres de clase, que Goodreads cambia sin aviso.
Paso 4: Extrae datos de libros desde una sola página de lista
Vamos a analizar la página de la lista y extraer información básica de cada libro:
1import requests
2from bs4 import BeautifulSoup
3headers = {
4 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
5 "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
6 "Accept-Language": "en-US,en;q=0.9",
7}
8url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
9response = requests.get(url, headers=headers, timeout=15)
10soup = BeautifulSoup(response.text, "lxml")
11books = []
12rows = soup.select('tr[itemtype="http://schema.org/Book"]')
13for row in rows:
14 title_tag = row.select_one("a.bookTitle")
15 author_tag = row.select_one("a.authorName")
16 rating_tag = row.select_one("span.minirating")
17 title = title_tag.get_text(strip=True) if title_tag else ""
18 book_url = "https://www.goodreads.com" + title_tag["href"] if title_tag else ""
19 author = author_tag.get_text(strip=True) if author_tag else ""
20 rating_text = rating_tag.get_text(strip=True) if rating_tag else ""
21 books.append({
22 "title": title,
23 "author": author,
24 "rating_info": rating_text,
25 "book_url": book_url,
26 })
27print(f"Encontrados {len(books)} libros en la página 1")
28for b in books[:3]:
29 print(b)
Deberías ver unos 100 libros por página de lista. Cada entrada tendrá título, autor, una cadena de valoración como "4.28 avg rating — 9,031,257 ratings" y la URL de la página de detalle del libro.
Paso 5: Extrae subpáginas para obtener información detallada del libro
La página de listado te da lo básico, pero el verdadero valor —ISBN, descripción completa, etiquetas de géneros, número de páginas, fecha de publicación— está en la página individual de cada libro. Aquí es donde JSON-LD brilla.
1import json
2import time
3def scrape_book_detail(book_url, headers):
4 """Visita una página de libro y extrae metadatos detallados mediante JSON-LD."""
5 resp = requests.get(book_url, headers=headers, timeout=15)
6 if resp.status_code != 200:
7 return {}
8 soup = BeautifulSoup(resp.text, "lxml")
9 script = soup.find("script", {"type": "application/ld+json"})
10 if not script:
11 return {}
12 data = json.loads(script.string)
13 agg = data.get("aggregateRating", {})
14 # Las etiquetas de géneros no están en JSON-LD; usamos HTML como respaldo
15 genres = [g.get_text(strip=True) for g in soup.select('span.BookPageMetadataSection__genreButton a span')]
16 return {
17 "isbn": data.get("isbn", ""),
18 "pages": data.get("numberOfPages", ""),
19 "language": data.get("inLanguage", ""),
20 "format": data.get("bookFormat", ""),
21 "avg_rating": agg.get("ratingValue", ""),
22 "rating_count": agg.get("ratingCount", ""),
23 "review_count": agg.get("reviewCount", ""),
24 "description": data.get("description", "")[:200], # recorte para vista previa
25 "genres": ", ".join(genres[:5]),
26 }
27# Ejemplo: enriquecer los primeros 3 libros
28for book in books[:3]:
29 details = scrape_book_detail(book["book_url"], headers)
30 book.update(details)
31 print(f"Extraído: {book['title']} — ISBN: {book.get('isbn', 'N/A')}")
32 time.sleep(4) # respeta los límites de uso
Añade time.sleep() de 3 a 8 segundos entre peticiones. El rate limiting de Goodreads suele activarse alrededor de 20-30 solicitudes por minuto desde una misma IP, y empezarás a ver 403 o CAPTCHA si vas más rápido.
Este enfoque en dos pasadas —primero obtener todas las URLs de libros de las páginas de listado, y después visitar cada página de detalle— es más fiable y más fácil de reanudar si algo se interrumpe. Es la estrategia que usan la mayoría de scrapers de Goodreads que funcionan bien.
Nota: puede hacer esto automáticamente con scraping de subpáginas. La IA visita cada página de detalle del libro y enriquece tu tabla con ISBN, descripción, géneros y más —sin código, sin bucles, sin temporizadores.
Paso 6: Maneja el contenido renderizado por JavaScript con Selenium
Para las páginas en las que el contenido que necesitas se carga mediante JavaScript —reseñas, desglose de valoraciones, secciones de "más detalles"— necesitarás una herramienta de automatización del navegador. Aquí tienes un ejemplo con Selenium:
1from selenium import webdriver
2from selenium.webdriver.chrome.options import Options
3from selenium.webdriver.common.by import By
4from selenium.webdriver.support.ui import WebDriverWait
5from selenium.webdriver.support import expected_conditions as EC
6options = Options()
7options.add_argument("--headless=new")
8options.add_argument("--disable-blink-features=AutomationControlled")
9options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
10 "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36")
11driver = webdriver.Chrome(options=options)
12driver.get("https://www.goodreads.com/book/show/5907.The_Hobbit")
13# Esperar a que carguen las reseñas
14try:
15 WebDriverWait(driver, 10).until(
16 EC.presence_of_element_located((By.CSS_SELECTOR, "article.ReviewCard"))
17 )
18except:
19 print("Las reseñas no cargaron: la página puede requerir login o el JS tardó demasiado")
20# Ahora analizamos el HTML completamente renderizado
21page_source = driver.page_source
22soup = BeautifulSoup(page_source, "lxml")
23reviews = soup.select("article.ReviewCard")
24for rev in reviews[:3]:
25 text = rev.select_one("span.Formatted")
26 stars = rev.select_one("span.RatingStars")
27 print(f"Valoración: {stars.get_text(strip=True) if stars else 'N/A'}")
28 print(f"Reseña: {text.get_text(strip=True)[:150] if text else 'N/A'}...")
29 print()
30driver.quit()
Cuándo usar Selenium frente a requests:
- Usa
requests+ BeautifulSoup para metadatos de libros (JSON-LD), páginas de lista, páginas de estanterías (página 1) y datos de Choice Awards - Usa Selenium o Playwright para reseñas, distribución de valoraciones y cualquier contenido que no aparezca en el HTML en bruto
Playwright suele ser la mejor opción para proyectos nuevos: es más rápido, consume menos memoria y tiene mejores ajustes de stealth por defecto. Aun así, Selenium tiene una comunidad más grande y más ejemplos de código existentes para Goodreads.
Paginación que realmente funciona: extraer listas completas de Goodreads
La paginación es el punto de fallo más común en los scrapers de Goodreads, y no he encontrado un solo tutorial de la competencia que la explique bien. Así es como se hace correctamente.
Cómo funcionan las URLs de paginación en Goodreads
Goodreads usa un parámetro sencillo ?page=N en la mayoría de las páginas paginadas:
- Listas:
https://www.goodreads.com/list/show/1.Best_Books_Ever?page=2 - Estanterías:
https://www.goodreads.com/shelf/show/thriller?page=2 - Búsqueda:
https://www.goodreads.com/search?q=fantasy&page=2
Cada página de lista suele mostrar 100 libros. Las estanterías muestran 50 por página.
Cómo escribir un bucle de paginación que sepa cuándo parar
1import time
2all_books = []
3base_url = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
4for page_num in range(1, 50): # límite de seguridad en 50 páginas
5 url = f"{base_url}?page={page_num}"
6 resp = requests.get(url, headers=headers, timeout=15)
7 if resp.status_code != 200:
8 print(f"Página {page_num}: estado {resp.status_code}, deteniendo.")
9 break
10 soup = BeautifulSoup(resp.text, "lxml")
11 rows = soup.select('tr[itemtype="http://schema.org/Book"]')
12 if not rows:
13 print(f"Página {page_num}: no se encontraron libros, se alcanzó el final.")
14 break
15 for row in rows:
16 title_tag = row.select_one("a.bookTitle")
17 author_tag = row.select_one("a.authorName")
18 title = title_tag.get_text(strip=True) if title_tag else ""
19 book_url = "https://www.goodreads.com" + title_tag["href"] if title_tag else ""
20 author = author_tag.get_text(strip=True) if author_tag else ""
21 all_books.append({"title": title, "author": author, "book_url": book_url})
22 print(f"Página {page_num}: extraídos {len(rows)} libros (total: {len(all_books)})")
23 time.sleep(5) # pausa de 5 segundos entre páginas
24print(f"\nListo. Total de libros recopilados: {len(all_books)}")
Puedes detectar la última página comprobando si la lista de resultados está vacía (no se encuentran elementos tr[itemtype="http://schema.org/Book"]) o verificando que no exista el enlace "next" (a.next_page).
Caso especial: se requiere login a partir de la página 5
Esta es la trampa en la que cae casi todo el mundo: algunas páginas de estanterías y listas de Goodreads devuelven silenciosamente el contenido de la página 1 cuando pides la página 6 o superior sin autenticación. Sin error, sin redirección: solo los mismos datos repetidos.
Para solucionarlo, exporta la cookie _session_id2 desde tu navegador (usa una extensión para exportar cookies o Chrome DevTools > Application > Cookies) e inclúyela en tus peticiones:
1session = requests.Session()
2session.headers.update(headers)
3session.cookies.set("_session_id2", "YOUR_SESSION_COOKIE_VALUE_HERE", domain=".goodreads.com")
4# Ahora usa session.get() en lugar de requests.get()
5resp = session.get(f"{base_url}?page=6", timeout=15)
Thunderbit gestiona de forma nativa tanto la paginación por clics como la de scroll infinito, sin código ni gestión de cookies. Si tu lógica de paginación sigue fallando, merece la pena considerarlo.
Script completo de Python listo para copiar y pegar
Aquí tienes el script completo y unificado. Maneja headers, paginación, scraping de subpáginas vía JSON-LD, limitación de velocidad y exportación a CSV. Lo he probado con páginas reales de Goodreads a mediados de 2025.
1"""
2goodreads_scraper.py — Extrae una lista de Goodreads con paginación y enriquecimiento de detalles.
3Uso: python goodreads_scraper.py
4Salida: goodreads_books.csv
5"""
6import csv, json, time, requests
7from bs4 import BeautifulSoup
8HEADERS = {
9 "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
10 "(KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
11 "Accept-Language": "en-US,en;q=0.9",
12 "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
13}
14BASE_URL = "https://www.goodreads.com/list/show/1.Best_Books_Ever"
15MAX_PAGES = 3 # ajusta según necesites
16DELAY_LISTING = 5 # segundos entre páginas de listado
17DELAY_DETAIL = 4 # segundos entre páginas de detalle
18OUTPUT_FILE = "goodreads_books.csv"
19def scrape_listing_page(url):
20 """Devuelve una lista de dicts con title, author, book_url de una página de listado."""
21 resp = requests.get(url, headers=HEADERS, timeout=15)
22 if resp.status_code != 200:
23 return []
24 soup = BeautifulSoup(resp.text, "lxml")
25 rows = soup.select('tr[itemtype="http://schema.org/Book"]')
26 books = []
27 for row in rows:
28 t = row.select_one("a.bookTitle")
29 a = row.select_one("a.authorName")
30 if t:
31 books.append({
32 "title": t.get_text(strip=True),
33 "author": a.get_text(strip=True) if a else "",
34 "book_url": "https://www.goodreads.com" + t["href"],
35 })
36 return books
37def scrape_book_detail(book_url):
38 """Visita una página de libro y extrae metadatos vía JSON-LD + respaldo HTML."""
39 resp = requests.get(book_url, headers=HEADERS, timeout=15)
40 if resp.status_code != 200:
41 return {}
42 soup = BeautifulSoup(resp.text, "lxml")
43 script = soup.find("script", {"type": "application/ld+json"})
44 if not script:
45 return {}
46 data = json.loads(script.string)
47 agg = data.get("aggregateRating", {})
48 genres = [g.get_text(strip=True)
49 for g in soup.select("span.BookPageMetadataSection__genreButton a span")]
50 return {
51 "isbn": data.get("isbn", ""),
52 "pages": data.get("numberOfPages", ""),
53 "avg_rating": agg.get("ratingValue", ""),
54 "rating_count": agg.get("ratingCount", ""),
55 "review_count": agg.get("reviewCount", ""),
56 "description": (data.get("description", "") or "")[:300],
57 "genres": ", ".join(genres[:5]),
58 "language": data.get("inLanguage", ""),
59 "format": data.get("bookFormat", ""),
60 "published": data.get("datePublished", ""),
61 }
62def main():
63 all_books = []
64 # --- Paso 1: recopilar URLs de libros desde páginas de listado ---
65 for page in range(1, MAX_PAGES + 1):
66 url = f"{BASE_URL}?page={page}"
67 page_books = scrape_listing_page(url)
68 if not page_books:
69 print(f"Página {page}: vacía — deteniendo la paginación.")
70 break
71 all_books.extend(page_books)
72 print(f"Página {page}: {len(page_books)} libros (total: {len(all_books)})")
73 time.sleep(DELAY_LISTING)
74 # --- Paso 2: enriquecer cada libro con datos de su página de detalle ---
75 for i, book in enumerate(all_books):
76 details = scrape_book_detail(book["book_url"])
77 book.update(details)
78 print(f"[{i+1}/{len(all_books)}] {book['title']} — ISBN: {book.get('isbn', 'N/A')}")
79 time.sleep(DELAY_DETAIL)
80 # --- Exportar a CSV ---
81 if all_books:
82 fieldnames = list(all_books[0].keys())
83 with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as f:
84 writer = csv.DictWriter(f, fieldnames=fieldnames)
85 writer.writeheader()
86 writer.writerows(all_books)
87 print(f"\nGuardados {len(all_books)} libros en {OUTPUT_FILE}")
88 else:
89 print("No se extrajeron libros.")
90if __name__ == "__main__":
91 main()
Con MAX_PAGES = 3, este script recopila unos 300 libros de la lista "Best Books Ever", visita la página de detalle de cada libro y lo guarda todo en un CSV. En mi equipo, tarda aproximadamente 25 minutos (principalmente por las pausas de 4 segundos entre peticiones a páginas de detalle). Tu CSV de salida tendrá columnas como title, author, book_url, isbn, pages, avg_rating, rating_count, review_count, description, genres, language, format y published.
Exportar más allá de CSV: Google Sheets con gspread
Si quieres los datos en Google Sheets en lugar de, o además de, un CSV, añade esto después de la exportación:
1import gspread
2from oauth2client.service_account import ServiceAccountCredentials
3scope = ["https://spreadsheets.google.com/feeds", "https://www.googleapis.com/auth/drive"]
4creds = ServiceAccountCredentials.from_json_keyfile_name("credentials.json", scope)
5client = gspread.authorize(creds)
6sheet = client.open("Goodreads Scrape").sheet1
7header = list(all_books[0].keys())
8sheet.append_row(header)
9for book in all_books:
10 sheet.append_row([str(book.get(k, "")) for k in header])
11print("Datos enviados a Google Sheets.")
Necesitarás una cuenta de servicio de Google Cloud con las APIs de Sheets y Drive activadas. La explica la configuración en unos 5 minutos. Usa operaciones por lotes (append_rows() con una lista de listas) si vas a subir más de unos pocos cientos de filas; el límite de Google es de 300 solicitudes por 60 segundos y proyecto.
Por supuesto, si toda esta configuración te parece excesiva, Thunderbit exporta a Google Sheets, Airtable, Notion, Excel, CSV y JSON con —sin instalar bibliotecas, sin archivo de credenciales y sin cuotas de API.
La alternativa sin código: extraer Goodreads con Thunderbit
No todo el mundo quiere mantener un script de Python. Tal vez eres un editor haciendo un análisis puntual de mercado, o un bloguero literario que solo quiere una hoja de cálculo con los bestsellers del año. Justo para eso creamos Thunderbit.
Cómo extraer Goodreads con Thunderbit
- Instala la extensión de Chrome de Thunderbit desde la y abre una página de listas, estanterías o resultados de búsqueda de Goodreads.
- Haz clic en "AI Suggest Fields" en la barra lateral de Thunderbit. La IA lee la página y sugiere columnas —normalmente título, autor, valoración, URL de la portada y enlace del libro.
- Haz clic en "Scrape" —los datos se extraen en una tabla estructurada en segundos.
- Exporta a Google Sheets, Excel, Airtable, Notion, CSV o JSON.
Para datos detallados del libro (ISBN, descripción, géneros, número de páginas), la función de scraping de subpáginas de Thunderbit visita cada página de detalle y enriquece la tabla automáticamente —sin bucles, sin pausas, sin depuración.
Thunderbit también gestiona listas paginadas de forma nativa. Solo le indicas que haga clic en "Next" o que haga scroll, y recopila datos de todas las páginas sin escribir código.
La diferencia es sencilla: el script en Python te da control total y es gratis, salvo el tiempo que inviertas; Thunderbit sacrifica algo de flexibilidad a cambio de un enorme ahorro de tiempo y cero mantenimiento. Para una lista de 300 libros, el script en Python tarda unos 25 minutos de ejecución, más el tiempo que hayas dedicado a escribirlo y depurarlo. Thunderbit obtiene los mismos datos en unos 3 minutos, con dos clics.
Extraer Goodreads de forma responsable: robots.txt, Términos de servicio y ética
Esto merece una respuesta clara, no un párrafo de aviso genérico.
Qué dice realmente el robots.txt de Goodreads
El robots.txt público de Goodreads es sorprendentemente específico. Las páginas de detalle de libros (/book/show/), listas públicas (/list/show/), estanterías públicas (/shelf/show/) y páginas de autores (/author/show/) no están bloqueadas. Lo que sí está bloqueado: /api, /book/reviews/, /review/list, /review/show, /search y varias rutas más. GPTBot y CCBot (Common Crawl) están completamente bloqueados con Disallow: /. Hay una directiva Crawl-delay: 5 para bingbot, pero no un retraso global.
Los Términos de servicio de Goodreads, en lenguaje claro
Los Términos de servicio (revisados por última vez el 28 de abril de 2021) prohíben "cualquier uso de data mining, robots o herramientas similares de recopilación y extracción de datos". Es una redacción amplia y merece tomarse en serio; sin embargo, los tribunales han sostenido de forma consistente que violar unos Términos de servicio no equivale por sí solo a un acceso no autorizado de carácter penal. La resolución afirmó que "criminalizar las violaciones de los términos de servicio corre el riesgo de convertir cada sitio web en su propia jurisdicción penal".
Buenas prácticas
- Introduce pausas entre peticiones: 3-8 segundos (el propio robots.txt de Goodreads sugiere 5 segundos para bots)
- Mantente por debajo de 5.000 peticiones al día desde una sola IP
- Extrae solo páginas públicas —evita grandes volúmenes de datos que solo estén disponibles tras login
- No redistribuyas comercialmente el texto bruto de las reseñas —las reseñas son obras creativas protegidas por copyright
- Guarda solo lo que necesites y define una política de retención de datos
- Uso personal/investigación vs. uso comercial: extraer datos públicos para análisis personal o investigación académica suele aceptarse. El riesgo legal aumenta cuando hay redistribución comercial.
Usar una herramienta como Thunderbit (que extrae datos a través de tu propia sesión de navegador) mantiene la interacción visualmente igual que una navegación normal, pero los mismos principios éticos aplican sin importar la herramienta que elijas. Si quieres profundizar en las , hemos tratado ese tema por separado.
Consejos y errores comunes
Consejo: empieza siempre por JSON-LD. Antes de escribir selectores CSS complejos, comprueba si los datos que necesitas están dentro de la etiqueta <script type="application/ld+json">. Es más estable, más fácil de analizar y menos propenso a romperse cuando Goodreads actualiza su frontend.
Consejo: usa la estrategia de dos pasadas. Primero recoge todas las URLs de libros desde las páginas de listado y después visita cada página de detalle. Esto hace que tu scraper sea más fácil de reanudar si falla a mitad del proceso, y puedes guardar la lista de URLs en disco como punto de control.
Error común: no manejar campos faltantes. No todas las páginas de libros tienen ISBN, géneros o descripción. Usa siempre .get() con un valor por defecto, o envuelve los selectores en comprobaciones if. Un solo error NoneType puede tumbar una ejecución de scraping de 3 horas.
Error común: ir demasiado rápido. Sé que da tentación poner time.sleep(0.5) y avanzar a toda velocidad. Pero Goodreads empezará a devolver 403 tras unas 20-30 peticiones rápidas, y una vez marcado, quizá tengas que esperar horas o cambiar de IP. Un retraso de 4-5 segundos suele ser el punto óptimo.
Error común: confiar en tutoriales antiguos. Si una guía menciona la API de Goodreads o usa clases como .field.value o #bookTitle, probablemente esté desactualizada. Verifica siempre los selectores en la página real antes de construir tu scraper.
Para saber más sobre cómo elegir las herramientas y frameworks de scraping adecuados, consulta nuestras guías sobre y .
Conclusión y puntos clave
Extraer datos de Goodreads con Python es totalmente posible; solo necesitas saber dónde están las trampas. Resumen rápido:
- La API de Goodreads desapareció (desde diciembre de 2020). El scraping es la forma principal de obtener datos estructurados de libros en la plataforma.
- Los resultados vacíos casi siempre se deben a contenido renderizado por JS, selectores obsoletos, headers faltantes o problemas de autenticación en la paginación; no significa que tu código esté mal.
- JSON-LD es tu mejor aliado para metadatos de libros. Es estable, estructurado y cambia muy poco.
- La paginación requiere autenticación en muchas páginas de estanterías y listas a partir de la página 5. Incluye la cookie
_session_id2. - El rate limiting es real. Usa pausas de 3-8 segundos y no superes 5.000 peticiones al día.
- La estrategia de dos pasadas (primero URLs, luego páginas de detalle) es más fiable y fácil de reanudar.
- Para quienes no programan (o para cualquiera que valore su tarde), resuelve todo esto —renderizado JS, paginación, enriquecimiento de subpáginas y exportación— en unos dos clics.
Extrae con responsabilidad, respeta el robots.txt y ojalá tus datos de libros vuelvan siempre con algo más que [].
Preguntas frecuentes
¿Todavía se puede usar la API de Goodreads?
No. Goodreads descontinuó su API pública en diciembre de 2020 y ya no emite nuevas claves de desarrollador. Las claves existentes que estuvieron inactivas durante 30 días se desactivaron automáticamente. Hoy, las opciones para acceder a datos de libros de forma programática son el web scraping o APIs alternativas como Open Library o Google Books.
¿Por qué mi scraper de Goodreads devuelve resultados vacíos?
La causa más común es el contenido renderizado por JavaScript. Goodreads carga reseñas, distribuciones de valoraciones y muchas secciones de detalle mediante React/JavaScript, algo que una simple llamada requests.get() no puede ver. Para esas páginas, cambia a Selenium o Playwright. Otras causas incluyen selectores CSS obsoletos (Goodreads cambió el HTML), falta de headers User-Agent (lo que provoca bloqueos 403) o peticiones no autenticadas en páginas de estanterías paginadas.
¿Es legal extraer datos de Goodreads?
Extraer datos públicamente disponibles para uso personal o de investigación suele aceptarse según precedentes legales actuales (hiQ v. LinkedIn, Meta v. Bright Data). Sin embargo, los Términos de servicio de Goodreads prohíben la recopilación automatizada de datos, y siempre deberías revisar su robots.txt. Evita redistribuir comercialmente texto de reseñas con copyright y limita el volumen de solicitudes para no sobrecargar el sitio.
¿Cómo extraigo varias páginas en Goodreads?
Añade ?page=N a la URL de la lista o estantería y recorre los números de página en un bucle. Comprueba si no hay resultados o si falta el enlace "next" para detectar la última página. Importante: algunas páginas de estanterías requieren autenticación (la cookie _session_id2) para devolver resultados más allá de la página 5; sin ella, recibirás silenciosamente la página 1 repetida.
¿Puedo extraer Goodreads sin escribir código?
Sí. es una extensión de Chrome que te permite extraer Goodreads en dos clics: la IA sugiere los campos de datos, tú haces clic en "Scrape" y exportas directamente a Google Sheets, Excel, Airtable o Notion. Maneja automáticamente contenido renderizado por JavaScript, paginación y enriquecimiento de subpáginas, sin necesidad de Python ni programación.
Más información