Lección 15 de 16 · Nivel 4: Experto
Web scraping con Python: requests y BeautifulSoup paso a paso
- ⏱ 9 min de lectura
- ❓ 3 preguntas de repaso
- ✍️ Ejercicio con solución
En esta lección
- Saber cuándo el web scraping es legal y ético, y cuándo no.
- Descargar una página con requests y extraer datos con BeautifulSoup y selectores CSS.
- Recorrer varias páginas de forma responsable y guardar los datos en CSV.
- Saber qué hacer con páginas que cargan sus datos con JavaScript.
Muchos datos útiles no vienen en un archivo ni tienen una API: precios de un catálogo, ofertas de empleo, horarios, listados públicos. El web scraping es la técnica de extraer esa información de las páginas web con un programa. Es una habilidad muy valorada en análisis de datos, investigación de mercado y automatización, y con lo que ya sabes de requests (lección 12) estás a un paso.
Antes de empezar: las reglas
Que una página sea pública no significa que puedas hacer cualquier cosa con sus datos. Antes de extraer datos de un sitio:
- Busca primero una API o una descarga oficial. Si existe, úsala: es más estable y está permitida (lección 12).
- Lee los términos de uso y el archivo
robots.txtdel sitio (por ejemplo,https://sitio.com/robots.txt), que indica qué partes no quieren que recorran los programas. - No sobrecargues el servidor: haz pausas entre peticiones y descarga solo lo que necesitas.
- Cuidado con los datos personales: nombres, correos o teléfonos de personas están protegidos por las leyes de protección de datos, aunque estén publicados.
- No te saltes barreras: inicios de sesión, muros de pago o sistemas anti-bots indican que el sitio no autoriza el acceso automatizado.
Para practicar usaremos books.toscrape.com, una librería ficticia creada precisamente para aprender scraping: 1.000 libros repartidos en 50 páginas, con permiso explícito para practicar.
Instalar y descargar la página
pip install requests beautifulsoup4 pandas
import requests
from bs4 import BeautifulSoup
URL = "https://books.toscrape.com/"
cabeceras = {"User-Agent": "curso-python-scraper/1.0 (practica educativa)"}
resp = requests.get(URL, headers=cabeceras, timeout=10)
resp.raise_for_status() # error si la respuesta no es 200
soup = BeautifulSoup(resp.content, "html.parser")
print(soup.title.get_text(strip=True))
All products | Books to Scrape - Sandbox
Pasamos resp.content (los bytes) y no resp.text para que BeautifulSoup detecte la codificación de la página. Con resp.text, a veces el símbolo £ aparece como "£".
Encontrar los datos: inspeccionar el HTML
En tu navegador, haz clic derecho sobre el título de un libro y elige Inspeccionar. Verás que cada libro tiene esta estructura (simplificada):
<article class="product_pod">
<p class="star-rating Three"></p>
<h3><a href="catalogue/a-light-in-the-attic_1000/index.html"
title="A Light in the Attic">A Light in the ...</a></h3>
<div class="product_price">
<p class="price_color">£51.77</p>
<p class="instock availability">In stock</p>
</div>
</article>
Con esa estructura escribes selectores CSS, los mismos que se usan para dar estilo a una web:
| Selector | Selecciona |
|---|---|
article.product_pod | Cada etiqueta article con la clase product_pod (un libro) |
p.price_color | El párrafo del precio |
h3 a | El enlace dentro del h3 |
li.next a | El enlace a la página siguiente |
En BeautifulSoup, soup.select(selector) devuelve una lista con todos los elementos que coinciden y soup.select_one(selector), solo el primero.
libros = soup.select("article.product_pod")
print(len(libros))
primero = libros[0]
print(primero.h3.a["title"]) # atributo title
print(primero.select_one("p.price_color").get_text(strip=True))
20
A Light in the Attic
£51.77
1. El título completo de cada libro está en el atributo title del enlace, mientras que el texto visible aparece cortado ("A Light in the ..."). ¿Cómo obtienes el título completo?
Los atributos de una etiqueta se leen como en un diccionario. get_text() devuelve solo el texto visible, que aquí está recortado.
Una función que extrae todos los libros de una página
from urllib.parse import urljoin
ESTRELLAS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
def extraer_libros(soup, url_pagina):
libros = []
for art in soup.select("article.product_pod"):
enlace = art.h3.a
precio_txt = art.select_one("p.price_color").get_text(strip=True)
clases = art.select_one("p.star-rating")["class"] # ["star-rating", "Three"]
libros.append({
"titulo": enlace["title"],
"precio": float(precio_txt.replace("£", "")),
"estrellas": ESTRELLAS.get(clases[1], 0),
"disponible": "In stock" in art.select_one("p.availability").get_text(),
"url": urljoin(url_pagina, enlace["href"]),
})
return libros
Dos detalles profesionales: convertimos el precio en número (para poder calcular) y usamos urljoin para transformar los enlaces relativos (catalogue/…) en direcciones completas.
Recorrer varias páginas con responsabilidad
import time
import pandas as pd
def scrapear(url_inicio, max_paginas=3, pausa=1.0):
todos, url, pagina = [], url_inicio, 0
with requests.Session() as sesion:
sesion.headers.update(cabeceras)
while url and pagina < max_paginas:
resp = sesion.get(url, timeout=10)
resp.raise_for_status()
soup = BeautifulSoup(resp.content, "html.parser")
todos.extend(extraer_libros(soup, url))
pagina += 1
siguiente = soup.select_one("li.next a")
url = urljoin(url, siguiente["href"]) if siguiente else None
time.sleep(pausa) # no saturar el servidor
return todos
libros = scrapear("https://books.toscrape.com/", max_paginas=3)
df = pd.DataFrame(libros)
print(len(df), "libros")
df.to_csv("libros.csv", index=False, encoding="utf-8")
60 libros
requests.Session()reutiliza la conexión: es más rápido y más amable con el servidor.max_paginasevita que un error en la paginación te deje descargando para siempre.time.sleep(pausa)espera entre páginas. Una pausa de uno o dos segundos es una cortesía básica.
Ya tienes un DataFrame: aplica lo que aprendiste en la lección 11. Por ejemplo, el precio medio según la valoración: df.groupby("estrellas")["precio"].mean().round(2), o los 5 libros más caros: df.nlargest(5, "precio")[["titulo", "precio"]].
2. ¿Para qué sirve time.sleep(1) dentro del bucle del scraper?
Muchas peticiones seguidas pueden saturar un sitio pequeño y hacer que bloqueen tu IP. Las pausas son parte de un scraping responsable.
Cuando la página se arma con JavaScript
Algunos sitios envían una página casi vacía y luego cargan los datos con JavaScript. Si haces requests.get y no encuentras los datos que ves en el navegador, ese es el motivo. Opciones, en orden de preferencia:
- Busca la API interna: en las herramientas del navegador, pestaña Red (Network), filtra por Fetch/XHR y recarga. Muchas veces los datos llegan en un JSON que puedes pedir directamente con requests, más limpio que el HTML.
- Usa un navegador automatizado como Playwright o Selenium, que ejecutan el JavaScript como un navegador real. Son más lentos y pesados: úsalos solo si no hay otra opción.
Si el sitio cambia su diseño, tus selectores dejarán de encontrar los datos. Comprueba siempre que extraes lo esperado (por ejemplo, que cada página dé 20 libros) y haz que tu programa avise con un error claro si no es así, en lugar de guardar un CSV vacío sin decir nada.
3. Descargas una página con requests y la lista de productos que ves en el navegador no aparece en el HTML. ¿Qué es lo primero que revisas?
Si los datos se cargan con JavaScript, no están en el HTML inicial. Pedir directamente ese JSON suele ser la solución más simple y robusta.
Ejercicio práctico: libros baratos y bien valorados
- Scrapea las 5 primeras páginas de books.toscrape.com con una pausa de 1 segundo.
- Comprueba que obtuviste 100 libros y que ninguno tiene precio vacío.
- Filtra los libros con 4 o 5 estrellas y precio menor de 20 libras, ordénalos por precio y guárdalos en
recomendados.csv.
Ver solución
libros = scrapear("https://books.toscrape.com/", max_paginas=5, pausa=1.0)
df = pd.DataFrame(libros)
assert len(df) == 100, f"Se esperaban 100 libros y llegaron {len(df)}"
assert df["precio"].notna().all()
recomendados = (
df[(df["estrellas"] >= 4) & (df["precio"] < 20)]
.sort_values("precio")
[["titulo", "precio", "estrellas", "url"]]
)
recomendados.to_csv("recomendados.csv", index=False, encoding="utf-8")
print(len(recomendados), "libros recomendados")
print(recomendados.head())
assert detiene el programa con un mensaje claro si algo no cuadra. Es la forma más sencilla de saber que tu scraper se rompió antes de que alguien use datos incompletos.
Resumen
- Antes de scrapear: busca una API, lee los términos y el robots.txt, no sobrecargues el sitio y protege los datos personales.
- requests descarga la página; BeautifulSoup la analiza; los selectores CSS encuentran los datos.
- Convierte los textos en números, completa los enlaces con urljoin y valida lo que extraes.
- Recorre páginas con un límite y pausas, y guarda el resultado con pandas.
- Si los datos vienen por JavaScript, busca la API interna en la pestaña Red o usa Playwright.
En la próxima lección, la última del curso, darás tus primeros pasos en machine learning: entrenarás un modelo con scikit-learn para predecir ventas y aprenderás a evaluar si sus predicciones sirven.
Crea tu cuenta en 30 segundos, marca tus lecciones y descarga el certificado al terminar.