Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Proyecto 3: Web Scraping Básico

🧭 Navegación:

🌐 El problema: Extraer datos de la web

Internet es una fuente inagotable de información, pero muchas veces los datos que necesitamos no están disponibles en un formato fácil de procesar. Quizás quieras:

  • Monitorear precios de productos en tiendas online
  • Recopilar noticias sobre un tema específico
  • Extraer información de contacto de directorios
  • Obtener datos meteorológicos o financieros
  • Recopilar estadísticas deportivas

Copiar esta información manualmente sería extremadamente tedioso y propenso a errores. Aquí es donde entra el web scraping: la técnica de extraer automáticamente datos de sitios web.

🎯 Objetivo del proyecto

Desarrollaremos un script de Python que:

  1. Acceda a páginas web de forma automática
  2. Extraiga información específica usando selectores HTML
  3. Procese y limpie los datos obtenidos
  4. Guarde la información en formatos útiles (CSV, JSON)
  5. Respete las políticas de los sitios web (robots.txt, límites de velocidad)
  6. Pueda programarse para ejecutarse periódicamente

⚠️ Consideraciones éticas y legales

Antes de comenzar, es importante entender algunas consideraciones:

  • Términos de servicio: Muchos sitios prohíben el scraping en sus términos de uso
  • robots.txt: Archivo que indica qué partes de un sitio pueden ser rastreadas
  • Límites de velocidad: Hacer demasiadas peticiones puede sobrecargar servidores
  • Datos personales: Extraer información personal puede violar leyes de privacidad
  • Derechos de autor: El contenido puede estar protegido por copyright

Para este proyecto, usaremos sitios que permiten explícitamente el scraping o sitios de ejemplo creados con este propósito.

📋 Planificación del sistema

Componentes principales:

  1. Solicitud HTTP: Obtener el contenido HTML de la página
  2. Parsing HTML: Analizar la estructura y extraer datos específicos
  3. Procesamiento: Limpiar y estructurar la información obtenida
  4. Almacenamiento: Guardar los datos en formatos útiles
  5. Programación: Automatizar la ejecución periódica

Bibliotecas que utilizaremos:

import requests         # Para realizar peticiones HTTP
from bs4 import BeautifulSoup  # Para analizar HTML
import pandas as pd     # Para manipular datos estructurados
import json             # Para manejar formato JSON
import csv              # Para manejar archivos CSV
import time             # Para pausas entre peticiones
import logging          # Para registrar eventos
import schedule         # Para programar ejecuciones

💻 Implementación paso a paso

Paso 1: Configuración inicial y funciones de utilidad

# web_scraper.py

import requests
from bs4 import BeautifulSoup
import pandas as pd
import json
import csv
import time
import logging
import os
from datetime import datetime
from urllib.parse import urlparse

class WebScraper:
    """Clase base para web scraping."""
    
    def __init__(self, base_url, output_dir="./data", delay=2):
        """
        Inicializa el scraper.
        
        Args:
            base_url: URL base del sitio a scrapear
            output_dir: Directorio donde se guardarán los datos
            delay: Tiempo de espera entre peticiones (segundos)
        """
        self.base_url = base_url
        self.output_dir = output_dir
        self.delay = delay
        
        # Extraer el dominio para nombrar archivos
        parsed_url = urlparse(base_url)
        self.domain = parsed_url.netloc
        
        # Crear directorio de salida si no existe
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)
        
        # Configurar logging
        log_file = os.path.join(output_dir, f"{self.domain}_scraper.log")
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s - %(levelname)s - %(message)s',
            handlers=[
                logging.FileHandler(log_file),
                logging.StreamHandler()
            ]
        )
        self.logger = logging.getLogger()
        
        # Headers para simular un navegador
        self.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.8,en-US;q=0.5,en;q=0.3',
        }
        
        self.logger.info(f"Scraper inicializado para: {base_url}")
    
    def fetch_page(self, url):
        """
        Obtiene el contenido HTML de una página.
        
        Args:
            url: URL de la página a obtener
            
        Returns:
            BeautifulSoup: Objeto con el contenido parseado o None si hay error
        """
        try:
            # Verificar si la URL es relativa
            if not url.startswith('http'):
                url = f"{self.base_url.rstrip('/')}/{url.lstrip('/')}"
            
            self.logger.info(f"Obteniendo página: {url}")
            
            # Realizar la petición HTTP
            response = requests.get(url, headers=self.headers)
            
            # Verificar si la petición fue exitosa
            if response.status_code == 200:
                # Parsear el HTML
                soup = BeautifulSoup(response.text, 'html.parser')
                
                # Esperar para no sobrecargar el servidor
                time.sleep(self.delay)
                
                return soup
            else:
                self.logger.error(f"Error al obtener {url}: Código {response.status_code}")
                return None
                
        except Exception as e:
            self.logger.error(f"Error al obtener {url}: {str(e)}")
            return None
    
    def save_to_csv(self, data, filename):
        """
        Guarda datos en formato CSV.
        
        Args:
            data: Lista de diccionarios con los datos
            filename: Nombre del archivo (sin extensión)
        """
        if not data:
            self.logger.warning("No hay datos para guardar en CSV")
            return
        
        filepath = os.path.join(self.output_dir, f"{filename}.csv")
        
        try:
            # Convertir a DataFrame y guardar
            df = pd.DataFrame(data)
            df.to_csv(filepath, index=False, encoding='utf-8')
            self.logger.info(f"Datos guardados en CSV: {filepath}")
        except Exception as e:
            self.logger.error(f"Error al guardar CSV: {str(e)}")
    
    def save_to_json(self, data, filename):
        """
        Guarda datos en formato JSON.
        
        Args:
            data: Datos a guardar (lista o diccionario)
            filename: Nombre del archivo (sin extensión)
        """
        if not data:
            self.logger.warning("No hay datos para guardar en JSON")
            return
        
        filepath = os.path.join(self.output_dir, f"{filename}.json")
        
        try:
            with open(filepath, 'w', encoding='utf-8') as f:
                json.dump(data, f, ensure_ascii=False, indent=4)
            self.logger.info(f"Datos guardados en JSON: {filepath}")
        except Exception as e:
            self.logger.error(f"Error al guardar JSON: {str(e)}")

