Web Scraping en Python

Curso completo desde cero hasta nivel avanzado

Lección 1 de 10

Introducción al Web Scraping

El web scraping es una técnica para extraer información de sitios web de manera automatizada. En este curso aprenderás a utilizar Python para recolectar datos de la web de forma eficiente y ética.

¿Qué es el Web Scraping?

El web scraping consiste en:

  • Acceder a páginas web mediante código
  • Analizar su estructura HTML
  • Extraer información específica
  • Almacenar o procesar los datos obtenidos

Casos de uso comunes

  • Monitoreo de precios en e-commerce
  • Recolección de datos para análisis
  • Automatización de tareas repetitivas
  • Integración de datos entre sistemas
  • Investigación académica

Consideraciones legales y éticas

Antes de hacer scraping a un sitio web, revisa su archivo robots.txt (ejemplo: sitio.com/robots.txt) y sus términos de servicio. Respeta las reglas y no sobrecargues los servidores con muchas solicitudes.

Herramientas que usaremos

  • Requests: Para hacer peticiones HTTP
  • BeautifulSoup: Para analizar HTML
  • Selenium: Para interactuar con páginas dinámicas
  • Scrapy: Framework completo para scraping

Ejercicio de preparación

1. Instala Python en tu computadora si no lo tienes (versión 3.6 o superior)

2. Crea un nuevo entorno virtual para este curso

3. Instala las librerías básicas ejecutando: pip install requests beautifulsoup4 selenium scrapy

Conceptos Básicos de HTML y HTTP

Para hacer web scraping efectivo, es fundamental entender cómo funcionan las páginas web y el protocolo HTTP.

Estructura básica de HTML

El HTML está compuesto por elementos (tags) que forman una estructura jerárquica (DOM - Document Object Model).

<!DOCTYPE html> <html> <head> <title>Ejemplo</title> </head> <body> <div id="contenido"> <h1 class="titulo">Título principal</h1> <p>Este es un párrafo con un <a href="https://ejemplo.com">enlace</a>.</p> <ul> <li>Elemento 1</li> <li>Elemento 2</li> </ul> </div> </body> </html>

Selectores comunes

  • Por etiqueta: h1, p, a
  • Por clase: .titulo
  • Por ID: #contenido
  • Por atributo: [href="https://ejemplo.com"]

Protocolo HTTP

Cuando visitas una página web, tu navegador hace una petición HTTP al servidor, que responde con el contenido HTML.

GET /pagina-ejemplo HTTP/1.1 Host: www.ejemplo.com User-Agent: Mozilla/5.0 Accept: text/html Accept-Language: es-ES
HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 1234 <!DOCTYPE html> <html> ...contenido HTML... </html>

Códigos de estado HTTP

  • 200: OK - Solicitud exitosa
  • 301/302: Redirección
  • 403: Prohibido - Acceso denegado
  • 404: No encontrado
  • 500: Error interno del servidor

Ejercicio práctico

1. Abre las herramientas de desarrollador de tu navegador (F12)

2. Visita una página web y examina su estructura HTML

3. Identifica elementos usando selectores CSS

4. Revisa las peticiones HTTP en la pestaña Network

Primeros Pasos con Requests y BeautifulSoup

Vamos a comenzar con las dos bibliotecas más utilizadas para web scraping en Python: Requests para obtener páginas web y BeautifulSoup para analizarlas.

Instalación

pip install requests beautifulsoup4

Obtener una página web con Requests

import requests url = 'https://ejemplo.com' response = requests.get(url) print(response.status_code) # Código de estado HTTP print(response.text) # Contenido HTML de la página

Analizar HTML con BeautifulSoup

from bs4 import BeautifulSoup # Crear objeto BeautifulSoup soup = BeautifulSoup(response.text, 'html.parser') # Obtener el título de la página title = soup.title print(title.text)

Métodos básicos de búsqueda

