Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Web scraping es el proceso de extraer información de sitios web de forma automatizada.

Mientras que las APIs proporcionan interfaces estructuradas para acceder a datos, el web scraping permite obtener información de sitios que no ofrecen APIs o cuando se necesita acceder a datos que no están disponibles a través de ellas.

Los artefactos que realizan web scraping se conocen comúnmente como “scrapers”, “spiders” o “crawlers”. Estos programas navegan por las páginas web, descargan su contenido HTML y extraen la información relevante.

Los buscadores web utilizan crawlers para indexar el contenido de la web y hacer que sea accesible a través de búsquedas. De alguna manera, los crawlers son la columna vertebral de los motores de búsqueda que permite a los buscadores descubrir y organizar la vasta cantidad de información disponible en Internet, almacenando en sus bases de datos no solo las URLs, sino también fragmentos de texto y metadatos asociados a cada página.

El siguiente diagrama ilustra la arquitectura básica de un crawler:

Arquitectura básica de un crawler

Arquitectura básica de un crawler

Arquitectura básica de un crawler

Arquitectura básica de un crawler

URLs semilla
Puntos de partida para el crawler. Una serie de URLs iniciales desde donde comenzar la exploración.
Frontera de URLs
Estructura de datos que almacena las URLs pendientes de visitar. Cada vez que el crawler visita una página, extrae nuevas URLs y las añade a esta frontera.
Obtenedor de HTML
Componente que realiza solicitudes HTTP para descargar el contenido HTML de las páginas web.
Analizador HTML
Procesa el HTML descargado para extraer información relevante, como texto, enlaces, imágenes, etc.
Detección de duplicados
Módulo que verifica si una URL ya ha sido visitada para evitar procesarla nuevamente.
Extractor de URLs
Extrae todas las URLs presentes en la página web analizada.
Filtro de URLs
Aplica reglas para decidir qué URLs deben ser añadidas a la frontera (por ejemplo, solo URLs del mismo dominio).
Cargador/Detector de URLs
Añade nuevas URLs a la frontera y marca las URLs visitadas.
Almacenamiento de URLs
Base de datos o archivo donde se guardan las URLs visitadas y pendientes.
Resolutor DNS
Convierte nombres de dominio en direcciones IP para realizar las solicitudes HTTP.
Cacheo
Almacena temporalmente respuestas HTTP para mejorar la eficiencia y reducir la carga en los servidores web.

1Robustez del Crawler

Un crawler robusto debe estar diseñado para manejar la imprevisibilidad de la Web. Tres desafíos principales son:

Spider Traps (Trampas de araña)
Son estructuras de sitios web que causan que un crawler entre en un bucle infinito de URLs (común en calendarios generados dinámicamente o directorios anidados).
Cuellos de botella en DNS
La resolución de nombres de dominio puede ser lenta. Los crawlers de alto rendimiento suelen usar resolutores multi-hilo o caches de DNS locales para evitar que la red sea el limitante.
Cortesía (Politeness)
Un crawler no debe saturar un servidor. Reglas como esperar un intervalo entre peticiones al mismo host y respetar el archivo robots.txt son obligatorias para un comportamiento ético.

2Estrategias de la Frontera de URLs

La frontera no es simplemente una cola. En sistemas complejos Manning et al., 2008, se utilizan esquemas de priorización:

Almacenamiento de datos
Base de datos o archivo donde se guardan los datos extraídos del contenido web.

El proceso de web scraping puede variar en complejidad dependiendo del sitio web objetivo y de los datos que se desean extraer. Algunos sitios pueden tener estructuras HTML simples, mientras que otros pueden utilizar JavaScript para cargar contenido dinámicamente, lo que requiere técnicas más avanzadas.

En general el proceso de búsqueda inicia con una lista de URLs semilla, que son las páginas iniciales que el crawler visitará. A partir de estas páginas, el crawler descarga el contenido HTML y lo analiza para extraer información relevante y nuevas URLs. Estas nuevas URLs se añaden a la frontera de URLs pendientes de visitar, y el proceso se repite hasta que se alcanzan ciertos límites, como un número máximo de páginas visitadas o una profundidad máxima de exploración.