Paso 2: Implementar un scraper específico para un sitio de libros

Vamos a crear un scraper para extraer información de libros de un sitio de ejemplo:

# book_scraper.py

from web_scraper import WebScraper
import re

class BookScraper(WebScraper):
    """Scraper especializado para extraer información de libros."""
    
    def __init__(self, base_url="http://books.toscrape.com", output_dir="./data"):
        """Inicializa el scraper de libros."""
        super().__init__(base_url, output_dir)
    
    def scrape_books(self, pages=1):
        """
        Extrae información de libros de varias páginas.
        
        Args:
            pages: Número de páginas a scrapear
            
        Returns:
            list: Lista de diccionarios con información de libros
        """
        all_books = []
        
        for page in range(1, pages + 1):
            # URL de la página actual
            if page == 1:
                url = self.base_url
            else:
                url = f"{self.base_url}/catalogue/page-{page}.html"
            
            # Obtener la página
            soup = self.fetch_page(url)
            if not soup:
                continue
            
            # Encontrar todos los libros en la página
            book_containers = soup.select("article.product_pod")
            self.logger.info(f"Encontrados {len(book_containers)} libros en página {page}")
            
            # Procesar cada libro
            for book in book_containers:
                book_data = self._extract_book_info(book)
                if book_data:
                    all_books.append(book_data)
        
        # Guardar resultados
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        self.save_to_csv(all_books, f"books_{timestamp}")
        self.save_to_json(all_books, f"books_{timestamp}")
        
        return all_books
    
    def _extract_book_info(self, book_container):
        """
        Extrae información de un libro individual.
        
        Args:
            book_container: Elemento HTML que contiene la información del libro
            
        Returns:
            dict: Diccionario con la información del libro
        """
        try:
            # Extraer título
            title_element = book_container.select_one("h3 a")
            title = title_element["title"] if title_element else "Sin título"
            
            # Extraer URL
            book_url = title_element["href"] if title_element else ""
            if book_url and not book_url.startswith("http"):
                book_url = f"{self.base_url}/catalogue/{book_url.lstrip('../')}"
            
            # Extraer precio
            price_element = book_container.select_one("p.price_color")
            price_text = price_element.text if price_element else "0"
            # Limpiar el precio (quitar símbolo de moneda y convertir a float)
            price = float(re.sub(r'[^\d.]', '', price_text))
            
            # Extraer disponibilidad
            stock_element = book_container.select_one("p.availability")
            in_stock = "in stock" in stock_element.text.lower() if stock_element else False
            
            # Extraer valoración (estrellas)
            rating_element = book_container.select_one("p.star-rating")
            rating = rating_element["class"][1] if rating_element and len(rating_element["class"]) > 1 else "No rating"
            
            # Extraer imagen
            img_element = book_container.select_one("img")
            img_url = img_element["src"] if img_element else ""
            if img_url and not img_url.startswith("http"):
                img_url = f"{self.base_url}/{img_url}"
            
            # Crear diccionario con la información
            return {
                "title": title,
                "url": book_url,
                "price": price,
                "in_stock": in_stock,
                "rating": rating,
                "image_url": img_url,
                "scraped_at": datetime.now().isoformat()
            }
            
        except Exception as e:
            self.logger.error(f"Error al extraer información del libro: {str(e)}")
            return None
    
    def get_book_details(self, book_url):
        """
        Obtiene detalles adicionales de un libro específico.
        
        Args:
            book_url: URL de la página del libro
            
        Returns:
            dict: Diccionario con detalles del libro
        """
        soup = self.fetch_page(book_url)
        if not soup:
            return None
        
        try:
            # Extraer descripción
            desc_element = soup.select_one("article.product_page p:not(.price_color):not(.availability)")
            description = desc_element.text.strip() if desc_element else "Sin descripción"
            
            # Extraer información de la tabla de producto
            product_info = {}
            info_table = soup.select_one("table.table-striped")
            
            if info_table:
                rows = info_table.select("tr")
                for row in rows:
                    header = row.select_one("th")
                    value = row.select_one("td")
                    if header and value:
                        key = header.text.strip()
                        product_info[key] = value.text.strip()
            
            # Extraer categoría
            breadcrumb = soup.select("ul.breadcrumb li")
            category = breadcrumb[2].text.strip() if len(breadcrumb) > 2 else "Sin categoría"
            
            return {
                "description": description,
                "category": category,
                "product_info": product_info
            }
            
        except Exception as e:
            self.logger.error(f"Error al obtener detalles del libro: {str(e)}")
            return None

