Scraping y datos públicos

Sesión 3 · viernes 2 de octubre de 2026

Diego A. Guerrero

Herramientas para el Nowcasting del Crecimiento del PIB · Bolivia

Tres maneras de traer un dato

cómo ejemplo en este taller
API se piden datos a un servicio que responde en formato estructurado FRED (metales, tasas de EE.UU.)
Archivo directo se descarga un .xlsx / .csv desde una URL fija EIA (Brent), US Census (comercio)
Página web se lee el HTML, se encuentra el enlace o la tabla y se descarga INE, BCB

Preferir en ese orden: una API es estable y documentada; una página web cambia sin aviso.

Un ejemplo completo

El PIB trimestral del INE, de la página al CSV

La receta

  1. Mirar la página
  2. Pedir la página
  3. Encontrar el enlace
  1. Descargar el archivo
  2. Ordenar la hoja
  3. Guardar y comprobar


www.ine.gob.bo/referencia2017/pib_trimestral.html

1 · Mirar la página

  • La página lista 24 cuadros, cada uno con un enlace de descarga
  • Clic derecho sobre el enlace → Inspeccionar
  • El enlace no termina en .xlsx: se reconoce por su texto
<a class="flex items-center text-red-700 ..." download=""
   href="https://nube.ine.gob.bo/index.php/s/1RFsK6XS7ZuMXaH/download">
  <svg> … </svg>
  <span>BOLIVIA: MEDIDAS DE VOLUMEN ENCADENADAS DEL
        PRODUCTO INTERNO BRUTO POR GRUPOS DE ACTIVIDAD
        ECONÓMICA SEGÚN TRIMESTRE, 2017 - 2025</span>
</a>

2 · Pedir la página

import requests

URL = "https://www.ine.gob.bo/referencia2017/pib_trimestral.html"
r = requests.get(URL, headers={"User-Agent": "Mozilla/5.0"}, timeout=60)

r.status_code, r.headers["content-type"], len(r.content)
# (200, 'text/html', 42746)
código significa
200 todo bien
403 el servidor rechaza al cliente: probar otro user-agent
404 la dirección ya no existe
500, 503 falla del servidor: esperar y reintentar una vez

3 · Encontrar el enlace

from bs4 import BeautifulSoup

sopa = BeautifulSoup(r.content, "lxml")
enlaces = [(a.get_text(" ", strip=True), a["href"])
           for a in sopa.find_all("a", href=True) if "/download" in a["href"]]
len(enlaces)   # 24
[ 1] BOLIVIA: PRODUCTO INTERNO BRUTO  POR GRUPOS DE ACTIVIDAD ECONÓMICA ...
[ 7] BOLIVIA: MEDIDAS DE VOLUMEN ENCADENADAS DEL PRODUCTO INTERNO BRUTO ...
[ 8] BOLIVIA: INDICES DE VOLUMEN ENCADENADOS DEL PRODUCTO INTERNO BRUTO ...
[ 9] BOLIVIA: VARIACIÓN DE LAS MEDIDAS DE VOLUMEN ENCADENADAS DEL PRODUCTO ...
[11] BOLIVIA: CONTRIBUCIÓN A LA VARIACIÓN DE LAS MEDIDAS DE VOLUMEN ...

Siete títulos contienen «volumen encadenado» y «producto». Buscar por palabras clave no basta.

3 · … por su título completo

import unicodedata

def clave(s):
    """Sin tildes, en minúsculas y con un solo espacio entre palabras."""
    s = unicodedata.normalize("NFKD", s).encode("ascii", "ignore").decode()
    return " ".join(s.lower().split())

buscado = clave("BOLIVIA: MEDIDAS DE VOLUMEN ENCADENADAS DEL PRODUCTO INTERNO BRUTO POR GRUPOS")
hits = [href for texto, href in enlaces if clave(texto).startswith(buscado)]
assert len(hits) == 1, "el INE cambió el título: revisar la lista"