Para gestionar la frontera de URLs, se pueden utilizar diferentes estructuras de datos como colas (FIFO) para una exploración en anchura o pilas (LIFO) para una exploración en profundidad. Además, es importante implementar mecanismos para evitar visitar la misma URL múltiples veces, lo que se puede lograr mediante el uso de conjuntos o bases de datos para rastrear las URLs ya visitadas.

También se pueden establecer reglas para filtrar las URLs que se añaden a la frontera, como limitar la exploración a un dominio específico o evitar ciertos tipos de contenido.

3Consideraciones Legales y Éticas

Antes de realizar una exploración de la web, es fundamental considerar aspectos legales y éticos. Algunos sitios web prohíben el scraping en sus términos de servicio, y es importante respetar estas políticas para evitar problemas legales.

También es crucial ser respetuoso con los servidores web, evitando sobrecargar el sitio con demasiadas solicitudes en poco tiempo.

Los servidores pueden tener mecanismos para detectar y bloquear actividades sospechosas, como un número excesivo de solicitudes en un corto período.

En general las políticas de acceso a un sitio web por parte de los scrapers se regulan mediante:

robots.txt
Archivo en la raíz del sitio web que especifica qué partes pueden ser accedidas por robots automatizados.
import requests

# Verificar el archivo robots.txt
url_robots = 'https://python.org/robots.txt'
response = requests.get(url_robots, timeout=10)

print("Contenido de robots.txt de python.org:")
print('\n'.join(response.text.split('\n')))
Output
Contenido de robots.txt de python.org:
# Directions for robots.  See this URL:
# http://www.robotstxt.org/robotstxt.html
# for a description of the file format.

User-agent: HTTrack
User-agent: puf
User-agent: MSIECrawler
Disallow: /

# The Krugle web crawler (though based on Nutch) is OK.
User-agent: Krugle
Allow: /
Disallow: /~guido/orlijn/
Disallow: /webstats/

# No one should be crawling us with Nutch.
User-agent: Nutch
Disallow: /

# Hide old versions of the documentation and various large sets of files.
User-agent: *
Disallow: /~guido/orlijn/
Disallow: /webstats/

El formato típico de un archivo robots.txt incluye directivas como User-agent, Disallow, y Allow para controlar el acceso de diferentes tipos de bots a distintas partes del sitio web.

El protocolo Robots Exclusion Standard define cómo los bots deben interpretar estas directivas para respetar las políticas del sitio. Este protocolo se encuentra estandarizado a través de la RFC 9309.

Términos de Servicio
Muchos sitios web prohíben explícitamente el scraping en sus términos de uso.
Leyes de Protección de Datos
Regulaciones como GDPR en Europa o leyes locales de protección de datos personales.
Propiedad Intelectual
El contenido scrapeado puede estar protegido por derechos de autor.
Identificarse correctamente
Usar un User-Agent descriptivo que permita al administrador del sitio contactarte.
Uso responsable de los datos
No usar los datos scrapeados para propósitos no éticos o ilegales.

4Web Scraping Manual con Python

Python ofrece excelentes bibliotecas para web scraping. Las más populares son requests para realizar solicitudes HTTP y BeautifulSoup para parsear HTML.

4.1Instalación de Bibliotecas

pip install requests beautifulsoup4 lxml

BeautifulSoup es una biblioteca para parsear documentos HTML y XML, facilitando la navegación y búsqueda de elementos dentro del árbol del documento.

4.2Ejemplo Básico de un crawler

import requests
from bs4 import BeautifulSoup
from urllib.parse import urljoin, urlparse
import csv
import time

def es_mismo_dominio(url, dominio_base):
    """Verifica si la URL pertenece al mismo dominio base."""
    return urlparse(url).netloc == dominio_base

