El web scraping es la recopilación automatizada de información publicada en páginas web. En lugar de copiar manualmente precios, titulares, enlaces o filas de una tabla, un programa puede descargar el HTML, localizar los elementos relevantes, limpiar los datos y guardarlos en un formato estructurado.
Esta guía explica web scraping con Python utilizando Requests y BeautifulSoup. Aprenderás a descargar páginas con límites de tiempo, inspeccionar HTML, seleccionar elementos, extraer atributos, seguir paginación, utilizar sesiones, controlar errores y exportar resultados.
Revisar las reglas antes de recopilar datos
Que una página sea visible públicamente no significa que cualquier recopilación esté permitida. Revisa los términos de servicio, la política de privacidad, las condiciones de copyright y el archivo robots.txt. Evita datos personales o sensibles y nunca intentes eludir autenticación, controles de acceso, CAPTCHAs o bloqueos técnicos.
Cuando exista una API oficial, normalmente es la opción más estable. Un scraper responsable utiliza un ritmo conservador y se detiene cuando el servidor indica limitación o rechazo.
Instalar Requests y BeautifulSoup
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS o Linux
source .venv/bin/activate
python -m pip install requests beautifulsoup4La guía de entornos virtuales en Python explica cómo aislar dependencias. Las referencias principales son la documentación oficial de Requests y la documentación oficial de Beautiful Soup.
Descargar una página
Utiliza un timeout y comprueba el estado HTTP:
import requests
url = "https://example.com"
response = requests.get(url, timeout=15)
response.raise_for_status()
print(response.status_code)
print(response.text[:200])raise_for_status() convierte respuestas de error en excepciones. El tutorial de Requests con Python explica headers, parámetros, JSON, sesiones y errores de red.
Utilizar un User-Agent honesto
headers = {
"User-Agent": "EducationalScraper/1.0 (contacto: [email protected])"
}
response = requests.get(
"https://example.com/products",
headers=headers,
timeout=15,
)
response.raise_for_status()Identifica la herramienta de manera responsable. No utilices headers falsos para intentar evadir restricciones.
Analizar el HTML
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, "html.parser")
print(soup.title)
print(soup.title.get_text(strip=True))html.parser está incluido con Python. Otros parsers pueden ser más rápidos o tolerantes, pero requieren paquetes adicionales.
Inspeccionar la estructura
Abre la página en el navegador, utiliza la herramienta de inspección y busca etiquetas, IDs, clases o atributos estables. Evita selectores basados en clases generadas automáticamente o posiciones muy profundas.
<article class="product-card" data-id="42">
<h2 class="product-name">Teclado mecánico</h2>
<span class="price">$89.90</span>
<a class="details" href="/products/42">Ver producto</a>
</article>Localizar elementos
card = soup.find("article", class_="product-card")
name = card.find("h2", class_="product-name")
print(name.get_text(strip=True))Con selectores CSS:
card = soup.select_one("article.product-card")
name = card.select_one(".product-name")
price = card.select_one(".price")select_one() devuelve el primer elemento o None. select() devuelve todos los elementos encontrados.
Extraer texto y atributos
from urllib.parse import urljoin
name = card.select_one(".product-name").get_text(" ", strip=True)
price_text = card.select_one(".price").get_text(" ", strip=True)
link_element = card.select_one("a.details")
product_url = urljoin(response.url, link_element.get("href"))
product_id = card.get("data-id")urljoin() convierte enlaces relativos en URLs absolutas. Usa get() para atributos opcionales.
Recorrer varias tarjetas
products = []
for card in soup.select("article.product-card"):
name_element = card.select_one(".product-name")
price_element = card.select_one(".price")
link_element = card.select_one("a.details")
if not name_element or not price_element:
continue
products.append({
"name": name_element.get_text(" ", strip=True),
"price_text": price_element.get_text(" ", strip=True),
"url": urljoin(
response.url,
link_element.get("href", "") if link_element else "",
),
})Comprobar elementos opcionales evita que una tarjeta incompleta detenga todo el proceso.
Limpiar precios
from decimal import Decimal, InvalidOperation
def parse_price(text: str) -> Decimal:
cleaned = text.replace("$", "").replace(",", "").strip()
try:
return Decimal(cleaned)
except InvalidOperation as error:
raise ValueError(f"Precio inválido: {text!r}") from errorAdapta la limpieza al formato regional del sitio. No asumas que la coma y el punto significan lo mismo en todos los países.
Utilizar una sesión
with requests.Session() as session:
session.headers.update(headers)
response = session.get(url, timeout=15)
response.raise_for_status()Una sesión reutiliza conexiones y headers. También puede conservar cookies cuando ese comportamiento esté permitido y sea necesario.
Reintentos limitados
from requests import Session
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
retry_policy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET"],
respect_retry_after_header=True,
)
session = Session()
session.mount("https://", HTTPAdapter(max_retries=retry_policy))Una respuesta 429 indica demasiadas solicitudes. Respeta Retry-After y reduce el ritmo.
Seguir paginación
from time import sleep
from urllib.parse import urljoin
next_url = "https://example.com/products"
all_products = []
max_pages = 5
for _ in range(max_pages):
if not next_url:
break
response = session.get(next_url, timeout=15)
response.raise_for_status()
soup = BeautifulSoup(response.text, "html.parser")
# Extraer tarjetas aquí.
next_link = soup.select_one("a.next-page")
next_url = (
urljoin(response.url, next_link.get("href"))
if next_link and next_link.get("href")
else None
)
if next_url:
sleep(2)Define un máximo durante el desarrollo para evitar ciclos inesperados.
Exportar a CSV
import csv
with open("products.csv", "w", newline="", encoding="utf-8") as file:
writer = csv.DictWriter(
file,
fieldnames=["name", "price", "url"],
)
writer.writeheader()
writer.writerows(products)La guía de archivos CSV con Python explica lectura, escritura y validación. Para análisis tabular, utiliza Pandas.
Exportar a JSON
import json
with open("products.json", "w", encoding="utf-8") as file:
json.dump(products, file, ensure_ascii=False, indent=2)JSON conserva mejor las estructuras anidadas y es práctico para aplicaciones y APIs.
Manejar errores de red
def fetch_soup(session, url):
try:
response = session.get(url, timeout=15)
response.raise_for_status()
except requests.Timeout as error:
raise RuntimeError(f"Tiempo agotado: {url}") from error
except requests.HTTPError as error:
raise RuntimeError(
f"Error HTTP {error.response.status_code}: {url}"
) from error
except requests.RequestException as error:
raise RuntimeError(f"Solicitud fallida: {url}") from error
return BeautifulSoup(response.text, "html.parser")La guía de excepciones en Python explica cómo conservar el contexto del error.
Páginas que dependen de JavaScript
Requests descarga la respuesta del servidor, pero algunos sitios construyen el contenido después con JavaScript. Comprueba el HTML recibido y el código fuente. Cuando exista una API pública documentada, úsala. Para automatización permitida de navegadores, consulta la guía de Selenium con Python.
Probar el parser
La estructura de una página puede cambiar. Separa descarga y análisis, guarda pequeñas muestras autorizadas de HTML y escribe pruebas para los selectores. El tutorial de Pytest ayuda a probar funciones sin contactar el sitio en cada ejecución.
Errores frecuentes
- No utilizar timeout.
- Ignorar códigos HTTP.
- Enviar solicitudes demasiado rápido.
- Suponer que todos los elementos existen.
- Utilizar selectores frágiles.
- Guardar texto sin limpiar como número.
- Recopilar datos personales sin base legítima.
- Intentar eludir autenticación o CAPTCHAs.
- Ignorar una API oficial disponible.
Conclusión
Un scraper responsable separa tres tareas: descargar páginas cuidadosamente, analizar y validar la información, y guardar resultados estructurados. Utiliza timeouts, comprobación de estado, sesiones, reintentos limitados, selectores estables y pausas respetuosas. Revisa siempre las reglas del sitio y prefiere APIs oficiales cuando existan.






