Fundamentos del web crawling — BeautifulSoup
Al finalizar este tema
Podrás implementar el flujo básico para obtener páginas web con requests y extraer los datos deseados con BeautifulSoup.
¿Qué es el web crawling?
El web crawling consiste en que un programa visite páginas web y recopile datos automáticamente. También se le conoce como scraping.
Es realizar con código lo que hace un navegador:
- Enviar una solicitud (request) al servidor web
- Recibir el HTML
- Extraer los datos deseados del HTML
Paso 1 — Obtener el HTML (requests)
import requests
url = "https://example.com"response = requests.get(url)
print(response.status_code) # 200 = correctoprint(response.text[:200]) # Primeros 200 caracteres del HTMLrequests.get(url) envía una solicitud al servidor como un navegador y recibe el HTML enviado por el servidor en response.text.
# Comprueba el código de estadoif response.status_code == 200: html = response.textelif response.status_code == 404: print("No se encontró la página")elif response.status_code == 403: print("Acceso denegado")Paso 2: Análisis de HTML (BeautifulSoup)
from bs4 import BeautifulSoup
html = """<html><body> <h1 class="title">Título de la noticia</h1> <div class="content"> <p>Este es el primer párrafo.</p> <p>Este es el segundo párrafo.</p> </div> <ul id="tags"> <li>Python</li> <li>Datos</li> <li>Crawling</li> </ul></body></html>"""
soup = BeautifulSoup(html, "html.parser")BeautifulSoup analiza una cadena HTML y la convierte en un objeto con estructura de árbol. En este objeto, se pueden localizar las partes deseadas mediante etiquetas, clases, id, entre otros.
Paso 3 — Extracción de datos
Búsqueda por etiqueta
# Primera etiqueta h1title = soup.find("h1")print(title.text) # "Título de la noticia"print(title["class"]) # ["title"]
# Todas las etiquetas pparagraphs = soup.find_all("p")for p in paragraphs: print(p.text)# "Este es el primer párrafo."# "Este es el segundo párrafo."Búsqueda mediante selectores CSS
# select: usa selectores CSS, igual que querySelectorAllitems = soup.select("ul#tags li")for item in items: print(item.text)# "Python"# "Datos"# "Crawling"
# Selección por clasecontent = soup.select_one("div.content")print(content.text.strip())select/select_one es más intuitivo que find/find_all para quienes están familiarizados con CSS.
Ejemplo práctico — Extracción de datos de tablas
table_html = """<table> <tr><th>Nombre</th><th>Puntuación</th></tr> <tr><td>Carlos</td><td>85</td></tr> <tr><td>Ana</td><td>92</td></tr> <tr><td>Luis</td><td>78</td></tr></table>"""
soup = BeautifulSoup(table_html, "html.parser")rows = soup.select("tr")
data = []for row in rows[1:]: # Excluye la cabecera cols = row.find_all("td") data.append({ "nombre": cols[0].text, "puntuación": int(cols[1].text) })
print(data)# [{'nombre': 'Carlos', 'puntuación': 85}, {'nombre': 'Ana', 'puntuación': 92}, {'nombre': 'Luis', 'puntuación': 78}]# Convierte directamente en un DataFrame de pandasimport pandas as pddf = pd.DataFrame(data)print(df)Precauciones
Intervalo entre solicitudes
import time
urls = ["https://example.com/page/1", "https://example.com/page/2"]for url in urls: response = requests.get(url) time.sleep(1) # Espera un segundo para no sobrecargar el servidorRealizar solicitudes rápidas y continuas puede sobrecargar el servidor o provocar el bloqueo de la IP.
Configuración del User-Agent
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/91.0"}response = requests.get(url, headers=headers)Algunos servidores verifican el User-Agent para bloquear bots.
robots.txt
https://example.com/robots.txtEste archivo especifica las rutas permitidas y prohibidas para el rastreo (crawling). Verificar primero la política de rastreo del sitio es una cuestión de cortesía y una medida de seguridad legal.
Limitaciones del rastreo
BeautifulSoup solo puede procesar HTML estático. El contenido que se carga dinámicamente mediante JavaScript (SPA, scroll infinito, etc.) no se incluye en el HTML y, por lo tanto, no puede extraerse.
| Situación | Herramienta |
|---|---|
| HTML estático | requests + BeautifulSoup |
| Carga dinámica de JavaScript | Selenium, Playwright |
| API disponible | Llamada directa a la API con requests (más eficiente) |
Si existe una API, se debe priorizar su uso sobre el rastreo. Esto permite obtener datos estructurados con precisión y reduce la carga en el servidor.
Resumen del flujo principal
1. requests.get(url) → texto HTML
2. BeautifulSoup(html) → árbol analizado
3. soup.select("selector CSS") → elementos deseados
4. element.text / element["attr"] → extracción de datosManejo de errores
En el rastreo real se producen diversos errores:
import requestsfrom bs4 import BeautifulSoup
def safe_fetch(url, retries=3): for attempt in range(retries): try: response = requests.get(url, timeout=10) response.raise_for_status() return response.text except requests.exceptions.Timeout: print(f"Tiempo de espera agotado ({attempt + 1}/{retries})") except requests.exceptions.HTTPError as e: print(f"Error HTTP: {e}") return None except requests.exceptions.ConnectionError: print(f"Falló la conexión ({attempt + 1}/{retries})") return NoneSi no se configura timeout, el programa esperará indefinidamente ante un servidor que no responde. raise_for_status() lanza una excepción ante respuestas 4xx/5xx.
Crawl de múltiples páginas
import time
base_url = "https://example.com/articles?page="all_titles = []
for page in range(1, 11): html = safe_fetch(f"{base_url}{page}") if html is None: continue
soup = BeautifulSoup(html, "html.parser") titles = soup.select("h2.article-title") all_titles.extend([t.text.strip() for t in titles])
print(f"Página {page}: {len(titles)} elementos recopilados") time.sleep(1)
print(f"Recopilación completada: {len(all_titles)} elementos en total")Guardar resultados de la extracción de datos
import jsonimport csv
# Guarda como JSONwith open("articles.json", "w", encoding="utf-8") as f: json.dump(all_titles, f, ensure_ascii=False, indent=2)
# Guarda como CSVwith open("articles.csv", "w", encoding="utf-8", newline="") as f: writer = csv.writer(f) writer.writerow(["número", "título"]) for i, title in enumerate(all_titles, 1): writer.writerow([i, title])Los datos recopilados se guardan en formato JSON o CSV para permitir su análisis directo con pandas.
Extracción de enlaces — atributo href
# Extrae todos los enlaces de la páginalinks = soup.select("a[href]")for link in links: url = link["href"] text = link.text.strip() print(f"{text}: {url}")
# Solo enlaces con un patrón concretoarticle_links = [ a["href"] for a in soup.select("a[href]") if "/article/" in a.get("href", "")]El crawling es el primer paso de la "técnica para obtener datos". La recopilación de datos es el punto de partida en todos los ámbitos, como el análisis de datos, el aprendizaje automático y el monitoreo.