Curso completo desde cero hasta nivel avanzado
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.
El web scraping consiste en:
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.
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
Para hacer web scraping efectivo, es fundamental entender cómo funcionan las páginas web y el protocolo HTTP.
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>
h1
, p
, a
.titulo
#contenido
[href="https://ejemplo.com"]
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>
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
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.
pip install requests beautifulsoup4
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
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)
# 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')
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
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.
Ahora que conoces los fundamentos, profundicemos en técnicas más avanzadas de extracción y manipulación de datos.
# 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
# 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')
# 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))
# 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://'))
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
Algunas páginas pueden tener HTML mal formado. BeautifulSoup es bastante tolerante, pero para casos extremos considera usar lxml como parser: BeautifulSoup(html, 'lxml')
Muchos sitios web requieren interacción con formularios o mantenimiento de sesiones. Aprenderemos a manejar estos casos.
# 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")
# 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)
# 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)
# 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)
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
Nunca guardes credenciales directamente en tu código. Usa variables de entorno o archivos de configuración seguros.
Cuando las páginas cargan contenido dinámicamente con JavaScript, Requests y BeautifulSoup no son suficientes. Ahí entra Selenium.
pip install selenium
Además, necesitarás el driver correspondiente a tu navegador (ChromeDriver para Chrome, GeckoDriver para Firefox).
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')
# 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')
# 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')
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
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
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.
Una vez extraídos los datos, necesitamos almacenarlos en formatos útiles para su posterior análisis o uso.
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'])
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)
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()
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')
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
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.
Los scrapers deben ser robustos y manejar adecuadamente los errores que puedan ocurrir durante la ejecución.
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
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
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"
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}")
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
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
Los sitios web pueden detectar y bloquear scrapers. Aprende a evitar bloqueos y a hacer scraping de forma ética.
# 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)
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
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
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 es un framework completo para scraping a gran escala. Aprenderemos sus conceptos básicos.
pip install scrapy
scrapy startproject mi_proyecto
cd mi_proyecto
scrapy genspider ejemplo ejemplo.com
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 y guardar en JSON
scrapy crawl productos -o productos.json
# Ejecutar en modo interactivo
scrapy shell 'https://ejemplo.com/productos'
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
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.