# Encontrar el primer elemento que coincida first_paragraph = soup.find('p') print(first_paragraph.text) # Encontrar por clase featured = soup.find('div', class_='featured') # Encontrar por ID header = soup.find(id='header')
# Encontrar todos los elementos que coincidan all_links = soup.find_all('a') for link in all_links: print(link.get('href')) # Limitar resultados three_paragraphs = soup.find_all('p', limit=3)
# Usar selectores CSS articles = soup.select('div.article') for article in articles: title = article.select_one('h2.title') print(title.text) # Selector complejo items = soup.select('ul#lista > li.item')

Ejercicio práctico

1. Obtén la página principal de Wikipedia en español

2. Extrae todos los títulos de las secciones principales

3. Extrae todos los enlaces de la página

4. Cuenta cuántas imágenes hay en la página

Tip profesional

Siempre incluye un User-Agent en tus peticiones para identificarte como un scraper legítimo. Puedes usar: headers = {'User-Agent': 'MiScraper/1.0'} y pasarlo a requests.get() como parámetro.

Scraping Avanzado con BeautifulSoup

Ahora que conoces los fundamentos, profundicemos en técnicas más avanzadas de extracción y manipulación de datos.

Navegación por el árbol DOM

# Acceder al padre de un elemento parent = element.parent # Obtener hijos directos children = element.children # Obtener todos los descendientes descendants = element.descendants # Navegar entre hermanos next_sibling = element.next_sibling previous_sibling = element.previous_sibling

Extracción de atributos

# Obtener un atributo específico link = soup.find('a') url = link['href'] # o link.get('href') # Obtener todos los atributos img = soup.find('img') all_attributes = img.attrs # Extraer datos de atributos data_id = img.get('data-id', 'default')

Manejo de texto

# Obtener todo el texto de un elemento paragraph = soup.find('p') print(paragraph.text) # o paragraph.get_text() # Con parámetros text = paragraph.get_text(separator=' ', strip=True)
# Obtener textos individuales for string in paragraph.strings: print(repr(string)) # Textos sin espacios for string in paragraph.stripped_strings: print(repr(string))

Filtros avanzados

# Filtrar por función def has_class_but_no_id(tag): return tag.has_attr('class') and not tag.has_attr('id') results = soup.find_all(has_class_but_no_id) # Filtrar por expresión regular import re soup.find_all(text=re.compile('importante')) soup.find_all(href=re.compile('^https://'))

Ejercicio práctico

1. Extrae una tabla completa de Wikipedia y conviértela a un DataFrame de pandas

2. Crea una función que reciba una URL y devuelva un diccionario con todos los metadatos de la página (title, description, keywords)

3. Extrae todos los enlaces que contengan "pdf" en su URL

Precaución

Algunas páginas pueden tener HTML mal formado. BeautifulSoup es bastante tolerante, pero para casos extremos considera usar lxml como parser: BeautifulSoup(html, 'lxml')

Manejo de Formularios y Sesiones

Muchos sitios web requieren interacción con formularios o mantenimiento de sesiones. Aprenderemos a manejar estos casos.

Enviar formularios con Requests

# Datos del formulario login_data = { 'username': 'tu_usuario', 'password': 'tu_contraseña', 'csrf_token': 'token_obtenido_del_formulario' } # Enviar POST request session = requests.Session() response = session.post('https://ejemplo.com/login', data=login_data) # Verificar login if 'Bienvenido' in response.text: print("Login exitoso!") else: print("Error en login")

Mantener sesiones

# Usar sesión para mantener cookies with requests.Session() as session: # Primera petición (login) session.post(login_url, data=login_data) # Peticiones posteriores mantienen la sesión profile_page = session.get(profile_url) print(profile_page.text)

Manejo de CSRF Tokens

# Primero obtener la página de login login_page = session.get(login_url) soup = BeautifulSoup(login_page.text, 'html.parser') # Extraer el token CSRF csrf_token = soup.find('input', {'name': 'csrf_token'})['value'] # Usar el token en el login login_data['csrf_token'] = csrf_token session.post(login_url, data=login_data)

Subida de archivos

# Preparar archivo para subir files = {'file': ('report.pdf', open('report.pdf', 'rb'), 'application/pdf')} # Enviar POST con archivo response = session.post(upload_url, files=files)

Ejercicio práctico

1. Automatiza el login en un sitio web de prueba

2. Extrae información de una página que requiera autenticación

