Proyecto: Web Scraping con BeautifulSoup y Requests en Python

👤 Admin 📅 27 de octubre, 2025 ⏱ 19 min 🏷 Proyectos Python

¿Qué es Web Scraping?

Web scraping es la técnica de extraer datos de sitios web de forma automatizada. Es útil para recopilar información, analizar precios, monitorear contenido y crear datasets para análisis.

Instalación de Bibliotecas

# Instalar bibliotecas necesarias
pip install requests
pip install beautifulsoup4
pip install lxml
pip install pandas  # Opcional, para guardar datos

Conceptos Básicos

Hacer una Request

import requests

# GET request simple
url = 'https://example.com'
respuesta = requests.get(url)

# Verificar status code
print(respuesta.status_code)  # 200 = OK

# Obtener contenido HTML
html = respuesta.text
print(html[:200])  # Primeros 200 caracteres

# Headers
print(respuesta.headers)

# Encoding
print(respuesta.encoding)

Parsear HTML con BeautifulSoup

from bs4 import BeautifulSoup
import requests

# Obtener página
url = 'https://example.com'
respuesta = requests.get(url)

# Crear objeto BeautifulSoup
soup = BeautifulSoup(respuesta.text, 'lxml')

# Ver HTML formateado
print(soup.prettify()[:500])

Seleccionar Elementos

Por Etiqueta

# Primer elemento
titulo = soup.find('h1')
print(titulo.text)

# Todos los elementos
parrafos = soup.find_all('p')
for p in parrafos:
    print(p.text)

# Limitar cantidad
primeros_3 = soup.find_all('p', limit=3)

Por Clase

# Clase específica
elementos = soup.find_all(class_='nombre-clase')

# Múltiples clases
elementos = soup.find_all(class_=['clase1', 'clase2'])

# Con find
elemento = soup.find('div', class_='contenedor')

Por ID

# Por ID
elemento = soup.find(id='mi-id')

# Con atributos
elemento = soup.find('div', {'id': 'mi-id'})

Selectores CSS

# Select (todos los elementos)
elementos = soup.select('.clase')
elementos = soup.select('#id')
elementos = soup.select('div.clase')
elementos = soup.select('div > p')  # hijos directos
elementos = soup.select('div p')    # descendientes

# Select_one (primer elemento)
elemento = soup.select_one('.clase')

Extraer Datos

Texto

# Obtener texto
elemento = soup.find('h1')
texto = elemento.text
# o
texto = elemento.get_text()

# Limpiar espacios
texto = elemento.get_text(strip=True)

# Con separador personalizado
texto = elemento.get_text(separator=' | ')

Atributos

# Obtener atributo
enlace = soup.find('a')
href = enlace.get('href')
# o
href = enlace['href']

# Verificar si existe atributo
if enlace.has_attr('href'):
    print(enlace['href'])

# Múltiples atributos
print(enlace.attrs)  # Diccionario con todos

Enlaces e Imágenes

# Extraer todos los enlaces
enlaces = soup.find_all('a')
for enlace in enlaces:
    texto = enlace.text
    url = enlace.get('href')
    print(f"{texto}: {url}")

# Extraer todas las imágenes
imagenes = soup.find_all('img')
for img in imagenes:
    src = img.get('src')
    alt = img.get('alt', 'Sin descripción')
    print(f"Imagen: {alt} - {src}")

Navegación del Árbol HTML

# Padres
elemento = soup.find('p')
padre = elemento.parent
print(padre.name)  # Nombre de etiqueta del padre

# Hijos
div = soup.find('div')
hijos = div.children  # Generador
lista_hijos = list(div.children)

# Hermanos
elemento = soup.find('p')
siguiente = elemento.next_sibling
anterior = elemento.previous_sibling

# Todos los hermanos
hermanos = elemento.find_next_siblings()

Proyecto 1: Scraper de Noticias

