Muchos sitios publican precios, clasificaciones, calendarios, indicadores y catálogos dentro de tablas HTML. Copiar esos datos manualmente es lento y propenso a errores. Python y Pandas pueden detectar tablas, convertirlas en DataFrames, limpiar columnas y exportar los resultados a CSV o Excel. Sin embargo, no todas las páginas utilizan tablas HTML reales, y cualquier extracción debe respetar los términos del sitio, la privacidad y los límites de acceso.
Para seguir esta guía conviene conocer Pandas para principiantes, la biblioteca Requests en Python, el manejo de archivos CSV y los fundamentos de try y except. La extracción confiable combina descarga, validación, limpieza y pruebas.
Instalar las dependencias
python -m venv .venv
# Windows
.venv\Scripts\activate
# Linux o macOS
source .venv/bin/activate
python -m pip install pandas lxml requests beautifulsoup4 openpyxlpandas.read_html() puede utilizar analizadores como lxml. openpyxl es útil para exportar a Excel.
Leer todas las tablas de una página
import pandas as pd
url = "https://example.com/datos"
tablas = pd.read_html(url)
print(f"Tablas encontradas: {len(tablas)}")
for indice, tabla in enumerate(tablas):
print(indice, tabla.shape)read_html() devuelve una lista de DataFrames. Cada elemento corresponde a una tabla detectada. La documentación oficial de pandas.read_html explica parámetros como match, attrs, header y decimal.
Elegir una tabla concreta
No dependas únicamente de su posición, porque el sitio puede insertar otra tabla antes. Busca texto relevante:
tablas = pd.read_html(url, match="Población")
df = tablas[0]También puedes filtrar por atributos HTML:
tablas = pd.read_html(
url,
attrs={"id": "tabla-principal"}
)Inspecciona el código de la página para identificar un id o una clase estable.
Descargar primero con Requests
Para controlar encabezados, tiempo de espera y errores, descarga la página antes de analizarla:
import requests
from io import StringIO
respuesta = requests.get(
url,
timeout=20,
headers={"User-Agent": "MiAnalizador/1.0 [email protected]"}
)
respuesta.raise_for_status()
tablas = pd.read_html(StringIO(respuesta.text))Un identificador claro ayuda a los administradores a reconocer el tráfico. No finjas ser un navegador para evadir restricciones.
Limpiar encabezados de varias filas
Algunas tablas usan encabezados jerárquicos y producen columnas MultiIndex:
df.columns = [
"_".join(str(parte) for parte in columna if "Unnamed" not in str(parte))
if isinstance(columna, tuple)
else str(columna)
for columna in df.columns
]Después normaliza:
df.columns = (
pd.Index(df.columns)
.str.strip()
.str.lower()
.str.replace(" ", "_", regex=False)
)Eliminar filas repetidas de encabezado
Las tablas largas pueden repetir el encabezado dentro del cuerpo:
df = df[df["pais"] != "País"]La regla debe ser específica para no eliminar una fila legítima.
Convertir números
df["poblacion"] = (
df["poblacion"]
.astype("string")
.str.replace(".", "", regex=False)
.str.replace(",", ".", regex=False)
)
df["poblacion"] = pd.to_numeric(
df["poblacion"],
errors="coerce"
)Los separadores varían según el idioma. En algunos casos puedes utilizar los parámetros thousands y decimal durante la lectura.
Eliminar notas y símbolos
df["valor"] = (
df["valor"]
.astype("string")
.str.replace(r"\[\d+\]", "", regex=True)
.str.replace("%", "", regex=False)
.str.strip()
)Conserva el valor original en otra columna cuando los símbolos tengan significado.
Extraer enlaces junto con el texto
read_html() se concentra en el contenido visible. Si necesitas enlaces, utiliza Beautiful Soup. La documentación oficial de Beautiful Soup describe selectores y navegación del árbol HTML.
from bs4 import BeautifulSoup
sopa = BeautifulSoup(respuesta.text, "html.parser")
tabla_html = sopa.select_one("#tabla-principal")
for fila in tabla_html.select("tr"):
enlace = fila.select_one("a[href]")
if enlace:
print(enlace.get_text(strip=True), enlace.get("href"))Combinar Pandas y Beautiful Soup
Una estrategia útil es utilizar Pandas para los valores y Beautiful Soup para metadatos no incluidos en las celdas. Asegúrate de que el orden de las filas coincida antes de unir resultados.
Procesar varias páginas
resultados = []
for pagina in range(1, 6):
url_pagina = f"https://example.com/datos?page={pagina}"
respuesta = requests.get(url_pagina, timeout=20)
respuesta.raise_for_status()
tabla = pd.read_html(StringIO(respuesta.text))[0]
tabla["pagina_origen"] = pagina
resultados.append(tabla)
completo = pd.concat(resultados, ignore_index=True)Añade pausas y respeta los límites publicados. Una descarga masiva puede afectar al servidor.
Evitar duplicados entre páginas
completo = completo.drop_duplicates(
subset=["id"],
keep="last"
)Si no existe un identificador, crea una clave con varias columnas. No elimines duplicados basándote solo en el nombre cuando pueden existir entidades distintas.
Exportar a CSV y Excel
completo.to_csv(
"datos_web.csv",
index=False,
encoding="utf-8"
)
completo.to_excel(
"datos_web.xlsx",
index=False,
sheet_name="Datos"
)Incluye columnas con URL y fecha de extracción para mantener trazabilidad.
Guardar una copia del HTML
from datetime import datetime
from pathlib import Path
marca = datetime.now().strftime("%Y%m%d_%H%M%S")
Path(f"capturas/pagina_{marca}.html").write_text(
respuesta.text,
encoding="utf-8"
)Guardar el HTML ayuda a reproducir errores si el sitio cambia. Crea la carpeta previamente y considera el espacio de almacenamiento.
Páginas renderizadas con JavaScript
Si el HTML descargado no contiene la tabla, los datos pueden cargarse mediante JavaScript. Antes de usar un navegador automatizado, abre las herramientas de desarrollo y busca una solicitud JSON pública. Consumir un endpoint documentado suele ser más estable y eficiente que controlar un navegador.
Cuando sea imprescindible, herramientas como Selenium o Playwright pueden esperar a que la tabla aparezca. Esto consume más recursos y requiere más mantenimiento.
Detectar cambios de estructura
columnas_esperadas = {"pais", "valor", "fecha"}
actuales = set(df.columns)
if not columnas_esperadas.issubset(actuales):
raise ValueError(
f"La estructura cambió. Columnas: {sorted(actuales)}"
)Estas validaciones evitan exportar silenciosamente datos incorrectos cuando el sitio modifica encabezados.
Reintentos y errores temporales
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
sesion = requests.Session()
reintentos = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504]
)
sesion.mount("https://", HTTPAdapter(max_retries=reintentos))No repitas solicitudes indefinidamente. El código 429 indica que debes reducir la frecuencia.
Aspectos éticos y legales
Consulta los términos de uso, robots.txt y políticas de la fuente. No extraigas datos personales, contenido protegido o información detrás de autenticación sin permiso. Identifica tu aplicación, limita la velocidad y utiliza APIs oficiales cuando existan.
Errores frecuentes
Los fallos más comunes son asumir que la primera tabla siempre es la correcta, ignorar separadores numéricos, depender de clases CSS inestables, no establecer timeout, descargar demasiado rápido y no validar columnas. También es común confundir una tabla visual construida con div con una verdadera etiqueta table.
Conclusión
pandas.read_html() permite convertir tablas HTML en DataFrames con muy poco código. Requests añade control sobre la descarga y Beautiful Soup ayuda a recuperar enlaces y elementos específicos. Un extractor confiable valida la estructura, respeta límites, conserva la fuente y registra la fecha. Así puedes convertir tablas públicas en datos limpios y reproducibles sin depender de copiar y pegar manualmente.