3. Implementa un scraper que navegue por múltiples páginas manteniendo la sesión

Seguridad

Nunca guardes credenciales directamente en tu código. Usa variables de entorno o archivos de configuración seguros.

Scraping de Páginas Dinámicas con Selenium

Cuando las páginas cargan contenido dinámicamente con JavaScript, Requests y BeautifulSoup no son suficientes. Ahí entra Selenium.

Instalación y configuración

pip install selenium

Además, necesitarás el driver correspondiente a tu navegador (ChromeDriver para Chrome, GeckoDriver para Firefox).

Configuración básica

from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys # Configurar el driver (ejemplo para Chrome) driver = webdriver.Chrome(executable_path='ruta/a/chromedriver') # Abrir una página driver.get('https://ejemplo.com')

Localización de elementos

# Diferentes métodos de búsqueda element = driver.find_element(By.ID, 'search') element = driver.find_element(By.CLASS_NAME, 'btn') element = driver.find_element(By.CSS_SELECTOR, 'div.content') element = driver.find_element(By.XPATH, '//div[@class="content"]') # Para múltiples elementos elements = driver.find_elements(By.TAG_NAME, 'a')

Interacción con elementos

# Escribir en un campo search_box = driver.find_element(By.NAME, 'q') search_box.send_keys('web scraping') search_box.send_keys(Keys.RETURN) # Click en botón button = driver.find_element(By.XPATH, '//button[text()="Enviar"]') button.click() # Desplegar dropdown from selenium.webdriver.support.ui import Select dropdown = Select(driver.find_element(By.ID, 'opciones')) dropdown.select_by_visible_text('Opción 2')

Esperas explícitas e implícitas

from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC # Espera explícita (hasta 10 segundos) element = WebDriverWait(driver, 10).until( EC.presence_of_element_located((By.ID, 'resultados')) ) # Espera implícita (afecta a todas las búsquedas) driver.implicitly_wait(5) # segundos

Ejercicio práctico

1. Automatiza una búsqueda en Google y extrae los primeros 5 resultados

2. Navega por un sitio con scroll infinito cargando más contenido

3. Rellena y envía un formulario complejo con múltiples pasos

Rendimiento

Selenium es más lento que Requests. Úsalo solo cuando sea estrictamente necesario para contenido dinámico. Para páginas normales, sigue usando Requests + BeautifulSoup.

Almacenamiento de Datos Extraídos

Una vez extraídos los datos, necesitamos almacenarlos en formatos útiles para su posterior análisis o uso.

CSV (Excel)

import csv # Escribir CSV with open('datos.csv', 'w', newline='', encoding='utf-8') as file: writer = csv.writer(file) writer.writerow(['Nombre', 'Precio', 'URL']) # Encabezados for producto in productos: writer.writerow([producto['nombre'], producto['precio'], producto['url']]) # Leer CSV with open('datos.csv', 'r', encoding='utf-8') as file: reader = csv.DictReader(file) for row in reader: print(row['Nombre'], row['Precio'])

JSON

import json # Guardar datos como JSON datos = { 'pagina': 'https://ejemplo.com', 'productos': [ {'nombre': 'Producto 1', 'precio': 100}, {'nombre': 'Producto 2', 'precio': 200} ] } with open('datos.json', 'w', encoding='utf-8') as file: json.dump(datos, file, indent=2, ensure_ascii=False) # Leer JSON with open('datos.json', 'r', encoding='utf-8') as file: datos_leidos = json.load(file)

Bases de datos SQL (SQLite)

import sqlite3 # Crear/conectar a base de datos conn = sqlite3.connect('productos.db') cursor = conn.cursor() # Crear tabla cursor.execute('''CREATE TABLE IF NOT EXISTS productos (id INTEGER PRIMARY KEY AUTOINCREMENT, nombre TEXT NOT NULL, precio REAL, url TEXT UNIQUE)''') # Insertar datos for producto in productos: cursor.execute("INSERT INTO productos (nombre, precio, url) VALUES (?, ?, ?)", (producto['nombre'], producto['precio'], producto['url'])) # Guardar cambios y cerrar conn.commit() conn.close()