def crawler_frontera(url_semilla,
                     max_paginas=50,
                     retraso=1,
                     archivo_csv='enlaces.csv'):
    """
    Función para realizar crawling web utilizando una frontera de enlaces tipo
    FIFO (cola).
    Recorre páginas web comenzando desde una URL semilla, siguiendo enlaces
    encontrados hasta un máximo de páginas.

    Parámetros:
    - url_semilla (str): URL inicial desde donde comienza el crawling.
    - max_paginas (int, opcional): Número máximo de páginas a visitar
      (por defecto 50).
    - retraso (int o float, opcional): Tiempo de espera (en segundos) entre
      solicitudes para evitar sobrecargar el servidor (por defecto 1).
    - archivo_csv (str, opcional): Nombre del archivo CSV donde se
      guardarán los enlaces encontrados (por defecto 'enlaces.csv').
    """
    frontera = [url_semilla]
    visitadas = set()
    enlaces_extraidos = []

    dominio_base = urlparse(url_semilla).netloc

    while frontera and len(visitadas) < max_paginas:
        url_actual = frontera.pop(0)
        if url_actual in visitadas:
            continue

        print(f"Visitando: {url_actual}")
        try:
            response = requests.get(url_actual, timeout=10, headers={
                'User-Agent': 'MiCrawler/1.0 (contacto@ejemplo.com)'
            })
            response.raise_for_status()
        except Exception as e:
            print(f"  Error al acceder: {e}")
            continue

        soup = BeautifulSoup(response.text, 'lxml')
        visitadas.add(url_actual)

        # Extraer y guardar enlaces
        for enlace in soup.find_all('a', href=True):
            # En una página html los enlaces están en etiquetas <a href="...">
            url_encontrada = urljoin(url_actual, enlace['href'])
            url_encontrada = url_encontrada.split('#')[0]  # Quitar fragmentos
            if es_mismo_dominio(url_encontrada, dominio_base):
                if url_encontrada not in visitadas \
                   and url_encontrada not in frontera:
                    frontera.append(url_encontrada)
                enlaces_extraidos.append({'pagina': url_actual,
                                        'enlace': url_encontrada})

        time.sleep(retraso)  # Ser respetuoso con el servidor

    # Guardar enlaces en un archivo CSV
    with open(archivo_csv, 'w', newline='', encoding='utf-8') as f:
        writer = csv.DictWriter(f, fieldnames=['pagina', 'enlace'])
        writer.writeheader()
        writer.writerows(enlaces_extraidos)

    print(f"\nTotal de páginas visitadas: {len(visitadas)}")
    print(f"Enlaces guardados en: {archivo_csv}")

# Ejemplo de uso:
crawler_frontera('https://quotes.toscrape.com/', max_paginas=10,
                 archivo_csv='enlaces_quotes.csv')
Output
Visitando: https://quotes.toscrape.com/
Visitando: https://quotes.toscrape.com/login
Visitando: https://quotes.toscrape.com/author/Albert-Einstein
Visitando: https://quotes.toscrape.com/tag/change/page/1/
Visitando: https://quotes.toscrape.com/tag/deep-thoughts/page/1/
Visitando: https://quotes.toscrape.com/tag/thinking/page/1/
Visitando: https://quotes.toscrape.com/tag/world/page/1/
Visitando: https://quotes.toscrape.com/author/J-K-Rowling
Visitando: https://quotes.toscrape.com/tag/abilities/page/1/
Visitando: https://quotes.toscrape.com/tag/choices/page/1/

Total de páginas visitadas: 10
Enlaces guardados en: enlaces_quotes.csv
Contenido de enlaces_quotes.csv

A continuación se muestra el contenido del archivo generado:

| pagina                       | enlace                                                |
|:-----------------------------|:------------------------------------------------------|
| https://quotes.toscrape.com/ | https://quotes.toscrape.com/                          |
| https://quotes.toscrape.com/ | https://quotes.toscrape.com/login                     |
| https://quotes.toscrape.com/ | https://quotes.toscrape.com/author/Albert-Einstein    |
| https://quotes.toscrape.com/ | https://quotes.toscrape.com/tag/change/page/1/        |
| https://quotes.toscrape.com/ | https://quotes.toscrape.com/tag/deep-thoughts/page/1/ |
| https://quotes.toscrape.com/ | https://quotes.toscrape.com/tag/thinking/page/1/      |
| https://quotes.toscrape.com/ | https://quotes.toscrape.com/tag/world/page/1/         |
| https://quotes.toscrape.com/ | https://quotes.toscrape.com/author/J-K-Rowling        |
| https://quotes.toscrape.com/ | https://quotes.toscrape.com/tag/abilities/page/1/     |
| https://quotes.toscrape.com/ | https://quotes.toscrape.com/tag/choices/page/1/       |

