CODER SALE 💸

Aprovecha hasta 70% OFF y hasta 12 meses sin intereses en CURSOS y CARRERAS

|

Hasta el 04/09 ⏰

CODER SALE 💸

Aprovecha hasta 70% OFF y hasta 12 meses sin intereses en CURSOS y CARRERAS

|

Hasta el 04/09 ⏰

Hasta el 04/09 ⏰

CODER SALE 💸

Aprovecha hasta 70% OFF y hasta 12 meses sin intereses en CURSOS y CARRERAS

Cómo hacer web scraping con Python: guía paso a paso para analistas de datos y marketers

Tutoriales gratuitos

Descargartutoriales gratuitos

Tutoriales y videos prácticos gratuitos para incorporar nuevas herramientas de IA y empleabilidad.

Ver los tutoriales

Francisco Rhaiel

AI Growth Engineer

Data

Cómo hacer web scraping con Python: guía paso a paso para analistas de datos y marketers

Publicado el

El web scraping resuelve un problema concreto: necesitás datos que existen en una página pero no en un CSV descargable ni en una API. Con unas 30 líneas de Python podés extraer precios de competidores, listados de vacantes, catálogos de productos o resultados de búsqueda, y convertirlos en una tabla lista para analizar. Esta guía va del primer request hasta el script completo, incluye qué hacer cuando la página carga con JavaScript y explica dónde están los límites legales y éticos que conviene no cruzar.

La aclaración importante antes de empezar: scraping no es lo mismo que hacerlo bien. La diferencia entre un script que funciona una vez y uno que podés correr todas las semanas está en tres cosas —respetar el sitio, manejar los errores y estructurar la salida. Vamos a las tres.

Qué es el web scraping y cuándo conviene usarlo

Scraping es extraer datos del HTML de una página de forma automatizada. La página está pensada para que la lea una persona; el script la lee como estructura y se queda con las partes que importan.

Antes de escribir código, verificá tres alternativas en este orden:

  • ¿Hay una API? Si el sitio ofrece una, usala. Es más estable, más rápida y explícitamente permitida.

  • ¿Hay una descarga? Muchos portales de datos públicos publican CSV o JSON directo.

  • ¿La página hace llamadas internas a una API? Muchísimos sitios modernos cargan sus datos vía fetch a un endpoint JSON. Abriendo la pestaña Network del navegador podés encontrar ese endpoint y consultarlo directamente. Es la mejor opción de todas: datos limpios, sin parsear HTML.

Solo si ninguna aplica, scrapeá el HTML.

Las reglas que hay que respetar

Esta sección no es un trámite: es lo que separa un proyecto profesional de uno que te bloquea la IP o te mete en problemas.

  • Leé el robots.txt. Está en sitio.com/robots.txt e indica qué rutas el sitio pide no rastrear. El estándar está documentado por Google en su especificación oficial de robots.txt. No es legalmente vinculante en todas las jurisdicciones, pero ignorarlo es una señal clara de mala fe.

  • Revisá los términos de uso. Algunos sitios prohíben explícitamente la extracción automatizada.

  • No scrapees datos personales. Nombres, correos, teléfonos y perfiles caen bajo normativa de protección de datos en la mayoría de los países de la región. El riesgo no vale el dato.

  • Poné pausas entre requests. Un segundo entre pedidos es lo mínimo razonable. Sin pausas estás haciendo, en la práctica, una prueba de carga no solicitada.

  • Identificate. Un User-Agent honesto con forma de contacto es mejor que uno falso.

  • Guardá una copia local del HTML. Así iterás tu parser sin volver a pegarle al sitio en cada prueba.

Paso 1: el entorno

Necesitás Python 3.10 o superior y tres librerías. En una terminal:

  • pip install requests — para hacer los pedidos HTTP

  • pip install beautifulsoup4 — para navegar el HTML

  • pip install pandas — para estructurar y exportar

Si Python es territorio nuevo para vos, conviene cubrir la base antes: Python para análisis de datos: guía paso a paso para principiantes arranca desde cero.

Paso 2: traer el HTML

El primer pedido siempre incluye headers y verificación de estado:

import requests

HEADERS = {
    "User-Agent": "Mozilla/5.0 (compatible; proyecto-analisis/1.0; [email protected])"
}

url = "https://ejemplo.com/productos"
resp = requests.get(url, headers=HEADERS, timeout=15)
resp.raise_for_status()
html = resp.text
import requests

HEADERS = {
    "User-Agent": "Mozilla/5.0 (compatible; proyecto-analisis/1.0; [email protected])"
}

url = "https://ejemplo.com/productos"
resp = requests.get(url, headers=HEADERS, timeout=15)
resp.raise_for_status()
html = resp.text
import requests