Pandas DataFrame

import pandas as pd # Crear DataFrame df = pd.DataFrame(productos) # Guardar en varios formatos df.to_csv('productos.csv', index=False) df.to_excel('productos.xlsx', index=False) df.to_json('productos.json', orient='records') # Leer datos df = pd.read_csv('productos.csv')

Ejercicio práctico

1. Crea un scraper que guarde los datos en CSV y JSON

2. Implementa una base de datos SQLite para almacenar los datos de forma estructurada

3. Compara el rendimiento de los diferentes métodos de almacenamiento

Consejo

Para grandes volúmenes de datos, considera usar bases de datos más robustas como PostgreSQL o MongoDB, o sistemas de almacenamiento en la nube.

Manejo de Errores y Robustez

Los scrapers deben ser robustos y manejar adecuadamente los errores que puedan ocurrir durante la ejecución.

Errores comunes en web scraping

  • Conexiones fallidas o timeouts
  • Cambios en la estructura del HTML
  • Bloqueos por parte del sitio
  • Páginas con contenido dinámico no esperado
  • Limitaciones de tasa (rate limiting)

Manejo básico de errores

import requests from requests.exceptions import RequestException import time def safe_request(url, max_retries=3, timeout=10): for attempt in range(max_retries): try: response = requests.get(url, timeout=timeout) response.raise_for_status() # Lanza error para códigos 4xx/5xx return response except RequestException as e: print(f"Intento {attempt + 1} fallido: {str(e)}") if attempt < max_retries - 1: wait_time = 2 ** attempt # Backoff exponencial print(f"Esperando {wait_time} segundos antes de reintentar...") time.sleep(wait_time) else: print(f"Error: No se pudo obtener {url} después de {max_retries} intentos") return None

Verificación de elementos

from bs4 import BeautifulSoup def extract_title(soup): title_tag = soup.find('title') if title_tag: return title_tag.text.strip() return "Título no encontrado" def extract_links(soup): links = [] for a in soup.find_all('a', href=True): href = a['href'] if href.startswith('http'): # Filtrar enlaces absolutos links.append(href) return links if links else None

Manejo de cambios estructurales

def extract_price(soup): # Intentar múltiples selectores posibles selectors = [ {'method': 'find', 'args': ['span', {'class': 'price'}]}, {'method': 'find', 'args': ['div', {'class': 'precio-actual'}]}, {'method': 'select_one', 'args': ['p.precio']} ] for selector in selectors: method = getattr(soup, selector['method']) element = method(*selector['args']) if element: return element.text.strip() return "Precio no encontrado"

Logging para depuración

import logging # Configurar logging logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', filename='scraper.log' ) # Ejemplo de uso try: response = requests.get(url) response.raise_for_status() except Exception as e: logging.error(f"Error al obtener {url}: {str(e)}") else: logging.info(f"Successfully scraped {url}")

Ejercicio práctico

1. Implementa manejo de errores en un scraper existente

2. Crea funciones que verifiquen la presencia de elementos antes de extraerlos

3. Configura un sistema de logging para registrar el progreso y errores

Mejores prácticas

1. Siempre maneja los posibles errores

2. Implementa reintentos con backoff exponencial

3. Registra suficiente información para depurar problemas

4. Valida los datos extraídos antes de almacenarlos

Evitar Bloqueos y Scraping Ético

Los sitios web pueden detectar y bloquear scrapers. Aprende a evitar bloqueos y a hacer scraping de forma ética.

Técnicas comunes de bloqueo

  • Detección de User-Agent
  • Límite de solicitudes por IP
  • CAPTCHAs
  • Comportamiento no humano (velocidad, patrones)
  • Análisis de JavaScript y cookies

Evitar bloqueos

# Rotación de User-Agents user_agents = [ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) ...', 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) ...', 'Mozilla/5.0 (X11; Linux x86_64) ...' ] headers = { 'User-Agent': random.choice(user_agents), 'Accept-Language': 'es-ES,es;q=0.9', 'Referer': 'https://www.google.com/' } # Uso de proxies proxies = { 'http': 'http://10.10.1.10:3128', 'https': 'http://10.10.1.10:1080' } response = requests.get(url, headers=headers, proxies=proxies)

