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
- 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.txtson 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:
Frescura: Priorizar la revisión de páginas que cambian frecuentemente (como sitios de noticias).
Calidad (PageRank): Priorizar páginas que son consideradas más importantes o autoritativas.
Detección de duplicados: Antes de añadir a la frontera, se suele calcular un hash (o shingle) del contenido para ver si la información ya fue procesada bajo una URL distinta.
- 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 lxmlBeautifulSoup 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:
Gestión automática de solicitudes concurrentes
Manejo de robots.txt
Extracción de datos con selectores CSS y XPath
Exportación a múltiples formatos (JSON, CSV, XML)
Middleware para personalizar el comportamiento
5.1Instalación de Scrapy¶
pip install scrapy6Proyecto 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.com6.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.py6.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 item6.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 resultados6.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¶
Múltiples categorías: Modificar
start_urlspara incluir más categoríasImágenes: Agregar extracción de URLs de imágenes de libros
Detalles adicionales: Visitar páginas individuales de libros para más información
Base de datos: Cambiar el pipeline para guardar en SQLite o PostgreSQL
Monitoreo: Agregar logging y métricas de rendimiento
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¶
Verificar legalidad: Revisar términos de servicio y robots.txt
Identificarse: Usar un User-Agent descriptivo
Ser respetuoso: Limitar la frecuencia de solicitudes
Manejar errores: Anticipar cambios en la estructura del sitio
Considerar alternativas: Preferir APIs cuando estén disponibles
Mantener el código: Si los sitios cambian, el scraper debe actualizarse
9Herramientas y Bibliotecas Adicionales¶
9.1Parseo y Análisis¶
lxml: Parser XML/HTML muy rápido
html5lib: Parser que simula el comportamiento de navegadores
parsel: Librería de extracción usada por Scrapy
9.2Automatización de Navegadores¶
Selenium: Control de navegadores web para automatización
Playwright: Alternativa moderna a Selenium
Puppeteer: Control de Chrome/Chromium (Node.js)
9.3Gestión de Solicitudes¶
httpx: Cliente HTTP asíncrono moderno
aiohttp: Cliente HTTP asíncrono
requests-html: Requests con soporte para JavaScript
9.4Almacenamiento y Procesamiento¶
pandas: Análisis y manipulación de datos
SQLAlchemy: ORM para bases de datos
MongoDB: Base de datos NoSQL para datos no estructurados
10Referencias y Recursos Adicionales¶
10.1Documentación Oficial¶
10.2Sitios para Practicar Web Scraping¶
Quotes to Scrape - Sitio diseñado para practicar scraping
Books to Scrape - Tienda de libros ficticia para scraping
Scrape This Site - Ejercicios de scraping
10.3Aspectos Legales¶
10.4Libros y Referencias Académicas¶
En el capítulo 20: Web crawling and indexes del libro Manning et al., 2008 se explican los conceptos básicos de web scraping.
En el libro Mitchell, 2024 se profundiza en el tema de web scraping con Python. Este libro se puede leer online en formato html, por un tiempo limitado.
- Manning, C. D., Raghavan, P., & Schütze, H. (2008). Introduction to Information Retrieval. Cambridge University Press. https://nlp.stanford.edu/IR-book/
- 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/