HEADERS = {
    "User-Agent": "Mozilla/5.0 (compatible; proyecto-analisis/1.0; [email protected])"
}

url = "https://ejemplo.com/productos"
resp = requests.get(url, headers=HEADERS, timeout=15)
resp.raise_for_status()
html = resp.text
import requests

HEADERS = {
    "User-Agent": "Mozilla/5.0 (compatible; proyecto-analisis/1.0; [email protected])"
}

url = "https://ejemplo.com/productos"
resp = requests.get(url, headers=HEADERS, timeout=15)
resp.raise_for_status()
html = resp.text

raise_for_status() corta la ejecución si el servidor devolvió un error, en lugar de dejarte parseando una página de error 404. timeout evita que el script quede colgado indefinidamente.

Paso 3: encontrar los selectores correctos

Este es el paso que define si el script va a sobrevivir un mes o romperse mañana.

Abrí la página en el navegador, click derecho sobre el dato que querés, "Inspeccionar". Vas a ver el HTML alrededor. Buscá el patrón: casi siempre cada ítem está en un contenedor repetido con la misma clase.

La regla práctica: preferí selectores semánticos y estables. Un data-testid, un atributo itemprop o una etiqueta como article sobreviven mucho más que una clase con hash generada por el framework de CSS. Si el único selector disponible es css-1x7f9a2, sabé que ese script tiene fecha de vencimiento corta.

Paso 4: extraer los datos

from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "html.parser")

filas = []
for card in soup.select("article.producto"):
    titulo = card.select_one("h2")
    precio = card.select_one(".precio")
    link = card.select_one("a")

    filas.append({
        "titulo": titulo.get_text(strip=True) if titulo else None,
        "precio": precio.get_text(strip=True) if precio else None,
        "url": link["href"] if link and link.has_attr("href") else None,
    })

print(f"Extraídos {len(filas)} registros")
from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "html.parser")

filas = []
for card in soup.select("article.producto"):
    titulo = card.select_one("h2")
    precio = card.select_one(".precio")
    link = card.select_one("a")

    filas.append({
        "titulo": titulo.get_text(strip=True) if titulo else None,
        "precio": precio.get_text(strip=True) if precio else None,
        "url": link["href"] if link and link.has_attr("href") else None,
    })

print(f"Extraídos {len(filas)} registros")
from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "html.parser")

filas = []
for card in soup.select("article.producto"):
    titulo = card.select_one("h2")
    precio = card.select_one(".precio")
    link = card.select_one("a")

    filas.append({
        "titulo": titulo.get_text(strip=True) if titulo else None,
        "precio": precio.get_text(strip=True) if precio else None,
        "url": link["href"] if link and link.has_attr("href") else None,
    })

print(f"Extraídos {len(filas)} registros")
from bs4 import BeautifulSoup

soup = BeautifulSoup(html, "html.parser")

filas = []
for card in soup.select("article.producto"):
    titulo = card.select_one("h2")
    precio = card.select_one(".precio")
    link = card.select_one("a")

    filas.append({
        "titulo": titulo.get_text(strip=True) if titulo else None,
        "precio": precio.get_text(strip=True) if precio else None,
        "url": link["href"] if link and link.has_attr("href") else None,
    })

print(f"Extraídos {len(filas)} registros")

El detalle clave son los chequeos if. En cualquier página real va a haber ítems sin precio, sin imagen o con estructura distinta. Sin esas verificaciones, el script muere en el registro 47 de 300 y perdés todo lo anterior.

Paso 5: limpiar y exportar

El HTML devuelve texto; el análisis necesita números y fechas.

import pandas as pd
import re

df = pd.DataFrame(filas)

# "$ 12.499,00" -> 12499.00
df["precio_num"] = (
    df["precio"]
      .fillna("")
      .apply(lambda s: re.sub(r"[^\d,.]", "", s))
      .str.replace(".", "", regex=False)
      .str.replace(",", ".", regex=False)
)
df["precio_num"] = pd.to_numeric(df["precio_num"], errors="coerce")

df = df.drop_duplicates(subset=["url"])
df["extraido_el"] = pd.Timestamp.now()

df.to_csv("productos.csv", index=False, encoding="utf-8-sig")
import pandas as pd
import re

df = pd.DataFrame(filas)

# "$ 12.499,00" -> 12499.00
df["precio_num"] = (
    df["precio"]
      .fillna("")
      .apply(lambda s: re.sub(r"[^\d,.]", "", s))
      .str.replace(".", "", regex=False)
      .str.replace(",", ".", regex=False)
)
df["precio_num"] = pd.to_numeric(df["precio_num"], errors="coerce")

df = df.drop_duplicates(subset=["url"])
df["extraido_el"] = pd.Timestamp.now()

df.to_csv("productos.csv", index=False, encoding="utf-8-sig")
import pandas as pd
import re

