Hay una escena que se repite en todas las empresas de manufactura.
Lunes, 9 de la mañana. El analista de supply chain abre 15 pestañas del navegador. Entra al portal del proveedor A, copia el precio. Entra al portal del proveedor B, copia el precio. Entra al portal de logística, copia el estado del envío. Pega todo en Excel. El proceso le toma 2 horas.
El miércoles, hay que repetirlo. El viernes, otra vez. Ese no es un trabajo analítico: es un trabajo mecánico que una máquina puede hacer en 30 segundos.
Y ahí es donde entra Python. No es magia ni necesitas ser ingeniero de software para usarlo. Es una herramienta que le dice a tu computadora: "cada lunes a las 9, abre estas páginas, extrae estos datos y déjamelos en una hoja."
En este módulo vas a escribir tu primer script. No uno genérico de tutorial: uno que extrae datos reales (precios, stock, estados) de fuentes externas y los guarda limpios en un archivo que abres en Excel.
Cuando termines vas a tener un superpoder que el 99% de los PMs no tiene: crear tus propias herramientas de datos sin pedirle nada a ingeniería.
Vamos.
2.1. ¿Qué es un Script para un PM?
Un script en Python es un archivo de texto (.py) que contiene instrucciones para la computadora. No se compila ni genera una app: solo se ejecuta, hace su trabajo y termina. No es más complejo que una macro de Excel, pero es mucho más potente porque no depende de Excel.
Extraer, Transformar, Guardar: todo script de datos del PM hace solo estas tres cosas. Extraer de una fuente (web, API, archivo), transformar (limpiar, estructurar, calcular) y guardar el resultado (CSV, Excel, base de datos). Si entiendes este patrón, entiendes el módulo entero.
2.2. El Pipeline de Datos del PM
Un script es como una línea de ensamblaje. Entra materia prima (los datos crudos de la web), pasa por estaciones de trabajo (httpx pide, bs4 limpia, pandas estructura) y sale un producto terminado: un CSV listo para análisis.
Antes — proceso manual
- 15 pestañas abiertas, una por proveedor
- Copiar y pegar precio por precio en Excel
- ~2 horas cada lunes, miércoles y viernes
- Errores de tipeo y celdas pegadas mal
- Sin histórico: cada semana se sobreescribe
Después — un script
- Un comando:
uv run monitoreo.py - Extrae, limpia y estructura solo
- ~30 segundos, repetible cuando quieras
- Tipos correctos y columnas consistentes
- CSV histórico que se acumula automáticamente
2.3. Las Librerías que Vas a Usar
requests-
3.1. Preparar el entorno con uv
uv es el gestor de entornos virtuales más rápido para Python, creado por Astral (mismos creadores de Ruff). Reemplaza a
pipyvenvtradicionales.Paso 1 — Instalar uv
Windows (PowerShell como administrador):
powershell -c "irm https://astral.sh/uv/install.ps1 | iex"macOS/Linux:
curl -LsSf https://astral.sh/uv/install.sh | shVerifica la instalación:
uv --versionPaso 2 — Crear el proyecto
# Crea la carpeta del proyecto mkdir scraper-proveedores cd scraper-proveedores # Inicializa el proyecto con Python 3 uv init # Activa el entorno virtual # En Windows (Git Bash): source .venv/Scripts/activate # En macOS/Linux: # source .venv/bin/activatePaso 3 — Instalar las librerías
# Todas las librerías que necesitas en un solo comando uv add httpx beautifulsoup4 pandas lxmllxmles el motor de parsing que usa beautifulsoup4. Es opcional pero hace todo más rápido. -
3.2. Tu primer script: pedir datos a una API
Crea un archivo
01-consulta-api.py:import httpx url = "https://jsonplaceholder.typicode.com/posts" try: respuesta = httpx.get(url, timeout=30.0) respuesta.raise_for_status() datos = respuesta.json() print(f"Se obtuvieron {len(datos)} registros") for post in datos[:3]: print(f"ID: {post['id']} — Título: {post['title']}") except httpx.RequestError as e: print(f"Error de conexión: {e}") except httpx.HTTPStatusError as e: print(f"Error HTTP: {e.response.status_code}")Ejecútalo:
uv run 01-consulta-api.pyQué hace cada línea:
import httpx→ Trae la librería.httpx.get(url, timeout=30.0)→ Pide datos a la URL.timeoutevita que el script se cuelgue si el servidor no responde.respuesta.raise_for_status()→ Si el servidor devolvió un error (404, 500), lanza una excepción.respuesta.json()→ Convierte la respuesta en una lista/diccionario de Python.try/except→ Si algo falla (red caída, servidor caído), el script no se rompe: muestra un mensaje y termina ordenadamente.
-
3.3. Segundo script: scraping de una página web (simulada)
Vamos a hacer scraping de una página simulada de proveedores de manufactura. Primero creamos la página HTML local para practicar, luego veremos cómo hacerlo con una página real.
Paso 1 — Crear una página HTML simulada
Crea
proveedores.html:<!DOCTYPE html> <html> <head><title>Portal de Proveedores — Manufactura</title></head> <body> <h1>Listado de Precios — Proveedores</h1> <table id="tabla-precios"> <tr> <th>Proveedor</th> <th>Material</th> <th>Precio Unitario (USD)</th> <th>Stock (unidades)</th> <th>Tiempo Entrega (días)</th> </tr> <tr> <td>ACME Corp</td> <td>Acero Inoxidable 304</td> <td>12.50</td> <td>5000</td> <td>14</td> </tr> <tr> <td>Industrial Parts SA</td> <td>Rodamiento SKF 6205</td> <td>8.75</td> <td>1200</td> <td>7</td> </tr> <tr> <td>Global Logistics Co</td> <td>Sensor de Temperatura PT100</td> <td>45.00</td> <td>300</td> <td>21</td> </tr> <tr> <td>ACME Corp</td> <td>Rodamiento SKF 6205</td> <td>9.00</td> <td>800</td> <td>10</td> </tr> <tr> <td>QualiParts Inc</td> <td>Acero Inoxidable 304</td> <td>11.80</td> <td>3000</td> <td>7</td> </tr> </table> <p class="nota">* Precios sujetos a cambio sin previo aviso</p> </body> </html>Paso 2 — Escribir el scraper
Crea
02-scraper-proveedores.py:import httpx from bs4 import BeautifulSoup import pandas as pd from pathlib import Path # 1. LEER EL HTML (desde archivo local) ruta_html = Path("proveedores.html") try: with open(ruta_html, "r", encoding="utf-8") as archivo: html = archivo.read() except FileNotFoundError: print(f"Error: No se encontró el archivo {ruta_html}") exit(1) # 2. PARSEAR EL HTML CON BEAUTIFULSOUP soup = BeautifulSoup(html, "lxml") # 3. ENCONTRAR LA TABLA tabla = soup.find("table", id="tabla-precios") if not tabla: print("Error: No se encontró la tabla de precios") exit(1) # 4. EXTRAER ENCABEZADOS encabezados = [] for th in tabla.find("tr").find_all("th"): encabezados.append(th.text.strip()) # 5. EXTRAER FILAS DE DATOS datos = [] for fila in tabla.find_all("tr")[1:]: # Saltar la fila de encabezados columnas = fila.find_all("td") if len(columnas) == len(encabezados): fila_datos = { encabezados[0]: columnas[0].text.strip(), encabezados[1]: columnas[1].text.strip(), encabezados[2]: float(columnas[2].text.strip()), encabezados[3]: int(columnas[3].text.strip()), encabezados[4]: int(columnas[4].text.strip()), } datos.append(fila_datos) # 6. CREAR DATAFRAME CON PANDAS df = pd.DataFrame(datos) # 7. ANÁLISIS BÁSICO print("=== DATOS EXTRAÍDOS ===") print(df.to_string(index=False)) print("\n=== RESUMEN ===") print(f"Total de registros: {len(df)}") print(f"Proveedores únicos: {df['Proveedor'].nunique()}") print(f"Materiales únicos: {df['Material'].nunique()}") print(f"Precio promedio: ${df['Precio Unitario (USD)'].mean():.2f}") print(f"Stock total: {df['Stock (unidades)'].sum():,}") # 8. GUARDAR A CSV archivo_salida = "datos_proveedores.csv" df.to_csv(archivo_salida, index=False) print(f"\n✅ Datos guardados en: {archivo_salida}")Ejecútalo:
uv run 02-scraper-proveedores.py -
3.4. Tercer script: scraping de una página web real
Ahora vamos a hacer scraping de datos reales. Usaremos una página pública que no requiere autenticación.
Scraping éticoSiempre verifica el robots.txt de un sitio antes de hacerle scraping (ej.
sitio.com/robots.txt). Algunos sitios lo prohíben: respeta las reglas, no satures el servidor con peticiones y prefiere siempre una API oficial cuando exista.Crea
03-scraper-real.py:import httpx from bs4 import BeautifulSoup import pandas as pd url = "https://books.toscrape.com/" try: respuesta = httpx.get(url, timeout=30.0) respuesta.raise_for_status() except httpx.RequestError as e: print(f"Error de conexión: {e}") exit(1) except httpx.HTTPStatusError as e: print(f"Error HTTP: {e.response.status_code}") exit(1) soup = BeautifulSoup(respuesta.text, "lxml") libros = soup.find_all("article", class_="product_pod") datos = [] for libro in libros: titulo = libro.h3.a["title"] precio_texto = libro.find("p", class_="price_color").text precio = float(precio_texto.replace("£", "").replace("Â", "")) disponibilidad = libro.find("p", class_="instock availability") stock = "En stock" if disponibilidad else "Agotado" datos.append({ "titulo": titulo, "precio": precio, "stock": stock, }) df = pd.DataFrame(datos) print("=== LIBROS DISPONIBLES ===") print(df.to_string(index=False)) print(f"\n=== RESUMEN ===") print(f"Total de libros: {len(df)}") print(f"Precio promedio: £{df['precio'].mean():.2f}") print(f"Más caro: £{df['precio'].max():.2f}") print(f"Más barato: £{df['precio'].min():.2f}") df.to_csv("libros_scrapeados.csv", index=False) print("\n✅ Datos guardados en: libros_scrapeados.csv") -
3.5. Cuarto script: monitoreo de precios de proveedores
Este script simula un caso real de supply chain: monitorear precios de un material específico en múltiples proveedores.
Crea
04-monitoreo-proveedores.py:""" Simulador de monitoreo de precios de proveedores. En un escenario real, cada proveedor sería una URL distinta. Aquí simulamos consultas a una API local para demostrar el patrón. """ import httpx import pandas as pd from datetime import datetime proveedores = [ {"nombre": "ACME Corp", "url": "https://jsonplaceholder.typicode.com/posts/1"}, {"nombre": "Industrial Parts", "url": "https://jsonplaceholder.typicode.com/posts/2"}, {"nombre": "QualiParts Inc", "url": "https://jsonplaceholder.typicode.com/posts/3"}, {"nombre": "Global Logistics", "url": "https://jsonplaceholder.typicode.com/posts/4"}, ] resultados = [] for proveedor in proveedores: try: respuesta = httpx.get(proveedor["url"], timeout=15.0) respuesta.raise_for_status() data = respuesta.json() # Simulamos extraer datos relevantes del JSON resultados.append({ "proveedor": proveedor["nombre"], "material": "Rodamiento SKF 6205", "precio_unitario": round(abs(hash(data["title"])) % 20 + 5, 2), "stock": int(abs(hash(data["body"])) % 5000 + 100), "fecha_consulta": datetime.now().strftime("%Y-%m-%d %H:%M"), "estado": "OK", }) except httpx.RequestError as e: resultados.append({ "proveedor": proveedor["nombre"], "material": "Rodamiento SKF 6205", "precio_unitario": 0, "stock": 0, "fecha_consulta": datetime.now().strftime("%Y-%m-%d %H:%M"), "estado": f"Error: {e}", }) except httpx.HTTPStatusError as e: resultados.append({ "proveedor": proveedor["nombre"], "material": "Rodamiento SKF 6205", "precio_unitario": 0, "stock": 0, "fecha_consulta": datetime.now().strftime("%Y-%m-%d %H:%M"), "estado": f"HTTP {e.response.status_code}", }) df = pd.DataFrame(resultados) print("=== MONITOREO DE PROVEEDORES ===") print(df.to_string(index=False)) # Análisis: ¿cuál es el mejor precio? if df[df["estado"] == "OK"].empty: print("\n⚠️ Todos los proveedores fallaron. Revisa conexiones.") else: mejor = df.loc[df["precio_unitario"].idxmin()] print(f"\n🏆 Mejor precio: {mejor['proveedor']} — ${mejor['precio_unitario']:.2f}") print(f" Stock disponible: {mejor['stock']:,} unidades") # Guardar histórico (modo append) try: historico = pd.read_csv("historico_precios.csv") df_completo = pd.concat([historico, df], ignore_index=True) except FileNotFoundError: df_completo = df df_completo.to_csv("historico_precios.csv", index=False) print(f"\n✅ Historial actualizado: {len(df_completo)} registros totales")
4.1. El Flujo Semanal de Monitoreo Automatizado
4.2. Programar el Script para que se Ejecute Solo
Windows (Task Scheduler):
- Abre "Task Scheduler".
- Crea una tarea básica.
- Disparador: "Daily" a las 8:00 AM.
- Acción: "Start a program".
- Programa:
C:\Users\TU_USUARIO\.local\bin\uv.exe - Argumentos:
run C:\ruta\completa\04-monitoreo-proveedores.py - Iniciar en:
C:\ruta\completa\
- Programa:
Usa siempre la ruta completa al ejecutable de uv y al script. El programador de tareas no sabe dónde está tu proyecto: si pones rutas relativas, la tarea fallará en silencio.
4.3. Diagnóstico Rápido
Sí → ¿podrías automatizarlos con un script de 30 líneas? Probablemente sí.
No → ¿seguro? Revisa tu calendario de la semana pasada.
Esta semana → urgente automatizar.
El mes pasado → ya es tarde para no automatizar.
Nunca → o eres muy preciso, o no te has dado cuenta.
Tienen try/except → sí, fallan ordenadamente.
No lo tienen → van a romperse y no te vas a enterar.
4.4. Entregable del Módulo
Parte A — Script de extracción. Crea un script en Python que:
- Tome datos de una fuente externa (API pública, página web o el HTML simulado de este módulo).
- Use
httpxpara la petición (o lectura de archivo). - Use
beautifulsoup4para parsear (si es HTML) o.json()(si es API). - Use
pandaspara estructurar los datos en un DataFrame. - Guarde los resultados en un CSV.
- Incluya manejo de errores con
try/exceptpara al menos: error de conexión, error HTTP y archivo no encontrado (si lee local). - Imprima un resumen en la terminal al ejecutarse.
Parte B — Documentación.
- Un párrafo explicando qué hace el script y cada cuánto debería ejecutarse.
- Instrucciones de cómo ejecutarlo (un comando:
uv run script.py).
Formato de entrega: archivo .py funcional (que corra sin errores) + CSV de salida generado por el script + captura de la terminal mostrando la ejecución y su resultado.
- Error: scrapear sin revisar
robots.txtni los términos del sitio. Corrección: respeta las reglas del sitio: datos públicos, ritmo razonable y identificación honesta. - Error: selectores CSS frágiles que mueren con el primer rediseño. Corrección: ancla a atributos estables (id, data-*) y valida que el selector siga devolviendo datos.
- Error: un script sin
try/exceptque muere en el registro 47 de 500. Corrección: captura el error, registra qué registro falló y continúa: el lote termina y tú depuras después. - Error: no fijar versiones de dependencias.
Corrección: congela versiones exactas en
requirements.txt/pyproject.tomlo el script morirá en 6 meses. - Error: sobrescribir el CSV anterior en cada corrida. Corrección: guarda un archivo por fecha: la serie histórica es la mitad del valor del monitoreo.
| Criterio | No Aprobado (0) | Aprobado (1) | Sobresaliente (2) |
|---|---|---|---|
| 1. El script se ejecuta sin errores y produce datos limpios | El script tiene errores de sintaxis, no corre, o produce datos corruptos (mezcla tipos, columnas mal nombradas) | El script corre y produce un CSV, pero los datos tienen problemas menores (espacios extras, tipos incorrectos, filas duplicadas) | El script corre limpiamente, el CSV tiene tipos correctos (números como números, no como texto), encabezados claros, y sin filas vacías |
| 2. Manejo de errores cubre al menos 2 escenarios de fallo | No hay try/except o solo hay un bloque genérico que oculta errores |
Hay try/except pero captura errores genéricamente sin distinguir entre error de conexión y error HTTP |
Hay bloques separados para httpx.RequestError, httpx.HTTPStatusError, y al menos un error adicional; cada uno con un mensaje específico |
| 3. El script tiene un propósito claro para el negocio | El script extrae datos sin un objetivo claro (no hay resumen, no hay análisis, no hay contexto de por qué esos datos importan) | El script extrae datos útiles y muestra un resumen, pero no responde una pregunta de negocio específica | El script responde una pregunta concreta: "¿qué proveedor tiene el mejor precio?", "¿cómo han cambiado los precios esta semana?", y el resumen final comunica esa respuesta claramente |
Aprobación: 2 de 3 criterios en "Aprobado" o superior.
Modifica tu script para que, en lugar de imprimir en pantalla, envíe los resultados a un Google Sheet usando las APIs de Google. Si puedes hacer eso, estás listo para Apps Script y GCP.
- Todo script de datos del PM hace solo tres cosas: Extraer, Transformar, Guardar. Ese es el pipeline completo.
- Tres librerías bastan:
httpxpide,beautifulsoup4parsea HTML,pandasestructura y exporta a CSV. - Usa
uvpara crear el entorno e instalar dependencias en un solo comando, yuv runpara ejecutar. - Un script sin
try/exceptse rompe en silencio: maneja error de conexión, error HTTP y archivo no encontrado por separado. - El scraping es ético: respeta el
robots.txt, no satures el servidor y prefiere una API oficial cuando exista. - El valor no está en extraer datos, sino en responder una pregunta de negocio: ¿qué proveedor tiene el mejor precio?
Kit: Python Scraping / APIs
| Archivo | Descripción |
|---|---|
| ⬇ 01-consulta-api.py | Script básico: consulta a API pública con httpx |
| ⬇ 02-scraper-proveedores.py | Scraping local con HTML simulado de proveedores |
| ⬇ 03-scraper-real.py | Scraping real con URL pública configurable |
| ⬇ 04-monitoreo-proveedores.py | Monitoreo periódico de proveedores con pandas |
| ⬇ proveedores.html | HTML simulado con datos de proveedores para testing |
| ⬇ requirements.txt | Dependencias: httpx, beautifulsoup4, pandas, lxml |