import requests
from bs4 import BeautifulSoup
import pandas as pd
from datetime import datetime

class ScraperNoticias:
    def __init__(self, url):
        self.url = url
        self.noticias = []
    
    def obtener_pagina(self):
        try:
            headers = {
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
            }
            respuesta = requests.get(self.url, headers=headers, timeout=10)
            respuesta.raise_for_status()
            return respuesta.text
        except requests.exceptions.RequestException as e:
            print(f"Error al obtener página: {e}")
            return None
    
    def extraer_noticias(self, html):
        soup = BeautifulSoup(html, 'lxml')
        
        # Ajusta estos selectores según el sitio
        articulos = soup.find_all('article', class_='noticia')
        
        for articulo in articulos:
            try:
                titulo = articulo.find('h2', class_='titulo').text.strip()
                resumen = articulo.find('p', class_='resumen').text.strip()
                enlace = articulo.find('a')['href']
                fecha = articulo.find('time', class_='fecha').text.strip()
                
                noticia = {
                    'titulo': titulo,
                    'resumen': resumen,
                    'enlace': enlace,
                    'fecha': fecha,
                    'fecha_scraping': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
                }
                
                self.noticias.append(noticia)
            
            except AttributeError as e:
                print(f"Error extrayendo noticia: {e}")
                continue
    
    def guardar_csv(self, nombre_archivo='noticias.csv'):
        if self.noticias:
            df = pd.DataFrame(self.noticias)
            df.to_csv(nombre_archivo, index=False, encoding='utf-8')
            print(f"Guardadas {len(self.noticias)} noticias en {nombre_archivo}")
        else:
            print("No hay noticias para guardar")
    
    def ejecutar(self):
        print("Obteniendo página...")
        html = self.obtener_pagina()
        
        if html:
            print("Extrayendo noticias...")
            self.extraer_noticias(html)
            print(f"Extraídas {len(self.noticias)} noticias")
            
            self.guardar_csv()
            return self.noticias
        
        return []

# Usar el scraper
if __name__ == "__main__":
    url = 'https://ejemplo-noticias.com'
    scraper = ScraperNoticias(url)
    noticias = scraper.ejecutar()
    
    # Mostrar primeras 5
    for noticia in noticias[:5]:
        print(f"\nTítulo: {noticia['titulo']}")
        print(f"Resumen: {noticia['resumen'][:100]}...")

Proyecto 2: Scraper de Precios E-commerce

import requests
from bs4 import BeautifulSoup
import json
import time

class ScraperPrecios:
    def __init__(self):
        self.productos = []
        self.headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
        }
    
    def extraer_producto(self, url):
        try:
            respuesta = requests.get(url, headers=self.headers, timeout=10)
            soup = BeautifulSoup(respuesta.text, 'lxml')
            
            # Ajusta selectores según el sitio
            nombre = soup.find('h1', class_='producto-nombre').text.strip()
            
            # Limpiar precio (ejemplo: "$1,299.99" -> 1299.99)
            precio_texto = soup.find('span', class_='precio').text
            precio = float(precio_texto.replace('$', '').replace(',', ''))
            
            disponible = soup.find('span', class_='disponibilidad').text.strip()
            
            # Extraer imágenes
            imagenes = [img['src'] for img in soup.find_all('img', class_='producto-img')]
            
            # Especificaciones
            specs_div = soup.find('div', class_='especificaciones')
            especificaciones = {}
            if specs_div:
                items = specs_div.find_all('li')
                for item in items:
                    key, value = item.text.split(':', 1)
                    especificaciones[key.strip()] = value.strip()
            
            producto = {
                'nombre': nombre,
                'precio': precio,
                'disponible': disponible,
                'url': url,
                'imagenes': imagenes,
                'especificaciones': especificaciones,
                'fecha': datetime.now().isoformat()
            }
            
            return producto
        
        except Exception as e:
            print(f"Error extrayendo producto: {e}")
            return None
    
    def extraer_lista_productos(self, url_categoria):
        respuesta = requests.get(url_categoria, headers=self.headers)
        soup = BeautifulSoup(respuesta.text, 'lxml')
        
        # Enlaces de productos
        enlaces = soup.find_all('a', class_='producto-link')
        urls_productos = [enlace['href'] for enlace in enlaces]
        
        for url in urls_productos:
            print(f"Extrayendo: {url}")
            producto = self.extraer_producto(url)
            
            if producto:
                self.productos.append(producto)
            
            # Esperar entre requests (ser respetuoso)
            time.sleep(2)
        
        return self.productos
    
    def guardar_json(self, nombre_archivo='productos.json'):
        with open(nombre_archivo, 'w', encoding='utf-8') as f:
            json.dump(self.productos, f, ensure_ascii=False, indent=4)
        print(f"Guardados {len(self.productos)} productos en {nombre_archivo}")
    
    def comparar_precios(self):
        if not self.productos:
            return
        
        # Ordenar por precio
        ordenados = sorted(self.productos, key=lambda x: x['precio'])
        
        print("\n=== COMPARACIÓN DE PRECIOS ===")
        for producto in ordenados:
            print(f"{producto['nombre']}: ${producto['precio']:.2f}")