df = pd.DataFrame(filas)

# "$ 12.499,00" -> 12499.00
df["precio_num"] = (
    df["precio"]
      .fillna("")
      .apply(lambda s: re.sub(r"[^\d,.]", "", s))
      .str.replace(".", "", regex=False)
      .str.replace(",", ".", regex=False)
)
df["precio_num"] = pd.to_numeric(df["precio_num"], errors="coerce")

df = df.drop_duplicates(subset=["url"])
df["extraido_el"] = pd.Timestamp.now()

df.to_csv("productos.csv", index=False, encoding="utf-8-sig")
import pandas as pd
import re

df = pd.DataFrame(filas)

# "$ 12.499,00" -> 12499.00
df["precio_num"] = (
    df["precio"]
      .fillna("")
      .apply(lambda s: re.sub(r"[^\d,.]", "", s))
      .str.replace(".", "", regex=False)
      .str.replace(",", ".", regex=False)
)
df["precio_num"] = pd.to_numeric(df["precio_num"], errors="coerce")

df = df.drop_duplicates(subset=["url"])
df["extraido_el"] = pd.Timestamp.now()

df.to_csv("productos.csv", index=False, encoding="utf-8-sig")

Tres decisiones que valen la pena: errors="coerce" convierte lo que no se puede parsear en nulo en lugar de romper; la columna de timestamp te permite comparar extracciones a lo largo del tiempo; y utf-8-sig hace que Excel abra el archivo sin destrozar los acentos.

Paso 6: paginación

Casi ningún listado real cabe en una sola página.

import time

todas = []
for pagina in range(1, 11):
    r = requests.get(f"{url}?page={pagina}", headers=HEADERS, timeout=15)
    if r.status_code != 200:
        break
    s = BeautifulSoup(r.text, "html.parser")
    items = s.select("article.producto")
    if not items:
        break
    todas.extend(items)
    time.sleep(1.5)
import time

todas = []
for pagina in range(1, 11):
    r = requests.get(f"{url}?page={pagina}", headers=HEADERS, timeout=15)
    if r.status_code != 200:
        break
    s = BeautifulSoup(r.text, "html.parser")
    items = s.select("article.producto")
    if not items:
        break
    todas.extend(items)
    time.sleep(1.5)
import time

todas = []
for pagina in range(1, 11):
    r = requests.get(f"{url}?page={pagina}", headers=HEADERS, timeout=15)
    if r.status_code != 200:
        break
    s = BeautifulSoup(r.text, "html.parser")
    items = s.select("article.producto")
    if not items:
        break
    todas.extend(items)
    time.sleep(1.5)
import time

todas = []
for pagina in range(1, 11):
    r = requests.get(f"{url}?page={pagina}", headers=HEADERS, timeout=15)
    if r.status_code != 200:
        break
    s = BeautifulSoup(r.text, "html.parser")
    items = s.select("article.producto")
    if not items:
        break
    todas.extend(items)
    time.sleep(1.5)

Las dos condiciones de corte —status distinto de 200 y lista vacía— evitan que el loop siga pidiendo páginas que no existen. Y time.sleep(1.5) es la línea que más te va a servir para no quedar bloqueado.

Cuando la página carga con JavaScript

Si resp.text viene casi vacío o sin los datos que ves en el navegador, la página los carga después con JavaScript. requests no ejecuta JavaScript, así que hay dos caminos.

Opción A: buscar el endpoint interno (recomendada)

Abrí las herramientas de desarrollo, pestaña Network, filtro XHR/Fetch, y recargá. Vas a ver las llamadas que hace la página. Si encontrás una que devuelve JSON con los datos, consultala directo con requests. Es más rápido, más estable y te ahorra todo el parseo de HTML.

Opción B: un navegador automatizado

Si no hay endpoint accesible, necesitás Playwright o Selenium, que abren un navegador real y ejecutan el JavaScript. Es más lento y consume más recursos, pero funciona donde nada más funciona. Para volúmenes grandes o scraping recurrente, Scrapy es el framework que agrega concurrencia, reintentos y control de velocidad de forma nativa.

Casos de uso reales para analistas y marketers

  • Monitoreo de precios. Extraer precios de la competencia semanalmente y detectar cambios antes de que el equipo comercial se entere por un cliente.

  • Análisis de demanda laboral. Scrapear portales de empleo para ver qué tecnologías se piden en las vacantes de tu sector y con qué frecuencia.

  • Investigación de contenido. Relevar qué temas cubren los blogs de referencia de tu vertical y detectar huecos.

  • Enriquecimiento de datos. Completar información faltante de un dataset propio a partir de fuentes públicas.

  • Seguimiento de catálogos. Detectar productos nuevos, discontinuados o cambios de stock.