5Scrapy

Scrapy es un framework de Python para web scraping a gran escala. Proporciona funcionalidades avanzadas como:

5.1Instalación de Scrapy

pip install scrapy

6Proyecto Práctico: Spider de Libros con Scrapy

A continuación se presenta un tutorial paso a paso para crear un spider con Scrapy que visite el sitio Books to Scrape y genere un archivo CSV con títulos y precios de los libros de la categoría “Horror”.

6.1Paso 1: Crear un Proyecto Scrapy

Crear un nuevo proyecto de Scrapy en el directorio actual:

Iniciar un nuevo proyecto y una spider:

scrapy startproject books_scraper
cd books_scraper
scrapy genspider books books.toscrape.com

6.2Paso 2: Estructura del Proyecto

El comando anterior crea la siguiente estructura de directorios:

books_scraper/
    scrapy.cfg
    books_scraper/
        __init__.py
        items.py
        middlewares.py
        pipelines.py
        settings.py
        spiders/
            __init__.py
            books.py

6.3Paso 3: Definir los Items

Editar el archivo items.py para definir la estructura de datos que queremos extraer:

# books_scraper/items.py
import scrapy


class BookItem(scrapy.Item):
    title = scrapy.Field()
    price = scrapy.Field()
    category = scrapy.Field()
    availability = scrapy.Field()
    rating = scrapy.Field()

6.4Paso 4: Implementar el Spider

Editar el archivo spiders/books.py con la lógica de extracción:

# books_scraper/spiders/books.py
import scrapy
from books_scraper.items import BookItem


class BooksSpider(scrapy.Spider):
    name = "books"
    allowed_domains = ["books.toscrape.com"]
    start_urls = [
        "https://books.toscrape.com/catalogue/category/books/horror_31/index.html"
    ]

    def parse(self, response):
        """Extrae información de libros de la página actual"""

        # Extraer todos los libros de la página
        books = response.xpath("//article[contains(@class,'product_pod')]")

        for book in books:
            item = BookItem()

            # Extraer título
            item["title"] = book.xpath(".//h3/a/@title").get().strip()

            # Extraer precio
            price_text = (
                book.xpath(".//p[contains(@class,'price_color')]/text()").get().strip()
            )
            item["price"] = price_text.replace("£", "") if price_text else None

            # Extraer disponibilidad
            availability_xpath = (
                ".//p[contains(@class,'instock') and contains(@class,'availability')]"
                "/text()"
            )
            availability = book.xpath(availability_xpath).getall()
            item["availability"] = (
                "".join(availability).strip() if availability else None
            )

            # Extraer calificación
            rating_class = book.xpath(
                ".//p[contains(@class,'star-rating')]/@class"
            ).get()
            if rating_class:
                rating = rating_class.split()[-1]
                item["rating"] = rating
            else:
                item["rating"] = None

            item["category"] = "Horror"

            yield item

        # Seguir a la siguiente página si existe
        next_page = response.xpath("//li[contains(@class,'next')]/a/@href").get()
        if next_page:
            next_page_url = response.urljoin(next_page)
            yield scrapy.Request(next_page_url, callback=self.parse)

6.5Paso 5: Configurar Pipeline para CSV

Crear un pipeline personalizado para exportar a CSV. Editar pipelines.py:

# books_scraper/pipelines.py
import csv
import os


class CsvExportPipeline:
    def __init__(self):
        self.file = None
        self.writer = None

    def open_spider(self, spider):
        """Se ejecuta cuando se abre el spider"""
        self.file = open("horror_books.csv", "w", newline="", encoding="utf-8")
        self.writer = csv.DictWriter(
            self.file,
            fieldnames=["title", "price", "category", "availability", "rating"],
        )
        self.writer.writeheader()

    def close_spider(self, spider):
        """Se ejecuta cuando se cierra el spider"""
        if self.file:
            self.file.close()

    def process_item(self, item, spider):
        """Procesa cada item extraído"""
        self.writer.writerow(dict(item))
        return item

