Web Scraping con Python: BeautifulSoup y requests paso a paso
Web scraping es la técnica de extraer datos de páginas web de forma automática. En Python, la combinación más sencilla es requests (descarga HTML) + BeautifulSoup4 (parsea HTML).
1. Instalación y primera petición
pip install requests beautifulsoup4 lxml
import requests
from bs4 import BeautifulSoup
URL = "https://books.toscrape.com/"
headers = {
"User-Agent": "Mozilla/5.0 (compatible; MiScraper/1.0)"
}
r = requests.get(URL, headers=headers, timeout=10)
r.raise_for_status() # Lanza excepción si hay error HTTP
soup = BeautifulSoup(r.text, "lxml")
print(soup.title.text) # Books to Scrape. We love being scraped!
2. Selectores: find, find_all y CSS selectors
# find: primer elemento
primer_h1 = soup.find("h1")
print(primer_h1.text)
# find_all: todos los elementos
links = soup.find_all("a", href=True)
for link in links[:5]:
print(link["href"], link.text.strip())
# Filtrar por clase CSS
articulos = soup.find_all("article", class_="product_pod")
print(f"Libros en portada: {len(articulos)}")
# CSS selectors con select()
titulos = soup.select("article.product_pod h3 a")
for t in titulos[:5]:
print(t["title"]) # Atributo title completo
precios = soup.select("p.price_color")
for p in precios[:5]:
print(p.text.strip()) # £51.77
3. Extraer datos estructurados de una página
import requests
from bs4 import BeautifulSoup
def extraer_libros(url):
r = requests.get(url, timeout=10)
soup = BeautifulSoup(r.text, "lxml")
libros = []
for art in soup.select("article.product_pod"):
titulo = art.select_one("h3 a")["title"]
precio = art.select_one("p.price_color").text.strip()
rating = art.select_one("p.star-rating")["class"][1] # "One","Two"...
stock = art.select_one("p.availability").text.strip()
enlace = art.select_one("h3 a")["href"]
libros.append({
"titulo": titulo,
"precio": precio,
"rating": rating,
"stock": stock,
"url": f"https://books.toscrape.com/{enlace}",
})
return libros
libros = extraer_libros("https://books.toscrape.com/")
for libro in libros[:3]:
print(libro)
4. Paginación: recorrer múltiples páginas
import requests
from bs4 import BeautifulSoup
import time
BASE = "https://books.toscrape.com/catalogue/"
todos_los_libros = []
pagina = 1
while True:
url = f"{BASE}page-{pagina}.html"
r = requests.get(url, timeout=10)
if r.status_code == 404:
print(f"Fin en página {pagina - 1}")
break
soup = BeautifulSoup(r.text, "lxml")
for art in soup.select("article.product_pod"):
todos_los_libros.append({
"titulo": art.select_one("h3 a")["title"],
"precio": art.select_one("p.price_color").text.strip(),
})
print(f"Página {pagina}: {len(todos_los_libros)} libros acumulados")
pagina += 1
time.sleep(1) # Pausa entre peticiones — sé amable con el servidor
print(f"Total libros: {len(todos_los_libros)}")
5. Extraer tablas HTML
import requests
from bs4 import BeautifulSoup
# Ejemplo: tabla de información de un libro
url = "https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"
r = requests.get(url, timeout=10)
soup = BeautifulSoup(r.text, "lxml")
tabla = soup.select_one("table.table-striped")
datos = {}
for fila in tabla.find_all("tr"):
clave = fila.find("th").text.strip()
valor = fila.find("td").text.strip()
datos[clave] = valor
print(datos)
# {'UPC': 'a897fe39b1053632', 'Product Type': 'Books', 'Price (excl. tax)': '£51.77', ...}
6. Descargar imágenes
import requests
from bs4 import BeautifulSoup
from pathlib import Path
import time
def descargar_imagen(url, destino: Path):
r = requests.get(url, timeout=15, stream=True)
r.raise_for_status()
with open(destino, "wb") as f:
for chunk in r.iter_content(8192):
f.write(chunk)
print(f"Guardada: {destino.name}")
BASE = "https://books.toscrape.com"
r = requests.get(BASE, timeout=10)
soup = BeautifulSoup(r.text, "lxml")
carpeta = Path("imagenes_libros")
carpeta.mkdir(exist_ok=True)
for i, img in enumerate(soup.select("img"), 1):
src = img["src"].replace("../", "")
url = f"{BASE}/{src}"
nombre = Path(src).name
descargar_imagen(url, carpeta / nombre)
time.sleep(0.5)
if i >= 5:
break
7. Sesiones y cookies
import requests
from bs4 import BeautifulSoup
# Usar Session para mantener cookies y reutilizar conexiones
session = requests.Session()
session.headers.update({
"User-Agent": "Mozilla/5.0 (compatible; MiScraper/1.0)",
"Accept-Language": "es-ES,es;q=0.9",
})
# Login con formulario (ejemplo genérico)
LOGIN_URL = "https://ejemplo.com/login"
# Primero obtener token CSRF
r = session.get(LOGIN_URL)
soup = BeautifulSoup(r.text, "lxml")
csrf = soup.find("input", {"name": "_token"})["value"]
# Enviar formulario
session.post(LOGIN_URL, data={
"_token": csrf,
"email": "usuario@ejemplo.com",
"password": "contraseña",
})
# Ahora puedes acceder a páginas protegidas
r2 = session.get("https://ejemplo.com/dashboard")
print(r2.status_code)
8. Manejar JavaScript con Playwright
Para sitios con contenido cargado por JavaScript, usa Playwright:
pip install playwright
playwright install chromium
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page()
page.goto("https://quotes.toscrape.com/js/")
page.wait_for_selector(".quote") # Esperar a que el JS cargue
citas = page.query_selector_all(".quote")
for cita in citas[:3]:
texto = cita.query_selector(".text").inner_text()
autor = cita.query_selector(".author").inner_text()
print(f'"{texto}" — {autor}')
browser.close()
9. Guardar datos en CSV
import csv
import requests
from bs4 import BeautifulSoup
from pathlib import Path
def scrape_libros_a_csv(num_paginas=3):
libros = []
BASE = "https://books.toscrape.com/catalogue/page-{}.html"
for pagina in range(1, num_paginas + 1):
r = requests.get(BASE.format(pagina), timeout=10)
soup = BeautifulSoup(r.text, "lxml")
for art in soup.select("article.product_pod"):
libros.append({
"titulo": art.select_one("h3 a")["title"],
"precio": art.select_one("p.price_color").text.strip(),
"rating": art.select_one("p.star-rating")["class"][1],
})
archivo = Path("libros.csv")
with open(archivo, "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["titulo", "precio", "rating"])
writer.writeheader()
writer.writerows(libros)
print(f"Guardados {len(libros)} libros en {archivo}")
scrape_libros_a_csv()
10. Buenas prácticas y ética
- Revisa robots.txt:
https://sitio.com/robots.txtindica qué rutas están permitidas para scraping. - Respeta el rate limiting: añade
time.sleep(1)entre peticiones para no saturar el servidor. - Cabecera User-Agent: identifica tu scraper de forma honesta.
- Usa caché local: guarda HTML descargado para no repetir peticiones durante el desarrollo.
- No hagas scraping de datos personales ni de sitios que lo prohíban explícitamente en sus Términos de Servicio.
- Maneja errores: usa
try/exceptyr.raise_for_status()para gestionar errores HTTP.
import requests
def get_seguro(url, reintentos=3):
for intento in range(reintentos):
try:
r = requests.get(url, timeout=10)
r.raise_for_status()
return r
except requests.exceptions.HTTPError as e:
print(f"HTTP {e.response.status_code}: {url}")
break
except requests.exceptions.ConnectionError:
print(f"Intento {intento+1}/{reintentos}: sin conexión")
time.sleep(2 ** intento) # Backoff exponencial
return None
Resumen de herramientas
| Herramienta | Uso | Maneja JS |
|---|---|---|
requests |
Descarga HTML/JSON | No |
BeautifulSoup4 |
Parsea HTML | No |
lxml |
Parser rápido de HTML/XML | No |
httpx |
Alternativa async a requests | No |
Playwright |
Automatización de navegador | Sí |
Selenium |
Automatización de navegador | Sí |
Scrapy |
Framework scraping completo | No (básico) |
Conversiones relacionadas
Conversiones frecuentes del catálogo: