Cómo construir un web scraper: una guía de Python desde cero
Advanced Data Extraction Specialist
Resumen:
- Un web scraper hace tres cosas: obtener una página, extraer campos del HTML, repetir para la siguiente página. Una vez que esos tres movimientos hacen clic, cada scraper que construyas después es una variación sobre el mismo bucle.
requestsmás BeautifulSoup manejan la web estática en menos de 30 líneas. En el sitio de práctica books.toscrape.com, un bucle recorre las 50 páginas de listado y devuelve 1,000 registros estructurados de libros.- La paginación es solo seguir el enlace "siguiente" hasta que deje de aparecer. Lees el ancla de la página siguiente en cada página, la resuelves a una URL absoluta y sigues hasta que el enlace desaparece.
- HTTP simple no puede ver el contenido renderizado por JavaScript. La página de práctica quotes.toscrape.com/js/ devuelve cero elementos de cita a través de
requests, porque el marcado se pinta del lado del cliente después de que la página carga. - Scrapeless Scraping Browser renderiza esas páginas del lado de la nube y devuelve el DOM pintado. La misma página de JavaScript devuelve 10 elementos de cita una vez que un navegador en la nube lo ejecuta, por lo que tu análisis actual con BeautifulSoup sigue funcionando sin cambios.
- Gratis para empezar. Nuevas cuentas de Scrapeless incluyen tiempo de ejecución gratuito de Scraping Browser: regístrate en app.scrapeless.com.
Introducción: construye un scraper real, luego maneja las páginas que luchan
Cada web scraper hace las mismas tres cosas: obtener una página, extraer los campos que deseas del HTML y pasar a la siguiente página. Las bibliotecas cambian, los sitios cambian, pero ese bucle no lo hace. Apréndelo una vez en una página que sea segura para golpear, y el patrón se transfiere a casi cualquier cosa que raspees después.
La fricción aparece más tarde. El primer scraper que la mayoría de las personas escriben funciona perfectamente en una página estática HTML y luego devuelve una carcasa vacía en el siguiente sitio que intentan, porque ese sitio renderiza su contenido con JavaScript después de la respuesta inicial. HTTP simple descarga los bytes que el servidor envía; no ejecuta la página. Así que los datos que puedes ver en tu navegador no están en la respuesta que recibe tu scraper.
Esta guía construye todo desde cero en Python. La capa estática utiliza requests y BeautifulSoup contra un sandbox de scraping de catálogo de libros y un sandbox de scraping de citas — dos sitios que existen específicamente para ser raspeados. Luego cambia al límite honesto: cuando HTTP simple devuelve una página vacía, el trabajo escala a Scrapeless Scraping Browser, un navegador en la nube que renderiza la página y devuelve el mismo DOM que podrías analizar localmente. Si prefieres trabajar en JavaScript, la misma división entre estático y dinámico está cubierta en el tutorial de Cheerio y Puppeteer.
Lo Que Puedes Hacer Con Esto
- Recopilar catálogos de productos. Recorrer un listado paginado y extraer título, precio y estado de stock en un archivo CSV o JSON.
- Hacer seguimiento de precios a lo largo del tiempo. Volver a ejecutar el mismo scraper en un horario y calcular las diferencias para observar caídas.
- Construir conjuntos de datos para análisis o IA. Convertir páginas de HTML desestructurado en filas limpias que tu cuaderno o modelo pueden leer.
- Monitorear disponibilidad. Comprobar si los artículos están en stock en muchas páginas sin hacer clic a través de ellas manualmente.
- Alimentar un pipeline de investigación. Extraer citas, artículos o reseñas en un almacenamiento estructurado para procesamiento posterior.
- Acceder a páginas renderizadas por JavaScript. Escalar las páginas que devuelven una carcasa vacía sobre HTTP simple a un navegador en la nube y mantener el mismo código de análisis.
Por qué Scrapeless Scraping Browser
Scrapeless Scraping Browser es un navegador en la nube personalizable y anti-detección diseñado para rastreadores web y agentes de IA. Para un scraper desde cero específicamente, resuelve el único problema que requests no puede: ejecutar la página — y trae:
- Renderizado de JavaScript del lado de la nube. Las páginas que pintan su contenido del lado del cliente devuelven un DOM completamente renderizado, por lo que BeautifulSoup analiza el resultado exactamente como lo haría con una página estática.
- Proxies residenciales en más de 195 países. Fija la geografía de salida con un código de país para que una página devuelva el mismo contenido que vería un visitante local.
- Huella de anti-detección. El navegador en la nube presenta una superficie de navegador consistente y similar a la humana en lugar de una simple firma de cliente HTTP.
- Una clave API para todo. El SDK de Python genera un
browser_ws_endpointal que te conectas con Playwright; la misma clave cubre el tiempo de ejecución.
Obtén tu clave API en el plan gratuito en app.scrapeless.com.
Requisitos Previos
- Python 3.10 o más reciente
requestsybeautifulsoup4para la capa estática- El SDK de
scrapelessyplaywrightpara la capa renderizada por JavaScript - Una cuenta de Scrapeless y una clave de API — regístrate en app.scrapeless.com
- Familiaridad básica con la terminal
Instalar
Instala primero las dos bibliotecas de nivel estático:
bash
pip install requests beautifulsoup4
requests obtiene páginas a través de HTTP; beautifulsoup4 analiza el HTML devuelto — marcado definido por el estándar HTML — en un árbol que puedes consultar con selectores CSS. Ese par es suficiente para cada página estática en los Pasos 1 a 4. La capa renderizada por JavaScript en el Paso 5 agrega dos paquetes más, instalados allí.
Paso 1 — Obtener una página y confirmar que obtuviste HTML real
Un scraper comienza con una solicitud. Envía un GET a la URL objetivo siguiendo la semántica HTTP, verifica el código de estado y confirma que el HTML realmente contiene los registros que esperas antes de escribir un solo selector.
python
import requests
from bs4 import BeautifulSoup
url = "http://books.toscrape.com/"
resp = requests.get(url, headers={"User-Agent": "Mozilla/5.0 (tutorial-scraper)"}, timeout=30)
print("estado:", resp.status_code, "bytes:", len(resp.text))
soup = BeautifulSoup(resp.text, "html.parser")
books = soup.select("article.product_pod")
print("tarjetas de libros en esta página:", len(books))
Contra books.toscrape.com esto imprime estado: 200 y tarjetas de libros en esta página: 20. La cabecera User-Agent identifica tu cliente; muchos servidores rechazan solicitudes que no envían ninguna. Si el conteo regresa como cero en un sitio real, esa es la primera señal de que la página está renderizada por JavaScript — la situación que maneja el Paso 5.
Paso 2 — Descubrir el registro, luego extraer sus campos
Primero elige el contenedor repetitivo y luego lee los campos dentro de él. Prefiere un gancho estable — una etiqueta semántica, un atributo data-* o una clase que describa los datos — sobre una clase hash que se rompa en el siguiente rediseño. En books.toscrape.com cada libro es un article.product_pod, y los campos cuelgan de selectores hijos predecibles.
python
import requests
from bs4 import BeautifulSoup
resp = requests.get(
"http://books.toscrape.com/",
headers={"User-Agent": "Mozilla/5.0 (tutorial-scraper)"},
timeout=30,
)
resp.encoding = resp.apparent_encoding # el catálogo usa £; deja que requests lo detecte
soup = BeautifulSoup(resp.text, "html.parser")
pod = soup.select_one("article.product_pod")
title = pod.h3.a["title"]
price = pod.select_one("p.price_color").get_text(strip=True)
in_stock = "En stock" in pod.select_one("p.instock.availability").get_text()
print(title, "|", price, "|", "en stock" if in_stock else "agotado")
Esto imprime Una luz en el ático | £51.77 | en stock. La línea resp.encoding = resp.apparent_encoding es importante: sin ella, el signo de la libra se decodifica como mojibake, porque los encabezados de respuesta y la codificación real del documento no coinciden. Lee el campo del elemento que ya lo sostiene — el título vive en el atributo title del enlace, por lo que un selector hace tanto el descubrimiento como la extracción.
Paso 3 — Seguir la paginación hasta que el enlace "siguiente" desaparezca
Una lista rara vez cabe en una sola página. El patrón confiable es leer el enlace a la siguiente página en cada página, resolverlo contra la URL actual y repetir hasta que no quede ningún enlace siguiente. Sin contador de páginas para adivinar, sin rango codificado para mantener.
python
import requests
from urllib.parse import urljoin
from bs4 import BeautifulSoup
session = requests.Session()
session.headers.update({"User-Agent": "Mozilla/5.0 (tutorial-scraper)"})
records = []
url = "http://books.toscrape.com/catalogue/page-1.html"
pages = 0
while url:
resp = session.get(url, timeout=30)
resp.encoding = resp.apparent_encoding
soup = BeautifulSoup(resp.text, "html.parser")
for pod in soup.select("article.product_pod"):
records.append({
"title": pod.h3.a["title"],
"price": pod.select_one("p.price_color").get_text(strip=True),
"in_stock": "En stock" in pod.select_one("p.instock.availability").get_text(),
})
pages += 1
next_link = soup.select_one("li.next > a")
url = urljoin(url, next_link["href"]) if next_link else None
print("páginas rastreadas:", pages, "| registros recolectados:", len(records))
En books.toscrape.com esto recorre todas las 50 páginas de listado y recolecta 1,000 registros. Una requests.Session reutiliza la conexión subyacente a través de las solicitudes, lo que es más rápido que un fresco requests.get cada vez. urljoin convierte el href relativo (como page-2.html) en una URL absoluta para que la siguiente solicitud se resuelva correctamente.
Obtén tu clave de API en el plan gratuito: app.scrapeless.com
Paso 4 — Escribe la salida estructurada
Los datos extraídos solo son útiles una vez que están estructurados. La lista records del Paso 3 ya es una lista de diccionarios con claves consistentes, así que escribirla en CSV o JSON son unas pocas líneas. Decide tu esquema de antemano — las mismas claves en cada fila — y los campos ausentes se convierten en un None, nunca en un fallo.
python
import csv
import json
# records es la lista de diccionarios construida en el Paso 3.
with open("books.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["title", "price", "in_stock"])
writer.writeheader()
writer.writerows(records)
with open("books.json", "w", encoding="utf-8") as f:
json.dump(records, f, ensure_ascii=False, indent=2)
print("escribió", len(records), "filas en books.csv y books.json")
Ese es un scraper completo y funcional: obtener, descubrir, extraer, paginar, almacenar. Manejará cualquier sitio estático HTML con los selectores intercambiados por el marcado de ese sitio.
Donde HTTP simple se detiene: páginas renderizadas con JavaScript
El scraper anterior se rompe en el momento en que un sitio genera su contenido con JavaScript. La página de práctica quotes.toscrape.com/js/ está construida precisamente para esta lección: las citas son inyectadas por un script después de que la página se carga, no presentes en el HTML inicial.
python
import requests
from bs4 import BeautifulSoup
resp = requests.get("https://quotes.toscrape.com/js/",
headers={"User-Agent": "Mozilla/5.0"}, timeout=30)
soup = BeautifulSoup(resp.text, "html.parser")
print("bytes:", len(resp.text), "| elementos de cita:", len(soup.select("div.quote")))
Esto devuelve bytes: 5806 y elementos de cita: 0. La solicitud tuvo éxito — estado 200, bytes reales — pero la página analizada no tiene citas, porque requests nunca ejecuta el JavaScript que las crea. Ningún ajuste de selector soluciona esto; los datos realmente no están en la respuesta. La página debe ser ejecutada por un navegador.
Paso 5 — Renderiza la página con Scrapeless Scraping Browser
Ejecutar un navegador completo localmente para cada página es pesado y se bloquea rápidamente. El camino más limpio es un navegador en la nube: Scrapeless crea una sesión, renderiza la página en su lado y devuelve el DOM terminado — el cual puedes analizar con el mismo código de BeautifulSoup que ya escribiste. Instala los dos paquetes adicionales y luego conéctate con Playwright a través del Protocolo DevTools de Chrome.
bash
pip install scrapeless playwright
playwright install chromium
playwright install chromium descarga un cliente de protocolo local una sola vez; la renderización real aún se ejecuta en la nube de Scrapeless. Exporta tu clave primero (export SCRAPELESS_API_KEY=tu_token_api_aquí), luego:
python
from scrapeless import Scrapeless
from scrapeless.types import ICreateBrowser
from playwright.sync_api import sync_playwright
from bs4 import BeautifulSoup
client = Scrapeless() # lee SCRAPELESS_API_KEY del entorno
session = client.browser.create(
ICreateBrowser(proxy_country="US", session_ttl=180)
)
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(session.browser_ws_endpoint)
ctx = browser.contexts[0] if browser.contexts else browser.new_context()
page = ctx.pages[0] if ctx.pages else ctx.new_page()
page.goto("https://quotes.toscrape.com/js/", wait_until="domcontentloaded", timeout=60_000)
page.wait_for_timeout(4_000) # deja que el script del lado del cliente pinte las citas
html = page.content()
browser.close()
soup = BeautifulSoup(html, "html.parser")
quotes = soup.select("div.quote")
print("bytes renderizados:", len(html), "| elementos de cita:", len(quotes))
print(quotes[0].select_one("small.author").get_text(strip=True))
Esto devuelve bytes renderizados: 9246 y elementos de cita: 10, y imprime Albert Einstein — la misma página que te dio cero sobre HTTP simple. session.browser_ws_endpoint es un URL wss://browser.scrapeless.com/...; connect_over_cdp de Playwright habla el Protocolo DevTools a ella, así que la página se ejecuta en la nube mientras tu código se ejecuta localmente. wait_until="domcontentloaded" más un corto asentamiento fijo es más confiable que esperar por la inactividad de red en una página impulsada por scripts. La documentación del SDK y la interfaz de línea de comandos está disponible en docs.scrapeless.com.
Lo que obtienes de vuelta
Una vez que el navegador en la nube devuelve HTML renderizado, el análisis es idéntico al nivel estático — mismos selectores, misma forma de diccionario:
json
[
{
"text": "El mundo tal como lo hemos creado es un proceso de nuestro pensamiento.",
"author": "Albert Einstein",
"tags": ["cambio", "pensamientos profundos", "pensar", "mundo"]
}
]
// El esquema refleja exactamente lo que emite el análisis de div.quote. Los valores son ejemplos ilustrativos.
Algunas observaciones honestas sobre las páginas renderizadas:
- El tiempo importa en las páginas renderizadas. Las citas aparecen solo después de que se ejecuta el script de la página, por lo que el corto asentamiento después de
domcontentloadedes lo que las hace presentes. Demasiado corto y parses una página vacía. - Los selectores todavía se desvían.
div.quote,span.textysmall.authorson el marcado de este sandbox; en un sitio real, vuelve a verificar los selectores cuando cambie la página. - Los campos ausentes son normales. Un registro puede carecer de una etiqueta o un precio; trata cada campo como anulable y tu escritor seguirá produciendo filas limpias.
- Fija tu salida.
proxy_country="US"mantiene el contenido renderizado consistente con un visitante de EE. UU.; cambia el código para que coincida con la región que necesitas.
Conclusión: de una página a un pipeline real
Un scraper que funcione se reduce a cuatro movimientos: obtener la página, descubrir el registro repetido, extraer sus campos y seguir la paginación hasta que termine. Eso cubre la web estática con requests y BeautifulSoup. El único caso que no puede alcanzar — páginas que construyen su contenido con JavaScript — se eleva limpiamente a Scrapeless Scraping Browser, que renderiza la página en la nube y devuelve el mismo DOM que tu parser ya entiende.
A partir de aquí, escálalo de la misma manera que escala cualquier scraper de producción: mantén tus selectores ajustados y vuelve a verificarlos cuando el marcado cambie, fija la geografía de salida para coincidir con la audiencia que la página sirve, trata los campos ausentes como anulables y recurre al navegador en la nube solo en las páginas que realmente necesitan renderización. Para la misma división en JavaScript, la guía de Cheerio y Puppeteer aborda la decisión estática versus dinámica en Node.js. Compara las opciones de tiempo de ejecución en la página de precios cuando estés listo para ejecutarlo a gran volumen.
¿Listo para construir tu pipeline de datos impulsado por IA?
Únete a nuestra comunidad para reclamar un plan gratuito y conectarte con desarrolladores que están construyendo pipelines de scraping: Discord · Telegram.
Regístrate en app.scrapeless.com para obtener tiempo de ejecución gratuito de Scraping Browser y adapta los patrones anteriores a los sitios, páginas y regiones que tu scraper necesita.
Preguntas Frecuentes
P: ¿Es legal el web scraping?
Scraping de datos públicamente visibles está ampliamente permitido en muchas jurisdicciones, pero las reglas varían según el país y el sitio. Revisa los Términos de Servicio del sitio objetivo y el Protocolo de Exclusión de Robots, evita datos personales o restringidos, y consulta con un abogado para cualquier cosa comercial. Los sitios de práctica en esta guía existen específicamente para ser raspados.
P: ¿Necesito un proxy para construir un scraper?
Para un scraper de bajo volumen contra un sitio permisivo, no. Para sitios que limitan tasas por IP o varían contenido por región, sí — dirígete a través de proxies residenciales y fija el país para que la página devuelva el contenido que vería un visitante local. La categoría de Scraping Browser en el Paso 5 incluye salida por proxy con la configuración proxy_country.
P: ¿Por qué mi scraper devuelve una página vacía?
Casi siempre porque la página renderiza su contenido con JavaScript después de la respuesta inicial. requests simple descarga los bytes del servidor, pero nunca ejecuta los scripts de la página, por lo que los datos que ves en un navegador no están en la respuesta. Confirma imprimiendo el recuento de elementos, como en la sección "Dónde se detiene HTTP normal", luego renderiza la página con un navegador en la nube.
P: ¿Cómo manejo un sitio que muestra "Acceso Denegado" o una página de desafío?
Eso es un control anti-bot en la solicitud. Renderiza la página a través de Scrapeless Scraping Browser con salida residencial de EE. UU. fijada, y calienta la sesión cargando primero la página de inicio del sitio en la misma sesión del navegador antes de llegar a la página objetivo, de modo que la solicitud lleve una superficie de navegador consistente y similar a la humana.
P: Mis selectores dejaron de funcionar después de un rediseño — ¿qué hago ahora?
El marcado rota. Vuelve a inspeccionar la página, prefiere ganchos estables (etiquetas semánticas, atributos data-*, clases que describen los datos) sobre clases CSS encriptadas, y actualiza los selectores. Construir contra el gancho más duradero disponible mantiene el scraper vivo por más tiempo entre correcciones.
P: ¿Cuántas páginas puedo raspar en paralelo?
Mantén la concurrencia modesta — alrededor de tres trabajadores por host es un techo razonable para un solo sitio, para que te mantengas dentro de tasas de solicitudes educadas y evites activar límites de tasa. Las sesiones de navegador en la nube son más escasas que las solicitudes HTTP, así que limita esas más que tus descargas estáticas.
P: ¿Tengo que usar un navegador para todo?
No, y no deberías. El nivel estático con requests y BeautifulSoup es más rápido y más barato, así que úsalo para cada página que envíe HTML renderizado. Escala al navegador en la nube solo para las páginas que realmente necesitan la ejecución de JavaScript.
En Scrapeless, solo accedemos a datos disponibles públicamente y cumplimos estrictamente con las leyes, regulaciones y políticas de privacidad del sitio web aplicables. El contenido de este blog es sólo para fines de demostración y no implica ninguna actividad ilegal o infractora. No ofrecemos garantías y renunciamos a toda responsabilidad por el uso de la información de este blog o enlaces de terceros. Antes de realizar cualquier actividad de scraping, consulte a su asesor legal y revise los términos de servicio del sitio web de destino u obtenga los permisos necesarios.