Paso 3: Script principal para ejecutar el scraper

# run_book_scraper.py

import argparse
import schedule
import time
from book_scraper import BookScraper

def scrape_books(pages=1, output_dir="./data"):
    """Ejecuta el scraper de libros."""
    scraper = BookScraper(output_dir=output_dir)
    books = scraper.scrape_books(pages)
    print(f"Se han extraído {len(books)} libros.")
    return books

def main():
    """Función principal del programa."""
    parser = argparse.ArgumentParser(description="Web Scraper de libros")
    
    parser.add_argument(
        "-p", "--pages",
        type=int,
        default=1,
        help="Número de páginas a scrapear (por defecto: 1)"
    )
    
    parser.add_argument(
        "-o", "--output",
        default="./data",
        help="Directorio de salida para los datos (por defecto: ./data)"
    )
    
    parser.add_argument(
        "-s", "--schedule",
        action="store_true",
        help="Programar ejecución diaria"
    )
    
    args = parser.parse_args()
    
    if args.schedule:
        print(f"Programando scraper para ejecutarse diariamente a las 10:00 AM...")
        schedule.every().day.at("10:00").do(scrape_books, args.pages, args.output)
        
        while True:
            schedule.run_pending()
            time.sleep(60)
    else:
        scrape_books(args.pages, args.output)

if __name__ == "__main__":
    main()

🚀 Uso del sistema

Instalación de dependencias

Antes de usar el scraper, necesitas instalar las bibliotecas requeridas:

pip install requests beautifulsoup4 pandas schedule

Uso básico

Para extraer información de la primera página:

python run_book_scraper.py

Para extraer información de varias páginas:

python run_book_scraper.py --pages 5

Para especificar un directorio de salida:

python run_book_scraper.py --output ./mis_datos

Para programar una ejecución diaria:

python run_book_scraper.py --schedule

🔍 Comprueba tu comprensión

  1. ¿Por qué es importante incluir un tiempo de espera entre peticiones?
  2. ¿Cómo modificarías el código para extraer información de un sitio web diferente?
  3. ¿Qué estrategias usarías para manejar cambios en la estructura HTML del sitio web?
  4. ¿Cómo podrías implementar un sistema de notificaciones cuando se encuentren nuevos libros?

🛠️ Ideas para mejoras

  • Proxy rotativo: Usar diferentes IPs para evitar bloqueos
  • Autenticación: Añadir soporte para sitios que requieren inicio de sesión
  • Manejo de JavaScript: Usar Selenium para sitios que cargan contenido dinámicamente
  • Filtros avanzados: Permitir buscar libros por categoría, precio o valoración
  • Análisis de datos: Generar gráficos y estadísticas de los datos extraídos
  • Detección de cambios: Notificar cuando hay nuevos elementos o cambios de precio
  • API: Crear una API REST para acceder a los datos extraídos

📝 Resumen

En este proyecto, has creado un sistema de web scraping que:

  • Extrae información estructurada de páginas web
  • Navega a través de múltiples páginas
  • Procesa y limpia los datos obtenidos
  • Guarda la información en formatos útiles (CSV, JSON)
  • Puede programarse para ejecutarse periódicamente

Este scraper no solo te permite obtener datos de forma automática, sino que también te ha permitido aplicar conceptos importantes de Python como el manejo de peticiones HTTP, el análisis de HTML, la manipulación de datos estructurados y la programación de tareas.

⚠️ Nota importante sobre el uso ético

Recuerda siempre:

  1. Respetar los términos de servicio de los sitios web
  2. Consultar el archivo robots.txt antes de scrapear
  3. Implementar límites de velocidad razonables
  4. No extraer información personal o protegida
  5. Usar los datos de forma ética y legal

El web scraping es una herramienta poderosa que debe usarse con responsabilidad.


¡Felicidades! Has completado los tres proyectos de automatización. Ahora tienes herramientas prácticas para:

  1. Crear copias de seguridad automáticas
  2. Organizar archivos por tipo
  3. Extraer información de sitios web

Estos proyectos no solo te han ayudado a aplicar tus conocimientos de Python, sino que también te han proporcionado herramientas útiles que puedes personalizar según tus necesidades específicas.

En el próximo capítulo, aprenderemos sobre Despliegue Básico para que puedas ejecutar tus scripts en la nube y programar tareas automáticas.