El título del PIB nominal tiene dos espacios seguidos: "BRUTO POR". Una comparación literal no lo encuentra; clave() sí.

4 · Descargar y abrir sin interpretar

from io import BytesIO
import pandas as pd

archivo = requests.get(hits[0], headers={"User-Agent": "Mozilla/5.0"}, timeout=120)
archivo.headers["content-disposition"]      # ... filename="T_02.01.xlsx"

raw = pd.read_excel(BytesIO(archivo.content), header=None)
raw.shape                                   # (32, 38)
  • header=None: pandas no adivina encabezados; se ve la hoja tal como es
  • El nombre real del archivo viene en la respuesta, no en la URL

5 · La hoja tal como llega

5 · De la hoja a una serie

ROMANO = {"I": 3, "II": 6, "III": 9, "IV": 12}         # último mes del trimestre

etiq   = raw[1].astype(str).str.strip()
fila   = etiq[etiq.str.startswith("ACTIVIDAD")].index[0]           # fila 10 de Excel
anio   = raw.iloc[fila, 2:].ffill().astype(str).str[:4]            # '2025 (p)' -> '2025'
mes    = raw.iloc[fila + 1, 2:].astype(str).str.strip().map(ROMANO) # 'I ' -> 3
fechas = pd.to_datetime(anio + "-" + mes.astype(str) + "-01")

valores = raw.loc[etiq.str.startswith("PRODUCTO INTERNO"), 2:].iloc[0]
pib = pd.Series(valores.astype(float).values, index=fechas, name="gdp")
2017-03-01    75234.2
2017-06-01    80176.5
...
2025-12-01    79771.4          36 trimestres

6 · Guardar y comprobar

pib.to_csv("raw/ine_gdp.csv")
  • El crudo se guarda sin editar
  • Se repite con el cuadro nominal y se divide uno por otro
  • El deflactor debe subir

Si el deflactor baja, se bajaron los cuadros al revés.

Otras fuentes, la misma receta

Lo que cambia de una fuente a otra

fuente 3 · encontrar 5 · ordenar
INE, otros 30 cuadros URL guardada en una tabla de fuentes tres diseños de hoja distintos
BCB, boletín semanal el nombre cambia cada semana: Semanal 38_2026.xlsx series por nombre; fechas repartidas en dos filas
EIA, Brent el único enlace .xls de la página la hoja cuyo nombre dice «data»; dos filas de título
Baker Hughes, taladros URL fija, que algún día cambia filtrar country == "BOLIVIA"

Cuando los datos están en la página

Si la página muestra una tabla HTML (<table>), una línea basta:

tablas = pd.read_html(r.text, decimal=",", thousands=".")   # lista de DataFrames
tablas[0].head()

La página del PIB no tiene ninguna <table>: los datos están en los Excel. Primero mirar, después elegir la herramienta.

Una API en cinco líneas

url = "https://api.stlouisfed.org/fred/series/observations"
params = {"series_id": "PZINCUSDM", "api_key": CLAVE, "file_type": "json"}
obs = requests.get(url, params=params, timeout=30).json()["observations"]

zinc = pd.DataFrame(obs)[["date", "value"]]
  • Sin HTML ni Excel: la respuesta ya es una tabla
  • La clave es gratuita y personal: no se comparte ni se sube a GitHub

Cuando el servidor no coopera

Síntomas

  • 403 Forbidden
  • la descarga se queda colgada
  • funciona un día y al otro no

Qué hacer

  • probar otro user-agent o librería
  • timeout y un reintento
  • si falla, avisar y seguir con lo que ya se tiene

Nunca sobrescribir datos buenos con una descarga parcial. Combinar lo nuevo con el archivo existente antes de guardar.

Buenas prácticas

  1. Un CSV crudo, sin editar, por fuente, en raw/
  2. Limpiar y unir después, en un paso aparte
  3. Una pausa entre consultas: el servidor es de todos
  4. Anotar de dónde salió cada serie: URL y fecha de descarga
  5. Comprobar con algo que se conoce: el deflactor, la caída de 2020