DATOS · 6 MIN DE LECTURA
Web scraping con Python: guía paso a paso con requests, BeautifulSoup y Playwright
Para hacer web scraping con Python, descargas la página con requests, buscas los datos con BeautifulSoup usando selectores CSS y los guardas en CSV. Si la página arma su contenido con JavaScript, la abres en un navegador real con Playwright. Antes de cualquiera de las dos, revisa si hay una API: muchas veces los datos ya vienen en JSON.
Respuesta corta: cómo hacer web scraping con Python
El web scraping tiene tres pasos: descargar la página, encontrar los datos dentro del HTML y guardarlos. En Python, requests descarga, BeautifulSoup busca y el módulo csv guarda. Cuando la página se arma con JavaScript en el navegador, requests recibe una página casi vacía y hace falta un navegador real controlado por código, como Playwright.
Todo el código de esta guía lo ejecuté el 19 de octubre de 2026 contra books.toscrape.com y quotes.toscrape.com, dos sitios creados para practicar scraping. Las salidas que ves son las reales.
Antes de escribir código: ¿deberías hacer scraping?
Cinco minutos de revisión te ahorran un proyecto que se cae o un problema legal:
- ¿Hay una API oficial o un archivo descargable? Si existe, úsalo: es más estable y tiene reglas claras.
- ¿Qué dicen los términos de uso del sitio? Algunos prohíben la extracción automática.
- ¿Qué permite su robots.txt? No es un candado, pero respetarlo es la práctica profesional.
- ¿Los datos son públicos y sin iniciar sesión? Si hay que entrar con usuario o saltar un captcha, detente.
- ¿Hay datos personales? Nombres, correos o teléfonos cambian por completo el análisis legal. En la guía sobre la legalidad del web scraping en México explico el detalle.
Qué necesitas instalar
Python 3.10 o más reciente. Para el nivel 1 bastan requests y BeautifulSoup; Playwright solo hace falta para páginas con JavaScript y descarga su propio navegador.
pip install requests beautifulsoup4
# Solo para el nivel 2:
pip install playwright
python -m playwright install chromiumNivel 1: páginas HTML con requests y BeautifulSoup
Primero abre la página en tu navegador, haz clic derecho sobre un dato y elige «Inspeccionar». Ahí ves qué etiqueta y qué clase tiene: en books.toscrape.com cada libro es un article con clase product_pod, el precio está en p.price_color y la calificación es una clase de p.star-rating. Esos selectores CSS son los que usa el script.
Este script recorre las primeras tres páginas del catálogo, saca título, precio, disponibilidad, estrellas y enlace de cada libro, y guarda todo en un CSV. Revisa robots.txt antes de empezar, se identifica con un User-Agent que dice quién eres y espera un segundo entre páginas.
"""Nivel 1: extrae título, precio, disponibilidad y calificación de los libros de books.toscrape.com."""
import csv
import time
from urllib.parse import urljoin
from urllib.robotparser import RobotFileParser
import requests
from bs4 import BeautifulSoup
INICIO = "https://books.toscrape.com/catalogue/page-1.html"
AGENTE = "PruebaScraping/1.0 (+https://daniel-pereda.dev/contacto)"
PAUSA = 1.0 # segundos entre páginas: no hay prisa y el sitio no es tuyo
ESTRELLAS = {"One": 1, "Two": 2, "Three": 3, "Four": 4, "Five": 5}
def permitido(url):
robots = RobotFileParser(urljoin(url, "/robots.txt"))
robots.read() # si no existe (404), todo está permitido
return robots.can_fetch(AGENTE, url)
def leer_pagina(sesion, url):
respuesta = sesion.get(url, timeout=15)
respuesta.raise_for_status()
respuesta.encoding = "utf-8"
sopa = BeautifulSoup(respuesta.text, "html.parser")
libros = []
for tarjeta in sopa.select("article.product_pod"):
libros.append({
"titulo": tarjeta.h3.a["title"],
"precio": tarjeta.select_one("p.price_color").get_text(strip=True).lstrip("£"),
"disponible": "In stock" in tarjeta.select_one("p.availability").get_text(),
"estrellas": ESTRELLAS[tarjeta.select_one("p.star-rating")["class"][1]],
"url": urljoin(url, tarjeta.h3.a["href"]),
})
siguiente = sopa.select_one("li.next a")
return libros, urljoin(url, siguiente["href"]) if siguiente else None
def main(max_paginas=3):
if not permitido(INICIO):
raise SystemExit("robots.txt no permite esta ruta")
sesion = requests.Session()
sesion.headers["User-Agent"] = AGENTE
url, todos = INICIO, []
for _ in range(max_paginas):
libros, url = leer_pagina(sesion, url)
todos.extend(libros)
print(f"{len(libros)} libros leídos; total {len(todos)}")
if url is None:
break
time.sleep(PAUSA)
with open("libros.csv", "w", newline="", encoding="utf-8") as archivo:
escritor = csv.DictWriter(archivo, fieldnames=todos[0].keys())
escritor.writeheader()
escritor.writerows(todos)
print("Guardado en libros.csv")
if __name__ == "__main__":
main()urljoinconvierte los enlaces relativos (page-2.html) en direcciones completas. Olvidarlo es el error más común al seguir la paginación.raise_for_status()detiene el script si el sitio responde con error, en lugar de guardar páginas vacías sin que te des cuenta.max_paginaspone un límite. Sin él, un error en la paginación puede dejar el script dando vueltas.- Los precios de este sitio de práctica están en libras esterlinas; por eso se quita el símbolo £.
Resultado del nivel 1
Salida real del script y las primeras filas del CSV:
python libros.py
20 libros leídos; total 20
20 libros leídos; total 40
20 libros leídos; total 60
Guardado en libros.csv
titulo,precio,disponible,estrellas,url
A Light in the Attic,51.77,True,3,https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html
Tipping the Velvet,53.74,True,1,https://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html¿Cómo saber si la página necesita un navegador?
Compara lo que ves en pantalla con lo que llega al descargar la página. En quotes.toscrape.com/js/ el navegador muestra 10 frases, pero el HTML que descarga requests no trae ningún div.quote: lo comprobé y el conteo da cero. Las frases las inserta el JavaScript después de cargar.
Antes de pasar a Playwright, abre las herramientas de desarrollador en la pestaña «Red» (Network), filtra por Fetch/XHR y recarga. Muchas páginas piden sus datos a una API interna en JSON. Si la encuentras, leer ese JSON es más rápido y estable que controlar un navegador. La versión con scroll infinito de quotes.toscrape.com, por ejemplo, carga sus frases desde /api/quotes:
import requests
respuesta = requests.get("https://quotes.toscrape.com/api/quotes", params={"page": 1}, timeout=15)
datos = respuesta.json()
for frase in datos["quotes"][:3]:
print(frase["author"]["name"], "-", frase["text"][:50])
print("¿Hay más páginas?", datos["has_next"])- Salida real: «Albert Einstein - “The world as we have created it is a process of o…», seguida de dos frases más y «¿Hay más páginas? True».
- Una API interna no es una API pública con contrato: puede cambiar sin aviso y sus términos siguen aplicando.
Nivel 2: páginas con JavaScript usando Playwright
Cuando no hay API a la vista, Playwright abre un Chromium sin ventana, espera a que el JavaScript pinte el contenido y te deja leerlo con los mismos selectores CSS. Este script lee dos páginas de quotes.toscrape.com/js/ y guarda texto, autor y etiquetas en JSON.
"""Nivel 2: la página arma su contenido con JavaScript, así que se abre en un navegador real con Playwright."""
import json
import time
from playwright.sync_api import sync_playwright
INICIO = "https://quotes.toscrape.com/js/"
AGENTE = "PruebaScraping/1.0 (+https://daniel-pereda.dev/contacto)"
def main(max_paginas=2):
frases = []
with sync_playwright() as p:
navegador = p.chromium.launch()
pagina = navegador.new_page(user_agent=AGENTE)
pagina.goto(INICIO)
for _ in range(max_paginas):
pagina.wait_for_selector("div.quote") # espera a que el JavaScript pinte las frases
for bloque in pagina.locator("div.quote").all():
frases.append({
"texto": bloque.locator("span.text").inner_text(),
"autor": bloque.locator("small.author").inner_text(),
"etiquetas": bloque.locator("a.tag").all_inner_texts(),
})
siguiente = pagina.locator("li.next a")
if siguiente.count() == 0:
break
siguiente.click()
time.sleep(1)
navegador.close()
with open("frases.json", "w", encoding="utf-8") as archivo:
json.dump(frases, archivo, ensure_ascii=False, indent=2)
print(f"{len(frases)} frases guardadas en frases.json")
if __name__ == "__main__":
main()wait_for_selectores la clave: espera a que existan las frases antes de leerlas. Sin esa línea, el script lee la página antes de tiempo y no encuentra nada.- Salida real: «20 frases guardadas en frases.json». La primera es de Albert Einstein, con las etiquetas change, deep-thoughts, thinking y world.
- Un navegador consume mucho más que una petición con requests. Úsalo solo cuando de verdad haga falta.
Dónde guardar los datos
Para empezar, CSV o JSON son suficientes y se abren en cualquier lado. Cuando el scraper corre todos los días, conviene una base de datos: SQLite viene con Python (import sqlite3) y te permite guardar el historial y consultar cambios, por ejemplo, cómo varió un precio a lo largo del mes. Si después vas a analizar los datos, pandas lee el CSV con una línea.
Ritmo, reintentos y buena educación
Un scraper mal hecho se parece mucho a un ataque. Estas reglas evitan bloqueos y problemas:
- Una petición a la vez y una pausa entre páginas. No pongas a correr diez hilos contra el sitio de una pyme.
- Un User-Agent que diga quién eres y cómo contactarte.
- Si el sitio responde 429 (demasiadas solicitudes) o 503, espera y reintenta con pausas cada vez más largas; si persiste, detente.
- Guarda en caché lo que ya descargaste para no pedirlo de nuevo en cada prueba.
- Corre los procesos grandes en horarios de poco tráfico.
- No saltes inicios de sesión, captchas ni bloqueos. Si el sitio te bloquea, es su respuesta.
Errores comunes al hacer web scraping con Python
- Selectores que se rompen cuando el sitio cambia su diseño. Prefiere atributos estables y deja pruebas que avisen cuando un campo llega vacío.
- Acentos mal leídos: si ves «Ã©» en lugar de «é», fija la codificación con
respuesta.encoding = "utf-8". - Enlaces relativos que no se resuelven: usa
urljoin. - Leer la página antes de que cargue el JavaScript: usa
wait_for_selectoren Playwright. - Guardar datos que no necesitas, sobre todo personales. Extrae solo lo que vas a usar.
Cuándo conviene contratar a alguien
Un script como el de esta guía resuelve una extracción puntual. Cuando el proceso tiene que correr todos los días, con varios sitios, alertas si algo cambia, historial y un reporte al final, el trabajo ya no es el scraper sino mantenerlo vivo. Ese es el tipo de proyecto que hago: empiezo revisando cada fuente (si hay API, qué permiten sus términos y su robots.txt) y descarto las que prohíben la extracción. Mi proyecto NLP Pipeline, que extrae y normaliza contenido web por lotes, nació de ese mismo proceso.