Control de velocidad

import time import random # Espera aleatoria entre solicitudes def random_delay(min=1, max=5): time.sleep(random.uniform(min, max)) # Limitar tasa de solicitudes REQUEST_LIMIT = 5 # solicitudes por minuto last_requests = [] def make_request(url): now = time.time() # Eliminar registros antiguos last_requests[:] = [t for t in last_requests if now - t < 60] if len(last_requests) >= REQUEST_LIMIT: wait_time = 60 - (now - last_requests[0]) print(f"Esperando {wait_time:.1f} segundos (límite de tasa)") time.sleep(wait_time) response = requests.get(url) last_requests.append(time.time()) return response

Manejo de CAPTCHAs

  • Reducir velocidad de scraping
  • Usar servicios de resolución de CAPTCHA (2Captcha, Anti-CAPTCHA)
  • Para proyectos personales, resolver manualmente y guardar cookies
  • Considerar usar APIs oficiales si están disponibles

Directrices éticas

  • Respeta robots.txt
  • No sobrecargues los servidores
  • Extrae solo los datos que necesites
  • No scrapees datos personales sin consentimiento
  • Atribuye correctamente los datos obtenidos
  • Considera contactar al sitio para pedir acceso a datos

Ejercicio práctico

1. Implementa rotación de User-Agents en tu scraper

2. Crea un sistema de control de tasa de solicitudes

3. Revisa el archivo robots.txt de varios sitios web populares

Consideraciones legales

El scraping puede violar términos de servicio y en algunos casos leyes como la DMCA o GDPR. Consulta con un profesional legal si tienes dudas sobre la legalidad de tu proyecto.

Scrapy: Framework Profesional de Web Scraping

Scrapy es un framework completo para scraping a gran escala. Aprenderemos sus conceptos básicos.

Instalación

pip install scrapy

Crear un proyecto Scrapy

scrapy startproject mi_proyecto cd mi_proyecto scrapy genspider ejemplo ejemplo.com

Estructura de un proyecto

  • spiders/: Contiene los spiders (arañas) que definen cómo hacer scraping
  • items.py: Define la estructura de los datos a extraer
  • pipelines.py: Procesamiento posterior de los datos
  • middlewares.py: Personalización del comportamiento de las peticiones
  • settings.py: Configuración del proyecto

Spider básico

import scrapy class ProductoSpider(scrapy.Spider): name = 'productos' start_urls = ['https://ejemplo.com/productos'] def parse(self, response): for producto in response.css('div.producto'): yield { 'nombre': producto.css('h2::text').get(), 'precio': producto.css('.precio::text').get(), 'url': producto.css('a::attr(href)').get() } # Seguir paginación next_page = response.css('a.next-page::attr(href)').get() if next_page: yield response.follow(next_page, self.parse)

Ejecutar el spider

# Ejecutar y guardar en JSON scrapy crawl productos -o productos.json # Ejecutar en modo interactivo scrapy shell 'https://ejemplo.com/productos'

Ventajas de Scrapy

  • Built-in para manejo de peticiones asíncronas
  • Soporte para pipelines de procesamiento
  • Middleware para rotación de User-Agents y proxies
  • Exportación a múltiples formatos
  • Soporte para crawling (seguir enlaces)
  • Sistema de logging y estadísticas

Ejercicio práctico

1. Crea un proyecto Scrapy y genera tu primer spider

2. Implementa un spider que siga enlaces y extraiga datos de múltiples páginas

3. Configura un pipeline para limpiar y validar los datos extraídos

Consejo final

Scrapy tiene una curva de aprendizaje más pronunciada pero es la mejor opción para proyectos serios de scraping. Para tareas pequeñas, Requests + BeautifulSoup pueden ser suficientes.

Recursos adicionales

  • Documentación oficial de Scrapy: https://docs.scrapy.org/
  • BeautifulSoup documentation: https://www.crummy.com/software/BeautifulSoup/bs4/doc/
  • Selenium Python bindings: https://selenium-python.readthedocs.io/
  • Lista de User-Agents: https://developers.whatismybrowser.com/useragents/explore/