6.6Paso 6: Configurar Settings

Editar settings.py para activar el pipeline y configurar el comportamiento del spider:

# books_scraper/settings.py
BOT_NAME = "books_scraper"

SPIDER_MODULES = ["books_scraper.spiders"]
NEWSPIDER_MODULE = "books_scraper.spiders"

# Respetar robots.txt
ROBOTSTXT_OBEY = True

# Configurar pipelines
ITEM_PIPELINES = {
    "books_scraper.pipelines.CsvExportPipeline": 300,
}

# Configurar delays para ser respetuosos con el servidor
DOWNLOAD_DELAY = 1  # Esperar 1 segundo entre requests
RANDOMIZE_DOWNLOAD_DELAY = 0.5  # Variar el delay ±50%

# User agent personalizado
USER_AGENT = "books_scraper (untref.edu.ar)"

# Configuración de logging
LOG_LEVEL = "INFO"

6.7Paso 7: Ejecutar el Spider

Para ejecutar el spider y generar el archivo CSV:

cd books_scraper
scrapy crawl books
# Esto generará un archivo 'horror_books.csv' con los resultados

6.8Paso 8: Análisis de Resultados (Opcional)

Podemos analizar los resultados usando pandas:

import pandas as pd

# Cargar los datos
df = pd.read_csv(csv_path)

# Estadísticas básicas
print("Estadísticas de los libros de Horror:")
print(f"Total de libros: {len(df)}")
print(f"Precio promedio: £{df['price'].astype(float).mean():.2f}")
print(f"Precio mínimo: £{df['price'].astype(float).min():.2f}")
print(f"Precio máximo: £{df['price'].astype(float).max():.2f}")

# Distribución de calificaciones
print("\nDistribución de calificaciones:")
print(df['rating'].value_counts())
Output
Estadísticas de los libros de Horror:
Total de libros: 17
Precio promedio: £35.95
Precio mínimo: £10.56
Precio máximo: £57.86

Distribución de calificaciones:
rating
Two      4
One      4
Three    4
Four     3
Five     2
Name: count, dtype: int64

Descargar código completo del Spider

6.9Extensiones Posibles

7Comparación: APIs vs Web Scraping

Aspecto

APIs

Web Scraping

Acceso a datos

Estructurado y oficial

No estructurado, extraído del HTML

Estabilidad

Alta (con versionado)

Baja (cambios en el HTML rompen el código)

Legalidad

Generalmente legal con términos claros

Zona gris, depende del sitio

Límites de velocidad

Explícitos y documentados

Implícitos, basados en el comportamiento del servidor

Facilidad de uso

Diseñado para ser consumido

Requiere ingeniería inversa del HTML

Cobertura de datos

Solo lo que la API expone

Potencialmente todo lo visible en el sitio

Mantenimiento

Bajo (cambios notificados)

Alto (cambios no notificados)

8Mejores Prácticas para Web Scraping

  1. Verificar legalidad: Revisar términos de servicio y robots.txt

  2. Identificarse: Usar un User-Agent descriptivo

  3. Ser respetuoso: Limitar la frecuencia de solicitudes

  4. Manejar errores: Anticipar cambios en la estructura del sitio

  5. Considerar alternativas: Preferir APIs cuando estén disponibles

  6. Mantener el código: Si los sitios cambian, el scraper debe actualizarse

9Herramientas y Bibliotecas Adicionales

9.1Parseo y Análisis

9.2Automatización de Navegadores

9.3Gestión de Solicitudes

9.4Almacenamiento y Procesamiento

10Referencias y Recursos Adicionales

10.1Documentación Oficial

10.2Sitios para Practicar Web Scraping

10.3Aspectos Legales

10.4Libros y Referencias Académicas

References
  1. Manning, C. D., Raghavan, P., & Schütze, H. (2008). Introduction to Information Retrieval. Cambridge University Press. https://nlp.stanford.edu/IR-book/
  2. Mitchell, R. (2024). Web Scraping with Python: Data Extraction from the Modern Web (3rd ed.). O’Reilly Media. https://www.oreilly.com/library/view/web-scraping-with/9781098145347/