# Usar
scraper = ScraperPrecios()
url_categoria = 'https://ejemplo-tienda.com/laptops'
scraper.extraer_lista_productos(url_categoria)
scraper.guardar_json()
scraper.comparar_precios()

Manejo de Diferentes Formatos

Tablas HTML

# Extraer tabla
tabla = soup.find('table', class_='datos')

# Obtener encabezados
encabezados = [th.text.strip() for th in tabla.find_all('th')]

# Obtener filas
filas = []
for tr in tabla.find_all('tr')[1:]:  # Saltar encabezado
    celdas = [td.text.strip() for td in tr.find_all('td')]
    filas.append(celdas)

# Crear DataFrame
import pandas as pd
df = pd.DataFrame(filas, columns=encabezados)
print(df)

Listas

# Lista ordenada
ol = soup.find('ol')
items = [li.text.strip() for li in ol.find_all('li')]

# Lista desordenada
ul = soup.find('ul', class_='menu')
items = [li.text.strip() for li in ul.find_all('li')]

Manejo de Paginación

def scrape_todas_paginas(url_base):
    pagina = 1
    todos_resultados = []
    
    while True:
        url = f"{url_base}?page={pagina}"
        print(f"Scraping página {pagina}...")
        
        respuesta = requests.get(url, headers=headers)
        soup = BeautifulSoup(respuesta.text, 'lxml')
        
        # Extraer datos de la página
        items = soup.find_all('div', class_='item')
        
        if not items:
            break  # No hay más páginas
        
        for item in items:
            # Extraer datos
            datos = extraer_datos(item)
            todos_resultados.append(datos)
        
        # Verificar botón "siguiente"
        siguiente = soup.find('a', class_='siguiente')
        if not siguiente:
            break
        
        pagina += 1
        time.sleep(2)  # Respetar el servidor
    
    return todos_resultados

Headers y User-Agent

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
    'Accept-Language': 'es-ES,es;q=0.9,en;q=0.8',
    'Referer': 'https://www.google.com/',
    'DNT': '1'
}

respuesta = requests.get(url, headers=headers)

Manejo de Errores

import requests
from bs4 import BeautifulSoup
import time

def scrape_con_reintentos(url, max_intentos=3):
    for intento in range(max_intentos):
        try:
            respuesta = requests.get(url, headers=headers, timeout=10)
            respuesta.raise_for_status()
            
            soup = BeautifulSoup(respuesta.text, 'lxml')
            return soup
        
        except requests.exceptions.Timeout:
            print(f"Timeout en intento {intento + 1}")
        
        except requests.exceptions.HTTPError as e:
            print(f"Error HTTP: {e}")
            if respuesta.status_code == 404:
                return None  # Página no existe
        
        except requests.exceptions.RequestException as e:
            print(f"Error de conexión: {e}")
        
        # Esperar antes de reintentar
        if intento < max_intentos - 1:
            time.sleep(5)
    
    return None

