requests + BeautifulSoup frente a una API de scraping: cuándo basta con hacerlo tú mismo

Para sitios estáticos sin anti-bot, requests y BeautifulSoup no cuestan nada y funcionan bien. Aquí está exactamente dónde deja de ser así.

Existe una versión de este post que intenta venderte una API para un problema que requests y BeautifulSoup ya resuelven. Este no es ese post. Si el sitio que estás scrapeando es HTML estático sin anti-bot delante, la vía de hacerlo tú mismo no cuesta nada y funciona bien, y no deberías pagar nada por ello.

Cuándo hacerlo tú mismo es realmente suficiente

Digámoslo claramente: para una página estática, un script y una tarea poco frecuente, esta es toda la solución.

import requests
from bs4 import BeautifulSoup

resp = requests.get("https://example.com/products", timeout=30)
resp.raise_for_status()

soup = BeautifulSoup(resp.text, "html.parser")
for card in soup.select(".product-card"):
    name = card.select_one(".product-name").get_text(strip=True)
    price = card.select_one(".price").get_text(strip=True)
    print(name, price)

Sin dependencias más allá de dos librerías bien conocidas, sin factura de proxy, sin créditos gastados. Esto es correcto para: un sitio que renderiza el HTML completo en el servidor, sin CAPTCHA ni bloqueo por IP tras unas pocas solicitudes, y una tarea que ejecutas de vez en cuando en lugar de continuamente contra miles de URLs. Un script puntual para un proyecto de clase, una herramienta interna que comprueba un puñado de páginas al día, un seguimiento de precios personal en un par de páginas de producto: todo esto es terreno de hacerlo tú mismo, y añadir una API delante es pura sobrecarga.

Dónde deja de funcionar

Cuatro cosas rompen esta configuración, y lo hacen de forma independiente, por eso “simplemente añade proxies” o “simplemente añade un wait()” solo parchea un síntoma cada vez:

Anti-bot. Desafíos de Cloudflare, PerimeterX, rate limiting que escala a un CAPTCHA o directamente a un 403 tras un puñado de solicitudes. requests no tiene fingerprint de navegador, ni un comportamiento de cookies que coincida con una sesión real, ni forma de resolver un desafío. No es un problema de headers que se arregle copiando una cadena User-Agent; los sitios que se preocupan por esto detectan específicamente la ausencia de un navegador real.

Renderizado JS. Si los datos que quieres los inyecta JavaScript después de cargar el HTML inicial (una app React o Vue, contenido cargado mediante una llamada XHR en segundo plano, scroll infinito), resp.text simplemente no los contiene. BeautifulSoup analiza lo que envió el servidor, no lo que renderizaría el navegador. Este es el motivo más común por el que un scraper que funcionaba empieza a devolver resultados vacíos de un día para otro: el sitio no cambió su bloqueo, cambió su renderizado.

Escala. Una docena de páginas al día es un script. Diez mil páginas al día es infraestructura: gestión de concurrency, lógica de reintentos, un pool de proxies dimensionado al volumen de solicitudes, monitorización de la tasa de bloqueos al alza. Nada de eso es difícil por separado, pero juntas son un pequeño trozo de infraestructura que ahora es tuyo y que tienes que mantener en marcha.

Mantenimiento. Los selectores se pudren. Un rediseño del sitio cambia .product-card por .product-tile y tu parser deja de devolver nada, silenciosamente, o peor, devuelve el campo equivocado bajo el nombre correcto. Este es el coste que no aparece la primera semana y que nunca desaparece del todo.

La tabla de decisión

Señal Hacerlo tú mismo (requests + BeautifulSoup) Scraping API
HTML estático, sin bloqueo, volumen bajo Sí Excesivo
Tarea ocasional / puntual Sí Excesivo
El sitio devuelve 403/CAPTCHA tras unas pocas solicitudes No Sí (premium=true)
Contenido cargado por JavaScript tras cargar la página No Sí (dynamic=true)
Miles de páginas, sin supervisión, con calendario Doloroso de mantener Sí
El parser debe sobrevivir a rediseños sin que te enteres Ninguna garantía en ningún caso Los campos JSON estructurados reducen (no eliminan) esto

Cuándo cruzas la línea

En FastAF, /scrape cuesta 1 crédito para una página estática, exactamente el caso anterior pero sin que tengas que ejecutar tú la solicitud:

import requests

API_KEY = "faf_live_..."

resp = requests.get(
    "https://api.fastaf.co/v1/scrape",
    params={"url": "https://example.com/products"},
    headers={"Authorization": f"Bearer {API_KEY}"},
    timeout=60,
)
resp.raise_for_status()

La misma llamada con dynamic=true (5 créditos) renderiza primero la página con un navegador real, de modo que el contenido inyectado por JavaScript está presente en la respuesta. premium=true (10 créditos) enruta a través de una red residencial para sitios que bloquean por reputación de IP. Ambos juntos (25 créditos) cubren una página renderizada en JS que además necesita parecer un visitante residencial real, la combinación más difícil y más cara, y a la que solo hay que recurrir cuando el sitio realmente lo exige.

El sentido de los niveles de créditos es que pagas exactamente por el problema que tienes. Una página estática nunca debería costar más de 1 crédito, y pagar por premium=true en un sitio sin anti-bot es dinero gastado en un problema que no tienes.

En resumen

Empieza con requests y BeautifulSoup. Es gratis y basta para una gran parte de las tareas de scraping. Recurre a una API específicamente cuando choques con uno de los cuatro problemas de arriba, anti-bot, renderizado JS, escala o una carga de mantenimiento que no quieres asumir, y paga por URL exactamente por ese problema en lugar de construir un pool de proxies y un hábito de mantenimiento de parsers alrededor de ello. El plan gratuito te da 500 créditos sin tarjeta si quieres comprobar dónde caen tus objetivos reales en esta tabla.