Una vez que el script funciona, el siguiente paso natural es que corra solo. Podés programarlo con un cron o integrarlo en un flujo automatizado que además te avise cuando detecte un cambio relevante: Python para automatizar tareas de marketing digital muestra cómo encadenar este tipo de scripts con el resto del stack.

Errores frecuentes y cómo evitarlos

  • No manejar errores. Envolvé cada request en try/except y registrá qué falló. Un scraper que se cae sin dejar rastro es imposible de arreglar.

  • Guardar solo el resultado final. Guardá también el HTML crudo. Cuando el sitio cambie su estructura, vas a querer comparar.

  • Selectores frágiles. Si dependés de clases autogeneradas, el script se rompe con cada deploy del sitio.

  • Ignorar la codificación. Definí resp.encoding explícitamente si los acentos salen mal.

  • Scrapear sin pausas. Es la razón número uno de bloqueos y la más fácil de evitar.

Cursos recomendados de Coderhouse

Tres opciones según de dónde partas:

  • Curso de Python — nivel inicial y el punto de partida obligado. Todo lo de esta guía se apoya en manejo de estructuras de datos, funciones y control de errores, que es exactamente el contenido del curso.

  • Curso de Data Analytics — nivel intermedio. Scrapear datos es la mitad del trabajo; la otra mitad es limpiarlos, cruzarlos y sacar conclusiones defendibles. Acá se cubre esa segunda parte.

  • Carrera de Data Scientist — la formación más completa si querés que el scraping sea la fuente de alimentación de modelos y pipelines de datos propios.

Si tu objetivo es que el script corra de forma automática y notifique resultados, el Curso de AI Automation es el complemento directo.

Preguntas frecuentes

¿Es legal hacer web scraping?

Depende del qué, del dónde y del cómo. Extraer datos públicos y no personales de un sitio que no lo prohíbe en sus términos, respetando el robots.txt y sin sobrecargar el servidor, es una práctica generalizada y habitualmente tolerada. Se vuelve problemático cuando involucra datos personales, contenido protegido por derechos de autor, elusión de mecanismos de acceso o volúmenes que afectan el servicio. Ante cualquier duda con datos sensibles o uso comercial, conviene consultar asesoría legal: esta guía es técnica, no un dictamen jurídico.

¿Cuál es la diferencia entre BeautifulSoup, Selenium y Scrapy?

BeautifulSoup solo parsea HTML que ya tenés; es lo más simple y lo que conviene usar primero. Selenium y Playwright controlan un navegador real y ejecutan JavaScript, por lo que sirven para páginas dinámicas a costa de velocidad. Scrapy es un framework completo con concurrencia, reintentos, control de velocidad y pipelines de datos, pensado para scraping recurrente y a escala.

¿Qué hago si el sitio me bloquea?

Primero, revisá si el bloqueo es merecido: bajá la frecuencia, agregá pausas más largas, respetá el robots.txt y usá un User-Agent identificable. En la mayoría de los casos eso lo resuelve. Si el sitio implementó bloqueo activo con CAPTCHAs o detección de bots, es una señal explícita de que no quiere ser scrapeado: la respuesta correcta es buscar una fuente alternativa o pedir acceso formal, no escalar la evasión.

¿Cuánto Python necesito saber para empezar?

Bastante poco: variables, listas, diccionarios, loops for, condicionales y manejo básico de errores con try/except. Con eso podés escribir un scraper funcional. Lo que sí conviene sumar rápido después es pandas, porque la parte de limpiar y estructurar los datos suele llevar más tiempo que la extracción misma.

¿Cómo hago para que el scraper corra automáticamente?

Tres opciones según tu contexto. Un cron en un servidor propio o una máquina virtual es lo más simple. GitHub Actions con un schedule funciona bien y es gratis para volúmenes chicos. Y si querés que además notifique resultados o los cargue en una base, una plataforma de automatización como n8n te deja encadenar el script con el resto del flujo sin escribir la infraestructura desde cero.

Sobre el autor

Francisco Rhaiel

Soy Francisco Rhaiel, AI Growth Engineer en Coderhouse. Mi día a día consiste en automatizar y optimizar procesos aplicando lo último en inteligencia artificial, incluyendo Agentic AI y Gen AI. Soy graduado de la Universidad Torcuato Di Tella (UTDT) en Tecnología Digital, y mi recorrido me llevó a especializarme en la intersección entre tecnología, datos y negocio. Me mueve aprender, construir y aplicar tecnologías innovadoras para resolver problemas reales. Para profundizar en mi trayectoria, te espero en mi perfil de LinkedIn.

Imagen promocionando quiz gratis de Coderhouse para encontrar tu formación.
Imagen promocionando quiz gratis de Coderhouse para encontrar tu formación.