Guardar Imágenes

import os
import requests
from urllib.parse import urlparse

def descargar_imagenes(soup, carpeta='imagenes'):
    # Crear carpeta si no existe
    if not os.path.exists(carpeta):
        os.makedirs(carpeta)
    
    imagenes = soup.find_all('img')
    
    for i, img in enumerate(imagenes):
        src = img.get('src')
        if not src:
            continue
        
        # URL completa
        if not src.startswith('http'):
            src = f"https://ejemplo.com{src}"
        
        try:
            respuesta = requests.get(src, timeout=10)
            
            # Nombre de archivo
            nombre = f"imagen_{i}.jpg"
            ruta = os.path.join(carpeta, nombre)
            
            # Guardar
            with open(ruta, 'wb') as f:
                f.write(respuesta.content)
            
            print(f"Descargada: {nombre}")
        
        except Exception as e:
            print(f"Error descargando {src}: {e}")

Rate Limiting

import time
from datetime import datetime

class RateLimiter:
    def __init__(self, requests_per_second=1):
        self.delay = 1.0 / requests_per_second
        self.last_request = 0
    
    def wait(self):
        now = time.time()
        tiempo_desde_ultima = now - self.last_request
        
        if tiempo_desde_ultima < self.delay:
            time.sleep(self.delay - tiempo_desde_ultima)
        
        self.last_request = time.time()

# Usar
limiter = RateLimiter(requests_per_second=2)

for url in urls:
    limiter.wait()
    respuesta = requests.get(url)
    # Procesar respuesta...

Logging

import logging

# Configurar logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('scraper.log'),
        logging.StreamHandler()
    ]
)

logger = logging.getLogger(__name__)

def scrape_con_logging(url):
    logger.info(f"Iniciando scraping de {url}")
    
    try:
        respuesta = requests.get(url)
        logger.info(f"Status code: {respuesta.status_code}")
        
        # Procesar...
        logger.info("Scraping completado exitosamente")
    
    except Exception as e:
        logger.error(f"Error en scraping: {e}")

Consideraciones Éticas y Legales

  • robots.txt: Verifica las reglas del sitio
  • Terms of Service: Lee los términos de uso
  • Rate limiting: No sobrecargues el servidor
  • User-Agent: Identifícate apropiadamente
  • Respeta el copyright: No uses datos protegidos
  • APIs oficiales: Úsalas cuando estén disponibles

Robots.txt

from urllib.robotparser import RobotFileParser

def puede_scrapear(url):
    robot_parser = RobotFileParser()
    robot_parser.set_url(f"{url}/robots.txt")
    robot_parser.read()
    
    user_agent = 'MiScraper'
    puede = robot_parser.can_fetch(user_agent, url)
    
    return puede

# Verificar
url = 'https://ejemplo.com/pagina'
if puede_scrapear(url):
    print("Permitido scrapear")
else:
    print("No permitido")

Buenas Prácticas

  • Usa User-Agent: Identifica tu scraper
  • Respeta delays: Espera entre requests
  • Maneja errores: Implementa reintentos
  • Guarda progreso: Por si falla a mitad
  • Valida datos: Verifica que sean correctos
  • Logging: Registra todas las operaciones
  • Testing: Prueba con pocas páginas primero

Conclusión

Web scraping es una habilidad poderosa para recopilar datos. BeautifulSoup y Requests hacen que sea relativamente simple extraer información de sitios web. Recuerda siempre respetar las reglas del sitio, no sobrecargar servidores y usar los datos de manera ética y legal.