lección 5
Instalar requests: APIs y pedir datos al mundo exterior
Aprende a consumir APIs REST públicas para ingerir datos externos en tus pipelines.
⏱ 55 min
### ¿Qué es una API? La analogía del restaurante
Imagina que estás en un restaurante. No puedes entrar en la cocina a coger la comida tú mismo: usas al camarero. Le dices "quiero una pizza margarita" (petición), el camarero va a la cocina, y te trae el plato (respuesta). Una API funciona exactamente igual: tu programa (tú) le pide datos a un servidor (la cocina) a través de una interfaz estandarizada (el camarero). No necesitas saber cómo está organizada la cocina por dentro: solo conoces el menú (la documentación de la API).
API significa Application Programming Interface: una interfaz para que programas hablen con otros programas. Cuando abres la app del tiempo en tu móvil, esa app está haciendo una petición a una API meteorológica. Cuando pides un Uber, la app hace peticiones a APIs de mapa, de pricing, de drivers disponibles. El mundo moderno funciona con APIs.
Para ingeniería de datos, las APIs son una fuente de datos fundamental. Quieres datos del tiempo, de mercados financieros, de redes sociales, de tu CRM, de tu sistema de pagos: todos exponen APIs. Tu pipeline ingesta datos de APIs igual que de bases de datos o archivos.
### Paso 1: Instalar requests
1# requests: la librería más usada para hacer peticiones HTTP2pip install requests==2.32.334# Verificar5python -c "import requests; print(requests.__version__)"6# Salida: 2.32.3
requests: simple, elegante y usada por millones de proyectos
requests es una de esas librerías que definen un estándar. Su autor, Kenneth Reitz, la diseñó para que fuera "para humanos": la API es tan intuitiva que casi puedes adivinar cómo usarla. Fue creada en 2011 porque la librería estándar de Python (urllib) era innecesariamente compleja para tareas comunes.
### Anatomía de una petición HTTP
### Tu primera petición: JSONPlaceholder
JSONPlaceholder es una API pública gratuita para practicar. Devuelve datos falsos (usuarios, posts, comentarios) pero con la misma estructura que una API real. Perfecta para aprender sin necesitar API keys ni autenticación.
1import requests23# GET = "dame datos" (la operación más común)4response = requests.get('https://jsonplaceholder.typicode.com/users')56# Verificar que fue exitosa7print(f"Status code: {response.status_code}") # 200 = OK89# Convertir JSON a diccionario/lista Python10usuarios = response.json()1112print(f"Usuarios recibidos: {len(usuarios)}")13print(f"Primer usuario: {usuarios[0]['name']}")14print(f"Su email: {usuarios[0]['email']}")15print(f"Su ciudad: {usuarios[0]['address']['city']}")
Tu primera petición: pedir usuarios a JSONPlaceholder
### De JSON a DataFrame: el flujo completo
El patrón de ingesta desde API es siempre el mismo: pedir datos → verificar respuesta → convertir a DataFrame → limpiar/transformar. Aquí está el flujo completo:
1import requests2import pandas as pd34def ingerir_usuarios() -> pd.DataFrame:5 """Ingiere usuarios desde la API y devuelve un DataFrame limpio."""67 # 1. Pedir datos8 response = requests.get('https://jsonplaceholder.typicode.com/users')9 response.raise_for_status() # Lanza error si status != 2001011 # 2. Convertir a DataFrame (json_normalize para datos anidados)12 usuarios = response.json()13 df = pd.json_normalize(usuarios, sep='_')1415 # 3. Seleccionar y renombrar columnas útiles16 df = df[['id', 'name', 'email', 'address_city', 'company_name']].rename(columns={17 'address_city': 'ciudad',18 'company_name': 'empresa'19 })2021 # 4. Limpiar22 df['email'] = df['email'].str.lower()23 df['ciudad'] = df['ciudad'].str.title()2425 return df2627# Ejecutar28usuarios_df = ingerir_usuarios()29print(usuarios_df)30print(f"\nColumnas: {list(usuarios_df.columns)}")
Patrón completo: API → JSON → DataFrame → limpieza
### APIs con parámetros: filtrar desde la petición
Muchas APIs te permiten filtrar datos enviando parámetros en la URL. Es como el WHERE de SQL pero en la petición HTTP. Así evitas pedir TODOS los datos para luego filtrar en Python.
1import requests2import pandas as pd34# Open-Meteo: API del tiempo gratuita y sin API key5# Pedir el tiempo de Madrid (lat=40.42, lon=-3.70)6response = requests.get('https://api.open-meteo.com/v1/forecast', params={7 'latitude': 40.42,8 'longitude': -3.70,9 'daily': 'temperature_2m_max,temperature_2m_min,precipitation_sum',10 'timezone': 'Europe/Madrid',11 'forecast_days': 712})1314response.raise_for_status()15data = response.json()1617# Convertir a DataFrame18tiempo = pd.DataFrame({19 'fecha': data['daily']['time'],20 'temp_max': data['daily']['temperature_2m_max'],21 'temp_min': data['daily']['temperature_2m_min'],22 'lluvia_mm': data['daily']['precipitation_sum']23})2425tiempo['fecha'] = pd.to_datetime(tiempo['fecha'])26print(tiempo)
API del tiempo real: parámetros en la URL filtran los datos pedidos
Usa el parámetro params de requests.get() en vez de construir la URL a mano. requests se encarga de encodear caracteres especiales y formatear la URL correctamente. Menos errores, código más limpio.
### Manejo de errores en APIs: el mundo real falla
Las APIs fallan. El servidor se cae, tu internet se corta, llegas al límite de peticiones (rate limit), la API cambia sin avisarte. Un pipeline robusto maneja TODOS estos casos sin explotar.
1import requests2import time34def pedir_con_reintentos(url: str, params: dict = None, max_intentos: int = 3) -> dict:5 """Pide datos a una API con reintentos y backoff exponencial."""67 for intento in range(max_intentos):8 try:9 response = requests.get(url, params=params, timeout=10)1011 # 4xx (excepto 429): error del cliente, no reintentar12 if 400 <= response.status_code < 500 and response.status_code != 429:13 raise Exception(f"Error {response.status_code}: {response.text[:200]}")1415 response.raise_for_status()16 return response.json()1718 except requests.exceptions.RequestException as e:19 if intento < max_intentos - 1:20 espera = 2 ** intento21 print(f"Intento {intento+1} fallido: {e}. Esperando {espera}s...")22 time.sleep(espera)23 else:24 print(f"Todos los intentos fallaron para {url}")25 raise2627 raise Exception(f"Fallaron todos los {max_intentos} intentos para {url}")
Reintentos con backoff exponencial: el patrón que todo pipeline de producción necesita
SIEMPRE pon timeout en tus peticiones (requests.get(url, timeout=10)). Sin timeout, tu programa puede quedarse colgado INDEFINIDAMENTE esperando una respuesta que nunca llega. He visto pipelines bloqueados 8 horas por no tener timeout.
### Paginación: cuando la API no te da todo de golpe
Las APIs grandes no devuelven 1 millón de resultados en una sola respuesta. Te dan "páginas" de 100 o 1000 registros y tú debes ir pidiendo página a página. Es como leer un libro: no te dan todo el libro en una frase, te lo dan capítulo a capítulo.
1import requests2import pandas as pd34def ingerir_posts_paginados() -> pd.DataFrame:5 """Ingiere todos los posts de JSONPlaceholder página a página."""6 todos_los_posts = []7 page = 18 per_page = 10910 while True:11 response = requests.get(12 'https://jsonplaceholder.typicode.com/posts',13 params={'_page': page, '_limit': per_page}14 )15 response.raise_for_status()1617 datos = response.json()18 if not datos: # Lista vacía = no hay más páginas19 break2021 todos_los_posts.extend(datos)22 print(f"Página {page}: {len(datos)} posts")23 page += 12425 df = pd.DataFrame(todos_los_posts)26 print(f"Total ingerido: {len(df)} posts")27 return df2829posts = ingerir_posts_paginados()30print(posts.head())
Paginación: pedir datos en bucle hasta que no haya más
## ejercicios
Pipeline de ingesta del tiempo de 3 ciudades
Escribe una función que ingiera los datos del tiempo de 3 ciudades (Madrid, Barcelona, Sevilla) usando la API de Open-Meteo, combine los resultados en un solo DataFrame con una columna "ciudad", y guarde el resultado como CSV.
Ingerir todos los posts con paginación
La API de JSONPlaceholder tiene 100 posts. Escribe un script que los ingiera todos de 20 en 20 (paginando) y cree un DataFrame con las columnas: id, userId, title. Muestra cuántas peticiones fueron necesarias.
Función con reintentos y logging
Escribe una función pedir_datos_robusto(url) que haga una petición GET con timeout de 5 segundos, reintente hasta 3 veces con backoff exponencial (esperando entre intentos, no después del último), y lance una excepción clara si todos los intentos fallan. No debe reintentar errores 4xx (un 404 no se arregla repitiéndolo).
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...