¿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 datosConceptos 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 todosEnlaces 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_resultadosHeaders 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 NoneGuardar 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.